Home > database >  How to create multiple stopwatch timers in PyQt5?
How to create multiple stopwatch timers in PyQt5?

Time:10-10

I'm trying to make a GUI which have multiple timers. When I click one button, it will start a timer and display in a tableWidget. And this tableWidget have two columns which represent number and time, just like this:(each timer will run simultaneously)

Name Time Elapse
Thing#1 00:01
Thing#2 00:03
Thing#3 01:23

My original code is this:(not the whole part)

x=0
self.Button1.clicked.connect(self.btn)
def btn(self):
    #set Thing#1, Thing#2... to the Name column
    self.newItem = QTableWidgetItem('{}'.format(self.num))
    self.newItem.setTextAlignment(Qt.AlignCenter)
    self.tablewidget.setItem(self.x, 0, self.newItem)
                
    #start timer
    self.timer = QTimer()
    self.time = QTime(0, 0, 0)
    self.timer.start(1000)
    self.timer.timeout.connect(self.showTime)
        
    self.x  = 1

def showTime(self):
    self.time = self.time.addSecs(1)
    timeDisplay = self.time.toString("mm:ss")
    self.newItem2 = QTableWidgetItem(timeDisplay)
    self.newItem2.setTextAlignment(Qt.AlignCenter)
    self.tablewidget.setItem(self.x - 1, 1, self.newItem2)

And what I get is when I clicked that button in order to create Thing#1, and it works at first. However, when I clicked that button again, Thing#1 stopped and Thing#2 started. The strangest thing is the speed of time doubled, just like fastfoward 2x in watching video. And when I clicked again, it's 3x speed and so on.

I really don't know why, and I can't find anything about it on the Internet. However, I do find something about how to make multiple timer in PyQt, it said to create multiple QTimer, so I changed the code to this:

x=0
self.timer_dict={}
self.time_dict={}
self.Button1.clicked.connect(self.btn)
def btn(self):
    #set Thing#1, Thing#2... to the Name column
    self.newItem = QTableWidgetItem('{}'.format(self.num))
    self.newItem.setTextAlignment(Qt.AlignCenter)
    self.tablewidget.setItem(self.x, 0, self.newItem)
                
    #start timer
    self.timer_dict["{}".format(self.num)] = QTimer()
    self.time_dict["{}".format(self.num)] = QTime(0, 0, 0)
    self.timer_dict["{}".format(self.num)].start(1000)
    self.timer_dict["{}".format(self.num)].timeout.connect(self.showTime)
        
    self.x  = 1

def showTime(self):
    self.time_dict['{}'.format(self.num)] = self.time_dict['{}'.format(self.num)].addSecs(1)
    timeDisplay = self.time_dict['{}'.format(self.num)].toString("mm:ss")
    self.newItem2 = QTableWidgetItem(timeDisplay)
    self.newItem2.setTextAlignment(Qt.AlignCenter)
    self.tablewidget.setItem(self.x - 1, 1, self.newItem2)

It just for each 'Thing', create corresponding QTimer object. However, I got the same result as the original one.

But I do have some new findings. When I accidentally do this:

def btn(self):
    self.timer_dict={}
    self.time_dict={}
    #set Thing#1, Thing#2... to the Name column
    self.newItem = QTableWidgetItem('{}'.format(self.num))
    .........

It will reset the self.timer_dict and self.time_dict each time when I clicked that button. It can't make multiple timer, but at least the speed of time won't be 2x, 3x.

I guess that I use self.tablewidget.setItem(self.x - 1, 1, self.newItem2)(last line of code) to update the timer. If I clicked the button, it will moved to the next row and the last row will not update. Thus, I can only have one timer at one time although it should be multiple timer in the background.

And maybe I should use multithread or QThread to achieve my goal? I really don't get it.

UPDATE: Some reproducible code about my question, I think maybe the reason why I got speed up time is that I use another QTimer to update the current time?

from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import QTime, Qt, QTimer, QDateTime
from PyQt5.QtWidgets import *
import sys

