Search code examples
qtpyqtpyqt6

PyQT: How to show elapsed time after pressing a button


In this simple example I want to have:

  • a button which starts doing 2 seconds of work
  • a label which shows for how long the work has been running

When I run my file, it works fine but shows:

QObject::killTimer: Timers cannot be stopped from another thread

I'm new to PyQT, I have no idea how to fix this.

Does it even make sense to use threading.Thread, or should I use QThread for my use-case, or some entirely different code organization?

Is it common to use threading to not block the GUI or am I reinventing the wheel?

from PySide6.QtWidgets import (
    QApplication, QWidget, QVBoxLayout, QPushButton, QLabel
)
from PySide6.QtCore import QTime, QTimer
import threading
import time


class MyWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.resize(200, 100)
        self.button = QPushButton('Do work', self)
        self.button.clicked.connect(self.run_work_thread)
        self.timer_label = QLabel('', self)  # hide label at the start (but save its space)
        layout = QVBoxLayout(self)
        layout.addWidget(self.button)
        layout.addWidget(self.timer_label)
        self.setLayout(layout)

    def run_work_thread(self):
        """Do work in a separate thread, not to block the GUI"""
        self.button.setEnabled(False)
        thread = threading.Thread(target=self.do_work)
        thread.start()

        self.start_time = QTime(0, 0, 0)
        self.timer = QTimer(self)
        self.timer_label.setText('Doing work for: 0 s')
        self.timer.timeout.connect(self.update_timer)
        self.timer.start(1000)  # update every 1 sec

    def do_work(self):
        time.sleep(2)  # Simulate work
        self.timer.stop()
        self.timer_label.setText('')  # clear the timer label
        self.button.setEnabled(True)

    def update_timer(self):
        """Update the timer, and its label with the elapsed time"""
        self.start_time = self.start_time.addMSecs(1000)
        self.timer_label.setText(f'Doing work for: {self.start_time.toString("s")} s')


if __name__ == '__main__':
    app = QApplication([])
    window = MyWindow()
    window.show()
    app.exec()

Solution

  • Following the @musicamante 's helpful suggestion, here is the solution I did:

    from PySide6.QtWidgets import (
        QApplication, QWidget, QVBoxLayout, QPushButton, QLabel
    )
    from PySide6.QtCore import QTime, QTimer, QThread, Signal
    import time
    
    
    class Worker(QThread):
        def run(self):
            time.sleep(2)  # Simulate work
    
    
    class MyWindow(QWidget):
        def __init__(self):
            super().__init__()
            self.resize(200, 100)
            self.button = QPushButton('Do work', self)
            self.button.clicked.connect(self.run_work)
            self.timer_label = QLabel('', self)
            layout = QVBoxLayout(self)
            layout.addWidget(self.button)
            layout.addWidget(self.timer_label)
            self.setLayout(layout)
    
            ### ADDED: ###
            self.worker = Worker()
            self.worker.finished.connect(self.on_work_finished)
            ##############
    
        def run_work(self):
            """Start the work and the timer"""
            self.button.setEnabled(False)
            self.start_time = QTime(0, 0, 0)
            self.timer = QTimer(self)
            self.timer_label.setText('Doing work for: 0 s')
            self.timer.timeout.connect(self.update_timer)
            self.timer.start(1000)  # update every 1 sec
            self.worker.start()
    
        def on_work_finished(self):
            """Stop the timer and reset the UI when work is done"""
            self.timer.stop()
            self.timer_label.setText('')
            self.button.setEnabled(True)
    
        def update_timer(self):
            """Update the timer and its label with the elapsed time"""
            self.start_time = self.start_time.addSecs(1)
            self.timer_label.setText(f'Doing work for: {self.start_time.toString("s")} s')
    
    if __name__ == '__main__':
        app = QApplication([])
        window = MyWindow()
        window.show()
        app.exec()