PyQt5 - QThread + iterator + Slots and Signals

I was trying to implement a Stopwatch but I fear I might have pin myself to a corner of sorts. It measures time well but I am still not able to interact with it with commands like start, pause and reset to the clock.

Full Code with ######## pointing to lines of interest :

# Import Krita
from krita import *
from PyQt5 import QtWidgets, QtCore, QtGui, uic
import os.path
import time
import datetime

# Set Window Title Name
DOCKER_NAME = 'Timer Watch'

# Initialize Variables
switch = 0 # 0=Pause, 1=Play
counter = 0 # Number of clock ticks

# Docker Class
class TimerWatchDocker(DockWidget):
    """Control amount of work time spent or see the current time."""

    def __init__(self):
        super(TimerWatchDocker, self).__init__()

        # Window Title
        self.setWindowTitle(DOCKER_NAME)
        # Widget
        self.window = QTabWidget()
        self.layout = uic.loadUi(os.path.dirname(os.path.realpath(__file__)) + '/timer_watch.ui', self.window)
        self.setWidget(self.window)

        # Timer
        self.timer = QtCore.QTimer(self)
        self.timer.start(1000)

        # Start up Connections
        self.Connect()

    # Connect Funtions to Buttons
    def Connect(self):
        # Display Connections
        self.timer.timeout.connect(self.HM_Display) # Hour Display
        self.SW_Display(0) # Initialize Stopwatch display to Zero

        # Progression Bar Zero State
        self.layout.progressBar_1.setValue(0)
        self.layout.progressBar_2.setValue(0)

        # Stopwatch buttons
        self.layout.pushButton_startpause.clicked.connect( self.SW_StartPause )
        self.layout.pushButton_reset.clicked.connect( self.SW_Reset )

        # Threading ##################################### Creates Thread and Connects it
        self.thread = Clock() # Insert argument here if needed
        self.thread.signal_COUNTER.connect(self.SW_Display)

    # Functions
    def HM_Display(self):
        self.currentTime = QtCore.QTime.currentTime()
        self.layout.lcdNumber_1.setDigitCount(8)
        self.strCurrentTime = self.currentTime.toString('hh:mm:ss')
        self.layout.lcdNumber_1.display(self.strCurrentTime)

    def SW_Display(self, counter): # Input Clock counter here
        self.layout.lcdNumber_2.setDigitCount(8)
        if counter == 0 :
            self.strProgressTime = "00:00:00"
        else :
            self.strProgressTime = time.strftime('%H:%M:%S', time.gmtime(counter))
        self.layout.lcdNumber_2.display(self.strProgressTime)
        self.Status(str(counter))
        self.layout.progressBar_1.setValue(counter)
        self.layout.progressBar_2.setValue(counter)

    def SW_StartPause(self):
        # Select Operation
        if   switch == 0 : self.SW_Start()
        elif switch == 1 : self.SW_Pause()
        else : self.SW_Reset()

    def SW_Start(self):
        # Start Ready
        self.maximum = self.SW_Time()
        self.layout.progressBar_1.setValue(0)
        self.layout.progressBar_2.setValue(0)
        self.layout.progressBar_1.setMaximum(self.maximum)
        self.layout.progressBar_2.setMaximum(self.maximum)
        # Threading
        self.thread.start() #################################### Start Thread is here due to lack of implementation
        switch = 1
        ####################################################### Insert Start Command
        # UI
        if self.SW_Time() == 0 : # if User time == Zero
            self.layout.pushButton_startpause.setText("Pause:Zero")
            self.layout.timeEdit.setEnabled(False)
            self.layout.progressBar_1.setEnabled(False)
            self.layout.progressBar_2.setEnabled(False)
        else : # if User time is NOT Zero
            self.layout.pushButton_startpause.setText("Pause")
            self.layout.timeEdit.setEnabled(False)
            self.layout.progressBar_1.setEnabled(True)
            self.layout.progressBar_2.setEnabled(True)

    def SW_Pause(self):
        # Threading
        switch = 0
        ####################################################### Insert Pause Command
        # UI
        self.layout.pushButton_startpause.setText("Start")
        self.layout.timeEdit.setEnabled(False)
        self.layout.progressBar_1.setEnabled(True)
        self.layout.progressBar_2.setEnabled(True)

    def SW_Reset(self):
        # Threading
        switch = 0
        ######################################################## Insert Reset Command
        # UI
        self.layout.pushButton_startpause.setText("Start")
        self.layout.timeEdit.setEnabled(True)
        self.layout.progressBar_1.setEnabled(True)
        self.layout.progressBar_2.setEnabled(True)
        self.layout.progressBar_1.setValue(0)
        self.layout.progressBar_2.setValue(0)

    def SW_Time(self): # Convert Input Time
        hh = self.layout.timeEdit.time().hour()
        mm = self.layout.timeEdit.time().minute()
        ss = self.layout.timeEdit.time().second()
        conv = datetime.timedelta(hours=hh, minutes=mm)
        tsec = conv.total_seconds() + ss
        msec = tsec * 1000
        return tsec

    def Status(self, message):
        message = str(message)
        self.layout.label.setText(message)

    # Change the Canvas
    def canvasChanged(self, canvas):
        pass


