Maya PySide2 / PySide チュートリアル 初学編 ⑦ – Signals & Slotsを理解する

python,qt,PySide,PySide2,Tutorial

このチュートリアルはSignals & Slotsについて学んでいきます
これまでにウィンドウを作成し、シンプルなプッシュボタンのウィジェットを追加やレイアウトの設定などをしてきました
もちろんSignals & Slots(シグナルとスロット)も定義しましたがシグナルとスロットはQt(キュート)つまり、PySideではとても重要な要素となっています
前回はサラッと説明してきましたがもう少し深堀してみましょう

Signals & Slots

Signalsとは

シグナルは、何かが起こったときにウィジェットが発する通知です
何かとは、ボタンを押すことや入力ボックスのテキストが変化すること、マウスカーソルがウィンドウに入ったり、出たりなど、様々なことです
多くのシグナルはユーザーのアクションによって発しますが絶対のルールというわけではありません
また、シグナルは、何かが起こったことを通知するだけでなく、何が起こったかについてのコンテキストを提供するデータを送信することもできます
そして、独自のカスタムシグナルを作成することもできます(これは次のチュートリアルで説明します)

Slotsとは

スロットは、Qt(キュート)がシグナルの受信機として使用する名称です
Pythonの場合はアプリケーション内の任意の関数もしくはメソッドをスロットとして使用することができます
スロットを接続するとスロットがデータを送信すれば、受信側の関数もそのデータを受信することができます
PySideの多くのウィジェットはスロットを内蔵しており、PySideのウィジェットを直接接続することができます

import sys

try:
    from PySide2.QtWidgets import *
    from PySide2.QtCore import *
except ImportError:
    from PySide.QtGui import *
    from PySide.QtCore import *


class Example(QMainWindow):
    def __init__(self, parent=None, *args, **kwargs):
        super(Example, self).__init__(parent, *args, **kwargs)

        self.initUI()

    def initUI(self):
        self.setGeometry(500, 300, 400, 270)
        self.setWindowTitle("Signals & Slots")

        button = QPushButton("Press Me!")
        button.setCheckable(True)
        button.clicked.connect(self.slotClicked)

        self.setCentralWidget(button)

    def slotClicked(self):
        print("Clicked!")


def main():
    app = QApplication.instance()
    ex = Example()
    ex.show()
    sys.exit()
    app.exec_()


main()

このシンプルなアプリケーションでは、QMainWindowにQPushButtonが中心的なウィジェットとして設定されています
まずは、このボタンをPythonのカスタムメソッドに接続してみましょう
ここでは、slotClickedという名前のシンプルなカスタムスロットを作成し、QPushButtonからのclickedシグナルを受け取ります
20210825_02
ボタンをクリックすると、ScriptEditorに "Clicked!" というテキストが表示されるはずです

データを受信する

シグナルがデータを送信して、何が起こったかについてのコンテキストを提供するということを冒頭で説明したかと思いますがclicked()メソッドも例外ではなく、ボタンのチェック状態(またはトグル状態)Boolで提供しています
void QAbstractButton::clicked(bool checked = false)

通常のボタンでは、これは常に False なので最初のスロットではこのデータを無視しました
しかし、setCheckableでボタンをチェック可能にして、その効果を確認することができます

次の例では、チェック状態を出力する2つ目のスロットを追加しています

import sys

try:
    from PySide2.QtWidgets import *
    from PySide2.QtCore import *
except ImportError:
    from PySide.QtGui import *
    from PySide.QtCore import *


class Example(QMainWindow):
    def __init__(self, parent=None, *args, **kwargs):
        super(Example, self).__init__(parent, *args, **kwargs)

        self.initUI()

    def initUI(self):
        self.setGeometry(500, 300, 400, 270)
        self.setWindowTitle("Signals & Slots")

        button = QPushButton("Press Me!")
        button.setCheckable(True)
        # button.clicked.connect(self.slotClicked)
        button.clicked.connect(lambda: self.slotToggled(button.isChecked()))

        self.setCentralWidget(button)

    def slotClicked(self):
        print("Clicked!")

    def slotToggled(self, checked):
        print(checked)


def main():
    app = QApplication.instance()
    ex = Example()
    ex.show()
    sys.exit()
    app.exec_()


main()

シグナルには好きなだけ多くのスロットを接続することができ、スロット上で同時に異なるバージョンのシグナルに反応することができます

# button.clicked.connect(self.slotClicked)

