Python
Qt
PyQt5

PythonでGUI : PyQt5のご紹介

More than 1 year has passed since last update.


なぜPythonでGUIなのか?



なぜ Ruby, JS, C# ではなくPythonなのか?


  1. Python言語自体が書きやすい (vs JS)

  2. WindowsでもMacでも動作する (vs Ruby, C#)

  3. データ分析や画像処理などのライブラリが充実している

  4. ctypesやCythonでC/C++のコードを呼び出せる



Pythonの主なGUIライブラリ


  • tkinter


    • 標準添付



  • turtle


    • 標準添付・カメさんがカワイイ



  • Kivy


    • 新しい




  • PyQt5


    • 今日のオススメ





PyQt5のご紹介



QtはOSSのアプリケーションフレームワーク


  • 1992〜 (現在 v5.8)


  • C++


  • クロスプラットフォーム(OSネイティブのスタイルで描画)


  • 豊富なウィジェット _


  • 使いやすいAPI(シグナル、レイアウト、MV)


  • ツール (IDE, UIデザイナ)



PyQt5 は Qt5.x のラッパー

SIP というツールでC++ <=> Pythonの部分を生成している。


  • Qtのほぼ全ての機能を使える

  • Qtの使いやすいAPIはそのまま

  • よりPythonicな書き方


    • 文字列や配列はPythonの型に変換

    • メソッドにlambdaやメソッドを渡せる





PyQt5の例



標準のウィジェットを配置

スクロールバーの現在値を文字列で表示

from PyQt5.QtWidgets import *

from PyQt5.QtGui import *
from PyQt5.QtCore import *
import sys

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

s = QScrollBar(orientation=Qt.Horizontal)
t = QLabel()

s.valueChanged.connect(lambda v: t.setText(str(v)))

l = QVBoxLayout()
l.addWidget(s)
l.addWidget(t)
self.setLayout(l)

app = QApplication(sys.argv)
s = Sample1()
s.show()
sys.exit(app.exec_())



例1



レイアウト


  • .Net などと大体同じ

  • ウィンドウサイズが変わっても、適当に伸縮

  • QHBoxLayout, QVBoxLayout, QGridLayout, etc..



シグナル


  • 各ウィジェットは固有のシグナルを持つ

  • 値の変更(valueChanged) やクリック (clicked) などを通知する


  • .connect で通先の関数やメソッドを登録する

  • Observer パターン

→ ウィジェットがモデル層と密結合するのを防ぐ

→ コールバックメソッドより柔軟



シグナルを双方向につなぐ



  • connect / setValue をループしてもよい

  • 同じシグナルに複数回 connect したり、複数のシグナルに connect したりしてもよい

from PyQt5.QtWidgets import *

from PyQt5.QtGui import *
from PyQt5.QtCore import *
import sys

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

l = QVBoxLayout()

t = QLabel()
s1 = QScrollBar(orientation=Qt.Horizontal)
s1.valueChanged.connect(lambda v: t.setText(str(v)))

s2 = QScrollBar(orientation=Qt.Horizontal)
s3 = QSpinBox()
s4 = QSpinBox()
s5 = QSpinBox()

s1.valueChanged.connect(s2.setValue)
s2.valueChanged.connect(s3.setValue)
s3.valueChanged.connect(s4.setValue)
s4.valueChanged.connect(s5.setValue)
s5.valueChanged.connect(s1.setValue)

l.addWidget(s1)
l.addWidget(s2)
l.addWidget(s3)
l.addWidget(s4)
l.addWidget(s5)
l.addWidget(t)

self.setLayout(l)

app = QApplication(sys.argv)
s = Sample2()
s.show()
sys.exit(app.exec_())



例2



桂馬飛び


  • データ・モデル・ウィジェットを実装

  • モデルにもシグナルを定義できる


# データ(現在の盤面)
class BoardState:

# モデル
class BoardModel(QObject):
stateChanged = pyqtSignal(BoardState)
def moveKnightTo(self, x, y):
def rollback(self):

# ウィジェット(1個1個のセル)
class BoardCellWidget(QLabel):
knightMoved = pyqtSignal(tuple)
def setState(self, state):

# モデルを持ち、64個のセルを表示するウィジェット
class BoardWidget(QWidget):


from PyQt5.QtWidgets import *

from PyQt5.QtGui import *
from PyQt5.QtCore import *
import sys

class BoardState:
def __init__(self, x, y):
self._x = x
self._y = y

def knightIn(self, x, y):
return (self._x, self._y) == (x, y)

def canKnightMoveTo(self, x, y):
dx = abs(self._x - x)
dy = abs(self._y - y)
return (dx, dy) in [(1, 2), (2, 1)]

class BoardModel(QObject):
stateChanged = pyqtSignal(BoardState)

def __init__(self, parent=None):
super().__init__(parent=parent)
self._states = [BoardState(0, 0)]

def moveKnightTo(self, x, y):
newState = BoardState(x, y)
self._states.append(newState)
self.stateChanged.emit(newState)

def rollback(self):
if len(self._states) <= 1:
return
self._states = self._states[:-1]
self.stateChanged.emit(self._states[-1])

def state(self):
return self._states[-1]

class BoardCellWidget(QLabel):
knightMoved = pyqtSignal(tuple)

def __init__(self, parent, x, y):
super().__init__(parent=parent)
self._x = x
self._y = y

self.setMinimumSize(QSize(32, 32))
self.setMouseTracking(True)
self.setAcceptDrops(True)

def setState(self, state):
self._state = state
self.update()

def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
drag = QDrag(self)
data = QMimeData()
data.setText("knight")
drag.setMimeData(data)
drag.exec()

event.accept()

def dropEvent(self, event):
if self._state.canKnightMoveTo(self._x, self._y):
self.knightMoved.emit((self._x, self._y))
event.acceptProposedAction()

def dragEnterEvent(self, event):
if self._state.canKnightMoveTo(self._x, self._y):
event.acceptProposedAction()

def paintEvent(self, event):
p = QPainter(self)

if (self._x + self._y) % 2 == 1:
p.setBackground(QColor(0xD3, 0x8C, 0x40))
else:
p.setBackground(QColor(0xFF, 0xCF, 0x9B))
p.eraseRect(self.rect())

rect = QRect(4, 4, self.width() - 8, self.height() - 8)
if self._state.knightIn(self._x, self._y):
p.fillRect(rect, Qt.white)
elif self._state.canKnightMoveTo(self._x, self._y):
p.fillRect(rect, Qt.green)

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

self._model = BoardModel(parent=self)

l = QGridLayout()
for x in range(8):
for y in range(8):
cell = BoardCellWidget(parent, x, y)
cell.setState(self._model.state())
self._model.stateChanged.connect(cell.setState)
cell.knightMoved.connect(lambda xy: self._model.moveKnightTo(*xy))
l.addWidget(cell, x, y)

l1 = QHBoxLayout()
l1.addLayout(l, 1)
self._rollback = QPushButton("rollback", parent=self)
self._rollback.pressed.connect(self._model.rollback)
l1.addWidget(self._rollback, 0)
self.setLayout(l1)

app = QApplication(sys.argv)

b = BoardWidget()
b.show()
sys.exit(app.exec_())



例3



補足


  • QML・QtQuickというDSLもある


  • QtWebEngineWidgets でブラウザを埋め込める(→React.jsも使える)

  • AndroidでもNDKを使ってビルドすればアプリを作れる

  • 昔はビルドが大変だった。今は pip install でOK



残念なところ


  • ライセンスがGPL or 有償


    • 社内向け・個人向けなら問題無い?



  • インストール作業が必要


    • PyInstaller を使えばWindows向け実行ファイルを生成できる

    • Mac や Linuxでも同様のツールがあるはず





Q&A

Q. どうして、ウィジェットを a.valueChanged.connect(b.setValue) でループ状につないでいっても無限ループにならないのですか?

A. valueChangedは「値が変更された時」にしか発行されないからです。