PyQt5 – Signal and Slot Explained

Posted on 2020-04-29  376 Views


The first contact with the signal and slot mechanism of PyQt5 is that when developing PyChat chat software, it is necessary to implement the back-end service to receive messages and update the front-end GUI interface at the same time, involving object communication between different threads. There were many detours in the original development, and now I go back and organize and record this part of the content.

Part of the content is integrated from the following technical blog, thank you very much:
PyQt5 Quick Start (2) PyQt5 Signal Slot Mechanism - https://blog.51cto.com/9291927/2422187
Several advanced gameplay of PyQt 5 signal and slot - https://blog.csdn.net/broadview2006/article/details/80132757

What are signals and slots?

The signal-and-slot mechanism applies to every instance of QObject and is an important way to implement communication between objects in GUI programming. Signals and slots are matched to each other, a signal can be bound to multiple slots, and a slot can also listen to multiple signals. When a signal is triggered, the function of the slot bound to it is automatically executed. This mechanism is similar to the callback functions in the C/C++ language. Callback functions respond to specific events by passing a pointer to the function that needs to be called to the calling function.

To illustrate with a more understandable example, we think of signals and slots as a notification mechanism. For example, I fancy a product on e-commerce, and I want the e-commerce platform to notify me when its price drops. We can think of the signal as a price reduction, and the slot as the act of informing me. The "price reduction notification" request I submitted is the binding of the signal to the slot, and the process of e-commerce notifying me when the price is reduced is the reaction of the slot after receiving the signal.

In PyQt5, the signal-and-slot mechanism has the following characteristics:

  • Multiple slots can be connected to one signal
  • A slot can listen for multiple signals
  • One signal can be connected to another
  • Signal parameters can be of any Python type
  • Signal connections to slots can span threads
  • The signal can be disconnected

Define signals and slots

In PyQt5, we use the pyqtsignal class to define signals. This signal can only be defined in a subclass of QObject and must be defined when the class is created, not dynamically added. First, let's look at the __init__ function of the class:

class pyqtSignal:
    def __init__(self, *types, name: str = ..., 
    revision:int = 0, arguments = []) -> None: ...

*types can accept multiple Python basic data types, representing the parameter type of the signal; name accepts a custom signal name and defaults to using the property name of that signal class. For example, the notification object in the following code. Slot functions also need to be defined in a subclass of QObject and can be arbitrary functions. The revision and arguments parameters are used when connecting QML files, but this article will not discuss them for the time being. The defined signal will be automatically added to QObject's QMetaObject.

from PyQt5.QtCore import *

class SignalClass(QObject):
    # Define a notification signal, accept a list as a parameter, and the signal is named printprice
    notification = pyqtSignal(int,name = "printprice")

# Define a slot function
    def send_pushmsg(self, price):
        print("Current Price:", price)

Connect the signal with the slot & send the signal

After determining the signal to the slot, the signal needs to be bound to the slot to react to a specific event. Use
QObject.signal.connect(slotFunction, type, bool: no_receiver_check) to bind signals to slots; The default type is the automatic connection mode, and when the no_receiver_check is true, it will ignore the existence of the slot and force the signal to be sent;
Using QObject.singal.disconnect([slotFunctions]) to unbind signals and slots, multiple slots can be unbound at the same time. Use signal.emit(*args) for signal emission and parameter transfer. For example:

from PyQt5.QtCore import *

class SignalExample(QObject):
    # Define a notification signal, accept a list as a parameter, and the signal is named printprice
    notification = pyqtSignal(int,name = "printprice")

# Define a slot function
    def send_pushmsg(self, price):
        print("Current Price:", price)

signal = SignalExample()

# Connect the markdown signal to the push message function
signal.notification.connect(self.send_pushmsg)
# Aliases for signals can also be used here
# signal.printprice.connect(self.send_pushmsg)

# Transmit signal
signal.notification.emit(18)

Output: Current Price: 18

Use the slot decorator

The slot decorator defines signals and slot functions through the decorator's approach. In general, it slightly reduces the memory footprint and slightly increases the speed (since the type of the received parameter is declared directly in the decorator, the mapping from Python to the C++ interface becomes straightforward without the need for the program to automatically detect it). More importantly, slot decorators allow repeated overloads of a slot and claim different C++ signatures.