通常、上の文で問題なく機能するのですがMaya2019ではこれがつかえずlamdba関数を使って状態を取得できるようにしています

データを変数に格納する

ウィジェットの現在の状態をジェット同士や処理の結果に反映したい場合はPythonの変数に格納すると便利です
変数に格納することでオリジナルのウィジェットにアクセスすることなく、他のPython変数のように値を扱うことができ、個々の変数として格納することもできますし、辞書にも使用することもできます

ボタンのチェック済みの値を self.isCheckedという変数に格納しています。

import sys

try:
    from PySide2.QtWidgets import *
    from PySide2.QtCore import *
except ImportError:
    from PySide.QtGui import *
    from PySide.QtCore import *


class Example(QMainWindow):
    def __init__(self, parent=None, *args, **kwargs):
        super(Example, self).__init__(parent, *args, **kwargs)

        self.initUI()

    def initUI(self):
        self.setGeometry(500, 300, 400, 270)
        self.setWindowTitle("Signals & Slots")

        self.isChecked = True
        button = QPushButton("Press Me!")
        button.setCheckable(True)
        # button.clicked.connect(self.slotClicked)
        button.clicked.connect(lambda: self.slotToggled(button.isChecked()))
        button.setChecked(self.isChecked)

        self.setCentralWidget(button)

    def slotToggled(self, checked):
        self.isChecked = checked

        print(self.isChecked)


def main():
    app = QApplication.instance()
    ex = Example()
    ex.show()
    sys.exit()
    app.exec_()


main()

下の例のようにボタンのチェック済みの値を self.isCheckedという変数に格納することで別のwindowに情報を持っていくということがスムーズにできたりもします
もちろん直接UIにアクセスすることでできますが複数のウィジェットから発せられた情報をキャッチしたい場合、辞書型の変数が一つあれば済んだりもします

import sys

try:
    from PySide2.QtWidgets import *
    from PySide2.QtCore import *
except ImportError:
    from PySide.QtGui import *
    from PySide.QtCore import *


class outsideWidget(QMainWindow):
    def __init__(self, parent=None, *args, **kwargs):
        super(outsideWidget, self).__init__(parent, *args, **kwargs)

        self.initUI()

    def initUI(self):
        self.setGeometry(900, 300, 400, 270)
        self.setWindowTitle("Signals & Slots")

        self.label = QLabel("States?")
        self.setCentralWidget(self.label)


class Example(QMainWindow):
    def __init__(self, parent=None, *args, **kwargs):
        super(Example, self).__init__(parent, *args, **kwargs)

        self.__outsideWidget = None

        self.initUI()

    @property
    def outsideWidget(self):
        pass

    @outsideWidget.getter
    def outsideWidget(self):
        return self.__outsideWidget

    @outsideWidget.setter
    def outsideWidget(self, widget):
        self.__outsideWidget = widget

    def initUI(self):
        self.setGeometry(500, 300, 400, 270)
        self.setWindowTitle("Signals & Slots")

        self.isChecked = True
        button = QPushButton("Press Me!")
        button.setCheckable(True)

        button.clicked.connect(lambda: self.slotToggled(button.isChecked()))
        button.clicked.connect(self.send2OutSideWidget)
        button.setChecked(self.isChecked)

        self.setCentralWidget(button)

    def slotClicked(self):
        print("Clicked!")

    def slotToggled(self, checked):
        self.isChecked = checked

    def send2OutSideWidget(self):
        if self.__outsideWidget != None:
            self.__outsideWidget.label.setText("state: %s" % self.isChecked)


def main():
    app = QApplication.instance()
    ex1 = outsideWidget()
    ex1.show()
    ex2 = Example()
    ex2.outsideWidget = ex1
    ex2.show()
    sys.exit()
    app.exec_()


main()

20210825_03
実行後、QPushButtonを押すと片側のUIのQLabelが変化するはずです

少し話を基に戻します
ウィジェットが現在の状態を送信するシグナルを提供していない場合は、Event handler(イベントハンドラ)※1の中で直接ウィジェットから値を取得する必要があります
例えば、ここではpressed ハンドラでcheckedの状態をチェックしています
※1
対応すべき処理要求が発生した時にプログラムの流れを中断して要求された処理を実行するものです

ボタンが離されたときに released シグナルが発生しますが、チェック状態は送信されないので、代わりに .isChecked() を使用して、ハンドラ内のボタンからチェック状態を取得しています

import sys

