# Using QThreads in PyQt5 using worker model # There is so many conflicting posts on how QThreads should be used in pyqt. I had # been subclassing QThread, overriding the run function, and calling start to run # the thread. I did it this way, because I couldn't get the worker method (where # an object is moved to a thread using moveToThread) to do what I wanted. It turns # out that I just didn't understand why. But, once I worked it out I have stuck with # the moveToThread method, as this is recommended by the official docs. # # The key part for me to understand was that when I am running a heavy calculation in # my thread, the event loop is not being called. This means that I am still able to # send signals back to the gui thread, but the worker could no receive signals. This # was important to me, because I wanted to be able to use a QProgressDialog to show # the progress of the worker, but also stop the worker if the user closes the progress # dialog. My solution was to call processEvents(), to force events to be processed to # detect if the progress dialog had been canceled. There are a number of posts that # recommend not using processEvents at all, but instead use the event loop of the # thread or a QTimer to break up your slow loop into bits controlled by the event loop. # However, the pyqt documentation says that calling processEvents() is ok, and what # the function is intended for. However, calling it excessively may of course slow # down your worker. # This code creates a worker that has a slow calculation to do, defined in do_stuff. # It moves this worker to a thread, and starts the thread running to do the calculation. # It connects a QProgressDialog to the worker, which provides the user updates on the # progress of the worker. If the QProgressDialog is canceled (by pressing the cancel # button or the X), then a signal is send to the worker to also cancel. # Michael Hogg, 2020 import sys from PyQt5.QtWidgets import QMainWindow, QApplication, QPushButton, QProgressDialog from PyQt5.QtCore import QCoreApplication, QObject, QThread, pyqtSignal, pyqtSlot import time class Worker(QObject): started = pyqtSignal() finished = pyqtSignal() message = pyqtSignal(str) readyResult = pyqtSignal(str) updateProgress = pyqtSignal(int) updateProgressLabel = pyqtSignal(str) updateProgressRange = pyqtSignal(int,int) def __init__(self,parent=None): super().__init__(None) self.canceled = False @pyqtSlot(str, str) def do_stuff(self, label1, label2): self.label1 = label1 self.label2 = label2 self.started.emit() self.loop() self.finished.emit() def loop(self): self.message.emit('Worker started') if self.checkCanceled(): return self.updateProgressLabel.emit(self.label1) for i in range(5): if self.checkCanceled(): return time.sleep(2) # Blocking self.updateProgress.emit(i+1) self.message.emit(f'Cycle-{i+1}') if self.checkCanceled(): return self.updateProgressLabel.emit(self.label2) self.updateProgress.emit(0) self.updateProgressRange.emit(0,20) for i in range(20): if self.checkCanceled(): return time.sleep(0.2) # Blocking self.updateProgress.emit(i+1) self.message.emit(f'Cycle-{i+1}') if self.checkCanceled(): return self.readyResult.emit('Worker result') self.message.emit('Worker finished') def checkCanceled(self): """ Process events and return bool if the cancel signal has been received """ # Need to call processEvents, as the thread is being controlled by the # slow do_stuff loop, not the event loop. Therefore, although signals # can be send from the thread back to the gui thread, the thread will not # process any events sent to it unless processEvents is called. This # means that the canceled signal from the progress bar (which should stop # the thread) will not be received. If this happens, canceling the progress # dialog with have no effect, and the worker will continue to run until the # loop is complete QCoreApplication.processEvents() return self.canceled @pyqtSlot() def cancel(self): self.canceled = True class MainWin(QMainWindow): stopWorker = pyqtSignal() callWorkerFunction = pyqtSignal(str, str) def __init__(self): super().__init__() self.initUI() def initUI(self): btn1 = QPushButton("Button 1", self) btn1.move(25, 25) btn2 = QPushButton("Clear", self) btn2.move(150, 25) btn1.clicked.connect(self.buttonClicked) btn2.clicked.connect(self.clearStatusBar) self.statusBar() self.setGeometry(700, 500, 275, 100) self.setWindowTitle('Testing threaded worker with progress dialog') def buttonClicked(self): self.showMessageInStatusBar('Button pressed') # Setup progress dialog self.pb = QProgressDialog(self) self.pb.setAutoClose(False) self.pb.setAutoReset(False) self.pb.setMinimumWidth(400) self.pb.setLabelText('Doing stuff') self.pb.setRange(0,5) self.pb.setValue(0) # Setup worker and thread, then move worker to thread self.worker = Worker() # No parent! Otherwise can't move to another thread self.thread = QThread() # No parent! self.worker.moveToThread(self.thread) # Connect signals # Rather than connecting thread.started to the worker function we want to run (i.e. # do_stuff), connect a signal that can also be used to pass input data. #self.thread.started.connect(self.worker.do_stuff) self.callWorkerFunction.connect(self.worker.do_stuff) self.worker.readyResult.connect(self.processResult) # Progress bar related messages self.worker.started.connect(self.pb.show) self.worker.finished.connect(self.pb.close) self.worker.updateProgress.connect(self.pb.setValue) self.worker.updateProgressLabel.connect(self.pb.setLabelText) self.worker.updateProgressRange.connect(self.pb.setRange) # Status bar messages self.worker.message.connect(self.showMessageInStatusBar) # If Progress Bar is canceled, also cancel worker self.pb.canceled.connect(self.worker.cancel) # Clean-up worker and thread afterwards self.worker.finished.connect(self.thread.quit) self.worker.finished.connect(self.worker.deleteLater) self.thread.finished.connect(self.thread.deleteLater) # Start thread self.thread.start() self.callWorkerFunction.emit('Doing stuff No. 1', 'Doing stuff No. 2') @pyqtSlot(str) def processResult(self, result): print(f'process result = {result}') @pyqtSlot(str) def showMessageInStatusBar(self, msg): self.statusBar().showMessage(msg) def clearStatusBar(self): self.statusBar().showMessage('') if __name__ == '__main__': app = QApplication(sys.argv) main = MainWin() main.show() sys.exit(app.exec_())