PyQt5でさくっとGUIを作る

普段GUIを作成する場合はC++withQtで作成してます.実のところQtを使えば簡単にGUIが作成できるんですが,わざわざQtプロジェクトを作るのもあれだなーという時
Python用のQtバインディングであるPyQt5を使うともっとお手軽にできるのではないかと思って試してみました.(ちなみに,PyQt5は全然ドキュメントがないです.)


最初に一つ言っておくと,Python用のQtバインディングは2種類あって,1つがPyQt,もう一つがPySideです.PySideの方はQt開発元のNokiaがサポートしてます.今回PyQtを利用した理由としたのは2014/11/12現在Qt5をサポートしているのはPyQtのみだったからです.(Python3は両方サポートしてます).重要な点ですが,PyQt5のライセンスはGPLもしくは商用ライセンスです(Qt自体はLGPLもしくは商用ライセンス).PyQt4では,GPLに例外事項が設けられていて,PyQt4を用いて作成したスクリプトだけであればMITやBSDライセンスで配布することが許可されていましたが,どうやらPyQt5ではこの例外事項が削除されたようです.PySideの方はLGPLで利用できます.機能的にPyQtとPySideでそれほど大きな差はないようですが,特にシグナルとスロットを自分で定義する場合に違いがあるようです.基本的にはQtの関数をラップしてあるだけなので,GUIの機能の詳細についてはQtのドキュメントを参照することになります.


以下に載せてある今回作成したプログラムはGitHub - mmisono/pyqt5-example: PyQt5 exampleにあります.

インストール

(python3.4, Qt5.3.1を使ってMac OS Xにインストールしました).インストールは公式からSIPPyQt5のソースを落とします.
まずSIPをインストールします.

$ python configure.py
$ make
$ make install (virtualenvを使っている場合そのままmake installでokです)

SIPというのはC++プログラムをpythonで利用できるようにラップしてくれるもので,
これを利用してPyQt5はQtをPythonから使えるようにします.
SIPがインストールできたらPyQt5をインストールします.configure時にオプションでqmakeの場所を
教えて上げます.

$ python configure.py --qmake /usr/local/Cellar/qt5/5.3.1/bin/qmake
$ make (ちょっと時間がかかる)
$ make install

2014/11/12現在ではまだQt5.3.2はサポートされていないようです(makeに失敗する).


ちなみに,作業ディレクトリに日本語パスが含まれていると上手くいきません.windowsの場合はインストーラを使えばよさそうです.

例1: フロントエンドとしての利用

さて,それでは実際にPyQt5でプログラムを作ってみます.PyQt5で作るときには何よりも楽に速く作れることを目指すことにします(大きいの書くならC++使いますからね).

まず,よくありそうなパターンの1つである,フロントエンドとしてのPyQt5アプリケーションを作ってみました.階乗を計算するだけ(笑)のプログラムです.


pyqt5-example/factorial.py at master · mmisono/pyqt5-example · GitHub

#!/usr/bin/env python

from PyQt5.QtWidgets import (QApplication, QWidget,
                             QGridLayout, QVBoxLayout, QHBoxLayout,
                             QLabel, QLineEdit, QPushButton)

def factorial(n):
    if n < 0:
        return -1
    elif n == 0:
        return 1
    else:
        return n * factorial(n-1)

class MainWindow(QWidget):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)

        self.inputLine = QLineEdit()
        self.outputLine = QLineEdit()
        self.outputLine.setReadOnly(True)

        self.calcButton = QPushButton("&Calc")
        self.calcButton.clicked.connect(self.calc)

        lineLayout = QGridLayout()
        lineLayout.addWidget(QLabel("num"), 0, 0)
        lineLayout.addWidget(self.inputLine, 0, 1)
        lineLayout.addWidget(QLabel("result"), 1, 0)
        lineLayout.addWidget(self.outputLine, 1, 1)

        buttonLayout = QVBoxLayout()
        buttonLayout.addWidget(self.calcButton)

        mainLayout = QHBoxLayout()
        mainLayout.addLayout(lineLayout)
        mainLayout.addLayout(buttonLayout)

        self.setLayout(mainLayout)
        self.setWindowTitle("Factorial")

    def calc(self):
        n = int(self.inputLine.text())
        r = factorial(n)
        self.outputLine.setText(str(r))