class MainWindow(QMainWindow):
    # create the window
    def __init__(self):
        super(MainWindow, self).__init__()
        self.show_QinputDialog()

    x = 0

    # create widget on the window
    def initUI(self, n):
        self.setFixedSize(220, 320)
        self.setWindowTitle("Multiple Timer System")
        self.center()

        # set layout
        self.centralwidget = QtWidgets.QWidget(self)
        self.widget = QtWidgets.QWidget(self.centralwidget)
        self.widget.setGeometry(QtCore.QRect(10, 10, 200, 300))
        self.gridLayout = QtWidgets.QGridLayout(self.widget)
        self.gridLayout.setContentsMargins(0, 0, 0, 0)
        self.setCentralWidget(self.centralwidget)

        # Section 1: Set Timer
        self.Group1 = QtWidgets.QGroupBox(self.widget)
        self.horizontalLayout = QtWidgets.QHBoxLayout(self.Group1)
        self.comboBox1 = QtWidgets.QComboBox(self.Group1)
        for i in range(1, n   1):
            self.comboBox1.addItem("Timer#{}".format(i))
        self.horizontalLayout.addWidget(self.comboBox1)

        self.Button1 = QtWidgets.QPushButton(self.Group1)
        self.horizontalLayout.addWidget(self.Button1)

        self.gridLayout.addWidget(self.Group1, 0, 0, 1, 1)


        # Section 2: Display Current Time
        self.Group2 = QtWidgets.QGroupBox(self.widget)
        self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.Group2)

        self.label_1 = QtWidgets.QLabel(self.Group2)
        self.horizontalLayout_2.addWidget(self.label_1)
        self.showCurrentTime()

        self.timer = QTimer()
        self.timer.start(1000)
        self.timer.timeout.connect(self.showCurrentTime)

        self.gridLayout.addWidget(self.Group2, 1, 0, 1, 1)

        # Section 3: Table
        self.Group3 = QtWidgets.QGroupBox(self.widget)
        self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.Group3)

        self.tablewidget = QtWidgets.QTableWidget(self.Group3)
        self.tablewidget.setRowCount(n)
        self.tablewidget.setColumnCount(2)
        self.tablewidget.setHorizontalHeaderLabels(['Name', 'Time Elapse'])
        self.tablewidget.resizeRowsToContents()
        self.tablewidget.setColumnWidth(0, 76)
        self.tablewidget.setColumnWidth(1, 77)

        # set rowHeader invisible
        self.tablewidget.verticalHeader().setVisible(False)
        # ban editing
        self.tablewidget.setEditTriggers(QAbstractItemView.NoEditTriggers)
        # ban selection
        self.tablewidget.setSelectionMode(QAbstractItemView.NoSelection)

        self.verticalLayout_3.addWidget(self.tablewidget)

        self.gridLayout.addWidget(self.Group3, 2, 0, 1, 1)

        self.Group2.setTitle("Current Time")
        self.Group3.setTitle("Current Running Timer")
        self.Group1.setTitle("Start Timer")
        self.Button1.setText("Start")

        self.timer_dict={}
        self.time_dict={}

        self.Button1.clicked.connect(self.start_timer)

    def show_QinputDialog(self):
        # show dialog to let user input the number of timers
        dialog = QInputDialog(self)
        dialog.setIntRange(1, 100)
        dialog.setInputMode(QInputDialog.IntInput)
        dialog.setLabelText("Please enter the number of timer:(1-100)")
        dialog.setWindowTitle("Multiple Timer System")
        dialog.setIntValue(10)
        self.center()
        if dialog.exec_() == QtWidgets.QDialog.Accepted:
            self.n = dialog.intValue()
            self.timer_list = []
            for i in range(1, self.n   1):
                self.timer_list.extend(['Timer#{}'.format(i)])
            self.initUI(self.n)

    def center(self):
        screen = QDesktopWidget().screenGeometry()
        size = self.geometry()
        newLeft = (screen.width() - size.width()) / 2
        newTop = (screen.height() - size.height()) / 2
        self.move(newLeft, newTop)

    def start_timer(self):
        self.num_index = self.comboBox1.currentIndex()
        self.num = self.comboBox1.currentText()
        if self.num:
            self.comboBox1.removeItem(self.num_index)

            self.newItem = QTableWidgetItem('{}'.format(self.num))
            self.newItem.setTextAlignment(Qt.AlignCenter)
            self.tablewidget.setItem(self.x, 0, self.newItem)

            self.timer_dict["{}".format(self.num)] = QTimer()
            self.time_dict["{}".format(self.num)] = QTime(0, 0, 0)
            self.timer_dict["{}".format(self.num)].start(1000)
            self.timer_dict["{}".format(self.num)].timeout.connect(self.showTime)

            self.x  = 1

    def showCurrentTime(self):
        self.ctime = QDateTime.currentDateTime()
        timeDisplay = self.ctime.toString("yyyy-MM-dd hh:mm:ss")
        self.label_1.setText(timeDisplay)

    def showTime(self):
        self.time_dict['{}'.format(self.num)] = self.time_dict['{}'.format(self.num)].addSecs(1)
        timeDisplay = self.time_dict['{}'.format(self.num)].toString("mm:ss")
        self.newItem2 = QTableWidgetItem(timeDisplay)
        self.newItem2.setTextAlignment(Qt.AlignCenter)
        self.tablewidget.setItem(self.x - 1, 1, self.newItem2)


