Skip to content

到目前为止,我们所涵盖的主题已经足以使用PyQt6构建功能完善的桌面应用程序。在本章中,我们将探讨Qt框架中一些更具技术性且不太为人所知的方面,以加深对系统运作原理的理解。对于许多应用程序而言,本章所涉及的主题并非必要,但它们是工具箱中值得拥有的资源,以便在需要时随时调用!

31. 计时器

在应用程序中,您经常需要定期执行某些任务,甚至只是在未来某个时间点执行。在 PyQt6 中,这是通过使用定时器来实现的。QTimer 类为您提供两种不同类型的定时器——循环定时器或间隔定时器,以及单次定时器或一次性定时器。这两种定时器都可以与应用程序中的函数和方法关联,使其在需要时执行。在本章中,我们将探讨这两种定时器类型,并演示如何使用它们来自动化您的应用程序。

间隔计时器

使用 QTimer 类,您可以创建任何持续时间(以毫秒为单位)的间隔计时器。在每个指定的时间段,计时器都会超时。为了触发每次发生时都会发生的事情,您可以将计时器的超时信号连接到您想要执行的任何操作——就像处理其他信号一样。

在下面的示例中,我们设置了一个定时器,每 100 毫秒运行一次,该定时器旋转一个刻度盘。

Listing 232. further/timers_1.py

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

这只是一个简单的例子——您可以在连接的方法中做任何您想做的事情。但是,标准事件循环规则仍然适用,触发任务应快速返回,以避免阻塞图形用户界面。如果您需要执行定期的长期任务,可以使用计时器触发一个单独的线程或进程。

tips

必须在计时器运行期间始终保留对创建的计时器对象的引用。如果未保留引用,计时器对象将被删除,计时器将停止运行——且不会有任何警告。如果您创建了计时器但它似乎无法正常工作,请检查是否已保留对该对象的引用。

如果计时器的精度很重要,您可以通过将一个 Qt.QTimerType 值传递给 timer.setTimerType 来调整它。

Listing 233. further/timers_1b.py

python
        self.timer.setTimerType(Qt.TimerType.PreciseTimer)

可用的选项如下所示。不要让计时器比实际需要的更精确,否则可能会阻塞重要的 UI 更新。

计时器类型描述
Qt.TimerType.PreciseTimer0精准计时器力求保持毫秒级精度
Qt.TimerType.CoarseTimer1粗略计时器试图将精度保持在目标间隔的5%以内
Qt.TimerType.VeryCoarseTimer2非常粗糙的计时器仅能保持整秒精度

请注意,即使是最精确的计时器也只能保持毫秒级的精度。图形用户界面线程中的任何内容都可能被 UI 更新和您自己的 Python 代码阻塞。如果精度非常重要,请将工作放在另一个线程或您完全控制的进程中。

单次计时器

如果您想触发某个操作,但只希望它发生一次,可以使用单次触发定时器。这些定时器是通过 QTimer 对象的静态方法构建的。最简单的形式只需接受一个以毫秒为单位的时间参数,以及您希望在定时器触发时调用的可调用对象——例如,您希望运行的方法。

在下面的示例中,我们使用单次计时器在按下可切换的按钮后取消其选中状态。

Listing 234. further/timers_2.py

python
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()
  1. uncheck_button 方法将在 1000 毫秒后被调用。

运行此示例并按下按钮后,您会看到按钮被选中并变为红色——这是使用了自定义样式。一秒钟后,按钮将恢复为未选中状态。

为了实现这一点,我们使用单次计时器将两个自定义方法链接在一起。首先,我们将按钮的切换信号连接到方法 button_checked。这会触发单次计时器。当计时器超时时,它会调用 uncheck_button,该方法实际上会取消选中该按钮。这使我们能够将取消选中该按钮的时间推迟到可配置的时间。

与间隔定时器不同,您无需保留创建的定时器(QTimer)的引用 —— QTimer.singleShot() 方法不会返回定时器引用。

通过事件队列进行延迟处理

您可以使用零延迟单次定时器通过事件队列延迟操作。当定时器触发时,定时器事件会被添加到事件队列的末尾(因为它是新事件),并且只有在所有现有事件都被处理完毕后才会被处理。

请记住,信号(和事件)只有在您将控制权从 Python 返回事件循环后才会被处理。如果您在一个方法中触发了一系列信号,并且希望在它们发生后执行某些操作,则不能直接在同一个方法中执行。该方法中的代码将在信号生效之前被执行。

python
def my_method(self):
    self.some_signal.emit()
    self.some_other_signal.emit()
    do_something_here() #1
  1. 该函数将在两个信号生效之前执行。

通过使用单次计时器,您可以将后续操作推送到事件队列的末尾,并确保它最后执行。

python
def my_method(self):
    self.some_signal.emit()
    self.some_other_signal.emit()
    QTimer.singleShot(0, do_something_here) #1
  1. 这将在信号的效果执行之后执行。

alert

此技术仅保证 do_something_here 函数在先前的信号之后执行,而不保证这些信号的任何下游效果。不要试图通过增加 msecs 的值来解决这个问题,因为这会使您的应用程序依赖于系统计时。