Search code examples
pythonpyqtpyqt5qthreadqtimer

PyQt5: How to run QTimer in sub tread and return value to main Widget?


I must say it is a beginner's question. But I tried/searched a lot without success.

There is a QLabel, by running the code I want to have the values from a sub-thread shown onto it.

Also I want to have the QTimer in the sub-thread, because the time is controlled there(not in the main QThread).

This is the effect on the QLable I want to achieve...

0 (show for 1 second)
1 (show for 1 second)
2 (show for 1 second)
...
11 (show for 2 second)
12 (show for 2 second)
...
21 (show for 3 second)
22 (show for 3 second)
...

Here is my code:

# -*- coding: utf-8 -*-

import sys
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *


class WorkerThread(QThread):

    def __init__(self, parent=None):
        super(WorkerThread, self).__init__(parent)
        self._running = False

        self.timer = QTimer()  # Question: define the QTimer here?
        self.timer.timeout.connect(self.doWork)
        self.timer.start(1000)

    def run(self):
        self._running = True
        while self._running:
            self.doWork()

    def doWork(self):
        for i in range(40):  # Question: How to return the i onto the QLable?
            if i <= 10: 
                # show the value of i on the QLabel and wait for 1 second
                pass
            elif i <= 20:
                # show the value of i on the QLable and wait for 2 second
                pass
            elif i <= 30:
                # show the value of i on the QLable and wait for 3 second
                pass
            else:
                # show the value of i on the QLable and wait for 4 second
                pass

    def stop(self, wait = False):
        self._running = False
        if wait:
            self.wait()


class MyApp(QWidget):
    def __init__(self, parent= None):
        super(MyApp, self).__init__(parent)
        self.thread = WorkerThread()
        self.initUI()

    def initUI(self):
        self.text = "hello world"
        self.setGeometry(100, 100, 500, 100)
        self.setWindowTitle('test')

        self.lbl_1 = QLabel("for every number between 0-10 wait 1 second, for 11-20 wait 2 sec, ...", self)

        self.show()


if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = MyApp()
    window.show()
    sys.exit(app.exec_())

In summary there are two questions: 1. How to define the QTimer in the sub-thread? 2. How to return the value from the sub-thread to QLabel?

Thank you for the help!


Solution

  • It is not necessary to use a QTimer to perform the task you need, in a thread you can have blocking elements such as QThead::sleep() that generates a pause. To send the information we can use the parent() to pass the GUI and combining it with QMetaObject::invokeMethod() we can update the label respecting the rules of Qt:

    class WorkerThread(QThread):
        [...]    
        def doWork(self):
            for i in range(40):  # Question: How to return the i onto the QLable?
                if i <= 10:
                    QThread.sleep(1)
                elif i <= 20:
                    QThread.sleep(2)
                elif i <= 30:
                    QThread.sleep(3)
                else:
                    QThread.sleep(4)
                gui = self.parent()
                QMetaObject.invokeMethod(gui.lbl_1, "setText", Qt.QueuedConnection,
                                         Q_ARG(str, str(i)))
    
    [...]    
    
    class MyApp(QWidget):
        def __init__(self, parent=None):
            super(MyApp, self).__init__(parent) 
            self.thread = WorkerThread(self) # passing the window as a parent
            self.thread.start()
            self.initUI()
    
        def initUI(self):
            [...]
    

    We can also use QTimer together with QEventLoop to generate the same effect as QThread::sleep():

    def doWork(self):
        for i in range(40):  # Question: How to return the i onto the QLable?
            if i <= 10:
                interval = 1000
            elif i <= 20:
                interval = 2000
            elif i <= 30:
                interval = 3000
            else:
                interval = 4000
            loop = QEventLoop()
            QTimer.singleShot(interval, loop.quit)
            loop.exec_()
            gui = self.parent()
            QMetaObject.invokeMethod(gui.lbl_1, "setText", Qt.QueuedConnection,
                                     Q_ARG(str, str(i)))
    

    Instead of using QMetaObject::invokeMethod we can use signals:

    class WorkerThread(QThread):
        numberChanged = pyqtSignal(str)
        def __init__(self, parent=None):
            [...]
    
        def doWork(self):
            for i in range(40):  # Question: How to return the i onto the QLable?
                if i <= 10:
                    interval = 1000
                elif i <= 20:
                    interval = 2000
                elif i <= 30:
                    interval = 3000
                else:
                    interval = 4000
                loop = QEventLoop()
                QTimer.singleShot(interval, loop.quit)
                loop.exec_()
                self.numberChanged.emit(str(i))
    
    [...]
    
    class MyApp(QWidget):
        def __init__(self, parent=None):
            super(MyApp, self).__init__(parent)
            self.thread = WorkerThread(self)
            self.thread.start()
            self.initUI()
    
        def initUI(self):
            self.text = "hello world"
            self.setGeometry(100, 100, 500, 100)
            self.setWindowTitle('test')
            self.lbl_1 = QLabel("for every number between 0-10 wait 1 second, for 11-20 wait 2 sec, ...", self)
            self.thread.numberChanged.connect(self.lbl_1.setText, Qt.QueuedConnection)
            self.show()