if __name__ == '__main__':
    import sys
    app = QApplication(sys.argv)
    main_window = MainWindow()

    main_window.show()
    sys.exit(app.exec_())

この場合は,QWidgetをメインウィンドウとして利用します.コンストラクタでボタンや入力フィールドの配置とシグナルのセットアップをおこないます.Qtを使ったことがあればほぼ違和感なくソースが読めると思います.Qtで書いてるように書けばだいたいOKです.Qtを使ってなくてもやってることは分かりやすいと思います.レイアウトはQHBoxLayoutやQVBoxLayoutに任せるのが楽でいいです.アプリケーションとして重要なのがself.calcButton.clicked.connect(self.calc)の部分で,ここでcalcボタンを押したときにcalcが呼ばれます.

例2: 簡単に描画する

先ほどの例では描画機能がありませんでしたが,実際にGUIが欲しくなるのは何かを描画したいときが多いと思います.ということで今度は描画メインのプログラムを作ってみました.簡単なマルバツです.

pyqt5-example/tic_tac_toe.py at master · mmisono/pyqt5-example · GitHub

#!/usr/bin/env python

from PyQt5.QtCore import (QLineF, QPointF, QRectF, Qt)
from PyQt5.QtGui import (QBrush, QColor, QPainter)
from PyQt5.QtWidgets import (QApplication, QGraphicsView, QGraphicsScene, QGraphicsItem,
                             QGridLayout, QVBoxLayout, QHBoxLayout,
                             QLabel, QLineEdit, QPushButton)

class TicTacToe(QGraphicsItem):
    def __init__(self):
        super(TicTacToe, self).__init__()
        self.board = [[-1, -1, -1],[-1, -1, -1], [-1, -1, -1]]
        self.O = 0
        self.X = 1
        self.turn = self.O

    def reset(self):
        for y in range(3):
            for x in range(3):
                self.board[y][x] = -1
        self.turn = self.O
        self.update()

    def select(self, x, y):
        if x < 0 or y < 0 or x >= 3 or y >= 3:
            return
        if self.board[y][x] == -1:
            self.board[y][x] = self.turn
            self.turn = 1 - self.turn

    def paint(self, painter, option, widget):
        painter.setPen(Qt.black)
        painter.drawLine(0,100,300,100)
        painter.drawLine(0,200,300,200)
        painter.drawLine(100,0,100,300)
        painter.drawLine(200,0,200,300)

        for y in range(3):
            for x in range(3):
                if self.board[y][x] == self.O:
                    painter.setPen(Qt.red)
                    painter.drawEllipse(QPointF(50+x*100, 50+y*100), 30, 30)
                elif self.board[y][x] == self.X:
                    painter.setPen(Qt.blue)
                    painter.drawLine(20+x*100, 20+y*100, 80+x*100, 80+y*100)
                    painter.drawLine(20+x*100, 80+y*100, 80+x*100, 20+y*100)

    def boundingRect(self):
        return QRectF(0,0,300,300)

    def mousePressEvent(self, event):
        pos = event.pos()
        self.select(int(pos.x()/100), int(pos.y()/100))
        self.update()
        super(TicTacToe, self).mousePressEvent(event)

class MainWindow(QGraphicsView):
    def __init__(self):
        super(MainWindow, self).__init__()
        scene = QGraphicsScene(self)
        self.tic_tac_toe = TicTacToe()
        scene.addItem(self.tic_tac_toe)
        scene.setSceneRect(0, 0, 300, 300)
        self.setScene(scene)
        self.setCacheMode(QGraphicsView.CacheBackground)
        self.setWindowTitle("Tic Tac Toe")

    def keyPressEvent(self, event):
        key = event.key()
        if key == Qt.Key_R:
            self.tic_tac_toe.reset()
        super(MainWindow, self).keyPressEvent(event)

if __name__ == '__main__':
    import sys
    app = QApplication(sys.argv)
    mainWindow = MainWindow()

    mainWindow.show()
    sys.exit(app.exec_())

