In a PyQt5 app that uses a QtCore.QTimer() to update some labels the QTimer works fine at first but does not restart properly after I stop it once

Question:

This works perfectly fine and updates the labels by calling the update_label_colors function:

timer = QtCore.QTimer()
# connect the timer to the update function
timer.timeout.connect(update_label_colors)
# start the timer with an interval of 1000 milliseconds (1 second)
timer.start(1000)

But then after this is called:

def stop_build_timer():
    ...
    timer.stop()
    ...

And then this:

def start_build_timer():
    ...
    timer.timeout.connect(update_label_text)
    ...

The timer gets reinitialized but never seems to call the update_label_colors function.

Minimal example to reproduce the behavior:
Start the program and then press o on you keyboard to stop the timer. Then press l to restart and you will see that the label no longer gets updated.

import sys
from PyQt5 import QtWidgets, QtCore
from PyQt5.QtWidgets import QApplication
from pynput import keyboard


def on_press(key):
    if key.char == 'o':
        stop_build_timer()
    if key.char == 'l':
        start_build_timer()


def stop_build_timer():
    global timer, elapsed_time
    if timer:
        timer.stop()
        timer.deleteLater()
    elapsed_time = 0
    update_label_text()


def start_build_timer():
    global timer, elapsed_time
    elapsed_time = 0
    # create a new timer object
    timer = QtCore.QTimer()
    # connect the timer to the update function
    timer.timeout.connect(update_label_text)
    # start the timer with an interval of 1000 milliseconds (1 second)
    timer.start(1000)


def update_label_text():
    global elapsed_time
    elapsed_time += 1
    label.setText(f'Elapsed time: {elapsed_time} seconds')


elapsed_time = 0
app = QApplication(sys.argv)
window = QtWidgets.QWidget()
window.setGeometry(100, 100, 300, 200)
layout = QtWidgets.QVBoxLayout(window)
label = QtWidgets.QLabel('Elapsed time: 0 seconds')
layout.addWidget(label)
timer = QtCore.QTimer()
timer.timeout.connect(update_label_text)
timer.start(1000)
listener = keyboard.Listener(on_press=on_press)
listener.start()
window.show()
sys.exit(app.exec_())

In the comments Carl HR mentioned that they get an error message in the terminal when pressing the o or l keys:

    QObject::killTimer: Timers cannot be stopped from another thread
    QObject::startTimer: Timers can only be used with threads started with QThread

For some reason I didn’t get those when running my test program in pycharm, but this indicates that the timers are not being called from the correct main thread. I know there are built in methods in PyQT5 to add shortcuts to timers. Sadly I need to use another library like pyinput because I also need to capture keystrokes that happen while the window is not in focus.

Asked By: Neutral

||

Answers:

Note: I don’t know if this answer applies, because even if the application works without any crashes (at least on my PC), I never tested this type of approach before. Use at your risk.


In order to communicate from a PyQt5 thread to the main thread, you must use a PyqtSignal. According to the docs, we can use the QueuedConnection connection to pass data between threads in a safe way. But from all examples I’ve seen on the internet so far about multithreading on qt5 topic, there’re just examples about using QThreads. That raised me this doubt: does this really works with all kinds of threads, or just QThreads?

On the docs, it doesn’t specify anywhere that these threads need to be created with the Qt API. So, in theory, this approach should work with all kinds of threads (even from other modules). Which its pretty interesting if it’s true.

So, if we put this into practice, this is the modified version of your Mininimal Reproducible Example:

import sys
from PyQt5 import QtWidgets, QtCore
from PyQt5.QtWidgets import QApplication
from pynput import keyboard
from PyQt5.QtCore import QObject, pyqtSignal, Qt

# Added this custom class to store the signals. In order to
# initialize signals, they must be part of a QObject.
class SignalHandler(QObject):
    signal_stop = pyqtSignal()
    signal_start = pyqtSignal()

def on_press(key):
    if key.char == 'o':
        sh.signal_stop.emit() # Emit the signal instead of calling the
        #                       function directly
    if key.char == 'l':
        sh.signal_start.emit() # Same here

def stop_build_timer():
    global timer, elapsed_time

    print('Stop')
    if timer:
        timer.stop()
        timer.deleteLater()
    elapsed_time = 0
    update_label_text()

def start_build_timer():
    global timer, elapsed_time
    elapsed_time = 0

    print('Start')
    # create a new timer object
    timer = QtCore.QTimer()
    # connect the timer to the update function
    timer.timeout.connect(update_label_text)
    # start the timer with an interval of 1000 milliseconds (1 second)
    timer.start(1000)


def update_label_text():
    global elapsed_time
    elapsed_time += 1
    label.setText(f'Elapsed time: {elapsed_time} seconds')


elapsed_time = 0
app = QApplication(sys.argv)
window = QtWidgets.QWidget()
window.setGeometry(100, 100, 300, 200)
layout = QtWidgets.QVBoxLayout(window)
label = QtWidgets.QLabel('Elapsed time: 0 seconds')
layout.addWidget(label)
timer = QtCore.QTimer()
timer.timeout.connect(update_label_text)
timer.start(1000)

# Create the signals, and connect them to the respective functions
# using the QueuedConnection mode.
sh = SignalHandler()
sh.signal_stop.connect(stop_build_timer, Qt.QueuedConnection)
sh.signal_start.connect(start_build_timer, Qt.QueuedConnection)

listener = keyboard.Listener(on_press=on_press)
listener.start()
window.show()
sys.exit(app.exec_())

When executing it, the timer should be stopped and started with no problems. If a crash occurs, please post what happened on the comments 🙂

Answered By: Carl HR
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.