if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    ui = MainWindow()
    ui.show()
    sys.exit(app.exec_())

CodePudding user response:

The problem is that you are using the same attribute for the QTimer causing you to delete the previous object (the same for newItem and newItem2). A possible solution is to create a class that stores an access to the items for example using QPersintentModelIndex and for the time use QTimer and QElapsedTimer.

from dataclasses import dataclass
from functools import cached_property

from PyQt5.QtCore import (
    QElapsedTimer,
    QModelIndex,
    QPersistentModelIndex,
    Qt,
    QTime,
    QTimer,
)
from PyQt5.QtWidgets import (
    QApplication,
    QHeaderView,
    QPushButton,
    QTableWidget,
    QTableWidgetItem,
    QVBoxLayout,
    QWidget,
)


@dataclass
class TimerData:
    name_index: QPersistentModelIndex
    time_index: QPersistentModelIndex

    @cached_property
    def timer(self):
        timer = QTimer(interval=500)
        timer.timeout.connect(self._handle_timeout)
        return timer

    @cached_property
    def timer_elapsed(self):
        return QElapsedTimer()

    def start(self):
        self.timer_elapsed.start()
        self.timer.start()
        self._handle_timeout()

    def stop(self):
        self.timer.stop()

    def _handle_timeout(self):
        if self.time_index.isValid():
            time = QTime.fromMSecsSinceStartOfDay(self.timer_elapsed.elapsed())
            model = self.time_index.model()
            model.setData(QModelIndex(self.time_index), time.toString("mm:ss"))


class Widget(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)

        self._timer_datas = list()

        self.button = QPushButton("Add")
        self.table = QTableWidget(0, 2)
        self.table.horizontalHeader().setStretchLastSection(True)
        self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)

        lay = QVBoxLayout(self)
        lay.addWidget(self.button)
        lay.addWidget(self.table)

        self.button.clicked.connect(self.handle_clicked)

    def handle_clicked(self):
        self.add_timer()

    def add_timer(self):
        row = self.table.rowCount()
        name_item = QTableWidgetItem(f"name-{row}")
        name_item.setTextAlignment(Qt.AlignCenter)
        time_item = QTableWidgetItem()
        time_item.setTextAlignment(Qt.AlignCenter)
        self.table.insertRow(row)
        self.table.setItem(row, 0, name_item)
        self.table.setItem(row, 1, time_item)
        timer_data = TimerData(
            QPersistentModelIndex(self.table.indexFromItem(name_item)),
            QPersistentModelIndex(self.table.indexFromItem(time_item)),
        )
        self._timer_datas.append(timer_data)
        timer_data.start()


def main():
    import sys

    app = QApplication(sys.argv)

    w = Widget()
    w.resize(640, 480)
    w.show()

    sys.exit(app.exec_())


if __name__ == "__main__":
    main()
  • Related