18. More Signals & Slots

18.1 A Common Pitfall

We have already seen that a Python lambda can be used to pass additional arguments to a slot, however, there is a pitfall that makes lambdas a bit tricky. The value of a variable used in a lambda is looked up at the time it is called1. Let’s say you have five lambda functions and you want to pass them a loop index as the parameter:

1 functions = []
2 
3 for i in range(5):
4     functions.append(lambda: print(i))
5 
6 print("Calling the functions after the loop:")
7 for func in functions:
8     func()

The output is:

1 Calling the functions after the loop:
2 4
3 4
4 4
5 4
6 4

When the functions are executed (in the second loop) the i variable value is 4 so all four print 4 instead of 0, 1, 2, 3 and 4 as one would expect. You can change this by assigning the current index to a lambda argument:

1 functions = []
2 
3 for i in range(5):
4     functions.append(lambda x=i: print(x))
5 
6 print("Calling the fixed functions:")
7 for func in functions:
8     func()

Now, the output is:

1 Calling the fixed functions:
2 0
3 1
4 2
5 3
6 4
An icon of a clipboard-list1

You want to log each QCheckBox checked state change into multiple log files.

  1. Create the checkbox.

  2. Use a loop to connect its checkStateChanged() signal to the slots. In each loop iteration, capture the index and checkbox checkState() current values

  3. Log the checkbox state changes.

Now, if you check the checkbox, the output is:

 1 Logging to file no: 0
 2 State: CheckState.Checked
 3 Logging to file no: 1
 4 State: CheckState.Checked
 5 Logging to file no: 2
 6 State: CheckState.Checked
 7 Logging to file no: 3
 8 State: CheckState.Checked
 9 Logging to file no: 4
10 State: CheckState.Checked

18.2 Custom Signals

Most of the Qt classes provide a set of predefined signals. However, when creating your own QObject inherited class, you may want to provide custom signals to accompany it.

An icon of a clipboard-list1

Suppose we want to create a custom button class that that keeps track of the number of times the user has clicked on it. We also want the class to be able to notify other classes when the counter changes.

PySide6 provides the means to do this in a pythonic way:

  1. Import the Signal class from the PySide6.QtCore namespace. Signal provides the connect(), disconnect() and emit() methods.

  2. Create a QObject inherited class and add a signal to it. We inherit the class from QPushButton and add a signal named counterChanged() to it. The signal is declared as a class-level variable of the class and takes a list of Python types as argument. counterChanged() takes a single int argument. Each time the button is clicked we increment the counter and emit the signal, passing it the self.counter variable.

  3. In the main window use the custom signal the same way as the predefined Qt signals. We create an instance of the CounterButton, create a slot named on_counter_changed(), and connect the button’s counterChanged signal with it. Now, each time the button is clicked, the counter is incremented and the counterChanged() signal is emitted:

1 Counter:  1
2 Button clicked
3 Counter:  2
4 Button clicked
5 Counter:  3
6 Button clicked

18.3 Signal Blocking

At times, you may want to prevent signal emission, for instance during class initialization, or when making programmatic changes to a widget’s values that would trigger a signal.

An icon of a clipboard-list1

Your task is to allow the user to temporarily block a button’s clicked() signals

To temporarily block a signal, use the QObject.blockSignals() passing it a boolean value. If the value is True all signals emitted by the object are blocked. If the value is False, signals are not blocked.

In the example we have a button’s clicked() signal connected to a slot named on_button_clicked(). We use a QCheckBox() to block/unblock the button’s signals:

1 if state == Qt.CheckState.Checked:
2     self.button.blockSignals(True)
3     print('Signals blocked!')
4 else:
5     self.button.blockSignals(False)
6     print('Signals unblocked!')

When the checkbox is checked the button’s signals are blocked:

1 Button clicked, checked: False
2 Signals blocked!
3 Signals unblocked!
4 Button clicked, checked: False

18.4 Connection Objects

The Signal.connect() method has a return value of type QMetaObject.Connection. It is a handle to the signal-slot connection the Signal.connection() call established.

An icon of a clipboard-list1

You need to enable the user to connect or disconnect a button’s signals.

To use a connection object in your application:

  1. Store a reference to the Connection object that Signal.connect() returned. We store the reference in the instance variable named conn.

  2. Use the reference to disconnect the signal from the slot. Aside from connect() PuSide6 Signal object also have a method named disconnect() that we use to disconnect the signal from the slot.

The output is:

1 <class 'PySide6.QtCore.QMetaObject.Connection'>
2 Connection is valid
3 <class 'PySide6.QtCore.QMetaObject.Connection'>
4 Connection is invalid
5 Already disconnected

The first time we click the Disconnect button the connection is valid and succesfully disconnected. The second time we click it the connection is not valid so we don’t call disconnect()

If we didn’t check the connection validity we would have got a

1 RuntimeWarning: Failed to disconnect (<PySide6.QtCore.QMetaObject.Connection object at 0x7fee46154040>) from signal "clicked()".

18.5 Connecting Multiple Slots with a Signal

You can connect a slot with more than one signals. In the example we create three slots and connect them with the button.clicked signal.

The output is

1 Executed first
2 Executed second
3 Executed third

Notice that the slots are executed in the order in which they were connected to the signal. This not necessarily true for signals and slots across different threads.

18.6 Disconnecting

In the Qt.UniqueConnection example we saw that you can break a signal-slot connection using the Signal.disconnect(receiver) method.

1 self.button.clicked.disconnect(self.on_clicked)

This breaks up the connection between button.clicked signal and the on_clicked() slot. However, QObject also has a disconnect() method with several overloads that give you more control over which signal-slot connections are removed.

  • static disconnect(connection) lets you pass a connection object to it. In the example we store all connection objects in the Window.connections list. On clicking the Disconnect 1 button all connections are disconnected in a loop:
1 def on_disconnect_1(self):
2     for c in self.connections:
3         QObject.disconnect(c)
4     self.update_label()
  • static disconnect(sender, signal, receiver, member) lets you specify both the sender and the receiverobjects as well as thesignaland thememberie the slot. On clicking theDisconnect 2button we setself.buttonas thesenderand the other three arguments toNone. Noneacts as a wildcard so this disconnects **all** signal-slot connections whereself.button` is the signal sender.
1 def on_disconnect_2(self):
2     QObject.disconnect(self.button, None, None, None)
3     self.update_label()
  • On clicking the Disconnect 3 button we set self.button as sender and clicked(bool) as signal. receiverandmemberare still set toNone`.
1 def on_disconnect_3(self):
2     QObject.disconnect(self.button, SIGNAL('clicked(bool)'), None, None)
3     self.update_label()

This will disconnect only the slots with the signature clicked(bool) ie. Slot1 and Slot3. The connection between button.clicked and Slot2 remains.

  • On clicking the Disconnect 4 button we set sender to button and reciver to self (ie to the Window instance). signal and member are set to None. This breaks up all connections since all the slots are Window members.
1 def on_disconnect_4(self):
2     QObject.disconnect(self.button, None, self, None)
3     self.update_label()

  1. If you don’t implement __init__() in your class at all, the parent class gets initalized automatically.↩︎