如何在 PyQt5 中创建多个秒表计时器?

How to create multiple stopwatch timers in PyQt5?

我正在尝试制作一个具有多个计时器的 GUI。当我单击一个按钮时,它将启动一个计时器并显示在一个 tableWidget 中。而这个tableWidget有两列代表数字和时间,就像这样:(每个计时器将同时运行)

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

我的原代码是这样的:(不是全部)

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)

当我单击该按钮以创建 Thing#1 时,我得到的结果是,它一开始就起作用了。然而,当我再次点击那个按钮时,Thing#1 停止了,而 Thing#2 开始了。最奇怪的是时间的速度翻了一番,就像看视频快进2倍一样。当我再次点击时,它是 3 倍速等等。

真不知道为什么,网上也查不到。但是,我确实找到了一些关于如何在 PyQt 中制作多个计时器的内容,它说要创建多个 QTimer,所以我将代码更改为:

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)

它只是为每个'Thing',创建对应的QTimer对象。但是,我得到的结果和原来的一样。

但我确实有一些新发现。当我不小心这样做时:

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

每次单击该按钮时,它都会重置 self.timer_dict 和 self.time_dict。它不能做多个定时器,但至少时间的速度不会是2x、3x。

我想我使用 self.tablewidget.setItem(self.x - 1, 1, self.newItem2)(最后一行代码)来更新计时器。如果我点击按钮,它会移动到下一行,最后一行不会更新。因此,我一次只能有一个计时器,尽管它应该在后台有多个计时器。

也许我应该使用多线程或 QThread 来实现我的目标?实在是看不懂。

更新: 关于我的问题的一些可重现的代码,我想我加快时间的原因可能是我使用另一个 QTimer 来更新当前时间?

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_())

问题是您对 QTimer 使用了相同的属性,导致您删除了之前的对象(对于 newItem 和 newItem2 也是如此)。一个可能的解决方案是创建一个 class 来存储对项目的访问,例如使用 QPersintentModelIndex 并暂时使用 QTimer 和 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()