# Threading
class Clock(QThread):
    """Runs a clock counter thread for the Stopwatch."""

    # Signals to be Sent Back
    signal_COUNTER = pyqtSignal(int)

    def run(self):
        counter = 0
        max = 86400 # 24 hours
        while counter <= max:
            self.signal_COUNTER.emit(counter)
            counter = counter + 1 #switch  ######################### Faulty Iterator
            time.sleep(1)
        counter = 0
        self.signal_COUNTER.emit(counter)

The way I thought to be easier to implement was to create a Cycle of time that the stopwatch would the use with it’s iterator as the QThread start working and then the buttons would alter the variable of the iterator that adds to the counter like so:
idea: counter = counter + switch
start: counter = counter + 1
pause: counter = counter + 0
reset: counter = counter + (max-counter)

With pyqtSignal() I am able to send the counter value back to the Docker class, but I can’t use it to send a signal from the Docker into the Thread class or at least successfully.

Is there a better way to implement this Faulty Iterator? or there is better way to create a Start, Pause and Reset for the counter?

As I am now, once I hit the Start button it does not stop. While still remaining lightweight, with good time measure and with no crashes.

Thank you in Advance.

1 Like

I don’t really understand why you want to use an extra thread there, all it does seems to be sleeping. What’s wrong with timers?

Everything GUI-related has to be done in the GUI thread anyway, any signals you emit from other threads get queued and executed in the event loop of the GUI thread, just like timeout signals from timers.

Also signal->slot connections are unidirectional, so to send something the other direction, you need a separate signal and slot, which seems to be missing.

Last but not least, just sleeping for 1s between doing something does not give a precise time measurement, it will drift off the real time…

Well I already have done this Addon with the use of QTimer, but after I posted it github I noticed it was going very very slow, like 70% of normal time. I just gave up on the idea of it and just tryed to see other paths and strangely time.sleep(1) was the one that gave the best results without krita protesting in some way or another and the delay it gives for me seems acceptable for what I measured so far.

I do not want to create a extra thread, my aim is just to have the GUI thread and the clock thread. So queues or locks is not required.

I will try and implement again the signal from the GUI thread to the Clock thread, maybe showing it wrong will be better than showing the last working state. (I will post it in a bit)

the other version : https://github.com/EyeOdin/timer_watch

# Import Krita
from krita import *
from PyQt5 import QtWidgets, QtCore, QtGui, uic
import os.path
import time
import datetime

# Set Window Title Name
DOCKER_NAME = 'Timer Watch'

