QTimer on a Qthread

Viewed 5243

I have a GUI witch i need to update constantly using a Qtimer, for that I use a worker Qthread, this is my code :

from PyQt5.QtWidgets import QApplication, QPushButton, QWidget
from PyQt5.QtCore import QThread, QTimer
import sys
import threading


class WorkerThread(QThread):
    def run(self):
        print("thread started from :" + str(threading.get_ident()))
        timer = QTimer(self)
        timer.timeout.connect(self.work)
        timer.start(5000)
        self.exec_()

    def work(self):
        print("working from :" + str(threading.get_ident()))
        QThread.sleep(5)


class MyGui(QWidget):

    worker = WorkerThread()

    def __init__(self):
        super().__init__()
        self.initUi()
        print("Starting worker from :" + str(threading.get_ident()))
        self.worker.start()

    def initUi(self):
        self.setGeometry(500, 500, 300, 300)
        self.pb = QPushButton("Button", self)
        self.pb.move(50, 50)
        self.show()


app = QApplication(sys.argv)
gui = MyGui()
app.exec_()

the output is:

Starting worker from :824
thread started from :5916
working from :824
working from :824

the timer is working on the main thread witch freeze my Gui, How can i fix that ?

3 Answers

Answer: in your case I do not see the need to use QThread.

TL; DR;

When do I need to use another thread in the context of a GUI?

Only one thread should be used when some task can block the main thread called the GUI thread, and the blocking is caused because the task is time-consuming, preventing the GUI eventloop from doing its job normally. All modern GUIs are executed in an eventloop that is allows you to receive notifications from the OS like the keyboard, the mouse, etc. and also allows you to modify the status of the GUI depending on the user.

In your case I do not see any heavy task, so I do not see the need for a QThread, I do not really know what the task is that you want to run periodically.

Assuming you have a task that consumes a lot of time, say 30 seconds and you have to do it every half hour, then the thread is necessary. and in your case you want to use a QTimer for it.

Let's go in parts, QTimer is a class that inherits from a QObject, and a QObject belongs to the same as the parent, and if it does not have a parent it belongs to the thread where it was created. On the other hand many times it is thought that a QThread is a thread of Qt, but it is not, QThread is a class that allows to handle the life cycle of a native thread, and that is clearly stated in the docs: The QThread class provides a platform-independent way to manage threads.

Knowing the above, let's analyze your code:

timer = QTimer(self)

In the above code self is the parent of QTimer and self is the QThread, so QTimer belongs to the thread of the parent of QThread or where QThread was created, not to the thread that QThread handles.

Then let's see the code where QThread was created:

worker = WorkerThread()

As we see QThread has no parent, then QThread belongs to the thread where it was created, that is, QThread belongs to the main thread, and consequently its QTimer child also belongs to the main thread. Also note that the new thread that QThread handles only has the scope of the run() method , if the method is elsewhere belongs to the field where QThread was created, with all the above we see that the output of the code is correct, and the QThread.sleep(5) runs on the main thread causing the eventloop to crash and the GUI to freeze.

So the solution is to remove the parent of QTimer so that the thread it belongs to is the one of the run() method, and move the work function within the same method. On the other hand it is a bad practice to create static attributes unnecessarily, considering the above the resulting code is the following:

import sys
import threading
from PyQt5.QtCore import QThread, QTimer
from PyQt5.QtWidgets import QApplication, QPushButton, QWidget


class WorkerThread(QThread):
    def run(self):
        def work():
            print("working from :" + str(threading.get_ident()))
            QThread.sleep(5)
        print("thread started from :" + str(threading.get_ident()))
        timer = QTimer()
        timer.timeout.connect(work)
        timer.start(10000)
        self.exec_()

class MyGui(QWidget):
    def __init__(self):
        super().__init__()
        self.initUi()
        self.worker = WorkerThread(self)
        print("Starting worker from :" + str(threading.get_ident()))
        self.worker.start()

    def initUi(self):
        self.setGeometry(500, 500, 300, 300)
        self.pb = QPushButton("Button", self)
        self.pb.move(50, 50)


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

Output:

Starting worker from :140068367037952
thread started from :140067808999168
working from :140067808999168
working from :140067808999168

Observations:

  • The heavy task that has been emulated is 5 seconds, and that task must be executed every 10 seconds. If your task takes longer than the period you should create other threads.

  • If your task is to perform a periodic task that is not as heavy as showing time then do not use new threads because you are adding complexity to a simple task, besides this may cause the debugging and testing stage to be more complex.

Sorry, I have misunderstood the question. It might be that this answer on another question might help you. The main message is that you should use the main event loop in Qt to not freeze the GUI instead of executing the thread on __init__: Pyqt5 qthread + signal not working + gui freeze

You can do that by using Qt slots with the decorator @pyqtSlot().

------------ old (wrong) answer ---------

QTimer can already work on separate threads, so I think you might be able to do it without writing that part yourself. For example you could do what you already do in a function:

 def update_gui(self):
     # edit: here is where you can add your gui update code:
     self.setGeometry(500, 500, 300, 300)
     self.pb = QPushButton("Button", self)
     self.pb.move(50, 50)
     self.show()
     # /edit (this is obviously only the setup code, but you get the idea)

     self.update_timer = QTimer()
     self.update_timer.setInterval(int(5000))
     self.update_timer.timeout.connect(self.update_gui)
     self.update_timer.start()

and call it in __init__. That's how I implemented some textbox that clears itself after a couple of seconds.

Try it:

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

class WorkerThread(QThread):

    workSignal = pyqtSignal(str)

    def run(self):
        print("thread started from :" + str(threading.get_ident()))
        textLabel = "thread started from :" + str(threading.get_ident())
        self.workSignal.emit(textLabel)
        self.work()

    def work(self):
        print("working from :" + str(threading.get_ident()))
        textLabel = "working from :" + str(threading.get_ident())
        self.workSignal.emit(textLabel)


class MyGui(QWidget):

    worker = WorkerThread()

    def __init__(self):
        super().__init__()
        self.initUi()
        print("Starting worker from :" + str(threading.get_ident()))
        self.lbl.setText("Starting worker from :" + str(threading.get_ident()))

        self.worker.workSignal.connect(self.showLabel)

    def initUi(self):
        self.setGeometry(700, 350, 300, 150)

        self.lcdTime = QLCDNumber(self)
        self.lcdTime.setSegmentStyle(QLCDNumber.Filled)   # Outline Filled Flat
        self.lcdTime.setDigitCount(8)    

        self.timer = QTimer(self)
        self.lbl   = QLabel(self) 
        self.pb = QPushButton("Button Close", self, clicked=self.close)

        vbox = QVBoxLayout()
        vbox.addWidget(self.lcdTime)
        vbox.addWidget(self.lbl)
        vbox.addWidget(self.pb)
        self.setLayout(vbox)

        self.timer.timeout.connect(self.showTime)
        self.timer.start(1000)        
        self.numSec = 0
        self.show()

    def showTime(self):
        time = QTime.currentTime()
        text = time.toString("hh:mm:ss")           
        if ((time.second() % 2) == 0):
            text = text[0:2] + ' ' + text[3:5] + ' ' + text[6:]
        self.lcdTime.display(text)
        self.numSec += 1
        if self.numSec == 5:
            self.worker.start()
            self.numSec = 0

    def showLabel(self, textLabel):
        self.lbl.setText(textLabel)

app = QApplication(sys.argv)
gui = MyGui()
app.exec_()

enter image description here

Related