Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save shujaatak/10190ac79e32d493e751994690892f5f to your computer and use it in GitHub Desktop.
Save shujaatak/10190ac79e32d493e751994690892f5f 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_())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment