到目前为止,我们所涵盖的主题已经足以使用PyQt6构建功能完善的桌面应用程序。在本章中,我们将探讨Qt框架中一些更具技术性且不太为人所知的方面,以加深对系统运作原理的理解。对于许多应用程序而言,本章所涉及的主题并非必要,但它们是工具箱中值得拥有的资源,以便在需要时随时调用!
31. 计时器
在应用程序中,您经常需要定期执行某些任务,甚至只是在未来某个时间点执行。在 PyQt6 中,这是通过使用定时器来实现的。QTimer
类为您提供两种不同类型的定时器——循环定时器或间隔定时器,以及单次定时器或一次性定时器。这两种定时器都可以与应用程序中的函数和方法关联,使其在需要时执行。在本章中,我们将探讨这两种定时器类型,并演示如何使用它们来自动化您的应用程序。
间隔计时器
使用 QTimer
类,您可以创建任何持续时间(以毫秒为单位)的间隔计时器。在每个指定的时间段,计时器都会超时。为了触发每次发生时都会发生的事情,您可以将计时器的超时信号连接到您想要执行的任何操作——就像处理其他信号一样。
在下面的示例中,我们设置了一个定时器,每 100 毫秒运行一次,该定时器旋转一个刻度盘。
Listing 232. further/timers_1.py
import sys
from PyQt6.QtCore import QTimer
from PyQt6.QtWidgets import QApplication, QDial, QMainWindow
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.dial = QDial()
self.dial.setRange(0, 100)
self.dial.setValue(0)
self.timer = QTimer()
self.timer.setInterval(10)
self.timer.timeout.connect(self.update_dial)
self.timer.start()
self.setCentralWidget(self.dial)
def update_dial(self):
value = self.dial.value()
value += 1 # 递增
if value > 100:
value = 0
self.dial.setValue(value)
app = QApplication(sys.argv)
w = MainWindow()
w.show()
app.exec()
这只是一个简单的例子——您可以在连接的方法中做任何您想做的事情。但是,标准事件循环规则仍然适用,触发任务应快速返回,以避免阻塞图形用户界面。如果您需要执行定期的长期任务,可以使用计时器触发一个单独的线程或进程。
您必须在计时器运行期间始终保留对创建的计时器对象的引用。如果未保留引用,计时器对象将被删除,计时器将停止运行——且不会有任何警告。如果您创建了计时器但它似乎无法正常工作,请检查是否已保留对该对象的引用。
如果计时器的精度很重要,您可以通过将一个 Qt.QTimerType
值传递给 timer.setTimerType
来调整它。
Listing 233. further/timers_1b.py
self.timer.setTimerType(Qt.TimerType.PreciseTimer)
可用的选项如下所示。不要让计时器比实际需要的更精确,否则可能会阻塞重要的 UI 更新。
计时器类型 | 值 | 描述 |
---|---|---|
Qt.TimerType.PreciseTimer | 0 | 精准计时器力求保持毫秒级精度 |
Qt.TimerType.CoarseTimer | 1 | 粗略计时器试图将精度保持在目标间隔的5%以内 |
Qt.TimerType.VeryCoarseTimer | 2 | 非常粗糙的计时器仅能保持整秒精度 |
请注意,即使是最精确的计时器也只能保持毫秒级的精度。图形用户界面线程中的任何内容都可能被 UI 更新和您自己的 Python 代码阻塞。如果精度非常重要,请将工作放在另一个线程或您完全控制的进程中。
单次计时器
如果您想触发某个操作,但只希望它发生一次,可以使用单次触发定时器。这些定时器是通过 QTimer
对象的静态方法构建的。最简单的形式只需接受一个以毫秒为单位的时间参数,以及您希望在定时器触发时调用的可调用对象——例如,您希望运行的方法。
在下面的示例中,我们使用单次计时器在按下可切换的按钮后取消其选中状态。
Listing 234. further/timers_2.py
import sys
from PyQt6.QtCore import QTimer
from PyQt6.QtWidgets import QApplication, QMainWindow, QPushButton
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.button = QPushButton("Press me!")
self.button.setCheckable(True)
self.button.setStyleSheet(
# 将复选框状态设置为红色,以便更容易辨识.
"QPushButton:checked { background-color: red; }"
)
self.button.toggled.connect(self.button_checked)
self.setCentralWidget(self.button)
def button_checked(self):
print("Button checked")
QTimer.singleShot(1000, self.uncheck_button) #1
def uncheck_button(self):
print("Button unchecked")
self.button.setChecked(False)
app = QApplication(sys.argv)
w = MainWindow()
w.show()
app.exec()
uncheck_button
方法将在 1000 毫秒后被调用。
运行此示例并按下按钮后,您会看到按钮被选中并变为红色——这是使用了自定义样式。一秒钟后,按钮将恢复为未选中状态。
为了实现这一点,我们使用单次计时器将两个自定义方法链接在一起。首先,我们将按钮的切换信号连接到方法 button_checked
。这会触发单次计时器。当计时器超时时,它会调用 uncheck_button
,该方法实际上会取消选中该按钮。这使我们能够将取消选中该按钮的时间推迟到可配置的时间。
与间隔定时器不同,您无需保留创建的定时器(QTimer)的引用 —— QTimer.singleShot()
方法不会返回定时器引用。
通过事件队列进行延迟处理
您可以使用零延迟单次定时器通过事件队列延迟操作。当定时器触发时,定时器事件会被添加到事件队列的末尾(因为它是新事件),并且只有在所有现有事件都被处理完毕后才会被处理。
请记住,信号(和事件)只有在您将控制权从 Python 返回事件循环后才会被处理。如果您在一个方法中触发了一系列信号,并且希望在它们发生后执行某些操作,则不能直接在同一个方法中执行。该方法中的代码将在信号生效之前被执行。
def my_method(self):
self.some_signal.emit()
self.some_other_signal.emit()
do_something_here() #1
- 该函数将在两个信号生效之前执行。
通过使用单次计时器,您可以将后续操作推送到事件队列的末尾,并确保它最后执行。
def my_method(self):
self.some_signal.emit()
self.some_other_signal.emit()
QTimer.singleShot(0, do_something_here) #1
- 这将在信号的效果执行之后执行。
此技术仅保证
do_something_here
函数在先前的信号之后执行,而不保证这些信号的任何下游效果。不要试图通过增加msecs
的值来解决这个问题,因为这会使您的应用程序依赖于系统计时。