2022/03/22(火) [n年前の日記]
#1 [python] pycairoの描画結果をtkinterで表示
 Python + pycairo + tkinter を再勉強中。pycairo で描画した結果を、tkinter を使ってデスクトップ画面上に表示したい。
実験環境は、Windows10 x64 21H2。
 実験環境は、Windows10 x64 21H2。
- Python 2.7.18 32bit + Pillow 6.2.2 + pycairo 1.8.10
- Python 3.9.11 64bit + Pillow 9.0.1 + pycairo 1.21.0
◎ 各モジュールについて。 :
一応簡単に説明。pycairo は、Python から cairo という描画ライブラリを呼び出して図形を描くためのモジュール。
_cairo - Wikipedia
_Overview - Pycairo documentation
tkinter は、Python から Tk というGUIツールキットを呼び出してGUIアプリを作れるモジュール。
_Tcl/Tk - Wikipedia
_tkinter --- Tcl/Tk の Python インターフェース - Python 3.10.0b2 ドキュメント
加えて、Pillow (PIL) という画像処理モジュールも必要。pycairo で描画した結果を tkinter で表示できるデータに変換する際に利用する。
_Pillow - Pillow (PIL Fork) 9.0.1 documentation
_cairo - Wikipedia
_Overview - Pycairo documentation
tkinter は、Python から Tk というGUIツールキットを呼び出してGUIアプリを作れるモジュール。
_Tcl/Tk - Wikipedia
_tkinter --- Tcl/Tk の Python インターフェース - Python 3.10.0b2 ドキュメント
加えて、Pillow (PIL) という画像処理モジュールも必要。pycairo で描画した結果を tkinter で表示できるデータに変換する際に利用する。
_Pillow - Pillow (PIL Fork) 9.0.1 documentation
◎ 必要なモジュールのインストール。 :
 Windows版Pythonの場合、tkinter (Tkinter) は標準で同梱されているので、別途インストールしないで済む。
Python 3.x の場合、Pillow と pycairo は pip でインストールできる。
最後につけている「-U」は、更新も許可するオプション。今現在インストールされている版より新しい版がインターネットで公開されている場合は最新版に差し替えてくれる。
Python 2.7 の場合、Pillow は pip でインストールできるけど、pycairo は pip でインストールしようとするとエラーが出る。以下のページから、pygtk-all-in-one-2.24.2.win32-py2.7.msi を入手してインストールすれば、PyGTK、PyGObject、pycairo がインストールされる。
_Index of /binaries/win32/pygtk/2.24/
ちなみに、この PyGTK、PyGObject、pycairo は、昔のGIMP (2.6.x時代) で Python-Fu (GIMP-Python) を動かす際に必要になるモジュールだった。今の GIMP (Windows版、2.8.x以降) には最初から同梱されてると思う。
Python 3.x の場合、Pillow と pycairo は pip でインストールできる。
pip install Pillow -U pip install pycairo -U
最後につけている「-U」は、更新も許可するオプション。今現在インストールされている版より新しい版がインターネットで公開されている場合は最新版に差し替えてくれる。
Python 2.7 の場合、Pillow は pip でインストールできるけど、pycairo は pip でインストールしようとするとエラーが出る。以下のページから、pygtk-all-in-one-2.24.2.win32-py2.7.msi を入手してインストールすれば、PyGTK、PyGObject、pycairo がインストールされる。
_Index of /binaries/win32/pygtk/2.24/
ちなみに、この PyGTK、PyGObject、pycairo は、昔のGIMP (2.6.x時代) で Python-Fu (GIMP-Python) を動かす際に必要になるモジュールだった。今の GIMP (Windows版、2.8.x以降) には最初から同梱されてると思う。
◎ サンプルソース。 :
pycairo で描画して、Pillow で Tk 用に変換して、Tk でGUI表示をする動作サンプルを書いてみる。以下のサンプルを参考にした。
_python - Cairo with tkinter? - Stack Overflow
以下のような感じになった。
_01_pycairo_in_tkinter.py
実行すると、こんな感じのウインドウが表示される。
左上に赤、右上に緑、左下に青の四角を pycairo で描画して、結果を tkinter で表示してる。
_python - Cairo with tkinter? - Stack Overflow
以下のような感じになった。
_01_pycairo_in_tkinter.py
"""
pycairo in tkinter.
* Windows10 x64 21H2 + Python 2.7.18 32bit + Pillow 6.2.2 + pycairo 1.8.10
* Windows10 x64 21H2 + Python 3.9.11 64bit + Pillow 9.0.1 + pycairo 1.21.0
"""
import sys
if sys.version_info.major == 2:
    # Python 2.7
    import Tkinter as tk
else:
    # Python 3.x
    import tkinter as tk
from PIL import Image
from PIL import ImageTk
import cairo
def conv_surface_to_photo(surface):
    """Convert pycairo surface to Tk PhotoImage."""
    w, h = surface.get_width(), surface.get_height()
    m = surface.get_data()
    if sys.version_info.major == 2:
        # Python 2.7
        im = Image.frombuffer("RGBA", (w, h), m, "raw", "BGRA", 0, 1)
    else:
        # Python 3.x
        im = Image.frombuffer("RGBA", (w, h), m, "raw", "RGBA", 0, 1)
        # BGRA -> RGBA
        r, g, b, a = im.split()
        im = Image.merge("RGBA", (b, g, r, a))
        # data = im.tobytes("raw", "BGRA", 0, 1)
        # im = Image.frombytes("RGBA", (w, h), data)
    return ImageTk.PhotoImage(im)
