Created
June 21, 2020 09:59
-
-
Save mhogg/29c5d817cb6609ed847c07d45bd53826 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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
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:In short it is desirable to know the time
checkCanceled
function take.