PythonのGUI Toolkit比較(Tkinter, PyQt5, wxPython)

PythonにはGUIツールキットが、いっぱいあって、定番を決められないうちに、いくつも使うようになってしまいました。Pythonのドキュメントにも、Tkinter, wxPython, PyQt, Kivy, さらにpygletだけにとどまらず、いくつものGUIツールキットが紹介されており、あたかも全てが公式のようです。サポートの面からも、いずれもコミュニティがあまり生き生きしていないようで、「このツールキットなら困ったときに、すぐ教えてもらえる!」みたいな選び方も難しいように感じます。

と愚痴りつつ、これらのうち、Tkinter, wxPython, PyQt, Kivy, Pygletは少しいじりますので、うち最初の3つで、簡単なGUIを作って比較します。

■ それぞれのツールキットに関する独断と偏見

今のところ、いずれのツールキットへもライトユーザーのスタンスを貫いていますので、きちんとした比較は以下などを参照してください。

前提環境

  • Windows 10 - 64bit
  • Python 3.6 (Anaconda 4.4.0)

Tkinter

Python標準でついているということで、さくっと何か使って世の中に公開、あるいは社内展開するときなどに、ありがたい。グラフィクスをTkinterに扱わせようと考える場合、日本語・英語問わずWebで見つけられる資料は絶望的に近い。

PyQt5

QtのPythonバインドはPySideやPyQtなどいくつか種類があるそうで、そのうちPyQtはバージョンがいくつもあり、PyQt4使ったり、PyQt5使ったり混戦状態です。そのうえ、PyQt4とPyQt5で若干仕様が違ったりして、ググるときやStackOverflowなど読んだりするとき、健康に良くない。また、C++バインドの資料に行きあたることもあって、ややストレス。Python3時代となった今、PyQt5を選び、C++向け資料を見ながら、C++バインドとPython向けバインドの若干の差を吸収できるニュータイプ的な感性を身に着けることが処方箋。
Qt系は商用ライセンスがあったりなど面倒そうなので、今回の3つでは一番安定感を感じながら使えるものの、お仕事では避けた方が良い…のか?
インストールは、Anacondaなら最初から入っているので安心。標準Python入れてしまった人もインストーラなどで出来ます。ただ、「PyQtを入れないといけない」とわからない人に、PyQtを使ったプログラムをディストリビュートするときには若干の危険が伴う(※1)。依存関係が複雑なせいか、パスの設定関係でqtの共用dllを見つけられないと途端にプログラムが悲鳴を上げる(※2)。
※1 「動きません!何か字が出ました!」
※2 ‘This application failed to start because it could not find or load the Qt platform plugin “windows” in “”.

wxPython

細かい仕様(挙動)がいちいちWin32APIに似ていて、Win32プログラミングから入った私には直感的でわかりやすく、エラーが出た時も何が起きているか内部挙動が推定しやすいです。ただ、ライブラリのメモリ管理のお行儀が若干悪いらしく、githubでもissueが出てたりしました。実感がわきませんが、コミュニティは大きいらしい。
インストールは、最近は違うみたいですが、ちょっと前までpipに載っていなかったので(※3)、「wxPythonをインストールしないといけない」とわからない人に、wxPythonを使ったプログラムをディストリビュートするときは、PyQt以上の危険が伴った。
※3 wxPython(Phoenix)を pip でインストール – emptypage.jp blog

■ 図形描画をやってみる

シンプルに200px角のウィンドウを表示して、中央に直径100pxの円を描きます。
ソースはこちら >> Github
あたりまえですが、見た目はそう変わらんですね

Tkinter

ユニークなところは、このように平打ちでお絵かきしていくことでしょうか。イベントドリブン型は完全に隠蔽されていますね。
root = tk.Tk()
root.title("Tkinter Window Simple")
root.geometry("200x200")

# Define Canvas
canvas = tk.Canvas(root, width = 200, height = 200, bg="#fff")
canvas.place(x=0, y=0)

# Draw Circle on Canvas
dia = 100
centerX, centerY = 100, 100
id = canvas.create_oval(centerX - dia/2, centerY - dia/2,
                        centerX + dia/2, centerY + dia/2,
                        fill="#7092be")

# Call Main loop
root.mainloop()

PyQt5

まず、色々インポートする必要があります。
from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtGui import QPainter, QBrush, QPen
from PyQt5.Qt import QColor
で、ウィンドウを開いて絵を描くところは、QWidgetをオーバーライドして実施。
class MyWindow(QWidget): 

