Connecting Slots and Signals in Pyqt4 in a Loop

Connecting slots and signals in PyQt4 in a loop

This is just, how scoping, name lookup and closures are defined in Python.

Python only introduces new bindings in namespace through assignment and through parameter lists of functions. i is therefore not actually defined in the namespace of the lambda, but in the namespace of __init__(). The name lookup for i in the lambda consequently ends up in the namespace of __init__(), where i is eventually bound to 9. This is called "closure".

You can work around these admittedly not really intuitive (but well-defined) semantics by passing i as a keyword argument with default value. As said, names in parameter lists introduce new bindings in the local namespace, so i inside the lambda then becomes independent from i in .__init__():

self._numberButtons[i].clicked.connect(lambda checked, i=i: self._number(i))

UPDATE: clicked has a default checked argument that would override the value of i, so it must be added to the argument list before the keyword value.

A more readable, less magic alternative is functools.partial:

self._numberButtons[i].clicked.connect(partial(self._number, i))

I'm using new-style signal and slot syntax here simply for convenience, old style syntax works just the same.

Connecting multiples signal/slot in a for loop in pyqt

My preferred way of iterating over several widgets in pyqt is storing them as objects in lists.

myButtons = [self.ui.phase_scan_button, self.ui.etalon_scan_button,
self.ui.mirror_scan_button, self.ui.gain_scan_button]
for button in myButtons:
button.clicked.connect(lambda _, b=button: self.scan_callback(scan=b))

If you need the strings "phase", "etalon", "mirror" and "gain" separately, you can either store them in another list, or create a dictionary like

myButtons_dict = {"phase": self.ui.phase_scan_button,
"etalon": self.ui.etalon_scan_button,
"mirror": self.ui.mirror_scan_button,
"gain": self.ui.gain_scan_button}

for button in myButtons_dict:
myButtons_dict[button].clicked.connect(lambda: _, b=button self.scan_callback(scan=b))

Note, how I use the lambda expression with solid variables that are then passed into the function self.scan_callback. This way, the value of button is stored for good.

How to connect multiple PyQt Signals w/ variable arguments to single slot/signal repeater

You have to set all the signatures of the signals through several pyqtSlot:

from PyQt5.QtCore import pyqtSignal, pyqtSlot, QCoreApplication, QObject, QTimer

class MyClass(QObject):
signal_1 = pyqtSignal(str, int)
signal_2 = pyqtSignal(int, int)
signal_3 = pyqtSignal(str, int, int)

class OtherClass(QObject):
@pyqtSlot(str, int)
@pyqtSlot(int, int)
@pyqtSlot(str, int, int)
def unifiedResponse(self, *args):
print(args)

def main():
import sys

app = QCoreApplication(sys.argv)

sender = MyClass()
receiver = OtherClass()

sender.signal_1.connect(receiver.unifiedResponse)
sender.signal_2.connect(receiver.unifiedResponse)
sender.signal_3.connect(receiver.unifiedResponse)

def on_timeout():
sender.signal_1.emit("StackOverflow", 1)
sender.signal_2.emit(1, 2)
sender.signal_3.emit("StackOverflow", 1, 2)

QTimer.singleShot(1000, QCoreApplication.quit)

QTimer.singleShot(1000, on_timeout)

sys.exit(app.exec_())

if __name__ == "__main__":
main()

If in general you want to send several types of data then it is best to use a more generic data type such as a list (or an object):

class MyClass(QObject):
signal_1 = pyqtSignal(list)
signal_2 = pyqtSignal(list)
signal_3 = pyqtSignal(list)

class OtherClass(QObject):
@pyqtSlot(list)
def unifiedResponse(self, args):
print(args)

def main():
import sys

app = QCoreApplication(sys.argv)

sender = MyClass()
receiver = OtherClass()

sender.signal_1.connect(receiver.unifiedResponse)
sender.signal_2.connect(receiver.unifiedResponse)
sender.signal_3.connect(receiver.unifiedResponse)

def on_timeout():
sender.signal_1.emit(["StackOverflow", 1])
sender.signal_2.emit([1, 2])
sender.signal_3.emit(["StackOverflow", 1, 2])

QTimer.singleShot(1000, QCoreApplication.quit)

QTimer.singleShot(1000, on_timeout)

sys.exit(app.exec_())

Update:

There is no elegant way to access the last element, but the first ones since the slot signature must be a subset of the signal signature, for example the slot with signature "int" can accept signals with a signature that has the first type to "int", the other arguments are discarded:

from PyQt5.QtCore import pyqtSignal, pyqtSlot, QCoreApplication, QObject, QTimer

