21
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

プロもくチャットAdvent Calendar 2023

Day 8

PythonでGUIを作ろう! PySide編

Last updated at Posted at 2023-12-07

はじめに

本記事はプロもくチャット Adevent Calendar2023の8日目の記事です。

普段Pythonでツールを作っているとGUIも作りたくなることがあり、調べてみました。

PythonでGUIを作るための仕組みはいろいろとありますが、今回はPySide6を使用します。

動作確認環境

バージョン
OS Windows10 22H2
Python 3.10.10
PySide6 6.6.0

何を作るか

テキストの入力と出力、ボタンクリックを実装したプログラムを作ります。
今回はサンプルプログラムとして、数値を入力して[加算]ボタンを押すと加算結果を表示するプログラムを作ります。

002_AdditionUi.png

サンプルコード全体はgithubに公開しています。

どう作るか

今回実現したいこと

  • GUIの実装はボタンやテキストをドラッグ&ドロップで配置したい。

  • 内部の処理はPythonで書きたい。

  • GUIと内部処理は分離したい。

Viewの部分は今回使うPySide6に付属するツール(Qt Designer)から生成します。

このViewの中に内部処理を実装すると、Viewを作り直すたびに処理が消し飛んでしまうため、Viewをツールで生成する部分と、内部処理と連携する部分を分離して実装します。

内部処理の部分もModel、Controllerに分けて、MVCモデル風に実装します。

クラス図

全体像はこんな感じです。

PySide6について

PySide6は、GUI開発フレームワークのQtを、Pythonで使用できるようにしたパッケージです。
Qt自体はC++で開発されています。

QtをPython向けに使えるようにしたパッケージにはPySide6のほかに、PyQt6というものもあります。

ライセンス

  • オープンソースライセンス(LGPLv3 / GPLv2)
  • 商用ライセンス

法的な判断はできないため、ライセンスについては各々ご判断ください。

シグナルとスロット

Qt特有の仕組みに、シグナルとスロットというものがあります。
これらを使うことで、ボタンのクリックのような動的なイベントをトリガーに、何かしらの処理を紐づけて実行できます。

参考:

シグナル

シグナルは、テキストボックスの入力やボタンのクリックのように、何かが発生したことを他のオブジェクトに通知するための仕組みです。

通知とともに型を指定して値を渡すこともできます。
例えば、QLineEdit(テキストボックス)の持つtextChagnedシグナルは、QLineEditの文字が更新されたときに発行され、変更後の文字も渡されます。

Ctrl+Cを実行したとき等に使われるシグナルとは別者です。

スロット

スロットは、シグナルに紐づけて何かしらの処理を実行させることができます。
スロットで定義した関数の引数に、シグナルから渡された値を渡すことができます。

シグナルとスロットの連携

実際に見た方が分かりやすいと思うので実装例です。
Python上でPySide6を使ってシグナルとスロットを連携させる場合、以下のようにします。

# シグナルを定義
value_changed = Signal(float)

# スロットを定義
@Slot
def print_value(value):
    # 何かしら処理
    print(value)

# シグナルをスロットに接続
value_changed.connect(print_value)

例えば、プロパティのセッターにシグナルの発行を紐づけできます。

@property
def value(self):
    return self._value

@value.setter
def value(self, value):
    self._value = value
    # シグナルを発行
    value_changed.emit(value)

モデル/ビューアーキテクチャ

QtはMVC系アーキテクチャとして、モデル/ビューアーキテクチャが実装されています。
本記事では詳細は省略します。

こちらが参考になりました。

今回は以下のような構成で組んでみました。
ModelからViewへの流れは一方向で、Viewの操作をModelへ反映する場合は、Delegateを仲介します。
Deligateの部分をControllerとみなしています。

002_MVC.png

手順1:PySide6のインストール

ここからは実際に実装するための手順です。

以下を実行してPySide6をインストールします。

python -m pip install PySide6

