【PySide】動的に生成したWidgetのシグナルに接続する

ボタンのクリック時に特定の処理を実装するとき、ボタンの数が決まっている場合はそのボタンの数だけ接続するメソッドを書けばよいだけです。
が、ボタンの数が不定で実行するまで分からない場合はどうでしょうか。

f:id:tm8r:20200311095131g:plain
上の画像においてボタンの数が不定で動的生成されている場合について考えてみます。

senderを使う

        button = QtWidgets.QPushButton("test")
        button.clicked.connect(self._on_button_clicked)
(略)
    def _on_button_clicked(self):
        sender = self.sender()
        self.result_line.setText(sender.text())

このようにボタンに_on_button_clickedを接続し、self.senderを使用するとイベントの発火元のQObjectを取得できます。
ここでいうとQPushButtonのclickedに接続しているので、senderはQPushButtonになります。

textメソッドを持たないウィジェットからも接続される場合は分岐が必要ですが、この例のようにQPushButtonとしか接続されない場合はこれで十分です。

というわけで全文は以下のようになります。

lambda式を使用する

        button = QtWidgets.QPushButton("test")
        button.clicked.connect(lambda: self._on_button_clicked(button))
(略)
    def _on_button_clicked(self, button):
        self.result_line.setText(button.text())

pythonのlambda式を使用して引数にWidget(ここではQPushButton)を渡すことで発火元を取得するパターンになります。

ただし、「lambda: self._on_button_clicked(button)」のbuttonは実行時のオブジェクトが評価されるので、for文の中で使用する場合などは注意が必要です。

例えば、以下のようなコードを書いた場合、1つ目のボタンをクリックするとどうなるでしょうか。

        for i in xrange(5):
            button = QtWidgets.QPushButton("button_{0}".format(str(i)))
            button.clicked.connect(lambda: self._on_button_clicked(button))
            main_layout.addWidget(button)
(略)
    def _on_button_clicked(self, button):
        self.result_line.setText(button.text())

期待するのはクリックしたボタンのテキストが表示される挙動です。

しかし実際の結果は…
f:id:tm8r:20200311132711g:plain
どのボタンを押しても最後に追加されたボタンのテキストである「button_4」が表示されてしまっています。
かなしみ…。

前に書いた通り、この引数に指定したbuttonはlambda式でメソッドが実行された際のものが評価されます。
buttonはforで更新してしまっているので、結果的に一番最後に追加されたものが使用されてしまっています。

というわけで、lambda式を以下のように書き換えてやりましょう。

        for i in xrange(5):
            button = QtWidgets.QPushButton("button_{0}".format(str(i)))
            button.clicked.connect(lambda x=button: self._on_button_clicked(x))
            main_layout.addWidget(button)

xは式の作成時に評価されるので、このようにすることで引数を作成時のものに拘束することができます。
というわけで特に意図がなければ、forの中であろうが外であろうが後に書いたやり方をしておいたほうが安全かなと思います。

全文はこちら。

引数を持つシグナルにlambda式を用いて接続する

lambda式を使用することで任意の引数を動的に渡すことができることは分かりました。
しかし、シグナルによっては引数を持っているものが存在します。

例えばQLineEditのtextChangedは発火時のテキストを引数に持ちます。
これを生かしつつ、自身で引数を追加したい場合は以下のようにします。

        for i in xrange(5):
            label = "line_{0}".format(i)
            line_layout = QtWidgets.QHBoxLayout()
            main_layout.addLayout(line_layout)
            line_layout.addWidget(QtWidgets.QLabel(label))
            line = QtWidgets.QLineEdit()
            line.setObjectName(label)
            line.textChanged.connect(lambda text, widget=line: self._on_current_text_changed(text, widget))
            line_layout.addWidget(line)
(略)
    def _on_current_text_changed(self, text, line):
        self.result_line.setText("{0}: {1}".format(line.objectName(), text))

lambda式の引数を接続したいシグナルの引数分用意、さらに自身で追加したい分の引数をその後に記述することで実現できます。

結果はこの通り。
f:id:tm8r:20200311134218g:plain

全文はこちら。

おわり

QSignalMapperというものでも実現可能なようですが、ちらほらQt5では非推奨みたいな情報を見かけるのと、公式ドキュメントでも多くはlambda式で代用できるしシンプルに書けるよと書いてあるので、基本的には使わなくてよい気はします。

あと全文コードはMaya上で動作するものになっているので、それ以外で利用される場合は適宜主にWindowの継承まわりを読み替えてください。

スポンサーリンク