In addition, when cross-thread operations, especially when lambda functions are used as slots, it should be noted that PyQt will generate a proxy for lambda functions to meet the requirements of the signal/slot mechanism, and in the new version of PyQt (4.4+), the connection method between signals and slots will not be determined until the signal is emitted. When the lambda function is connected, the generated proxy is moved accordingly to the thread that accepts the object:

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

If the receiving object (QObject) of the signal has been moved to the correct thread, it will not cause a problem, and on the contrary, it may cause Proxy to remain on the main thread, resulting in non-normal operation. Using @pyqtSlot decorators avoids this problem by avoiding the generation proxy and directly generating a corresponding signature.

PyQt5.QtCore.pyqtSlot(*types, name, result, revision=0)*types
accepts the incoming parameter type, name can rename the slot, result specifies the data type of the return value, revision is also used to export slot functions to QML files, this article does not discuss.
The specific usage method is as follows:

from PyQt5.QtCore import *

class SignalExample(QObject):
    # Define a notification signal that accepts a list as a parameter, and the signal is named printlist
    notification = pyqtSignal(int,name = "printprice")

# Define a slot function
    # Here, we declare the accepted and returned parameter types
    @pyqtSlot(int, result=bool)
    def send_pushmsg(self, price):
        print("Current Price:", price)
        return True

signal = SignalExample()

# Connect the markdown signal to the push message function
signal.printprice.connect(signal.send_pushmsg)
# Transmit signal
signal.notification.emit(18)

Output: Current Price: 18

Disconnect the signal from the slot

Just use PyQt5.QtCore.pyqtSignal.disconnect (pyqtSlot).

Example: Signals and slots in multithreading

In this example, we will set up the simplest example of the price monitor mentioned earlier, using two threads, the main process is the GUI interface in the foreground, and one thread is the price monitoring in the background. The price on the main program form is updated in real time and will be prompted when the target price is reached. To facilitate testing, we set the initial price at 1000 and the target price at 900.

from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
import sys
import time

# Main program form
class MainWindow(QWidget):
    def __init__(self, parent = None):
        super(MainWindow, self).__init__(parent)
        self.setWindowTitle("Price Monitor")
        self.resize(400, 200)

# Create a price text box
        self.price = QLineEdit(self)
        self.price.setReadOnly(True)
        self.price.resize(400, 100)
        self.price.move(0, 20)

# Create an exit button
        self.exitbutton = QPushButton("Exit",self)
        self.exitbutton.resize(self.exitbutton.sizeHint())
        self.exitbutton.move(135, 150)

# Connect the exit button to QApplication's exit action
        self.exitbutton.clicked.connect(QCoreApplication.instance().quit)
        self.loadUI()

# Create a termination signal to control the termination of background monitoring threads
    terminal_sig = pyqtSignal()

@pyqtSlot(str) # Update price signal slot, which is used to update the price display in the GUI
    def update_price(self, price):
        self.price.setText(price)

@pyqtSlot() # notification slots to issue "target price reached" notifications and end the monitoring thread
    def notification(self):
        self.moniterThread.quit()
        self.price.setText(self.price.text() + "Reached Target Price!")
        # Sends a termination signal to the monitoring thread
        self.terminal_sig.emit()

def loadUI(self):
        # Create a price monitoring thread
        self.moniterThread = MonitorThread(1000, 900)
        # Connect the slot function that monitors the thread
        self.moniterThread.update_price.connect(self.update_price)
        self.moniterThread.notification.connect(self.notification)
        self.terminal_sig.connect(self.moniterThread.terminate)
        # Start the monitoring thread
        self.moniterThread.start()

# Price monitoring thread
class MonitorThread(QThread):
    
# Declare two signals, update price signals and alert signals
    update_price = pyqtSignal(str)
    notification = pyqtSignal()

def __init__(self, initPrice, targetPrice):
        super().__init__()
        self.init_price = initPrice
        self.target_price = targetPrice

def run(self):
        while True:
            # To facilitate testing, the price is reduced by 10 every 0.5 seconds
            self.init_price -= 10
            # Send a notification to the main process to update the price
            self.update_price.emit(str(self.init_price))
            if self.init_price == self.target_price:
                # When the target price is reached, a notification is sent to the main process
                self.notification.emit()
            time.sleep(0.5)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    monitor = MainWindow()
    monitor.show()
    sys.exit(app.exec_())

When the target price is reached, the effect is as shown in the figure: