PyQt4 のシグナル/スロットの新旧スタイルの違いを比較するサンプル

wikipedia:Qt ではオブジェクト間通信、具体的には「ボタンをクリックした」「ラベルの表示を変更する」といったオブジェクトやウィジェットの状態の変更を通知する仕組みにシグナルとスロットを使用します。シグナルとスロットの詳細はここ *1 を参照してください。

PyQt4 を使用してシグナルとスロットを使用する場合、2つのスタイルがあることに気付きました。既存のチュートリアル等では、旧スタイルのシグナルとスロット *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.directory = QtGui.QLineEdit("./")
        self.label = QtGui.QLabel("file num: 0")
        self.button = QtGui.QPushButton("&search")
        self.connect(self.button, QtCore.SIGNAL("clicked()"), self.search_files)
        self.connect(self, QtCore.SIGNAL("file_num(ulong)"), self.update_status)
        
        hbox = QtGui.QHBoxLayout()
        hbox.addWidget(self.directory)
        hbox.addWidget(self.button)
        hbox.addWidget(self.label)
        self.setLayout(hbox)
        self.setWindowTitle("Old-style Signal and Slot Sample")

    @QtCore.pyqtSignature("")
    def search_files(self):
        path = str(self.directory.text())
        if not os.access(path, os.R_OK):
            return
        file_num = 0
        for root, dirs, files in os.walk(path):
            file_num += len(files)
        self.emit(QtCore.SIGNAL("file_num(ulong)"), file_num)

    @QtCore.pyqtSignature("ulong")
    def update_status(self, file_num):
        self.label.setText("file num: %d" % file_num)

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

if __name__ == "__main__":
    main()

実行結果。

旧スタイルのシグナルは QtCore.SIGNAL() に文字列リテラルで指定し、その引数は C++ の型(このサンプルでは ulong を指定)で指定しなければなりません。

    self.connect(self.button, QtCore.SIGNAL("clicked()"), self.search_files)
    self.connect(self, QtCore.SIGNAL("file_num(ulong)"), self.update_status)
... (snip)
    self.emit(QtCore.SIGNAL("file_num(ulong)"), file_num)

そして connect() メソッドでシグナルとスロットのコネクションを作成し emit() メソッドでシグナルを送ります。このサンプルでは、検索ボタンが押下されたという clicked() シグナルが発生したときに search_files() メソッドを呼び出します。そして、search_files() メソッド内の os.walk() で再帰的に辿ったディレクトリ内のファイル数をスロットである update_status() へ通知します。最終的にダイアログの file_num が更新されます。

ここで、旧スタイルには以下の問題があります。

  • シグナルの引数に C++ の型を指定する必要がある
  • シグナル名や引数情報をタイプミスしても connect()/emit() で例外が発生しないのでエラーが混入し易い
  • 冗長である
  • Pythonic ではない


互換性のために旧スタイルもサポートされていますが PyQt 4.5 以上ならば新スタイル *3 を使用する方が良さそうです。

新スタイルでは QtCore.pyqtSignal() を使用して新たなシグナルを定義します。引数の型は Python の型か C++ の型名の文字列リテラルのどちらでも指定することができます。

    sig_status = QtCore.pyqtSignal(long)

ステータス情報に関するシグナル sig_status を使用してスロットを作成します。

    self.button.clicked.connect(self.search_files)
    self.sig_status.connect(self.update_status)
... (snip)
    self.sig_status.emit(file_num)

ここで QtGui.QPushButton の親クラスの QtGui.QAbstractButton クラスで4つの Qt シグナル(pressed(), released(), clicked(), toggled())が提供されているのでクリックシグナルを定義しなくても self.button.clicked に connect() します。新スタイルの connect()/emit() メソッドの引数は旧スタイルのそれらとは違うことに注意してください。

スロットに対するデコレータも QtCore.pyqtSignature() から QtCore.pyqtSlot() に変更されています。デコレータを使用すると、省メモリや若干パフォーマンスが良いようです。

    @QtCore.pyqtSlot(long)
    def update_status(self, file_num):
        self.label.setText("file num: %d" % file_num)

最終的に修正したソースは以下になります。

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

import sys
import os
from PyQt4 import QtCore, QtGui

class Dialog(QtGui.QDialog):

    sig_status = QtCore.pyqtSignal(long)

    def __init__(self, parent=None):
        super(Dialog, self).__init__(parent)
        self.directory = QtGui.QLineEdit("./")
        self.label = QtGui.QLabel("file num: 0")
        self.button = QtGui.QPushButton("&search")
        self.button.clicked.connect(self.search_files)
        self.sig_status.connect(self.update_status)
        
        hbox = QtGui.QHBoxLayout()
        hbox.addWidget(self.directory)
        hbox.addWidget(self.button)
        hbox.addWidget(self.label)
        self.setLayout(hbox)
        self.setWindowTitle("New-style Signal and Slot Sample")

    @QtCore.pyqtSlot()
    def search_files(self):
        path = str(self.directory.text())
        if not os.access(path, os.R_OK):
            return
        file_num = 0
        for root, dirs, files in os.walk(path):
            file_num += len(files)
        self.sig_status.emit(file_num)

    @QtCore.pyqtSlot(long)
    def update_status(self, file_num):
        self.label.setText("file num: %d" % file_num)

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

if __name__ == "__main__":
    main()

実行結果。

見た目も同じですが、旧スタイルと同じように動作します。