PyQt

【PyQt】signalとslotの基本

【PyQt】signalとslotの基本

PythonのGUIツールキットであるPyQtでイベントを処理するための仕組みであるsignalとslotの基本について解説します。

PyQtにおけるsignalとslotの概要

PyQtでは、画面操作のイベント処理にsignalslotという概念を使用します。

signal(シグナル)は、PyQtのオブジェクトの特別なメソッドで操作に対するイベントを出力するようなものになっています。ここでイベントと言っているのは、ユーザーがボタンを押すなどの操作をしたときやタイムアウト、非同期処理の呼び出し等があります。

slot(スロット)は、signalを受け取って動作するメソッドの事を言います。

例えば、以下の非常にシンプルな画面を考えてみましょう。「終了」というQPushButtonがあるのみの画面でボタンをクリックすると画面自体が閉じます。

この時にボタンをクリックすると「clicked」というsignalが発生します。このclicked signalを親画面(self)のslotであるcloseメソッドに接続します。この接続の事を「connect」と言います。

signalとslotの基本イメージ

上記は非常に簡単な例ですが、例えば「テキストボックスでテキストが入力されて変更されたときのsignal」等、それぞれのウィジェットに様々なsignalがあり、そのsignalを接続するslotのメソッドを決めていくことで画面の挙動を制御していきます。

本記事では、signal(シグナル)とslot(スロット)の基本的な使用方法について解説していきます。

signalとslotの使用方法

基本的な使い方

signalとslotの接続は正直いくらでもパターンが考えられるため、基本的な使い方の例をいくつか紹介することでsignalとslotの使い方のイメージを持ってもらおうと思います。

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

ウィジェットのsignalをslotに接続する

本記事の冒頭で紹介したボタンのclicked signalを親画面のclose slotに接続する例について、実際のコードを見ていきましょう。

import sys

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


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

    def __init__(self):
        super().__init__()
        # 画面タイトルの設定
        self.setWindowTitle("signalとslotの基本")
        # 画面サイズの設定
        self.resize(320, 100)

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

        # ボタンウィジェットの用意
        self.quitbutton = qtw.QPushButton("終了")
        # clickedのシグナルをcloseのスロットに接続(connect)
        self.quitbutton.clicked.connect(self.close)
  
        # ウィジェットをレイアウトに追加
        main_layout.addWidget(self.quitbutton)

        # 画面表示
        self.show()


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


if __name__ == "__main__":
    main()

【表示結果】

pyqt signalとslotの基本

上記画面では「終了」ボタンをクリックすると画面全体がクローズします。その動作を定義しているのが以下の部分です。

        # ボタンウィジェットの用意
        self.quitbutton = qtw.QPushButton("終了")
        # clickedのシグナルをcloseのスロットに接続(connect)
        self.quitbutton.clicked.connect(self.close)

まず、QPushButtonを用意します。self.quitbutton.clicked.connectの部分は「ボタンをクリックされたときのsignalを接続する」という意味です。その接続先が()内に指定されます。今回はself.closeとなっていますが、selfは画面の親クラスのMainWindowの事なので、MainWindowのcloseメソッドが呼ばれることで画面が閉じるわけです。

