如何将 QTimer 间隔与系统时间对齐

How to align a QTimer interval with the system time

如何每 5 分 20 秒启动一次函数?我用过:

self.timer = QtCore.QTimer(self)
self.timer.timeout.connect(self.updateplot)
self.timer.start(310000)

但我想在我的 PC 上设置它 - 所以如果我在 3:23 启动程序,更新轨道在 3:25:20、3:30:20 等.

为此需要两个计时器:单次到达起点,然后触发第二个计时器,每 5 分钟(第 20 秒)超时一次。起点可以用 QDateTime.msecsTo 计算,所以代码看起来像这样:

mins = 5
secs = 20
self.timer = QtCore.QTimer(self)
self.timer.timeout.connect(self.updateplot)
self.timer.setInterval(mins * 60 * 1000)
def start_point():
    self.timer.timeout.emit()
    self.timer.start()
d1 = QtCore.QDateTime.currentDateTimeUtc()
d2 = QtCore.QDateTime(d1)
t1 = d1.time()
d2.setTime(QtCore.QTime(t1.hour(), t1.minute(), secs))
if t1.second() > secs:
    d2 = d2.addSecs((mins - t1.minute() % mins) * 60)
QtCore.QTimer.singleShot(d1.msecsTo(d2), start_point)

请注意,这取决于系统时钟的准确性,并且在计算起点时也会有几分之一秒的延迟 - 所以不要指望它与网络时间保持完美同步.如果您需要长时间 运行,您可以在 updateplot 中查看当前时间,如果它开始漂移超过某个阈值则重新启动计时器。

更新:

这是一个使用上述方法的演示:

import sys
from PyQt5 import QtCore, QtWidgets

class Window(QtWidgets.QWidget):
    def __init__(self):
        super(Window, self).__init__()
        self.spinMins = QtWidgets.QSpinBox()
        self.spinMins.setRange(0, 59)
        self.spinMins.setValue(1)
        self.spinSecs = QtWidgets.QSpinBox()
        self.spinSecs.setRange(0, 59)
        self.spinSecs.setValue(5)
        self.button = QtWidgets.QPushButton('Start')
        self.button.clicked.connect(self.resetTimer)
        self.edit = QtWidgets.QTextEdit()
        self.edit.setReadOnly(True)
        layout = QtWidgets.QGridLayout(self)
        layout.addWidget(self.edit, 0, 0, 1, 3)
        layout.addWidget(self.spinMins, 1, 0)
        layout.addWidget(self.spinSecs, 1, 1)
        layout.addWidget(self.button, 1, 2)
        self.mainTimer = QtCore.QTimer(self)
        self.mainTimer.timeout.connect(self.updateplot)
        self.startTimer = QtCore.QTimer(self)
        self.startTimer.setSingleShot(True)
        def start_point():
            self.mainTimer.timeout.emit()
            self.mainTimer.start()
        self.startTimer.timeout.connect(start_point)
        self.resetTimer()

    def resetTimer(self):
        self.mainTimer.stop()
        self.startTimer.stop()
        mins = self.spinMins.value()
        secs = self.spinSecs.value()
        self.edit.append('restarting timer... (%dm %ds)' % (mins, secs))
        self.mainTimer.setInterval(mins * 60 * 1000)
        d1 = QtCore.QDateTime.currentDateTimeUtc()
        d2 = QtCore.QDateTime(d1)
        t1 = d1.time()
        d2.setTime(QtCore.QTime(t1.hour(), t1.minute(), secs))
        if t1.second() > secs:
            d2 = d2.addSecs((mins - t1.minute() % mins) * 60)
        self.startTimer.start(d1.msecsTo(d2))

    def updateplot(self, t=None):
        t = QtCore.QTime.currentTime()
        self.edit.append('timeout: %s' % t.toString('HH:mm:ss.zzz'))

if __name__ == '__main__':

    app = QtWidgets.QApplication(sys.argv)
    window = Window()
    window.setWindowTitle('Timer Test')
    window.setGeometry(600, 100, 300, 200)
    window.show()
    sys.exit(app.exec_())

我对这种计时器有一些经验,因为我为自己的目的开发了一个个人 "to do" 程序,其中包含以特定时间间隔触发的自定义提醒,其偏移量为 "hour/minute sensitive".
经过一番挣扎,我意识到最好的方法是子类化并实现自己的计时器;虽然这种实施方式乍一看似乎有点过火,但我相信它可以真正节省时间。

要注意的最重要方面是确保计时器始终与系统时钟保持一致,并且显然要尽可能正确地获取时间间隔。为此,我选择了 singleShot set timer 来设置每次超时的时间间隔,而不是连续计时器。由于我们可以假设这种间隔使用较高的值(通常大于一分钟),因此计算每次超时的间隔不是一个大问题,CPU-wise。

class SpecialTimer(QtCore.QTimer):
    publicTimeout = QtCore.pyqtSignal()
    def __init__(self, minutes=5, offsetSecs=20):
        super().__init__(singleShot=True)
        assert isinstance(minutes, int) and 0 < minutes < 60 and not 60 % minutes, \
            '"minutes" can only be a no-remainder-int-modulus of 60 (5, 6, 10, etc.)'
        self.minutes = minutes
        # offset can be greater than 60 secs: you might want to use a 5 minutes
        # interval, but emit the timeout at a 2 minute offset (23:02, 23:07, etc);
        # nonetheless that offset should always be < minutes * 60
        self.offsetMins, self.offsetSecs = divmod(offsetSecs, 60)
        assert self.offsetMins < minutes, 'offset has to be less than minutes'
        self.offsetMins %= minutes

        # "swap" the base timeout signal with a custom one; while this isn't
        # really necessary, it helps to keep a cleaner implementation, as calling
        # "QTimer.timeout.disconnect()" would disconnect the restart slot too;
        # in this way you can safely and transparently [dis]connect the "timeout"
        # signal while keeping its functionality in the meantime.
        self._timeout = self.timeout
        self._timeout.connect(self.start)
        self.timeout = self.publicTimeout
        self._timeout.connect(self.timeout)

    def start(self, *args):
        current = QtCore.QTime.currentTime()
        nextMinute = current.minute() + self.minutes - current.minute() % self.minutes
        nextMinute += self.offsetMins
        nextHour = current.hour()
        # take care of minute >= 60 and hour >= 24 exceptions, as QTime only
        # accepts 0-59 minute range and 0-23 hour range
        if nextMinute >= 60:
            nextMinute %= 60
            nextHour += 1
            nextHour %= 24
        nextTimeout = QtCore.QTime(nextHour, nextMinute, self.offsetSecs)
        # check the nextTimeout, as the minute/second offset could make a first
        # interval greater than it should be, which will result in skipping the
        # first possible timeout
        if nextTimeout.addSecs(-self.minutes * 60) > current:
            nextTimeout = nextTimeout.addSecs(-self.minutes * 60)
        # if the next timeout happens after midnight, "msecsTo" will return a
        # negative value, let's use the % modulus to get a positive value
        super().start(current.msecsTo(nextTimeout) % 86400000)