wxPython

wx.Frameをオーバーライドして、ウィンドウを開いて絵をかきます。
class MyWindow(wx.Frame): 
wx.Frameのinitのプロパティにウィンドウサイズを決めるsizeがあり、
super().__init__(parent, title=title, size=(640, 480))
などとできますが、残念、それはウィンドウサイズ。Tkinterやpyqtではクライアント領域の大きさでウィンドウサイズを決めていますので、タイトルバーなどがある分クライアントサイズが小さくなってしまいます。
Tkinterやpyqtと同じクライアント領域を確保したかったら、
self.SetClientSize(640, 480)
などとしてください。
参考:ウィンドウサイズとクライアントサイズの違いについて→DirectXTutorial.com

■ アニメーション

200px角のウィンドウをボールが跳ね回ります。
ソースはこちら >> Github

アニメーションの内容は共通して、跳ね回るボール。

# Update Circle Pos
self.x += self.vx
self.y += self.vy

if self.x > w or self.x < 0:
    self.vx = - self.vx
if self.y > h or self.y < 0:
    self.vy = - self.vy

Tkinter

静止画を描画するときには平打ちで良かったTkinterでも、アニメーションではイベントドリブンの作法でウィンドウを開く必要があります。→tkinter.Tkをオーバーライドします。

class MyWindow(tk.Tk):

タイマーは使わずに、絵を更新する関数を”20ms後にコールする”ようするのが、シンプルそうです。

self.after(20, self.onTimer)

PyQt5

タイマーイベントをセットします。

# Set Timer
self.timer = QTimer(self)
self.timer.timeout.connect(self.OnTimer)
self.timer.start(20)

タイマーイベントで、ボール位置を更新後、update()でpaintEventをトリガーしてやります。これは、Qtでは描画関係(デバイスコンテキストの操作)をpaintEvent以外では取り扱えないため。

def OnTimer(self):        
   (中略)
    # Paint Event
    self.update()

バックバッファー関係は特に意識しなくても上手にやってくれるので、普通に絵を描いても、ちらつきが出ません。

wxPython

タイマーイベントのセット

   # Set Timer
    self.timer = wx.Timer(self)
    self.Bind(wx.EVT_TIMER, self.OnTimer)       
    self.timer.Start(20)

タイマーイベントの中で描画まで出来ます。

def OnTimer(self, event):
    # Graphics Update
    w, h = self.GetClientSize()
    bdc = wx.BufferedDC(wx.ClientDC(self))
    bdc.SetBackground(wx.Brush("white"))  
    bdc.Clear()

    # Update Circle Pos
    self.x += self.vx
    self.y += self.vy

    if self.x > w or self.x < 0:
        self.vx = - self.vx
    if self.y > h or self.y < 0:
        self.vy = - self.vy

    # Draw Circle
    bdc.SetPen(wx.Pen(wx.Colour(0,0,0)))
    bdc.SetBrush(wx.Brush(wx.Colour(112,146,190)))
    bdc.DrawCircle(self.x, self.y, self.dia/2)

注意点は、描画の対象はバックバッファーにすること。ちらつきます。

bdc = wx.BufferedDC(wx.ClientDC(self))

■ まとめ

Tkinter PyQt5 wxPython
ウィンドウ作成
(クライアントサイズ320x240)
w = tk.Tk()
w.title(“Title”)
w.geometry(“320x240”)
app= QApplication(sys.argv)
w = QWidget()
w.setWindowTitle(“Title”)
w.resize(320,240) w.show()
app = wx.App()
w=wx.Frame(title=”Title”)
w.SetClientSize(320, 240)
w.Show()
円描画 canvas.create_oval(左上X, 左上Y,右下X,右下Y, fill=”#カラーコード”) qp.setBrush(QColor(R,G,B))
qp.drawEllipse(左上X, 左上Y, 幅, 高さ)
dc.SetBrush(wx.Brush(wx.Colour(R,G,B)))
dc.DrawCircle(中心X, 中心Y, 半径)
アニメーション タイマーは不要。20msecおきに自身を呼び出すように設定。 タイマーイベントで座標更新。描画はpaintEventをトリガーして、その中で実施。 タイマーイベント内で座標更新と描画の両方が可能。描画にバックバッファーを使わないとチラつく。

No comments:

Post a Comment