1

I'm writing a simple stopwatch with python (using pyqt5). So far, start/pause/resume functions are working fine but the problem is when I stop the counter and need to start from 0 a thread can't be started again I tried the subclass from here but it's not working.

import threading
from PyQt5 import QtWidgets
from stpw_ui import Ui_MainWindow
from time import sleep

# code snippet from stack overflow to stop a thread
class StoppableThread(threading.Thread):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._stop_event = threading.Event()

    def stop(self):
        self._stop_event.set()

    def stopped(self):
        return self._stop_event.is_set()

class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
    def __init__(self):
        QtWidgets.QMainWindow.__init__(self)
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)

        # buttons' actions
        self.ui.start.clicked.connect(self.stopwatch)
        self.ui.pause.clicked.connect(self.pse)
        self.ui.stop.clicked.connect(self.stp)
        self.ui.resume.clicked.connect(self.res)
        # self.ui.reset.clicked.connect(self.rst)

        self.t = StoppableThread(target=self.strt)

    def UpdateSec(self,s):
        self.ui.seconds.display(s)

    def UpdateMin(self,m):
        self.ui.minutes.display(m)

    def stopwatch(self):
        self.ui.stacked_buttons.setCurrentIndex(1)
        self.pause = False
        self.t.start()

    pause = False
    second = 0
    minute = 0

    def setpause(self,x):
        self.pause = x

    # start
    def strt(self):
        if (self.pause is True):
            sleep(0.1)
        while self.second <= 59 and self.pause == False:
            self.UpdateSec(self.second)
            self.second += 1
            sleep(1)
        if self.second==59:
            self.minute += 1
            self.UpdateMin(self.minute)
            self.second = 0
        self.strt()

    # pause
    def pse(self):
        self.ui.stacked_buttons.setCurrentIndex(2)
        self.setpause(True)


    # stop
    def stp(self):
        self.setpause(True)
        self.ui.stacked_buttons.setCurrentIndex(0)
        self.t.stop()

    # resume
    def res(self):
        self.ui.stacked_buttons.setCurrentIndex(1)
        self.setpause(False)

    # reset / ignore it for now
    # def rst(self):
    #    self.ui.stacked_buttons.setCurrentIndex(0)
    #    self.setpause(False)


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

So:

  1. How to make the stop button works? (what I'm doing wrong here?)
  2. Is there a better/simple approach to make a stopwatch? because I feel like I'm over-complicating it.

The GUI (stpw_ui.py) file is available at https://www.filedropper.com/stpwui

eyllanesc
  • 235,170
  • 19
  • 170
  • 241

1 Answers1

3

Do not use sleep in a GUI because you can freeze the GUI. On the other hand using a thread is too much for this question, in a GUI the use of thread is only justified if the task is heavy, but increasing a pair of data every second is not a heavy task.

Instead, you must use QTimer to perform repetitive tasks and QTime to get the elapsed time.

For the logic a state machine must be implemented, in this case it will be StopWatch that will emit the necessary signals to notify the changes to the GUI.

from PyQt5 import QtCore, QtWidgets
from stpw_ui import Ui_MainWindow

class State:
    STOPPED = 0
    PAUSE = 1
    RUNNING = 1 << 1

class StopWatch(QtCore.QObject, State):
    State = State
    QtCore.Q_ENUMS(State)

    secondChanged = QtCore.pyqtSignal(int)
    minuteChanged = QtCore.pyqtSignal(int)
    stateChanged = QtCore.pyqtSignal(State)

    def __init__(self, parent=None):
        super(StopWatch, self).__init__(parent)
        self._current_state = State.STOPPED
        self._time = QtCore.QTime()
        self._timer = QtCore.QTimer(self, interval=100, timeout=self.on_timeout)
        self._delta = 0
        self._seconds = 0
        self._minutes = 0

    def setCurrentState(self, state):
        self._current_state = state
        self.stateChanged.emit(state)

    @QtCore.pyqtSlot()
    def start(self):
        self._delta = 0
        self._timer.start()
        self._time.start()
        self.setCurrentState(State.RUNNING)

    @QtCore.pyqtSlot()
    def stop(self):
        if self._current_state != State.STOPPED:
            self._timer.stop()
            self.setCurrentState(State.STOPPED)

    @QtCore.pyqtSlot()
    def pause(self):
        if self._current_state == State.RUNNING:
            self._timer.stop()
            self.setCurrentState(State.PAUSE)
            self._delta += self._time.elapsed()

    @QtCore.pyqtSlot()
    def resume(self):
        if self._current_state == State.PAUSE:
            self._timer.start()
            self._time = QtCore.QTime()
            self._time.start()
            self.setCurrentState(State.RUNNING)

    @QtCore.pyqtSlot()
    def on_timeout(self):
        t = QtCore.QTime.fromMSecsSinceStartOfDay(self._delta + self._time.elapsed())
        s, m = t.second(), t.minute()
        if self._seconds != s:
            self._seconds = s
            self.secondChanged.emit(s)

        if self._minutes != m:
            self._minutes = m
            self.minuteChanged.emit(m)

class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)
        self.setupUi(self)

        self._stop_watch = StopWatch(self)

        self.start.clicked.connect(self._stop_watch.start)
        self.pause.clicked.connect(self._stop_watch.pause)
        self.stop.clicked.connect(self._stop_watch.stop)
        self.resume.clicked.connect(self._stop_watch.resume)

        self._stop_watch.secondChanged.connect(self.seconds.display)
        self._stop_watch.minuteChanged.connect(self.minutes.display)
        self._stop_watch.stateChanged.connect(self.on_stateChanged)

    @QtCore.pyqtSlot(StopWatch.State)
    def on_stateChanged(self, state):
        if state == StopWatch.RUNNING:
            self.stacked_buttons.setCurrentIndex(1)
        elif state == StopWatch.PAUSE:
            self.stacked_buttons.setCurrentIndex(2)
        elif state == StopWatch.STOPPED:
            self.stacked_buttons.setCurrentIndex(0)

if __name__ == '__main__':
    import sys
    app = QtWidgets.QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())
eyllanesc
  • 235,170
  • 19
  • 170
  • 241