# Initialize Variables
switch = 0 # 0=Pause, 1=Play
counter = 0 # Number of clock ticks

# Docker Class
class TimerWatchDocker(DockWidget):
    """Control amount of work time spent or see the current time."""

    signal_SWITCH = pyqtSignal(int) ######################## NEW Signal

    def __init__(self):
        super(TimerWatchDocker, self).__init__()

        # Window Title
        self.setWindowTitle(DOCKER_NAME)
        # Widget
        self.window = QTabWidget()
        self.layout = uic.loadUi(os.path.dirname(os.path.realpath(__file__)) + '/timer_watch.ui', self.window)
        self.setWidget(self.window)

        # Timer
        self.timer = QtCore.QTimer(self)
        self.timer.start(1000)

        # Start up Connections
        self.Connect()

    # Connect Funtions to Buttons
    def Connect(self):
        # Display Connections
        self.timer.timeout.connect(self.HM_Display) # Hour Display
        self.SW_Display(0) # Initialize Stopwatch display to Zero

        # Progression Bar Zero State
        self.layout.progressBar_1.setValue(0)
        self.layout.progressBar_2.setValue(0)

        # Stopwatch buttons
        self.layout.pushButton_startpause.clicked.connect( self.SW_StartPause )
        self.layout.pushButton_reset.clicked.connect( self.SW_Reset )

        # Threading ###################################### Thread
        self.thread = Clock() # Insert argument here if needed
        self.thread.signal_COUNTER.connect(self.SW_Display)
        self.signal_SWITCH.connect(self.thread.run) ############ New Connection from Docker to Clock Class Thread


    # Functions
    def HM_Display(self):
        self.currentTime = QtCore.QTime.currentTime()
        self.layout.lcdNumber_1.setDigitCount(8)
        self.strCurrentTime = self.currentTime.toString('hh:mm:ss')
        self.layout.lcdNumber_1.display(self.strCurrentTime)

    @pyqtSlot(int)
    def SW_Display(self, counter): # Input Clock counter here
        self.layout.lcdNumber_2.setDigitCount(8)
        if counter == 0 :
            self.strProgressTime = "00:00:00"
        else :
            self.strProgressTime = time.strftime('%H:%M:%S', time.gmtime(counter))
        self.layout.lcdNumber_2.display(self.strProgressTime)
        self.Status(str(counter))
        self.layout.progressBar_1.setValue(counter)
        self.layout.progressBar_2.setValue(counter)

    def SW_StartPause(self):
        # Select Operation
        if   switch == 0 : self.SW_Start()
        elif switch == 1 : self.SW_Pause()
        else : self.SW_Reset()

    def SW_Start(self):
        # Start Ready
        self.maximum = self.SW_Time()
        self.layout.progressBar_1.setValue(0)
        self.layout.progressBar_2.setValue(0)
        self.layout.progressBar_1.setMaximum(self.maximum)
        self.layout.progressBar_2.setMaximum(self.maximum)
        # Threading
        self.thread.start()
        # UI
        if self.SW_Time() == 0 : # if User time == Zero
            self.layout.pushButton_startpause.setText("Pause:Zero")
            self.layout.timeEdit.setEnabled(False)
            self.layout.progressBar_1.setEnabled(False)
            self.layout.progressBar_2.setEnabled(False)
        else : # if User time is NOT Zero
            self.layout.pushButton_startpause.setText("Pause")
            self.layout.timeEdit.setEnabled(False)
            self.layout.progressBar_1.setEnabled(True)
            self.layout.progressBar_2.setEnabled(True)

    def SW_Continue(self):
        pass

    def SW_Pause(self):
        # Threading
        switch = 0

        # UI
        self.layout.pushButton_startpause.setText("Start")
        self.layout.timeEdit.setEnabled(False)
        self.layout.progressBar_1.setEnabled(True)
        self.layout.progressBar_2.setEnabled(True)

    def SW_Reset(self):
        # Threading
        switch = 0
        self.signal_SWITCH.emit(switch)
        # UI
        self.layout.pushButton_startpause.setText("Start")
        self.layout.timeEdit.setEnabled(True)
        self.layout.progressBar_1.setEnabled(True)
        self.layout.progressBar_2.setEnabled(True)
        self.layout.progressBar_1.setValue(0)
        self.layout.progressBar_2.setValue(0)

    def SW_Time(self): # Convert Input Time
        hh = self.layout.timeEdit.time().hour()
        mm = self.layout.timeEdit.time().minute()
        ss = self.layout.timeEdit.time().second()
        conv = datetime.timedelta(hours=hh, minutes=mm)
        tsec = conv.total_seconds() + ss
        msec = tsec * 1000
        return tsec

    def Status(self, message):
        message = str(message)
        self.layout.label.setText(message)

    # Change the Canvas
    def canvasChanged(self, canvas):
        pass


