15
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

PythonでGUI : PyQt5のご紹介

Last updated at Posted at 2017-05-19
1 / 20

なぜ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は「値が変更された時」にしか発行されないからです。

15
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?