Maya PySide2 / PySide チュートリアル 初学編 ⑦ – Signals & Slotsを理解する
このチュートリアルは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
シグナルを受け取ります
ボタンをクリックすると、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()
実行後、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()
旧スタイル
新スタイルのシグナルとスロットは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を扱いました
ディスカッション
コメント一覧
まだ、コメントがありません