try:
    from PySide2.QtWidgets import *
    from PySide2.QtCore import *
except ImportError:
    from PySide.QtGui import *
    from PySide.QtCore import *


class Example(QMainWindow):
    def __init__(self, parent=None, *args, **kwargs):
        super(Example, self).__init__(parent, *args, **kwargs)

        self.initUI()

    def initUI(self):
        self.setGeometry(500, 300, 400, 270)
        self.setWindowTitle("Signals & Slots")

        self.isChecked = True
        self.button = QPushButton("Press Me!")
        self.button.setCheckable(True)
        self.button.released.connect(self.released)
        self.button.setChecked(self.isChecked)

        self.setCentralWidget(self.button)

    def released(self):
        self.isChecked = self.button.isChecked()

        print(self.isChecked)


def main():
    app = QApplication.instance()
    ex = Example()
    ex.show()
    sys.exit()
    app.exec_()


main()

インターフェースの変更

QPushButton以外にもシグナルはたくさん種類があります
例えばQSliderであればvalueChangedを活用することが多いのですがこのシグナルを使えば
スタイダーを動かすことでWindowのサイズを変えるというようなことも可能になります

import sys

try:
    from PySide2.QtWidgets import *
    from PySide2.QtCore import *
except ImportError:
    from PySide.QtGui import *
    from PySide.QtCore import *


class Example(QMainWindow):
    def __init__(self):
        super(Example, self).__init__()
        self.initUI()

    def initUI(self):
        self.setGeometry(500, 300, 400, 270)
        self.setWindowTitle("change WindowSize")

        self.animation = QPropertyAnimation(self, b"geometry")
        self.animation.setDuration(400)
        self.animation.setEasingCurve(QEasingCurve.InOutQuint)

        slide = QSlider(Qt.Horizontal, self)
        slide.setMinimum(200)
        slide.setMaximum(600)
        slide.valueChanged.connect(self.changeWindowSize(slide))

    def changeWindowSize(self, slide):
        def _changeWindowSize():
            x = self.geometry().x()
            y = self.geometry().y()
            width = slide.value()
            height = self.geometry().height()
            self.animation.setEndValue(QRect(x, y, width, height))
            self.animation.start()

        return _changeWindowSize


def main():
    app = QApplication.instance()
    ex = Example()
    ex.show()
    sys.exit()
    app.exec_()


main()

20210825_04

旧スタイル

新スタイルのシグナルとスロットはPyQt v4.5から導入されました、今ではこの新スタイルがなじみ深くなっていると思いますし、旧スタイルがあることを知らない人も多くいらっしゃるかもしれません
新スタイルはself.button.clicked.connect(self.slotClicked)とPythonicな構文でした

しかし、旧スタイルはマクロ経由のシグナルと接続先のスロットをオブジェクトに伝えるという書き方だったため、pythonに馴染みにくい構文になります

import sys

try:
    from PySide2.QtWidgets import *
    from PySide2.QtCore import *
except ImportError:
    from PySide.QtGui import *
    from PySide.QtCore import *


class Example(QMainWindow):
    def __init__(self, parent=None, *args, **kwargs):
        super(Example, self).__init__(parent, *args, **kwargs)

        self.initUI()

    def initUI(self):
        self.setGeometry(500, 300, 400, 270)
        self.setWindowTitle("Signals & Slots")

        button = QPushButton("Press Me!")
        button.setCheckable(True)
        self.connect(button, SIGNAL("clicked()"), self.slotClicked)

        self.setCentralWidget(button)

    def slotClicked(self):
        print("Clicked!")


def main():
    app = QApplication.instance()
    ex = Example()
    ex.show()
    sys.exit()
    app.exec_()


main()

self.connect
シグナルとメソッドを指定する際には、QtCore.SIGNAL()と QtCore.SLOT()マクロを使用する必要があります
旧スタイルのシグナルは新スタイルと異なり様々な問題がありました、QtCore.SIGNAL()に文字列リテラルで指定しなければいけませんし、その引数は C++ の型で指定しなければなりません

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

現在も互換性のために旧スタイルもサポートされていますがあまり使う必要はないので古いコードを引っ張ってきたり、参考にする場合は新スタイルに書き直したほうがいいでしょう

Qt(キュート)、PySideでGUIを作るうえでSignals & Slotsのことを理解していなければよいGUIは作れませんので是非、この機会に覚えて見てください

Maya PySide2 / PySide チュートリアルのこのパートではSignals & Slotsを扱いました