PyQt

【PyQt】カスタムsignalとslotの作成方法

【PyQt】カスタムsignalとslotの作成方法

PythonのGUIツールキットであるPyQtでイベントを処理するための仕組みであるsignalとslotにおいて、カスタムのsignalとslotを作成する方法について解説します。また、signalとslotのオーバーロードについても説明します。

PyQtにおけるsignalとslot

PyQtでは、画面操作のイベント処理にsignalとslotという概念を使用します。signalとslotの基本が分からない人は「signalとslotの基本」でまとめていますので興味があれば参考にしてください。

signalとslotは、任意の二つのPyQtオブジェクトが疎結合のまま連携できるような仕組みとなっていて、開発者が任意のカスタムsignalやslotを開発していくことができます。

本記事では、カスタムsignalとslotを作成する方法について紹介していきます。

カスタムsignalとslotの作成方法

開発画面の概要

カスタムsignalとslotの作成方法を見ていくために、以下のように2つの画面間でデータを連携して動作するプログラム例で考えていきましょう。

PyQt カスタムsignalとslot

操作の手順は以下のような形です。

  1. 親画面で「変更」ボタンをクリックする。
  2. 別フォームで文字列の入力画面が表示されるので、任意の文字列を入力して「登録」ボタンをクリックする。
  3. 別フォームは閉じて、親画面の文字列が別フォームで入力された文字列で変換される。

上記のような手続きを実施する場合には、親画面と別フォーム画面でデータを共有する必要があります。この時にカスタムのsignalやslotを用意すると柔軟な対応ができます。

プログラム実装

上記で説明した画面を実現するプログラムの実装について紹介し、内容を説明していきます。

なお、以降のプログラムにおいて画面全体のプログラム構造は「QWidgetを継承した画面開発のテンプレート」をベースにしていますので全体のプログラム構造の説明はしません。signalとslotのプログラム部分を中心に説明をしていきます。

import sys

from PyQt6 import QtCore as qtc
from PyQt6 import QtGui as qtg
from PyQt6 import QtWidgets as qtw


class FormWindow(qtw.QWidget):
    """フォームウィンドウ"""

    # strのシグナルを定義
    submitted = qtc.pyqtSignal(str)

    def __init__(self):
        """コンストラクタ"""
        super().__init__()
        # 画面タイトルの設定
        self.setWindowTitle("フォーム")
        # レイアウトの設定
        layout = qtw.QHBoxLayout()
        self.setLayout(layout)

        # ウィジェットの生成
        self.line_edit = qtw.QLineEdit()
        self.submit_button = qtw.QPushButton("登録", clicked=self.on_submit)

        # レイアウト配置
        layout.addWidget(self.line_edit)
        layout.addWidget(self.submit_button)

    def on_submit(self):
        """登録ボタン用スロット"""
        # ボタン押下時にline_editに入力された値を送信
        self.submitted.emit(self.line_edit.text())
        # フォームウィンドウを閉じる
        self.close()


class MainWindow(qtw.QWidget):
    """メインウィンドウ"""

    def __init__(self):
        super().__init__()
        # 画面タイトルの設定
        self.setWindowTitle("カスタムスロットの作成")
        # 画面サイズの設定
        self.resize(320, 100)

        # メインレイアウトの設定
        main_layout = qtw.QVBoxLayout()
        self.setLayout(main_layout)

        # ウィジェットの生成
        self.label = qtw.QLabel("文字列表示欄")
        self.change_button = qtw.QPushButton("変更")
        self.change_button.clicked.connect(self.on_change)

        # レイアウト配置
        main_layout.addWidget(self.label)
        main_layout.addWidget(self.change_button)
        main_layout.addStretch()

        # 画面表示
        self.show()

    def on_change(self):
        """フォーム呼び出し用スロット"""
        # フォームウィンドウをインスタンス化
        self.form_window = FormWindow()
        # フォームウィンドウのシグナルをラベルのsetTextに接続
        self.form_window.submitted.connect(self.label.setText)
        # フォームウィンドウを表示
        self.form_window.show()


def main():
    """メイン関数"""
    app = qtw.QApplication(sys.argv)
    mv = MainWindow()
    sys.exit(app.exec())


if __name__ == "__main__":
    main()

以降ではプログラムのポイントについて紹介していきます。

フォームウィンドウの実装

class FormWindow(qtw.QWidget):
    """フォームウィンドウ"""

    # strのシグナルを定義
    submitted = qtc.pyqtSignal(str)

FormWindowクラスは、MainWindowから呼び出される別のフォームウィンドウです。

このフォームウィンドウの上記部分ではカスタムsignalとしてsubmittedを定義しています。signalはpyqtSignalを使用して生成でき、引数には発する型を指定します。上記の例では、strを発するシグナルを用意しているということになります。

    def __init__(self):
        """コンストラクタ"""
        # ===== 省略

        # ウィジェットの生成
        self.line_edit = qtw.QLineEdit()
        self.submit_button = qtw.QPushButton("登録", clicked=self.on_submit)

        # ===== 省略

コンストラクタでは、QLineEditで入力欄(line_edit)を用意し、QPushButtonで登録用ボタン(submit_button)が用意されています。このボタンがクリックされたときのclicked signalに対するslotとして接続しているのがon_submitメソッドです。

    def on_submit(self):
        """登録ボタン用スロット"""
        # ボタン押下時にline_editに入力された値を送信
        self.submitted.emit(self.line_edit.text())
        # フォームウィンドウを閉じる
        self.close()

on_submit内では、以下のようなことを実行しています。

  1. submitted.emitでline_editに入力された値を送信
  2. フォームウィンドウを閉じる(self.close())

このように、カスタムsignalとして用意しておいたsubmitted.emitを使うことでsignalを発するようになります。

メインウィンドウの実装

class MainWindow(qtw.QWidget):
    """メインウィンドウ"""

    def __init__(self):
        super().__init__()
        # ===== 省略

        # ウィジェットの生成
        self.label = qtw.QLabel("文字列表示欄")
        self.change_button = qtw.QPushButton("変更")
        self.change_button.clicked.connect(self.on_change)

        # ===== 省略
        # 画面表示
        self.show()

メインウィンドウでは、文字列を表示するラベル(label)と、変更ボタン(change_button)が用意されています。そして、change_buttonがクリックされるとslotであるon_changeメソッドに接続されます。

    def on_change(self):
        """フォーム呼び出し用スロット"""
        # フォームウィンドウをインスタンス化
        self.form_window = FormWindow()
        # フォームウィンドウのシグナルをラベルのsetTextに接続
        self.form_window.submitted.connect(self.label.setText)
        # フォームウィンドウを表示
        self.form_window.show()

on_changeメソッドでは以下のようなことを実装しています。

  1. フォームウィンドウをインスタンス化する。
  2. フォームウィンドウのsubmitted signalをラベルのsetTextに接続する。
  3. フォームウィンドウを表示する。

このようにしてメインウィンドウ側からフォームウィンドウをインスタンス化して表示してあげます。この時に、FormWindowでは変更文字列を返すsubmitted signalというカスタムsignalを作ったので、これをラベルの文字文字列を変更できるsetTextへ接続します。

このようにカスタムsignalを作って連携することが可能になります。

signalとslotのオーバーロード

上記で紹介してきたようにカスタムのsignalとslotが作成できるということがお分かりいただけたかと思います。なお、signalとslotをオーバーロードすることも可能になっています。本以降では、signalとslotのオーバーロードの例について紹介していきます。

Note

オーバーロードとオーバーライドの違い

少し混乱しやすいオーバーロードとオーバーライドですが、オーバーロードは引数の異なる関数(メソッド)を複数定義することです。一方でオーバーライドは、親クラスのメソッドを子クラスで再定義して上書きすることを言います。

プログラム実装

カスタムsignalとslotの実装で使用したメインウィンドウとフォームウィンドウの実装において、フォームウィンドウで「文字列が入力された」場合と「数値が入力された」場合でsignalとslotの挙動を変えるような例について見てみましょう。

import sys

from PyQt6 import QtCore as qtc
from PyQt6 import QtGui as qtg
from PyQt6 import QtWidgets as qtw


class FormWindow(qtw.QWidget):
    """フォームウィンドウ"""

    # シグナルを定義(オーバーロード)
    submitted = qtc.pyqtSignal([str], [int, str])

    def __init__(self):
        """コンストラクタ"""
        super().__init__()
        # 画面タイトルの設定
        self.setWindowTitle("フォーム")
        # レイアウトの設定
        layout = qtw.QHBoxLayout()
        self.setLayout(layout)

        # ウィジェットの生成
        self.line_edit = qtw.QLineEdit()
        self.submit_button = qtw.QPushButton("登録", clicked=self.on_submit)

        # レイアウト配置
        layout.addWidget(self.line_edit)
        layout.addWidget(self.submit_button)

    def on_submit(self):
        """登録ボタン用スロット"""
        text = self.line_edit.text()
        if text.isdigit():
            # 数値の場合は数値と文字列を返却する
            self.submitted[int, str].emit(int(text), text)
        else:
            # その他の場合は文字列のみ返却する
            self.submitted[str].emit(text)

        # フォームウィンドウを閉じる
        self.close()


