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_())
There are two important aspects you need to account for:
time.sleep
is blocking, QTimer is not.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
), usetime.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()
).