PySide6をインストールすると、以下のフォルダ(Python 3.10の場合)にツール一式もインストールされます。

C:\Users\ユーザ名\AppData\Local\Programs\Python\Python310\Scripts

今回はこの中の以下のツールを使います。

  • pyside6-designer.exe
    Qt Designer。ドラッグ&ドロップでGUIを作成できるツール。
    xml形式で.uiファイルに保存されます。

  • pyside6-uic.exe
    Qt Designerで作った.uiファイルを、.pyファイルへと変換するためのツール。

手順2:GUIの作成

ウィジェットの配置

pyside6-designer.exeを実行します。

起動すると、こんな感じの見た目のQt Designerが立ち上がります。

002_DesignerOutside.png

新しいフォームウィンドウが表示されるため、Main Windowを選択して作成します。

002_NewForm.png

MainWindowが生成されます。

画面の左右のウィンドウからウィジェットを配置したり、プロパティを編集したりして、画面を作っていきます。

002_DesignerNewWindow.png

  • ウィジェットボックス

画面に配置するボタンやテキストボックスなどの要素をウィジェットといいます。
このウィジェットボックスから配置したいウィジェットをドラッグ&ドロップで配置することができます。

  • オブジェクトインスペクタ

配置したオブジェクトの階層構造を確認することができます。
オブジェクトを選択したり、オブジェクト名を編集したりするときに使います。

  • プロパティエディタ

ウィジェットのプロパティを編集できます。
ウィジェットごとにプロパティは異なります。

今回のサンプルプログラムでは、このような画面にしました。

002_DesignerAddition.png

編集したウィジェット

  • MainWindow
    サイズを300x200に設定

  • Vertical Layout、Horizontal Layout
    ウィジェットを整列

  • Label
    加算結果や+ =の表示

  • TextEdit
    足し合わせる数値の入力フォーム

  • PushButton
    加算を実行するボタン

配置が完了したら、Addition.uiとして保存します。

Pythonファイルへの変換

pyside6-uic.exeを使って.uiファイルを.pyファイルへと変換します。

Addition.uiを保存したフォルダに移動し、

C:\Users\ユーザ名\AppData\Local\Programs\Python\Python310\Scripts\pyside6-uic.exe Addition.ui -o Ui_Addition.py

を実行して変換します。
変換したファイルの中身は後ほど。

手順3:Main実装

ここからはPythonでプログラムを書いていきます。

以下のような構造で配置します。
Ui_Addition.pyは実行時には不要ですが、置いたままにしています。

PySide6Sample
├─controllers
│  └─controller.py
├─models
│  └─model.py
├─views
│  ├─Addition.ui
│  ├─Ui_Addition.py
│  └─view.py
└─app.py

まずはapp.pyのMainから。
MainではModelとViewとControllerを生成し、描画処理を起動します。

app.py
import sys
from PySide6.QtWidgets import QApplication
from models.model import Model
from controllers.controller import Controller
from views.view import View

class Main(QApplication):
    def __init__(self, argv):
        super(Main, self).__init__(argv)
        self._model = Model()
        self._controller = Controller(self._model)
        self._view = View(self._model, self._controller)
        self._view.show()

if __name__ == "__main__":
    app = Main(sys.argv)
    sys.exit(app.exec())

手順4:Model実装

Modelでは、プログラム内で使用するデータを保持し、データを使った処理を実装します。

足し算の答えを更新した後に、Viewへと通知する必要があるため、シグナルを定義しています。

model.py
from PySide6.QtCore import QObject, Signal