def main():
    print(sys.version)
    
    w, h = 640, 480
    root = tk.Tk()
    # root.geometry("{}x{}".format(w, h))
    root.surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, w, h)
    w = root.surface.get_width()
    h = root.surface.get_height()
    # draw something with pycairo
    ctx = cairo.Context(root.surface)
    ctx.scale(w, h)
    ctx.rectangle(0, 0, 0.5, 0.5)
    ctx.set_source_rgba(1, 0, 0, 0.8)
    ctx.fill()
    ctx.rectangle(0.5, 0, 0.5, 0.5)
    ctx.set_source_rgba(0, 1, 0, 0.8)
    ctx.fill()
    ctx.rectangle(0, 0.5, 0.5, 0.5)
    ctx.set_source_rgba(0, 0, 1, 0.8)
    ctx.fill()
    # convert cairo image surface to tk.PhotoImage
    root._image_ref = conv_surface_to_photo(root.surface)
    label = tk.Label(root, image=root._image_ref)
    label.pack(expand=True, fill="both")
    root.mainloop()
if __name__ == "__main__":
    main()
実行すると、こんな感じのウインドウが表示される。
左上に赤、右上に緑、左下に青の四角を pycairo で描画して、結果を tkinter で表示してる。
◎ 少し解説。 :
 Python 2.7 と 3.x に対応させるあたりでちょっと面倒だったのでメモ。
Python 2.7 では import Tkinter と記述するけど、Python 3.x では import tkinter と記述するように変わってた。
とりあえず、Python 2.7、3.x、どちらでも動かせるように、Python のメジャーバージョンを取得して、if文で処理を分けることにしてみた。
それと、pycairo の surface と Tk の PhotoImage は、RGBA と BGRA の違いがあるようで…。そのまま表示してしまうと、以下のように、R と B が入れ替わってしまう。
故に、pycairo で何かしらを描画した後、BGRA を RGBA に変換しないといけない。そのあたりは、conv_surface_to_photo(surface) の中で処理している。
ただ、Python 2.7 時代は Pillow 関係で「"BGRA"」という指定が使えたのだけど、Python 3.x では「"BGRA"」という指定はエラーになってしまった。今回は Pillow の .split() を使って R,G,B,Aの各チャンネルに分解してから、.merge() で並びを変更してみた。
あるいは、Pillow の .tobytes() と .frombytes() を使って BGRA と RGBA を変換することもできる。上記のソースではコメントアウトしているけれど、手元で試した感じでは動いてくれた。
Python 2.7 では import Tkinter と記述するけど、Python 3.x では import tkinter と記述するように変わってた。
とりあえず、Python 2.7、3.x、どちらでも動かせるように、Python のメジャーバージョンを取得して、if文で処理を分けることにしてみた。
import sys
if sys.version_info.major == 2:
    # Python 2.7
    import Tkinter as tk
else:
    # Python 3.x
    import tkinter as tk
それと、pycairo の surface と Tk の PhotoImage は、RGBA と BGRA の違いがあるようで…。そのまま表示してしまうと、以下のように、R と B が入れ替わってしまう。
故に、pycairo で何かしらを描画した後、BGRA を RGBA に変換しないといけない。そのあたりは、conv_surface_to_photo(surface) の中で処理している。
def conv_surface_to_photo(surface):
    """Convert pycairo surface to Tk PhotoImage."""
    w, h = surface.get_width(), surface.get_height()
    m = surface.get_data()
    if sys.version_info.major == 2:
        # Python 2.7
        im = Image.frombuffer("RGBA", (w, h), m, "raw", "BGRA", 0, 1)
    else:
        # Python 3.x
        im = Image.frombuffer("RGBA", (w, h), m, "raw", "RGBA", 0, 1)
        # BGRA -> RGBA
        r, g, b, a = im.split()
        im = Image.merge("RGBA", (b, g, r, a))
        # data = im.tobytes("raw", "BGRA", 0, 1)
        # im = Image.frombytes("RGBA", (w, h), data)
    return ImageTk.PhotoImage(im)
ただ、Python 2.7 時代は Pillow 関係で「"BGRA"」という指定が使えたのだけど、Python 3.x では「"BGRA"」という指定はエラーになってしまった。今回は Pillow の .split() を使って R,G,B,Aの各チャンネルに分解してから、.merge() で並びを変更してみた。
あるいは、Pillow の .tobytes() と .frombytes() を使って BGRA と RGBA を変換することもできる。上記のソースではコメントアウトしているけれど、手元で試した感じでは動いてくれた。
◎ 2022/03/24追記。 :
試していたら、Python 2.7.18 32bit Windows版 + tkinter も、import tkinter で動作することに気づいた…。import Tkinter じゃなくても良かったのか…。どこかの時点で変更されたのだろうか。それとも何かのモジュールをインストールすると使えるようになるのだろうか。ググってみたけれど、このあたりについて記述しているページが見つからない…。
「Python 2 は Tkinter だけど、Python 3 で tkinter にリネームされたから気をつけろ」と書かれたページなら見つかるのだけど…。
_tkinter Tutorial -> Getting started with tkinter
_24.1. Tkinter - Tcl/Tk への Python インタフェース - Python 2.7.18 ドキュメント
_TkInter - Python Wiki
「Python 2 は Tkinter だけど、Python 3 で tkinter にリネームされたから気をつけろ」と書かれたページなら見つかるのだけど…。
_tkinter Tutorial -> Getting started with tkinter
_24.1. Tkinter - Tcl/Tk への Python インタフェース - Python 2.7.18 ドキュメント
_TkInter - Python Wiki
[   ツッコむ ]
以上です。