class MyClass(QObject):
signal_1 = pyqtSignal(int, str)
signal_2 = pyqtSignal(int, int)
signal_3 = pyqtSignal(int, str, int)

class OtherClass(QObject):
@pyqtSlot(int)
def unifiedResponse(self, index):
print(index)

def main():
import sys

app = QCoreApplication(sys.argv)

sender = MyClass()
receiver = OtherClass()

sender.signal_1.connect(receiver.unifiedResponse)
sender.signal_2.connect(receiver.unifiedResponse)
sender.signal_3.connect(receiver.unifiedResponse)

def on_timeout():
sender.signal_1.emit(1, "StackOverflow")
sender.signal_2.emit(2, 2)
sender.signal_3.emit(3, "StackOverflow", 1)

QTimer.singleShot(1000, QCoreApplication.quit)

QTimer.singleShot(1000, on_timeout)

sys.exit(app.exec_())

if __name__ == "__main__":
main()

Differences between lambda and partial when connecting clicked signal in PyQt/PySide

Before discussing the differences between lambda/partial and some other issues, I will briefly cover some practical fixes for your code example.

There are two main problems. The first is a common issue with the binding of names in a for-loop, as discussed in this question: Lambda in a loop. The second is a PyQt-specific problem with default signal paramters, as discussed in this question: Unable to send signal from Button created in Python loop. Given this, your example can be fixed by connecting the signals like this:

bh.clicked.connect(lambda checked, n=n: self.printtext(n+1))

So this caches the current value of n in a default argument, and also adds a checked argument to stop n being overwritten by the parameter emitted by the signal.

The example could also benefit from being re-written in a more idiomatic way which eliminates the nasty eval hack:

layout = QtWidgets.QVBoxLayout(self)
for n in range(1, 4):
button = QtWidgets.QPushButton(f'button {n}')
button.clicked.connect(lambda checked, n=n: self.printtext(n))
layout.addWidget(button)
setattr(self, f'button{n}', button)

or using a QButtonGroup:

self.buttonGroup = QtWidgets.QButtonGroup(self)
layout = QtWidgets.QVBoxLayout(self)
for n in range(1, 4):
button = QtWidgets.QPushButton(f'button {n}')
layout.addWidget(button)
self.buttonGroup.addButton(button, n)
setattr(self, f'button{n}', button)
self.buttonGroup.buttonClicked[int].connect(self.printtext)

(Note that button-groups can also be created in Qt Designer by selecting the buttons and then choosing "Assign to button group" in the context-menu).


As for the question of the differences between lambda and partial: the main one is that the former implicitly creates a closure over the local variables, whereas the latter explicitly stores the arguments passed to it internally.

The common "gotcha" with closures in python is that functions defined inside a loop don't capture the current value of the enclosed variables. So when the function is called later, it can only access the last seen values. There was a discussion about changing this behaviour fairly recently on the python-ideas mailing list, but it didn't seem to reach any firm conclusions. Still, it's possible some future version of python will remove this little wart. The issue is completely avoided by the partial function because it creates a callable object which stores the arguments passed to it as read-only attributes. So the work-around of using default arguments in a lambda to explicitly store the variables is effectively using that same approach.

There is one other point worth covering here: the differences between PyQt and PySide when connecting to signals with default parameters. It appears that PySide treats all slots as if they were decorated with a slot decorator; whereas PyQt treats undecorated slots differently. Here is an illustration of the differences:

def __init__(self):
...
self.button_1.clicked.connect(self.slot1)
self.button_2.clicked.connect(self.slot2)
self.button_3.clicked.connect(self.slot3)

def slot1(self, n=1): print(f'slot1: {n=!r}')
def slot2(self, *args, n=1): print(f'slot2: {args=!r}, {n=!r}')
def slot3(self, x, n=1): print(f'slot1: {x=!r}, {n!r}')

After clicking each button in turn, the following output is produced:

PyQt5 output:

slot1: n=False
slot2: args=(False,), n=1
slot3: x=False, n=1

PySide2 output:

slot1: n=False
slot2: args=(), n=1
TypeError: slot3() missing 1 required positional argument: 'x'

If the slots are decorated with @QtCore.pyqtSlot(), the output from PyQt5 matches the PySide2 output shown above. So if you need a solution for lambda (or any other undecorated slot) that works the same for both PyQt and PySide, you should use this:

bh.clicked.connect(lambda *args, n=n: self.printtext(n))

PyQt connect inside for loop vs. separate calls results in different behavior

When you connect a signal to a lambda function, the contents of the lambda function are evaluated when the signal is emitted, not when the signal is connected. As such, the variables you use (marker_one and marker_two) always point to the objects created in the last iteration of the loop.

