Use of QTimer vs timer in PyQt

1.1k Views Asked by At

I am trying to wrap my head around how threads work in PyQt with Qtimer in a GUI app. Apologies for such a foundational question, but I've searched for other similar questions and failed to find informative answers.

I'm seeking to collect data from a series of sensors and process changes to the control structure based on the results. There is no user interaction required for this. However, there is a control panel of sorts and a series of status indicators that are intended to display as it runs. I have noted in other questions and examples that those using the normal time.sleep() function were typically directed to use QTimer instead.

In further digging I found the attached code example which works fine, using time.sleep(). As an object exercise, I attempted to replace this with various invocations of QTimer, none of which worked.

I also found references stating that QTimer can only work inside of QCoreApplication or similar. But the reason we're using Qtimer in the first place is that QApplication is running to support the GUI; without it we can't access any of the widgets. And without the GUI, we wouldn't need QTimer and could use time.sleep(), correct?

What am I missing here? Why does this code seem to work fine with time.sleep() when so many other examples seemed to require QTimer instead? If QTimer is used, how can that happen within QApplication?

#!/usr/bin/python3
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import RPi.GPIO as GPIO
import time
import datetime
import sys

class QCustomThread (QThread):
    startLoad    = pyqtSignal(int)
    progressLoad = pyqtSignal(int)
    statusLoad   = pyqtSignal(bool)

    def __init__ (self, parentQWidget = None):
        super(QCustomThread, self).__init__(parentQWidget)
        self.wasCanceled = False

    def run (self):
        # Simulate data load estimation
        numberOfprogress = 100
        self.startLoad.emit(numberOfprogress)
        for progress in range(numberOfprogress + 1):
            # Delay
            time.sleep(0.1)
            if not self.wasCanceled:
                self.progressLoad.emit(progress)
            else:
                break
        self.statusLoad.emit(True if progress == numberOfprogress else False)
        self.exit(0)

    def cancel (self):
        self.wasCanceled = True

class QCustomMainWindow (QMainWindow):
    def __init__ (self):
        super(QCustomMainWindow, self).__init__()
        # Create action with QPushButton
        self.startQPushButton = QPushButton('START')
        self.startQPushButton.released.connect(self.startWork)
        self.setCentralWidget(self.startQPushButton)
        # Create QProgressDialog
        self.loadingQProgressDialog = QProgressDialog(self)
        self.loadingQProgressDialog.setLabelText('Loading')
        self.loadingQProgressDialog.setCancelButtonText('Cancel')
        self.loadingQProgressDialog.setWindowModality(Qt.WindowModal)

    def startWork (self):
        myQCustomThread = QCustomThread(self)
        def startLoadCallBack (numberOfprogress):
            self.loadingQProgressDialog.setMinimum(0)
            self.loadingQProgressDialog.setMaximum(numberOfprogress)
            self.loadingQProgressDialog.show()
        def progressLoadCallBack (progress):
            self.loadingQProgressDialog.setValue(progress)
        def statusLoadCallBack (flag):
            print ('SUCCESSFUL') if flag else print ('FAILED')
        myQCustomThread.startLoad.connect(startLoadCallBack)
        myQCustomThread.progressLoad.connect(progressLoadCallBack)
        myQCustomThread.statusLoad.connect(statusLoadCallBack)
        self.loadingQProgressDialog.canceled.connect(myQCustomThread.cancel)
        myQCustomThread.start()

myQApplication = QApplication(sys.argv)
myQCustomMainWindow = QCustomMainWindow()
myQCustomMainWindow.show()
sys.exit(myQApplication.exec_())
2

There are 2 best solutions below

2
On

There are two important aspects you need to account for:

  • time.sleep is blocking, QTimer is not.
  • Q*Application is event driven, meaning that it waits for incoming events in its event queue, and should never be blocked, if not for really small amounts of time (usually the time required to execute a slot/function).

The problem with using blocking functions like sleep in the main Qt thread is that it doesn't allow the Qt application to process any event, no matter if it's a QApplication, a QGuiApplication or a QCoreApplication.

If a blocking function is called in the main Qt thread (no matter if the app is GUI driven or not), everything will be blocked and queued until that function returns, including any signal emission and consequent processing.

Note that this does not work in both ways when different threads are involved: if Qt detects that the sender and the receiver are in different threads, the signal is queued into the receiver event queue, and the sender thread returns emit() immediately.

Finally. If you have a separate thread that actually needs to wait for some time (usually needed within for/loop cycles to allow processing and avoid useless CPU calls with pass), use time.sleep or QThread's sleep functions (sleep() for seconds, msleep() for milliseconds, usleep() for microseconds).
If you don't need separate threads (meaning that the involved processing doesn't keep the CPU occupied), you can just use a QTimer (or, eventually, the static QTimer.singleShot()).

0
On

Always remember native python thread is UNcompatitable with QT. Once you use QT, timer and internet connections, everything that includes another job must been done through the module of QT itself. The cause is that QT redefines a thread in C++ which is not compatible with CPython thread.