如何将 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)
如何每 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)