Search code examples
multithreadingpyqt5qaction

Start and Stop thread using QAction toggled


I'm currently trying to build a PyQt5 app and it should consist of the main GUI and in the background there should be a different thread that should measure something in an infinite loop. And I want to start and stop this thread using a QAction or Checkbox.

So to say when I press the checkbox and the state is true the thread should be started and if I click it again it should be stopped.

Now what is the best way to implement this?

Currently I'm using a Worker thread like this:

class Worker(QtCore.QObject):
    def __init__(self):
        super(Worker, self).__init__()
        self._isRunning = True

    def task(self):
        if not self._isRunning:
            self._isRunning = True
        while self._isRunning:
            time.sleep(0.5)
            ... measure ...

    def stop(self):
        self._isRunning = False

and this in the main thread to make it run:

self.thread = QtCore.QThread()
self.thread.start()
self.worker = Worker()
self.worker.moveToThread(self.thread)

self.btn_start.clicked.connect(self.worker.task)
self.btn_stopped.clicked.connect(lambda: self.worker.stop())

So far this works. But I'm not really confident that this is the best way to do it and I'd also like it much better if I could make the same thing using a checkbox in the way described.


Solution

  • As it stands, the code you've posted is not multi-threaded. This is because worker.task() is started by code in the main thread, so it will also run in the main thread. You need to use the worker thread's started signal to start the task, and a custom signal on the worker to quit the thread.

    The demo script below should fix these issues:

    import sys, time
    from PyQt5 import QtCore, QtWidgets
    
    class Worker(QtCore.QObject):
        finished = QtCore.pyqtSignal()
        messageSent = QtCore.pyqtSignal(str)
    
        def __init__(self):
            super(Worker, self).__init__()
            self._isRunning = False
    
        def task(self):
            print('WKR thread:', QtCore.QThread.currentThread())
            self._isRunning = True
            count = 0
            while self._isRunning:
                time.sleep(0.5)
                count += 1
                self.messageSent.emit('count: %s' % count)
            self.finished.emit()
    
        def stop(self):
            self._isRunning = False
    
    class Window(QtWidgets.QWidget):
        def __init__(self):
            super(Window, self).__init__()
            self.button = QtWidgets.QCheckBox('Test', self)
            self.button.toggled.connect(self.handleButton)
            self.label = QtWidgets.QLabel(self)
            layout = QtWidgets.QVBoxLayout(self)
            layout.addWidget(self.label)
            layout.addWidget(self.button)
            self.thread = QtCore.QThread()
            self.worker = Worker()
            self.worker.moveToThread(self.thread)
            self.thread.started.connect(self.worker.task)
            self.worker.finished.connect(self.thread.quit)
            self.worker.messageSent.connect(self.label.setText)
    
        def handleButton(self, checked=False):
            print('GUI thread:', QtCore.QThread.currentThread())
            if checked:
                self.label.clear()
                self.thread.start()
            else:
                self.worker.stop()
    
    if __name__ == '__main__':
    
        app = QtWidgets.QApplication(sys.argv)
        window = Window()
        window.setGeometry(800, 150, 200, 50)
        window.show()
        sys.exit(app.exec_())