ここで渡しているのは関数やメソッド自体である事に注意してください。self.close()とする場合は、関数の実行を意味しますが()がないので、関数自体を渡していることになります。この考え方はPythonのデコレータ等でもベースになる高階関数の考え方を勉強していただければと思います。(参考:デコレータ(decorator)の基本的な使い方

また、もっとシンプルに以下のようにボタン定義時に指定することも可能です。

        # ボタンウィジェットの用意時に、signalとslotを設定
        self.quitbutton = qtw.QPushButton("終了", clicked=self.close)

PyQtには多くのウィジェットがあり、上記の例のclickedの他にもウィジェットごとにsignalが定義されています。slotメソッドは、上記で紹介したようなPyQtのウィジェットがデフォルトで提供するものも使えますし、もちろんユーザー独自で実装したメソッドを使用することも可能です。

オブジェクトのデータを受け渡す

signalを使うと、signalを発したオブジェクトのデータも一緒に渡すことができます。以下の例で見てみましょう。

import sys

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


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

    def __init__(self):
        super().__init__()
        # 画面タイトルの設定
        self.setWindowTitle("signalとslotの基本")
        # 画面サイズの設定
        self.resize(320, 100)

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

        # ウィジェットの用意
        self.line_edit1 = qtw.QLineEdit()
        self.line_edit2 = qtw.QLineEdit()
        # signalとslotの設定
        self.line_edit1.textChanged.connect(self.line_edit2.setText)
        
        # ウィジェットをレイアウトに追加
        main_layout.addWidget(self.line_edit1)
        main_layout.addWidget(self.line_edit2)
        main_layout.addStretch()
        
        # 画面表示
        self.show()


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


if __name__ == "__main__":
    main()

【表示結果】上のボックスに「abcde」と入力した後の表示

pyqt signalとslotの基本

この例では、上のline_edit1の方でテキストが変わったら、line_edit2の方のQLineEditに同じ文字列を設定する例になっています。

        # ウィジェットの用意
        self.line_edit1 = qtw.QLineEdit()
        self.line_edit2 = qtw.QLineEdit()
        # signalとslotの設定
        self.line_edit1.textChanged.connect(self.line_edit2.setText)

設定している部分は上記の部分です。line_edit1のtextChanged signalを、line_edit2のsetTextメソッドに接続しています。

この時に、特に明示的にデータを渡すことを指定しているわけではないのですが、setTextメソッドにはline_edit1に入力されている文字列が渡されています。setTextは渡された文字列を設定しているためline_edit2の方に同時に表示がされるわけです。

Pythonの関数へ接続する

上記で見てきた例はPyQtオブジェクト間の接続でしたが、接続するslotにはPythonの呼び出し可能な関数も指定できます。例えば、以下の例は、Pythonの「print」にsignalを接続している例です。

import sys

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


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

    def __init__(self):
        super().__init__()
        # 画面タイトルの設定
        self.setWindowTitle("signalとslotの基本")
        # 画面サイズの設定
        self.resize(320, 100)

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

        # ウィジェットの用意
        self.line_edit1 = qtw.QLineEdit()
        # signalとslotの設定 (printでコンソールへ出力)
        self.line_edit1.textChanged.connect(print)
        self.line_edit1.editingFinished.connect(lambda: print("編集終了"))

        # ウィジェットをレイアウトに追加
        main_layout.addWidget(self.line_edit1)
        main_layout.addStretch()

        # 画面表示
        self.show()


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


if __name__ == "__main__":
    main()

【表示結果】表示後に「aiueo」と入力した後に「Enter」を押した場合

【コンソール表示結果】表示後に「aiueo」と入力した後に「Enter」を押した場合
a
ai
aiu
aiue
aiueo
編集終了

この例では、以下の部分でPythonのprintに接続を行っています。

        # ウィジェットの用意
        self.line_edit1 = qtw.QLineEdit()
        # signalとslotの設定 (printでコンソールへ出力)
        self.line_edit1.textChanged.connect(print)
        self.line_edit1.editingFinished.connect(lambda: print("編集終了"))

line_edit1のtextChenged signalが発生したらprintに接続しているので、キーボードで「aiueo」と入力すると1文字入力するごとにsignalが発生し、printでコンソールに表示がされています。

また、Enterを押すとeditingFinished signalが発生します。その際には「編集終了」と表示できるよう用に少し工夫してlambdaを使ったprintの指定を行っています。

今回はprintを例にしましたが、他のPythonで呼び出し可能な関数をslotと指定することも可能です。

signalとslotの制約事項:引数の数

signalとslotの制約事項として引数の数には注意が必要です。

slotの引数が多い場合(エラー)

slotのメソッドの引数がsignalが渡すデータよりも多くなっているような以下のケースではエラーとなってしまいます。

import sys

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


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

    def __init__(self):
        super().__init__()
        # 画面タイトルの設定
        self.setWindowTitle("signalとslotの基本: 制限")
        # 画面サイズの設定
        self.resize(640, 360)

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

        # ボタンウィジェットの用意
        self.quitbutton = qtw.QPushButton("ボタン")
        # clickedのシグナルをcloseのスロットに接続(connect)
        self.quitbutton.clicked.connect(self.func_args)
  
        # ウィジェットをレイアウトに追加
        main_layout.addWidget(self.quitbutton)
        main_layout.addStretch()

        # 画面表示
        self.show()

    def func_args(self, arg1, arg2, arg3):
        """スロット関数

        Args:
            arg1: 引数1
            arg2: 引数2
            arg3: 引数3
        """
        print("func_args")


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


if __name__ == "__main__":
    main()

【表示結果】

【コンソール表示結果】「ボタン」を押した後
TypeError: MainWindow.func_args() missing 2 required positional arguments: 'arg2' and 'arg3'

上記のプログラムでは、slotのメソッドとして以下のようなメソッドを用意しています。

    def func_args(self, arg1, arg2, arg3):
        """スロット

        Args:
            arg1: 引数1
            arg2: 引数2
            arg3: 引数3
        """
        print("func_args")

このslotメソッドでは、引数としてarg1, arg2, arg3の3つの引数が必要です。ボタンを押したときには、signalは1つのデータを渡すためarg1の引数に設定されます。しかし、arg2, arg3に渡されるデータががないということでエラーとなっているわけです。

slotの引数が少ない場合(OK)

では、signalが渡すデータの数よりもslotの引数が少ない場合はどうなるでしょう。以下の例では、slotメソッドは引数を受け取らないパターンです。

import sys

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


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

    def __init__(self):
        super().__init__()
        # 画面タイトルの設定
        self.setWindowTitle("signalとslotの基本: 制限")
        # 画面サイズの設定
        self.resize(320, 100)

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

        # ボタンウィジェットの用意
        self.quitbutton = qtw.QPushButton("ボタン")
        # clickedのシグナルをcloseのスロットに接続(connect)
        self.quitbutton.clicked.connect(self.func_args)

        # ウィジェットをレイアウトに追加
        main_layout.addWidget(self.quitbutton)
        main_layout.addStretch()

        # 画面表示
        self.show()

    def func_args(self):
        """スロット"""
        print("func_args")


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


if __name__ == "__main__":
    main()

【表示結果】

【コンソール表示結果】「ボタン」を押した後
func_args

上記例ではボタンを押したときにエラーは発生しません。この例でのslot部分は以下になります。

    def func_args(self):
        """スロット"""
        print("func_args")

ボタンをクリックすると1つデータが渡されます。このslotでは引数は受け取っていませんが問題なく動作しています。

このようにsignalが送るデータよりもslotの引数が少ない場合には余分なデータは捨てられるので問題は発生しません。

まとめ

PythonのGUIツールキットであるPyQtでイベントを処理するための仕組みであるsignalとslotの基本について解説してきました。

簡単なプログラムを使って画面間の接続やオブジェクトデータの受け渡しについて説明し、signalとslotの引数に関する制約事項についても紹介しています。

PyQtでは、画面の挙動を定義していくためにsignalとslotの理解が必要不可欠なので概念をしっかり理解してもらえたらなと思います。