One simple solution is to explicitly pass in marker_one and marker_two as default arguments to variables of the same name, in the signature of the lambda function:

lambda marker_one=marker_one: self.update_marker_vals(marker_one, "Marker One")
lambda marker_two=marker_two: self.update_marker_vals(marker_two, "Marker Two")

There are several useful answers relating to a very similar problem here, specifically the answer by ekhumoro, if you would like to know more (my answer to that question my also be of use, although ekhumoro's solution is cleaner)

Using lambda expression to connect slots in pyqt

The QPushButton.clicked signal emits an argument that indicates the state of the button. When you connect to your lambda slot, the optional argument you assign idx to is being overwritten by the state of the button.

Instead, make your connection as

button.clicked.connect(lambda state, x=idx: self.button_pushed(x))

This way the button state is ignored and the correct value is passed to your method.

PyQt - defining signals for multiple objects in loops

Explanation and common solution here (there are many more, just search for e.g. "python lambda loop parameter": https://stackoverflow.com/a/938493

Another common solution is to use a partial function:

import functools
slot = functools.partial( self.checkState, self.fieldList["valueField" + str(i)] )
self.fieldList["valueField" + str(i)].cursorPositionChanged.connect( slot )

PyQt: Connecting a signal to a slot to start a background operation

It shouldn't matter whether the connection is made before or after moving the worker object to the other thread. To quote from the Qt docs:

Qt::AutoConnection - If the signal is emitted from a different
thread than the receiving object, the signal is queued, behaving as
Qt::QueuedConnection. Otherwise, the slot is invoked directly,
behaving as Qt::DirectConnection. The type of connection is
determined when the signal is emitted
. [emphasis added]

So, as long as the type argument of connect is set to QtCore.Qt.AutoConnection (which is the default), Qt should ensure that signals are emitted in the appropriate way.

The problem with the example code is more likely to be with the slot than the signal. The python method that the signal is connected to probably needs to be marked as a Qt slot, using the pyqtSlot decorator:

from QtCore import pyqtSlot

class Scanner(QObject):

@pyqtSlot()
def scan(self):
scan_value(start, stop, step)
progress.setValue(100)

EDIT:

It should be clarified that it's only in fairly recent versions of Qt that the type of connection is determined when the signal is emitted. This behaviour was introduced (along with several other changes in Qt's multithreading support) with version 4.4.

Also, it might be worth expanding further on the PyQt-specific issue. In PyQt, a signal can be connected to a Qt slot, another signal, or any python callable (including lambda functions). For the latter case, a proxy object is created internally that wraps the python callable and provides the slot that is required by the Qt signal/slot mechanism.

It is this proxy object that is the cause of the problem. Once the proxy is created, PyQt will simply do this:

    if (rx_qobj)
proxy->moveToThread(rx_qobj->thread());

which is fine if the connection is made after the receiving object (i.e. rx_qobj) has been moved to its thread; but if it's made before, the proxy will stay in the main thread.

Using the @pyqtSlot decorator avoids this issue altogether, because it creates a Qt slot more directly and does not use a proxy object at all.

Finally, it should also be noted that this issue does not currently affect PySide.

PyQt connect SIGNAL to multiple SLOT

The way you're doing it is fine. If you had quite a few things to do you could connect to a new function that handles everything for you.

I notice in your connected getValue functions you're getting the value straight from the object; are you aware that the value is passed as a parameter with the valueChanged(int) signal? If you change your getValue functions to accept an additional parameter there will be no need to get the value directly from the object. Of course you could do away with the getValue function all together and issue your print statement in the helper function.

self.connect(self.dial, SIGNAL("valueChanged(int)"), self.dial_value_changed)
self.connect(self.spinbox, SIGNAL("valueChanged(int)"),self.spinbox_value_changed)

def dial_value_changed(self, value):
self.spinbox.setValue(value)
self.getValue_dial(value)

def spinbox_value_changed(self, value):
self.dial.setValue(value)
self.getValue_spinbox(value)

def getValue_dial(self, value):
print value

def getValue_spinbox(self, value):
print value

Also, and this is down to preference, there is a new style for signals and slots which can make the code a little easier to read. It would change the

self.connect(self.dial, SIGNAL("valueChanged(int)"), self.dial_value_changed)
self.connect(self.spinbox, SIGNAL("valueChanged(int)"),self.spinbox_value_changed)

lines above to

self.dial.valueChanged.connect(self.dial_value_changed)
self.spinbox.valueChanged.connect(self.spinbox_value_changed)

But for the original question and for the two things you're doing I'd just connect the signal twice rather than have a helper function.



Related Topics



Leave a reply



Submit