Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save mhogg/29c5d817cb6609ed847c07d45bd53826 to your computer and use it in GitHub Desktop.
Save mhogg/29c5d817cb6609ed847c07d45bd53826 to your computer and use it in GitHub Desktop.
# 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_())
@shujaatak
Copy link

It would be awesome if you please add some logic to show how much extra time the following two lines in the checkCanceled function take:

QCoreApplication.processEvents()
return self.canceled

In short it is desirable to know the time checkCanceled function take.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment