This is a followup-question based on ekhumoro's answers here and here.
I thought to understand, that when a slot is correctly defined with pyqtSlot
and assigned to QThread
(e.g. with moveToThread()
), it will be executed in this QThread and not the calling one. Furthermore, making the connection with Qt.QueuedConnection
or Qt.AutoConnection
is also required.
I wrote Code to test this. My goal is to achive something quite simple like this:
Gui with a button, that starts some time-consuming-work and return with a result to display back in the GUI.
from PyQt5.Qt import *
class MainWindow(QMainWindow):
change_text = pyqtSignal(str)
def __init__(self):
super().__init__()
self.button = QPushButton('Push me!', self)
self.setCentralWidget(self.button)
print('main running in:', QThread.currentThread())
thread = Thread(change_text, self)
thread.start()
self.button.clicked.connect( thread.do_something_slow, Qt.QueuedConnection)
self.change_text.connect(self.display_changes, Qt.QueuedConnection)
@pyqtSlot(str)
def display_changes( self, text ):
self.button.setText(text)
class Thread(QThread):
def __init__(self, signal_to_emit, parent):
super().__init__(parent)
self.signal_to_emit = signal_to_emit
#self.moveToThread(self) #doesn't help
@pyqtSlot()
def do_something_slow( self ):
print('Slot doing stuff in:', QThread.currentThread())
import time
time.sleep(5)
self.signal_to_emit.emit('I did something')
if __name__ == '__main__':
app = QApplication([])
main = MainWindow()
main.show()
app.exec()
But .. the gui is blocking and the slot is called in the main-thread.
What am I missing? Have to be something small (I hope).
The problem is that you are confusing that QThread
is a Qt thread, that is, a new thread created by Qt, but no, QThread
is a class that handles native threads, only the run()
method is running on another thread, other methods live in the thread where the QThread
lives, which is a QObject
,
In what thread does a QObject live?
The thread in which a QObject
lives is that of the parent, if it does not have a parent it will be the thread where it was created. On the other hand, a QObject
can move to another thread using moveToThread()
, and all of its children will also move. Only moveToThread()
can be used if the QObject
has no parent, otherwise it will fail.
A methodology to use QThread
is to create a class that inherits from QThread
and override the run method and call start()
so that it starts running run()
, in the run()
method the heavy task would be done, but in your case not This form can be used because the task will not be executed continuously. A better option than the heavy task is part of a QObject
, and that QObject
move it to another thread.
class MainWindow(QMainWindow):
change_text = pyqtSignal(str)
def __init__(self):
super().__init__()
self.button = QPushButton('Push me!', self)
self.setCentralWidget(self.button)
print('main running in:', QThread.currentThread())
# A Worker without a parent is created
# so that it can be moved to another thread.
self.worker = Worker(self.change_text)
thread = QThread(self)
self.worker.moveToThread(thread)
thread.start()
# All methods of self.worker are now executed in another thread.
self.button.clicked.connect(self.worker.do_something_slow)
self.change_text.connect(self.display_changes)
@pyqtSlot(str)
def display_changes( self, text ):
self.button.setText(text)
class Worker(QObject):
def __init__(self, signal_to_emit, parent=None):
super().__init__(parent)
self.signal_to_emit = signal_to_emit
@pyqtSlot()
def do_something_slow( self ):
print('Slot doing stuff in:', QThread.currentThread())
import time
time.sleep(5)
self.signal_to_emit.emit('I did something')
On the other hand it is not necessary to indicate the type of connection since by default it is Qt::AutoConnection
, this type of connection decides in runtime if you use Qt::DirectConnection
if the receiver lives in the same wire from where the signal was emitted, otherwise, Qt::QueuedConnection
is used. As you realize, the Worker does not have a parent, so for it to have a life cycle equal to the class it must be an attribute of it since otherwise it would be a local variable that is eliminated. On the other hand, note that QThread
receives a parent than a MainWindow, so QThread
will live in the GUI thread but it handles a secondary thread.
With the Worker concept it is better that the change_text signal no longer belongs to the GUI but to the worker decoupling the objects better.
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.button = QPushButton('Push me!', self)
self.setCentralWidget(self.button)
print('main running in:', QThread.currentThread())
self.worker = Worker()
thread = QThread(self)
self.worker.moveToThread(thread)
thread.start()
self.button.clicked.connect(self.worker.do_something_slow)
self.worker.change_text.connect(self.display_changes)
@pyqtSlot(str)
def display_changes( self, text ):
self.button.setText(text)
class Worker(QObject):
change_text = pyqtSignal(str)
@pyqtSlot()
def do_something_slow( self ):
print('Slot doing stuff in:', QThread.currentThread())
import time
time.sleep(5)
self.change_text.emit('I did something')