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
When Do I Need to Call Mainloop in a Tkinter Application
Sqlite Parameter Substitution Problem
Secondary Axis with Twinx(): How to Add to Legend
What Soap Client Libraries Exist for Python, and Where Is the Documentation for Them
Python Pandas Remove Duplicate Columns
How to Get the Path of the Current Executed File in Python
Calculating a Directory's Size Using Python
How to Detach Matplotlib Plots So That the Computation Can Continue
Printing All Instances of a Class
What's the Simplest Way of Detecting Keyboard Input in a Script from the Terminal
Slicing a List in Python Without Generating a Copy
Opencv 2.4 Videocapture Not Working on Windows