# Threading ############################################ Threading
class Clock(QThread):
    """Runs a clock counter thread for the Stopwatch."""

    # Signals to be Sent Back
    signal_COUNTER = pyqtSignal(int)

    def __init__(self, parent=None): # insert var here to boot thread with a argument
        QThread.__init__(self, parent)
        self.switch = 1 ####################### Start Counting right Away

    # @pyqtSlot(int)
    def run(self, switch):
        counter = 0
        max = 86400 # 24 hours
        while counter <= max:
            self.switch = switch
            counter = counter + self.switch ############ New Switch to iterator Switch
            self.signal_COUNTER.emit(counter)
            time.sleep(1)
        counter = 0
        self.signal_COUNTER.emit(counter)

It only crashes when starting the thread.

yea I see you were using “timer.setInterval(1)” on github, that is one millisecond! So you were flooding the event queue at 1kHz (or at least trying to), while your UI probably updates at 60-120Hz at best, so it’s no surprise things didn’t work very well.

Again, separate thread or not, all signals will end up in the event queue of the GUI thread. Qt is event driven, it’s not like a game engine that continually renders frames as fast as it can.
So set sane timer intervals, and better determine elapsed time by querying the actual elapsed system time (there’s even a QElapsedTimer class for that)

So you think I should just try and correct the one I posted on github instead of pursuing the new one?

Yes, I think if you want to have any kind of correct time measurement, you should query the system time/elapsed time.
Qt docs do not state whether QTimer really schedules events in a way that ensures the given frequency (1/interval) in system time to prevent drift, but in any case it states that timeouts may be late depending on system load, and events also can be filtered/blocked/compressed etc.

The Qt example for an analog clock also queries QTime::currentTime() every update instead of counting the timer intervals (it is set to update every second)
https://doc.qt.io/qt-5/qtwidgets-widgets-analogclock-example.html

2 Likes

I think I managed to finish it in the way you told me to do it Lynx3d :slight_smile:
https://github.com/EyeOdin/timer_watch

Not sure if I set up the clocks the right way but it looks light and properly working. I am still gonna test the time for a couple of hours to see if there is any time difference but I only expect a really minor difference. After that i will post it in the proper area.

P.S. - In 3 hours it delayed 14 seconds. does not sound too bad :slight_smile:

1 Like

So you’re still just adding +1 on every timer interval, and as I was afraid, that doesn’t seem very precise over longer time periods.

My idea was to use QElapsedTimer that you start when the “Start” button gets clicked and use the periodic QTimer only to update the UI regularly, not to measure time.

You can just ask a QElapsedTimer how many milliseconds have passed since it was started, and should not be affected by system load.
For pausing you still need to accumulate the elapsed times of previous Start->Pause intervals for the total time, since QElapsedTimer doesn’t feature a pause function itself.

yeah that class does not have a pause sadley… would need to go about with the left over time of the last run in a different way :\