PyQt4 のスロットを関数名で対応付ける

Qt Designer を使用してウィンドウやフォームを作成すると .ui という XML ファイルが作成されます。そして pyuic4 というコマンドを使用して .ui ファイルから uic モジュール(Python モジュール)へ変換することができます。

ここで生成された uic モジュールの中身を覗いていて QMetaObject.connectSlotsByName (QObject) というメソッドが呼ばれていること気付きました。このメソッドを呼び出すことで、

def on_<object name>_<signal name>(<signal parameters>):

命名規則に従ってオブジェクトのシグナルに対するスロットを定義することができます。この命名規則に従う限り、個別にオブジェクトのシグナルとスロットを connect() する必要はありません。例えば、eric4 エディタでは、この仕組みを利用して ui ファイルからシグナルに対するスロットを自動生成する機能があったります。

それでは、実際に小さなサンプルを作成して動作を確認してみます。 *1 *2

#!/usr/env/bin python
# -*- coding: utf-8 -*-

import sys
import os
from PyQt4 import QtCore, QtGui

class Dialog(QtGui.QDialog):

    def __init__(self, parent=None):
        super(Dialog, self).__init__(parent)
        self.sample_button = QtGui.QPushButton(self)
        self.sample_button.setObjectName("sample_button")
        self.sample_button.setText("&click me")
        self.spinbox = QtGui.QSpinBox(self)
        self.spinbox.setObjectName("spinbox")
        self.spinbox.setPrefix("number: ")
        self.spinbox.setRange(1,10)

        hbox = QtGui.QHBoxLayout(self)
        hbox.addWidget(self.sample_button)
        hbox.addWidget(self.spinbox)
        self.setLayout(hbox)
        self.setWindowTitle("Connecting Slots By Name Sample")

        QtCore.QMetaObject.connectSlotsByName(self)

    @QtCore.pyqtSlot()
    def on_sample_button_clicked(self):
        print "sample button is clicked"
    
    @QtCore.pyqtSlot(int, name="on_spinbox_valueChanged")
    def spin_int_value(self, i):
        print "passed int value:", i
    
    @QtCore.pyqtSlot(str, name="on_spinbox_valueChanged")
    def spin_qstring_value(self, s):
        print "passed qstring value:", s

def main():
    app = QtGui.QApplication(sys.argv)
    dialog = Dialog()
    dialog.show()
    sys.exit(app.exec_())

if __name__ == "__main__":
    main()

実行結果。

これはボタンをクリックしたり、スピンボックスの操作をするとコンソールにメッセージが出力される簡単なアプリケーションになります。

sample button is clicked
passed qstring value: number: 2
passed int value: 2
passed qstring value: number: 3
passed int value: 3

connectSlotsByName() は引数で指定したオブジェクトとその全サブクラスを再帰的に探すので、QPushButton 等のウィジェットに self を渡して、そのウィジェットのオブジェクト名を setObjectName() で設定する必要があることに注意してください。

    self.sample_button = QtGui.QPushButton(self)
    self.sample_button.setObjectName("sample_button")
... (snip)
    QtCore.QMetaObject.connectSlotsByName(self)

用途によっては便利な状況もあるかもしれませんが、この方法はデバッグが難しくなることを認識しておいた方が良いです。対象のウィジェットが connectSlotsByName() のサブクラスになっているか、setObjectName() で指定したオブジェクト名と命名規則に従った関数名が一致しているかを確認する必要があるからです。例えば、オブジェクト名をタイプミスした場合にもエラーが発生しないので実際にウィジェットを動作させないと気付かないこともあるかもしれません。

比較対象として、このサンプルと全く同じ動作をするようにシグナルとスロットを connect() するサンプルを作成してみました。 *3

#!/usr/env/bin python
# -*- coding: utf-8 -*-

import sys
import os
from PyQt4 import QtCore, QtGui

class Dialog(QtGui.QDialog):

    def __init__(self, parent=None):
        super(Dialog, self).__init__(parent)
        self.sample_button = QtGui.QPushButton("&click me")
        self.spinbox = QtGui.QSpinBox()
        self.spinbox.setPrefix("number: ")
        self.spinbox.setRange(1,10)

        hbox = QtGui.QHBoxLayout(self)
        hbox.addWidget(self.sample_button)
        hbox.addWidget(self.spinbox)
        self.setLayout(hbox)
        self.setWindowTitle("Explicitly Connecting Slots Sample")

        self.sample_button.clicked.connect(self.on_sample_button_clicked)
        self.spinbox.valueChanged.connect(self.spin_int_value)
        QtCore.QObject.connect(self.spinbox, 
                               QtCore.SIGNAL("valueChanged(QString)"),
                               self.spin_qstring_value)

    @QtCore.pyqtSlot()
    def on_sample_button_clicked(self):
        print "sample button is clicked"

    @QtCore.pyqtSlot(int)
    def spin_int_value(self, i):
        print "passed int value:", i

    @QtCore.pyqtSignature("QString")
    def spin_qstring_value(self, s):
        print "passed qstring value:", s

def main():
    app = QtGui.QApplication(sys.argv)
    dialog = Dialog()
    dialog.show()
    sys.exit(app.exec_())

if __name__ == "__main__":
    main()

個人的にはデバッグの容易さを考慮して明示的に connect() でシグナル/スロットの対応付けを行う方が好きです。とはいえ、ビルトインのお決まりの処理に対して多くの connect() を書くのも、、、という気持ちも分かります。ビルトインのシグナルに対応するスロットは connectSlotsByName() を、ユーザ定義のシグナルは明示的に connect() で行うといった双方のメリットを組み合わせた方法も良いかもしれませんね。

2010/9/19 追記

上記サンプルではオブジェクト名を setObjectName() で定義しています。

    self.sample_button = QtGui.QPushButton(self)
    self.sample_button.setObjectName("sample_button")

setXxx() で定義する内容はインスタンス化するときにキーワード引数で定義できます。例えば、オブジェクト名の定義は以下になります。

    self.sample_button = QtGui.QPushButton(self,
                                           objectName="sample button")

人によっては、もともとの処理のように setObjectName() を使用して1行ずつ書く方が可読性が良いという見方もあるでしょう。どちらが良いと言うわけではないですが、私なら動的に設定したいのではなく初期値を設定したいという意図を表す意味で後者の方を選択するかな。

リファレンス:

*1:Connecting Slots By Name

*2:python - Problem in understanding connectSlotsByName() in pyqt? - Stack Overflow

*3: valueChanged(QString) を新スタイルでオーバーロードする方法が分からなかったので新スタイルと旧スタイルの記述が混在してます(T T)