class MainWindow(qtw.QWidget):
    """メインウィンドウ"""

    def __init__(self):
        super().__init__()
        # 画面タイトルの設定
        self.setWindowTitle("カスタムスロットの作成")
        # 画面サイズの設定
        self.resize(320, 100)

        # メインレイアウトの設定
        main_layout = qtw.QVBoxLayout()
        self.setLayout(main_layout)

        # ウィジェットの生成
        self.label = qtw.QLabel("文字列表示欄")
        self.change_button = qtw.QPushButton("変更")
        self.change_button.clicked.connect(self.on_change)

        # レイアウト配置
        main_layout.addWidget(self.label)
        main_layout.addWidget(self.change_button)
        main_layout.addStretch()

        # 画面表示
        self.show()

    def on_change(self):
        """フォーム呼び出し用スロット"""
        # フォームウィンドウをインスタンス化
        self.form_window = FormWindow()
        # フォームウィンドウのシグナルをラベルのsetTextに接続
        self.form_window.submitted[str].connect(self.on_submitted_str)
        self.form_window.submitted[int, str].connect(self.on_submitted_int_and_str)
        # フォームウィンドウを表示
        self.form_window.show()

    def on_submitted_str(self, text):
        """slot (strのsignal用)

        Args:
            text: 文字列
        """
        self.label.setText(text)

    def on_submitted_int_and_str(self, value, text):
        """slot (intとstrのsignal用)

        Args:
            value: 数値
            text: 文字列
        """
        disp_text = f"value:{value}, text:{text}"
        self.label.setText(disp_text)


def main():
    """メイン関数"""
    app = qtw.QApplication(sys.argv)
    mv = MainWindow()
    sys.exit(app.exec())


if __name__ == "__main__":
    main()

フォームウィンドウで文字列が入力された場合の実行結果

signalとslotのオーバーロード
signalとslotのオーバーロード

フォームウィンドウで数値が入力された場合の実行結果

signalとslotのオーバーロード
signalとslotのオーバーロード

以降で説明をしますが、ポイントを中心に説明していきます。

class FormWindow(qtw.QWidget):
    """フォームウィンドウ"""

    # シグナルを定義(オーバーロード)
    submitted = qtc.pyqtSignal([str], [int, str])

まず、シグナルを定義する際に上記のようにすることで[str]のみを送信するシグナルと[int, str]を送信シグナルを用意することができます。

    def on_submit(self):
        """登録ボタン用スロット"""
        text = self.line_edit.text()
        if text.isdigit():
            # 数値の場合は数値と文字列を返却する
            self.submitted[int, str].emit(int(text), text)
        else:
            # その他の場合は文字列のみ返却する
            self.submitted[str].emit(text)

        # フォームウィンドウを閉じる
        self.close()

signalの内容を定義している部分では、上記のように入力された値によって返却する内容を変更しています。入力文字が数値(isdgit()==True)の場合は、数値と文字列を送信(emit)しています。一方で数値でない文字列の場合は、textのみを送信(emit)しています。

    def on_change(self):
        """フォーム呼び出し用スロット"""
        # フォームウィンドウをインスタンス化
        self.form_window = FormWindow()
        # フォームウィンドウのシグナルをラベルのsetTextに接続
        self.form_window.submitted[str].connect(self.on_submitted_str)
        self.form_window.submitted[int, str].connect(self.on_submitted_int_and_str)
        # フォームウィンドウを表示
        self.form_window.show()

次にメインウィンドウのslotのon_changeについてです。こちらではsubmitted[str]のsignalの場合と、submitted[int, str]のsignalとで接続するメソッドを変更しています。

    def on_submitted_str(self, text):
        """slot (strのsignal用)

        Args:
            text: 文字列
        """
        self.label.setText(text)

    def on_submitted_int_and_str(self, value, text):
        """slot (intとstrのsignal用)

        Args:
            value: 数値
            text: 文字列
        """
        disp_text = f"value:{value}, text:{text}"
        self.label.setText(disp_text)

上記がslotメソッドの部分です。[str]signal用のslotと[int, str]signal用のslotとしてそれぞれのメソッドを用意しています。

strのみの場合はそのままラベルに設定し、int, strが来る場合は少し加工した文字列にしてラベルを設定しています。このようにsignalとslotでオーバーロードすることもできるようになっています。

まとめ

PythonのGUIツールキットであるPyQtでイベントを処理するための仕組みであるsignalとslotにおいて、カスタムのsignalとslotを作成する方法について解説しました。また、signalとslotのオーバーロードについても説明しています。

カスタムのsignalとslotについて理解できると実装の幅が広がりますので、是非色々と試して使ってみてもらえるとよいかと思います。