Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save mhogg/29c5d817cb6609ed847c07d45bd53826 to your computer and use it in GitHub Desktop.
Save mhogg/29c5d817cb6609ed847c07d45bd53826 to your computer and use it in GitHub Desktop.

Revisions

  1. mhogg created this gist Jun 21, 2020.
    184 changes: 184 additions & 0 deletions workerthreads_in_pyqt5_with_progressdialog.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,184 @@
    # 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_())