class Model(QObject):

    def __init__(self):
        super().__init__()
        self.left_value = 0
        self.right_value = 0
        self.answer = 0

    # Viewへ加算結果を通知するSignal
    answer_changed = Signal(float)

    @property
    def left_value(self):
        return self._left_value

    @left_value.setter
    def left_value(self, value: float):
        self._left_value = value

    @property
    def right_value(self):
        return self._right_value

    @right_value.setter
    def right_value(self, value: float):
        self._right_value = value

    @property
    def answer(self):
        return self._answer

    @answer.setter
    def answer(self, value: float):
        self._answer = value
        self.answer_changed.emit(value)

    def add(self):
        self.answer = self.left_value + self.right_value

手順5:Controller実装

Controllerでは、Viewで入力された値のチェックと、Modelへの値やイベントの受け渡しをしています。

入力値がfloatに変換できない場合、シグナルを発行します。

controller.py
from PySide6.QtCore import QObject, Signal, Slot
from models.model import Model

class Controller(QObject):
    # Viewへ入力値が不正であることを通知するSignal
    invalid_left_value = Signal()
    invalid_right_value = Signal()

    def __init__(self, model :Model):
        super().__init__()
        self._model = model

    def change_left_value(self, value: str):
        try:
            self._model.left_value = float(value)
        except:
            self._model.left_value = float(0)
            self.invalid_left_value.emit()

    def change_right_value(self, value: str):
        try:
            self._model.right_value = float(value)
        except:
            self._model.right_value = float(0)
            self.invalid_right_value.emit()

    # 加算ボタンクリック時に実行
    @Slot()
    def add(self):
        self._model.add()

手順6:View実装

ViewではGUI部分が発行するシグナルや、Model, Controllerが発行するシグナルを紐づけします。

QlineEditのtextChangedシグナルは数値を1桁入力するたびに発行されてしまうため、editingFinishingシグナルを使用しています。

数字でないものが入力されたら、入力をクリアします。

view.py
from PySide6.QtWidgets import QMainWindow
from PySide6.QtCore import Slot
from models.model import Model
from controllers.controller import Controller
from views.Ui_Addition import Ui_MainWindow

class View(QMainWindow):
    def __init__(self, model :Model, controller :Controller):
        super().__init__()
        self._model = model
        self._controller = controller
        self._ui = Ui_MainWindow()
        self._ui.setupUi(self)

        # UiMainViewのtextEdit編集終了通知Signalを、ViewのSlotに接続
        self._ui.lineEdit_left_value.editingFinished.connect(self.change_left_value)
        self._ui.lineEdit_right_value.editingFinished.connect(self.change_right_value)

        # UiMainViewの加算ボタンクリック通知Signalを、ControllerのSlotに接続
        self._ui.button_add.clicked.connect(self._controller.add)

        # Modelの加算結果の更新通知Signalを、ViewのSlotに接続
        self._model.answer_changed.connect(self.answer_changed)

        # Controllerの不正な入力値通知Signalを、ViewのSlotに接続
        self._controller.invalid_left_value.connect(self.invalid_left_value)
        self._controller.invalid_right_value.connect(self.invalid_left_value)

    # Modelの加算結果の更新通知を受信
    @Slot(float)
    def answer_changed(self, value: float):
        self._ui.label_answer.setText(str(value))

    # UiのtextEditの編集終了通知を受信
    @Slot()
    def change_left_value(self):
        self._controller.change_left_value(self._ui.lineEdit_left_value.text())

    @Slot()
    def change_right_value(self):
        self._controller.change_right_value(self._ui.lineEdit_right_value.text())

    # Controllerの不正な入力値通知を受信
    @Slot()
    def invalid_left_value(self):
        self._ui.lineEdit_left_value.clear()

    @Slot()
    def invalid_left_value(self):
        self._ui.lineEdit_right_value.clear()

参考:Qt Designerから変換したView

最後に、GUIから自動生成したソースです。

自動生成されたヘッダコメントにあるように、このファイルを直接編集すると、GUIを作り直したときに上書きされて消し飛びます。

Ui_Addition.py
# -*- coding: utf-8 -*-

################################################################################
## Form generated from reading UI file 'Addition.ui'
##
## Created by: Qt User Interface Compiler version 6.6.0
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################

from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
    QMetaObject, QObject, QPoint, QRect,
    QSize, QTime, QUrl, Qt)
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
    QFont, QFontDatabase, QGradient, QIcon,
    QImage, QKeySequence, QLinearGradient, QPainter,
    QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QApplication, QHBoxLayout, QLabel, QLineEdit,
    QMainWindow, QMenuBar, QPushButton, QSizePolicy,
    QStatusBar, QVBoxLayout, QWidget)

class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        if not MainWindow.objectName():
            MainWindow.setObjectName(u"MainWindow")
        MainWindow.resize(300, 200)
        self.centralwidget = QWidget(MainWindow)
        self.centralwidget.setObjectName(u"centralwidget")
        self.verticalLayoutWidget = QWidget(self.centralwidget)
        self.verticalLayoutWidget.setObjectName(u"verticalLayoutWidget")
        self.verticalLayoutWidget.setGeometry(QRect(40, 30, 160, 80))
        self.verticalLayout = QVBoxLayout(self.verticalLayoutWidget)
        self.verticalLayout.setObjectName(u"verticalLayout")
        self.verticalLayout.setContentsMargins(0, 0, 0, 0)
        self.horizontalLayout = QHBoxLayout()
        self.horizontalLayout.setObjectName(u"horizontalLayout")
        self.lineEdit_left_value = QLineEdit(self.verticalLayoutWidget)
        self.lineEdit_left_value.setObjectName(u"lineEdit_left_value")

        self.horizontalLayout.addWidget(self.lineEdit_left_value)

        self.label_add = QLabel(self.verticalLayoutWidget)
        self.label_add.setObjectName(u"label_add")

        self.horizontalLayout.addWidget(self.label_add)

        self.lineEdit_right_value = QLineEdit(self.verticalLayoutWidget)
        self.lineEdit_right_value.setObjectName(u"lineEdit_right_value")

        self.horizontalLayout.addWidget(self.lineEdit_right_value)

        self.label_equal = QLabel(self.verticalLayoutWidget)
        self.label_equal.setObjectName(u"label_equal")

        self.horizontalLayout.addWidget(self.label_equal)

        self.label_answer = QLabel(self.verticalLayoutWidget)
        self.label_answer.setObjectName(u"label_answer")

        self.horizontalLayout.addWidget(self.label_answer)


        self.verticalLayout.addLayout(self.horizontalLayout)

        self.button_add = QPushButton(self.verticalLayoutWidget)
        self.button_add.setObjectName(u"button_add")

        self.verticalLayout.addWidget(self.button_add)

        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QMenuBar(MainWindow)
        self.menubar.setObjectName(u"menubar")
        self.menubar.setGeometry(QRect(0, 0, 300, 22))
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QStatusBar(MainWindow)
        self.statusbar.setObjectName(u"statusbar")
        MainWindow.setStatusBar(self.statusbar)

        self.retranslateUi(MainWindow)

        QMetaObject.connectSlotsByName(MainWindow)
    # setupUi

    def retranslateUi(self, MainWindow):
        MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"MainWindow", None))
        self.label_add.setText(QCoreApplication.translate("MainWindow", u"+", None))
        self.label_equal.setText(QCoreApplication.translate("MainWindow", u"=", None))
        self.label_answer.setText("")
        self.button_add.setText(QCoreApplication.translate("MainWindow", u"\u52a0\u7b97", None))
    # retranslateUi

おわりに

PySide6を使ったGUIプログラムのひな型でした。

Qtのフレームワーク部分が結構むずかしく、PySideの情報も少なくてなかなか苦労したのでまとめてみました。

GUIを作る仕組みは他にもいろいろあるので、Tkinterだとか、pyscriptを使ってブラウザを画面にするようなものもかじってみたいですね。

参考資料

このstackoverflowの記事がとても参考になりました。

21
16
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
21
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?