pythonpyqt5signals-slotsqthreadqtimer

Problems getting QTimer to start when using QThread


I'm trying to implement a webcam using PyQt5 from an example I found (here, but not really relevant). Getting the example to work wasn't an issue, but I wanted to modify some things and I am stuck on one particular problem.

I have two classes, one QObject Capture which has a QBasicTimer that I want to start, and a QWidget MyWidget with a button that is supposed to start the timer of the Capture object, which is inside a QThread.

If I directly connect the button click to the method that starts the timer, everything works fine.

But I want to do some other things when I click the button, so I connected the button to a method of MyWidget first and call the start method of Capture from there. This, however, doesn't work: the timer doesn't start.

Here is a minimal working example:

from PyQt5 import QtCore, QtWidgets
import sys

class Capture(QtCore.QObject):
    def __init__(self, parent=None):
        super(Capture, self).__init__(parent)

        self.m_timer = QtCore.QBasicTimer()

    def start(self):
        print("capture start called")
        self.m_timer.start(1000, self)

    def timerEvent(self, event):
        print("time event")


class MyWidget(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super(MyWidget, self).__init__(parent)    

        lay = QtWidgets.QVBoxLayout(self)
        self.btn_start = QtWidgets.QPushButton("Start")
        lay.addWidget(self.btn_start)        

        self.capture = Capture()
        captureThread = QtCore.QThread(self)
        captureThread.start()
        self.capture.moveToThread(captureThread)
        
        # self.btn_start.clicked.connect(self.capture.start) # this works
        self.btn_start.clicked.connect(self.startCapture) # this doesn't
        # self.capture.start() # this doesn't either
        
        self.show()
        
    def startCapture(self):
        self.capture.start()


def run_app():
    app = QtWidgets.QApplication(sys.argv)
    mainWin = MyWidget()
    mainWin.show()
    app.exec_()

run_app()

It is some problem with the QThread, because if I don't use threading it works. I thought maybe it has something to do with the thread not being in some way available when called from a different method than the one it was created in, but calling self.capture.start() directly from the init does not work either.

I only have a very basic grasp of threads. Can someone tell me how I can properly call self.capture.start() from MyWidget and why it works without problems when directly connecting it to the button click?


Solution

  • If you connect the button's clicked signal to the worker's start slot, Qt will automatically detect that it's a cross-thread connection. When the signal is eventually emitted, it will be queued in the receiving thread's event-queue, which ensures the slot will be called within the worker thread.

    However, if you connect the button's clicked signal to the startCapture slot, there's no cross-thread connection, because the slot belongs to MyWidget (which lives in the main thread). When the signal is emitted this time, the slot tries to create the timer from within the main thread, which is not supported. Timers must always be started within the thread that creates them (otherwise Qt will print a message like "QBasicTimer::start: Timers cannot be started from another thread").

    A better approach is to connect the started and finished signals of the thread to some start and stop slots in the worker, and then call the thread's start and quit methods to control the worker. Here's a demo based on your script, which shows how to implement that:

    from PyQt5 import QtCore, QtWidgets
    import sys
    
    class Capture(QtCore.QObject):
        def __init__(self, parent=None):
            super(Capture, self).__init__(parent)
            self.m_timer = QtCore.QBasicTimer()
    
        def start(self):
            print("capture start called")
            self.m_timer.start(1000, self)
    
        def stop(self):
            print("capture stop called")
            self.m_timer.stop()
    
        def timerEvent(self, event):
            print("time event")
    
    class MyWidget(QtWidgets.QWidget):
        def __init__(self, parent=None):
            super(MyWidget, self).__init__(parent)
            lay = QtWidgets.QVBoxLayout(self)
            self.btn_start = QtWidgets.QPushButton("Start")
            lay.addWidget(self.btn_start)
            self.capture = Capture()
            self.captureThread = QtCore.QThread(self)
            self.capture.moveToThread(self.captureThread)
            self.captureThread.started.connect(self.capture.start)
            self.captureThread.finished.connect(self.capture.stop)
            self.btn_start.clicked.connect(self.startCapture)
            self.show()
    
        def startCapture(self):
            if not self.captureThread.isRunning():
                self.btn_start.setText('Stop')
                self.captureThread.start()
            else:
                self.btn_start.setText('Start')
                self.stopCapture()
    
        def stopCapture(self):
            self.captureThread.quit()
            self.captureThread.wait()
    
        def closeEvent(self, event):
            self.stopCapture()
    
    def run_app():
        app = QtWidgets.QApplication(sys.argv)
        mainWin = MyWidget()
        mainWin.show()
        app.exec_()
    
    run_app()