Pyqt: Connecting a Signal to a Slot to Start a Background Operation

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.

How do I connect a PyQt5 slot to a signal function in a class?

The error happens becuase you are using pyqtSlot decorator in a class which doesn't inherit from QObject. You can fix the problem by either getting rid of the decorator, or making HandlerClass a subclass of QObject.

The main purpose of pyqtSlot is to allow several different overloads of a slot to be defined, each with a different signature. It may also be needed sometimes when making cross-thread connections. However, these use-cases are relatively rare, and in most PyQt applications it is not necessary to use pyqtSlot at all. Signals can be connected to any python callable object, whether it is decorated as a slot or not.

PyQt5 Threads, Signal, and Slot. Connect Error

The pyqtSlot decorator is intended to be used for QObject subclasses, otherwise it will not work correctly.

So, the solution is pretty simple, as you only need to inherit from QObject:

class systemTray(QObject):
def start_tray(self):
# ...

On the other hand, using a pyqtSlot decorator is rarely needed and only for specific situations (see this related answer. In your case, it doesn't seem necessary.

Also, since you're already subclassing from QThread, you can just use its own start(), without using moveToThread() on a new one.

Why do I need to decorate connected slots with pyqtSlot?

The main purpose of pyqtSlot is to allow several different overloads of a slot to be defined, each with a different signature. It may also be needed sometimes when making cross-thread connections (see this answer for one such scenario). However, these use-cases are relatively rare, and in most PyQt applications it is not necessary to use pyqtSlot at all. Signals can be connected to any python callable object, whether it is decorated as a slot or not.

The PyQt docs also state:

Connecting a signal to a decorated Python method also has the advantage of reducing the amount of memory used and is slightly faster.

However, in practice, this advantage is generally very small and will often be swamped by other factors. It has very little affect on raw signalling speed, and it would be necessary to make thousands of emits/connections before it started to have a significant impact on cpu load or memory usage. For an in depth analysis of these points, see this Code Project article by @Schollii:

  • PyQt5 signal/slot connection performance

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.

Signals not received in correct thread using PyQt5

With thanks to @three_pinapples, his suggestion in the comments solves the problem: The slot needs to be decorated with QtCore.pyqtSlot:

class MyObject(QtCore.QObject):

aSignal = QtCore.pyqtSignal()

def __init__(self, *args, **kwargs):
super(MyObject, self).__init__(*args, **kwargs)

self.aSignal.connect(self.onASignal)

@QtCore.pyqtSlot()
def onASignal(self):
print('MyObject signal thread:\t %s'
% str(int(QtCore.QThread.currentThreadId())))

Qt Signals/Slots in threaded Python

It turns out I was just making a foolish Python mistake. The signal was being emitted correctly, and the event loop was running properly in all threads. My problem was that in my Main.__init__ function I made a VideoTableController object, but I did not keep a copy in Main, so my controller did not persist, meaning the slot also left. When changing it to

self.controller = VideoTableController(model, self.ui.videoView)

Everything stayed around and the slots were called properly.

Moral of the story: it's not always a misuse of the library, it may be a misuse of the language.

PyQt6 proper way to implement a periodic background task

Instead of using time.sleep you can use a QTimer to start a worker every T seconds, and then just run the heavy reading task on the other thread.

from PyQt6 import QtGui
from PyQt6 import QtWidgets
from PyQt6 import QtCore

from sensor import GetPromSensor

class WorkerSignals(QtCore.QObject):
result = QtCore.pyqtSignal(float)

class Worker(QtCore.QRunnable):
def __init__(self, prom_sensor, signals):
super(Worker, self).__init__()
self.prom_sensor = prom_sensor
self.signals = signals

def run(self):
value = self.prom_sensor.get_sensor_latest()
self.signals.result.emit(float(value))

class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.prom_sensor = GetPromSensor()

self.threadpool = QtCore.QThreadPool()
self.signals = WorkerSignals()
self.signals.result.connect(self.process_result)

self.previous = None

widget = QtWidgets.QWidget()
self.setCentralWidget(widget)
layout = QtWidgets.QGridLayout(widget)

self.label = QtWidgets.QLabel("Start")
self.label.setStyleSheet("background-color:rosybrown; border-radius:5px")
self.label.setAlignment(QtCore.Qt.Alignment.AlignCenter)
self.label.setFont(QtGui.QFont("Arial", 32))

layout.addWidget(self.label)

self.request_data()

def request_data(self):
worker = Worker(prom_sensor=self.prom_sensor, signals=self.signals)
self.threadpool.start(worker)
QtCore.QTimer.singleShot(5 * 1000, self.request_data)

@QtCore.pyqtSlot(float)
def process_result(self, temperature):
if temperature == self.previous:
return

self.previous = temperature
print("receiving:", temperature)
self.label.setNum(temperature)

if __name__ == "__main__":
app = QtWidgets.QApplication([])
window = MainWindow()
window.show()
app.exec()

Note: pyqtSlot only works on QObjects, and a QRunnable is not.



Related Topics



Leave a reply



Submit