今回はメインウィンドウとしてQGraphicsViewを利用しています(QGraphicsViewはQWidgetを継承してます).Qtの描画機能は,大雑把にいうとQGraphicsSceneを作って,その中にQGraphicsItemを配置して,最終的にQGraphicsSceneをQGraphicsViewにセットすることでおこないます.そこで,マルバツの本体はQGraphicsItemを継承する形で作成しています.QGraphicsItemで大事なのはpaint()関数とboudingRect()関数で,これを定義してあげることで描画がおこなわれます.また,mousePressEvent()を実装することでマウスイベントを,keyPressEvent()を実装することキーイベントがキャッチできます.今回はQGraphicsItemは一つしか使ってませんが,もちろん複数のQGraphicsItemを使う事ができます.QGraphicsItemを使うと,マウスイベントのディスパッチや衝突判定をQt側でおこなってくれるので,例えばグラフを可視化するプログラムを作って,グラフの頂点をマウスドラッグで動かせるようにするといったことが簡単にできます.ボタンや入力フィールドが無い場合はこの構成で十分だと思います.

例3: QWidgetとQGraphicsViewの組み合わせ

個人的にこれが一番使うかもしれません.メインウィンドウをQWidgetで作成して,その中にQGraphicsViewと操作用のボタンや入力フィールドを配置します.例として,1次元のセルオートマトンをシミュレートするプログラムを作ってみました.



pyqt5-example/cellular_automaton.py at master · mmisono/pyqt5-example · GitHub


このプログラムでは,QTimerを利用することでアニメーションをしています.QTimerの使い方は,Qtimerのインスタンスを作成してtimeoutシグナルに対し呼び出したい関数をconnectし,timerを起動させるという流れになります.このとき,QGraphicsItem自体はQObjectを継承していないためシグナルを扱う事が出来ないので*1,メインウィンドウ側でQTimerを実行し,定期的に描画をおこなう構成にしています.いろいろやり方はありますがこれがシンプルなんじゃないかなと思います.

例4: matplotlibtとの連携

PyQt5を使ってうれしい機能の一つに,matplotlibと簡単に連携できることがあげられます.matplotlibは1.4.0からPyQt5をサポートしてます.以下がmatplotlibをPyQt5から使用する例です.


pyqt5-example/plot.py at master · mmisono/pyqt5-example · GitHub

#!/usr/bin/env python

import random
import numpy as np
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure

from PyQt5.QtCore import (QLineF, QPointF, QRectF, Qt, QTimer)
from PyQt5.QtGui import (QBrush, QColor, QPainter)
from PyQt5.QtWidgets import (QApplication, QGraphicsView, QGraphicsScene, QGraphicsItem,
                             QGridLayout, QVBoxLayout, QHBoxLayout, QSizePolicy,
                             QLabel, QLineEdit, QPushButton)

# FigureCanvas inherits QWidget
class MainWindow(FigureCanvas):
    def __init__(self, parent=None, width=4, height=3, dpi=100):
        fig = Figure(figsize=(width, height), dpi=dpi)
        self.axes = fig.add_subplot(111)
        self.axes.hold(False)

        super(MainWindow, self).__init__(fig)
        self.setParent(parent)

        FigureCanvas.setSizePolicy(self,
                                   QSizePolicy.Expanding,
                                   QSizePolicy.Expanding)
        FigureCanvas.updateGeometry(self)

        timer = QTimer(self)
        timer.timeout.connect(self.update_figure)
        timer.start(50)

        self.x  = np.arange(0, 4*np.pi, 0.1)
        self.y  = np.sin(self.x)

        self.setWindowTitle("Sin Curve")

    def update_figure(self):
        self.axes.plot(self.x, self.y)
        self.y = np.roll(self.y,-1)
        self.draw()

if __name__ == '__main__':
    import sys
    app = QApplication(sys.argv)
    mainWindow = MainWindow()

    mainWindow.show()
    sys.exit(app.exec_())


matplotlibのFigureCanvasQTAggを継承してクラスを作って,その中でdraw()を呼ぶとグラフが描画されます.アニメーションをしたければQTimerを使ってdraw()を定期的に呼ぶようにすればokです(簡単!).こんなことができるのはFigureCanvasQTAggが(QGraphicsItemではなく)QWidgetを継承しているからです.QWidgetを継承しているのでQTimerのtimeoutシグナルも使えます.もちろんこれは一番単純な例で,セルオートマトンの例のように自分でQWidgetを使ってメインウィンドウを作り,その中にmatplotlibを埋め込むことができます.

例4: uiファイル(Qt Designer)を使う

さて,これまではQtのuiファイルを活用してきませんでしたが,ボタンや入力フィールドがたくさんあるような場合だと やはりQt Designerでuiファイルを作成した方が楽です. uiファイルをPyQt5で使う方法は単純で,まずQt Designerを使ってuiファイルを作成した後,pyuic5コマンドを利用してuiファイルをpythonスクリプトに変換します.以下に例を示します.


https://github.com/mfumi/pyqt5-example/tree/master/echo

echo_ui.py

# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'echo.ui'
#
# Created: Wed Nov 12 18:33:05 2014
#      by: PyQt5 UI code generator 5.3.2
#
# WARNING! All changes made in this file will be lost!

from PyQt5 import QtCore, QtGui, QtWidgets

class Ui_Echo(object):
    def setupUi(self, Echo):
        Echo.setObjectName("Echo")
        Echo.resize(250, 200)
        self.textBrowser = QtWidgets.QTextBrowser(Echo)
        self.textBrowser.setGeometry(QtCore.QRect(40, 90, 180, 79))
        self.textBrowser.setObjectName("textBrowser")
        self.horizontalLayoutWidget = QtWidgets.QWidget(Echo)
        self.horizontalLayoutWidget.setGeometry(QtCore.QRect(40, 30, 180, 41))
        self.horizontalLayoutWidget.setObjectName("horizontalLayoutWidget")
        self.horizontalLayout = QtWidgets.QHBoxLayout(self.horizontalLayoutWidget)
        self.horizontalLayout.setContentsMargins(0, 0, 0, 0)
        self.horizontalLayout.setObjectName("horizontalLayout")
        self.inputEdit = QtWidgets.QLineEdit(self.horizontalLayoutWidget)
        self.inputEdit.setObjectName("inputEdit")
        self.horizontalLayout.addWidget(self.inputEdit)
        self.submitButton = QtWidgets.QPushButton(self.horizontalLayoutWidget)
        self.submitButton.setObjectName("submitButton")
        self.horizontalLayout.addWidget(self.submitButton)

        self.retranslateUi(Echo)
        QtCore.QMetaObject.connectSlotsByName(Echo)

    def retranslateUi(self, Echo):
        _translate = QtCore.QCoreApplication.translate
        Echo.setWindowTitle(_translate("Echo", "Echo"))
        self.submitButton.setText(_translate("Echo", "Submit"))


echo.py

#!/usr/bin/env python

from PyQt5.QtWidgets import (QApplication, QWidget,
                             QGridLayout, QVBoxLayout, QHBoxLayout,
                             QLabel, QLineEdit, QPushButton)
import echo_ui

class MainWindow(QWidget):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)
        self.ui = echo_ui.Ui_Echo()
        self.ui.setupUi(self)
        self.ui.submitButton.clicked.connect(self.submit)

    def submit(self):
        text = self.ui.inputEdit.text()
        self.ui.textBrowser.append(text)

if __name__ == '__main__':
    import sys
    app = QApplication(sys.argv)
    main_window = MainWindow()

    main_window.show()
    sys.exit(app.exec_())

pyuic5で生成されたファイルは見ればわかるようにUIをセットアップする関数を含んでいるので,あとはこれを呼ぶだけです.便利!!!

まとめ

ということで実際にPyQt5を使っていくつかアプリケーションを作ってみました.ここにあるのはどれも慣れれば数十分で作成できます.まぁ,これはPyQt5というよりそもそもQt5が使いやすいからというのが大部分を占めてると思いますが.Qtはqmakeあるいはcmakeを前提にしているとはいえ,Qt Creatorを使えばかなり簡単に作成できるので,PyQt5を使うのと速度的にそれほど大きな差はないかもしれません.でも小さいものではやはりコンパイル無しの1ファイルで作れるPyQt5は楽だと思いますし,pythonで何か描画したいというときに重宝すると思いました*2.今回作ったサンプルもベースとして結構活用できるんじゃないかなと思います.

*1:QGraphicsObjectを使えばできますが

*2:勿論,もっと複雑なものも作れます