PySide6从0开始学习的笔记(十六) 定时器QTimer

PySide6 中的 QTimer 是用于实现定时任务的核心类,支持单次触发、重复触发、动态调整间隔等功能,广泛应用于界面刷新、定时检测、延时执行等场景。

一、核心概念

  • 定时器本质QTimer 依赖 Qt 的事件循环(QApplication.exec()),通过向事件队列发送定时事件实现触发,因此必须在事件循环运行的环境中使用。
  • 核心参数
    • 间隔(interval):定时触发的时间间隔,单位为毫秒(ms),默认 0(立即触发,但需等待事件循环)。
    • 单次触发(singleShot):是否仅触发一次(默认重复触发)。
    • 精度:默认精度为 1ms,但受系统调度影响,实际间隔可能略有偏差(毫秒级)。

二、基础用法

1. 基本使用步骤
  1. 导入 QTimer 类;
  2. 创建 QTimer 实例,绑定触发信号(timeout)到槽函数;
  3. 设置间隔 / 单次触发属性;
  4. 启动定时器。

示例 1:重复触发定时器(定时刷新界面)

复制代码
import sys
from PySide6.QtWidgets import QApplication, QWidget, QLabel, QVBoxLayout
from PySide6.QtCore import QTimer, Qt

class TimerDemo(QWidget):
    def __init__(self):
        super().__init__()
        self.count = 0  # 计数变量
        self.init_ui()
        self.init_timer()

    def init_ui(self):
        self.setWindowTitle("QTimer 重复触发示例")
        self.label = QLabel("计数:0", self)
        layout = QVBoxLayout()
        layout.addWidget(self.label)
        self.setLayout(layout)

    def init_timer(self):
        # 1. 创建定时器实例
        self.timer = QTimer(self)  # 绑定父对象,自动管理内存
        # 2. 设置间隔(1000ms = 1秒)
        self.timer.setInterval(1000)
        # 3. 绑定timeout信号到槽函数
        self.timer.timeout.connect(self.update_count)
        # 4. 启动定时器
        self.timer.start()

    def update_count(self):
        """定时器触发的槽函数:更新计数"""
        self.count += 1
        self.label.setText(f"计数:{self.count}")
        # 可选:计数到10停止定时器
        if self.count >= 5:
            self.timer.stop()
            self.label.setText("计数结束!")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = TimerDemo()
    window.show()
    sys.exit(app.exec())

示例 2:单次触发定时器(延时执行)

单次触发可直接使用 QTimer.singleShot() 静态方法

复制代码
import sys
from PySide6.QtWidgets import QApplication, QWidget, QPushButton, QVBoxLayout
from PySide6.QtCore import QTimer


class SingleShotDemo(QWidget):
    def __init__(self):
        super().__init__()
        self.init_ui()

    def init_ui(self):
        self.setWindowTitle("QTimer 单次触发示例")
        self.btn = QPushButton("点击后3秒提示", self)
        self.btn.clicked.connect(self.delayed_tip)

        layout = QVBoxLayout()
        layout.addWidget(self.btn)
        self.setLayout(layout)

    def delayed_tip(self):
        """点击按钮后,3秒后执行提示"""
        self.btn.setEnabled(False)
        # 静态方法:间隔(ms)、槽函数
        QTimer.singleShot(3000, self.show_tip)

    def show_tip(self):
        self.btn.setText("3秒到了!")
        self.btn.setEnabled(True)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = SingleShotDemo()
    window.show()
    sys.exit(app.exec())

三、核心 API 与常用属性

1. 常用方法
方法 说明
QTimer(parent=None) 构造函数,指定父对象可自动释放内存
setInterval(ms: int) 设置定时间隔(毫秒),最小为 1ms
interval() -> int 获取当前间隔
start(ms: int = None) 启动定时器;若传 ms,会覆盖之前的间隔
stop() 停止定时器
isActive() -> bool 判断定时器是否正在运行
setSingleShot(flag: bool) 设置是否单次触发(True = 单次,False = 重复)
isSingleShot() -> bool 判断是否为单次触发
QTimer.singleShot(ms, slot) 静态方法,快速创建单次定时器
2. 核心信号
信号 说明
timeout() 定时器间隔到达时触发(核心信号)
3. 静态常量(精度相关)
  • QTimer.PreciseTimer:高精度定时器(尽量保证间隔准确,适合对时间敏感的场景);
  • QTimer.CoarseTimer:低精度定时器(间隔误差 ±5%,默认,性能更好);
  • QTimer.VeryCoarseTimer:极低精度(间隔按秒对齐,适合无需精准的场景)。

设置精度示例:

复制代码
self.timer.setTimerType(QtCore.Qt.PreciseTimer)

四、高级技巧

1. 动态调整定时器间隔

可在运行时修改间隔,无需停止定时器:

复制代码
# 示例:点击按钮将间隔改为500ms
def change_interval(self):
    self.timer.setInterval(500)
    # 若定时器已启动,修改后立即生效
2. 多个定时器共享槽函数(区分来源)

通过 sender() 获取触发的定时器实例,实现多定时器统一处理:

复制代码
import sys
from PySide6.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel
from PySide6.QtCore import QTimer

class MultiTimerDemo(QWidget):
    def __init__(self):
        super().__init__()
        self.label1 = QLabel("定时器1:0")
        self.label2 = QLabel("定时器2:0")
        self.count1 = 0
        self.count2 = 0

        # 创建两个定时器
        self.timer1 = QTimer(self)
        self.timer1.setInterval(1000)
        self.timer1.timeout.connect(self.update_timer)
        
        self.timer2 = QTimer(self)
        self.timer2.setInterval(2000)
        self.timer2.timeout.connect(self.update_timer)

        # 启动定时器
        self.timer1.start()
        self.timer2.start()

        # 布局
        layout = QVBoxLayout()
        layout.addWidget(self.label1)
        layout.addWidget(self.label2)
        self.setLayout(layout)
        self.setWindowTitle("多定时器共享槽函数")

    def update_timer(self):
        """区分不同定时器更新计数"""
        timer = self.sender()  # 获取触发信号的定时器
        if timer == self.timer1:
            self.count1 += 1
            self.label1.setText(f"定时器1:{self.count1}")
        elif timer == self.timer2:
            self.count2 += 1
            self.label2.setText(f"定时器2:{self.count2}")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MultiTimerDemo()
    window.show()
    sys.exit(app.exec())
3. 定时器与多线程(关键注意事项)
  • QTimer 必须在创建它的线程中启动 / 停止,且该线程必须有事件循环(QThread.exec());

  • 不能在非主线程直接更新 UI(Qt UI 组件仅支持主线程操作),需通过信号槽跨线程通信。

    import sys
    from PySide6.QtWidgets import QApplication, QWidget, QPushButton
    from PySide6.QtCore import QTimer, QThread, Signal, QEventLoop

    class WorkerThread(QThread):
    progress = Signal(int)

    复制代码
      def __init__(self):
          super().__init__()
          self.count = 0
    
      def run(self):
          # 在子线程里再建定时器,这样它才属于子线程
          timer = QTimer()
          timer.setInterval(100)
          timer.timeout.connect(self.worker_task)
          timer.start()
    
          # 启动子线程事件循环
          self.exec()          # 用 self.exec() 而不是自建 QEventLoop
    
      def worker_task(self):
          self.count += 1
          self.progress.emit(self.count)
          if self.count >= 20:
              self.quit()      # 退出 exec() 事件循环

    class MainWindow(QWidget):
    def init(self):
    super().init()
    self.btn = QPushButton("启动子线程定时器", self)
    self.btn.clicked.connect(self.start_worker)
    self.resize(300, 100)

    复制代码
      def start_worker(self):
          self.thread = WorkerThread()
          self.thread.progress.connect(self.update_ui)  # 绑定信号到UI更新函数
          self.thread.start()
          self.btn.setEnabled(False)
    
      def update_ui(self, count):
          """主线程更新UI"""
          self.btn.setText(f"子线程计数:{count}")
          if count >= 20:
              self.btn.setText("任务完成!")
              self.btn.setEnabled(True)

    if name == "main":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())

五、常见问题与注意事项

1. 定时器不触发的原因
  • 未启动事件循环(未调用 app.exec());
  • 定时器实例被提前销毁(未设置父对象,且被 Python 垃圾回收);
  • 定时器所在线程无事件循环(子线程需调用 QThread.exec());
  • 间隔设置为 0 且非单次触发(会无限触发,可能阻塞事件循环)。
2. 避免定时器泄漏
  • QTimer 设置父对象(如 QTimer(self)),父对象销毁时自动释放;
  • 停止定时器后,断开信号槽连接(可选,父对象存在时无需)。
3. 高精度场景替代方案

若需微秒级精度,QTimer 无法满足,可使用:

  • QElapsedTimer:用于计算时间间隔(非定时器,仅计时);
  • 系统级定时器(如 time.perf_counter())+ 线程循环(需注意 CPU 占用)。
4. 避免定时器叠加触发

若槽函数执行时间超过定时器间隔,会导致 timeout 信号堆积,解决方案:

  • 槽函数内先停止定时器,执行完再启动:

    复制代码
    def update_count(self):
        self.timer.stop()
        # 耗时操作
        self.count += 1
        self.label.setText(f"计数:{self.count}")
        self.timer.start()
  • 增大定时器间隔,确保槽函数执行时间小于间隔。

六、总结

  • QTimer 是 Qt 事件循环驱动的定时器,核心是 timeout 信号,支持单次 / 重复触发;
  • 主线程使用时可直接绑定 UI 操作,子线程使用需配合事件循环和信号槽;
  • 优先使用 setSingleShot(True) 或静态方法 singleShot 实现延时任务;
  • 注意线程安全,UI 操作必须在主线程执行,高精度场景需选择合适的定时器类型。

七、一个特殊的应用范例 QTimer.singleShot(0, self.func)

QTimer.singleShot(0, self.func) 是 PySide6 中一个非常特殊且高频使用 的写法,其核心含义是:self.func 跳过当前代码执行流程,优先进入 Qt 事件循环的下一轮执行(而非真正 "延时 0 毫秒")。

  • 核心原理:为什么 "0 毫秒" 不代表立即执行?

Qt 的事件循环(QApplication.exec())是一个 "队列处理器",所有 UI 事件(点击、重绘)、信号槽、定时器事件都按队列顺序执行:

  1. 当你调用 QTimer.singleShot(0, func) 时,Qt 会把 func 封装成一个 "定时事件",插入到事件队列的尾部
  2. 当前正在执行的代码(比如按钮点击的槽函数、初始化代码)会先完整执行完毕;
  3. 事件循环处理完当前队列中排在前面的事件后,才会执行这个 "0 毫秒" 的定时事件。

简单说:0毫秒 ≠ 立即执行,而是 "当前代码执行完后,下一个事件循环周期执行"。

  • 关键使用场景(这才是它的核心价值)

这个写法看似简单,却是解决 Qt 界面开发中多个经典问题的 "神器",常见场景如下:

1. 避免 UI 阻塞 / 卡顿(让耗时操作 "延后" 执行)

如果在 UI 事件槽函数中执行耗时操作(比如循环计算、数据加载),会导致界面冻结(因为事件循环被阻塞,无法处理重绘、点击等事件)。用 0毫秒定时器 可以把耗时操作 "丢到" 当前事件处理完后执行,让 UI 先完成刷新。

示例:按钮点击后,由于执行了长耗时函数,按钮的文字并没有及时更新:

复制代码
import sys
from PySide6.QtWidgets import QApplication, QWidget, QPushButton, QLabel
from PySide6.QtCore import QTimer

class Demo(QWidget):
    def __init__(self):
        super().__init__()
        self.label = QLabel("未执行", self)
        self.btn = QPushButton("执行耗时操作", self)
        self.btn.clicked.connect(self.on_click)
        self.btn.move(0, 30)
        self.resize(300, 100)

    def on_click(self):
        self.label.setText("开始执行...")
        self.long_task()
        

    def long_task(self):
        # 模拟耗时操作
        total = 0
        for i in range(10_000_0000):
            total += i
        # 耗时操作完成后更新UI
        self.label.setText(f"执行完成!结果:{total}")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    w = Demo()
    w.show()
    sys.exit(app.exec())

点击按钮后界面会卡住(标签不会立即显示 "开始执行");而用 0毫秒定时器 后,标签会先更新,耗时操作在后台执行,界面保持响应。

python 复制代码
import sys
from PySide6.QtWidgets import QApplication, QWidget, QPushButton, QLabel
from PySide6.QtCore import QTimer

class Demo(QWidget):
    def __init__(self):
        super().__init__()
        self.label = QLabel("未执行", self)
        self.btn = QPushButton("执行耗时操作", self)
        self.btn.clicked.connect(self.on_click)
        self.btn.move(0, 30)
        self.resize(300, 100)

    def on_click(self):
        # 1. 先更新UI(让用户看到"开始执行"的反馈)
        self.label.setText("开始执行...")
        # 2. 把耗时操作丢到下一轮事件循环
        QTimer.singleShot(0, self.long_task)

    def long_task(self):
        # 模拟耗时操作(比如循环1000万次)
        total = 0
        for i in range(10_000_0000):
            total += i
        # 耗时操作完成后更新UI
        self.label.setText(f"执行完成!结果:{total}")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    w = Demo()
    w.show()
    sys.exit(app.exec())
2. 解决 "UI 初始化未完成时操作控件" 的问题

Qt 控件的初始化(比如 show() 之后)需要事件循环处理 "重绘事件" 才能完全就绪。如果在初始化代码中直接操作控件(比如设置焦点、滚动到指定位置),可能因为控件未就绪而失效。

示例:窗口显示后自动聚焦输入框

python 复制代码
import sys
from PySide6.QtWidgets import QApplication, QWidget, QLineEdit, QVBoxLayout
from PySide6.QtCore import QTimer

class Demo(QWidget):
    def __init__(self):
        super().__init__()
        self.input1 = QLineEdit(self)
        self.input2 = QLineEdit(self)
        # 错误写法:直接调用focus,控件未就绪,焦点可能不会生效
        self.input2.setFocus()
        layout = QVBoxLayout()
        layout.addWidget(self.input1)
        layout.addWidget(self.input2)
        self.setLayout(layout)
        self.resize(300, 100)



        # 正确写法:0毫秒定时器,等窗口初始化完成后再聚焦
        # QTimer.singleShot(0, self.input.setFocus)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    w = Demo()
    w.show()
    sys.exit(app.exec())
3. 拆分长任务,让事件循环有机会处理其他事件

如果有一个超长的循环任务,即使放到 0毫秒定时器 中,仍会阻塞界面。可以用 0毫秒定时器 把长任务拆分成多个小任务,让事件循环有间隙处理 UI 事件。

示例:拆分长循环,避免界面冻结

python

运行

python 复制代码
import sys
from PySide6.QtWidgets import QApplication, QWidget, QProgressBar, QVBoxLayout
from PySide6.QtCore import QTimer

class Demo(QWidget):
    def __init__(self):
        super().__init__()
        self.progress = QProgressBar(self)
        self.progress.setRange(0, 100)
        layout = QVBoxLayout()
        layout.addWidget(self.progress)
        self.setLayout(layout)
        self.resize(300, 100)

        self.current = 0
        # 启动第一个小任务
        QTimer.singleShot(0, self.do_task_chunk)

    def do_task_chunk(self):
        # 每次只执行1%的任务(小chunk)
        for _ in range(100000):  # 小循环,快速完成
            pass
        self.current += 1
        self.progress.setValue(self.current)
        # 如果任务没完成,继续调度下一个小任务
        if self.current < 100:
            QTimer.singleShot(0, self.do_task_chunk)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    w = Demo()
    w.show()
    sys.exit(app.exec())
4. 延迟信号槽的执行(解决信号触发时机问题)

某些场景下,信号会在控件状态未稳定时触发(比如 QComboBox.currentIndexChanged 在初始化时触发),用 0毫秒定时器 可以延后处理信号,确保拿到稳定的状态。

注意事项
  1. 并非真正的 "异步"0毫秒定时器 仍在主线程 执行,如果 func 是极耗时操作,依然会阻塞界面(真正的异步需要用 QThread);
  2. 执行顺序QTimer.singleShot(0, func) 的任务会排在当前事件队列的尾部,比其他待处理的事件(比如鼠标点击、重绘)后执行;
  3. processEvents() 的区别
    • QCoreApplication.processEvents() 是 "立即处理当前队列中的事件";
    • QTimer.singleShot(0, func) 是 "把 func 加入队列,等当前代码执行完后处理";
  4. 避免无限递归 :不要在 func 中再次调用 QTimer.singleShot(0, self.func)(除非有终止条件),否则会无限占用事件循环,导致界面无响应。

这是 Qt 开发中最实用的 "小技巧" 之一,尤其在 UI 初始化、事件处理、避免卡顿场景中高频使用。

相关推荐
ht巷子2 小时前
Qt:信号与槽
开发语言·c++·qt
北辰水墨2 小时前
【算法篇】单调栈的学习
c++·笔记·学习·算法·单调栈
航Hang*2 小时前
第3章:复习篇——第3节:数据查询与统计
数据库·笔记·sql·mysql
么么...2 小时前
SQL 学习指南:从零开始掌握DQL结构化查询语言
数据库·经验分享·笔记·sql
我不是程序猿儿2 小时前
【C#】软件设计,华为的IPD学习之需求开发心得
学习·华为·c#
日更嵌入式的打工仔2 小时前
两种核心消息队列:环形队列与RTOS消息队列解析
笔记·单片机
q_30238195562 小时前
YOLOv11训练NEU-DET钢材缺陷数据集并部署香橙派推理全流程
人工智能·python·深度学习·课程设计
WizLC2 小时前
【后端】面向对象编程是什么(附加几个通用小实例项目)
java·服务器·后端·python·设计语言
小白学大数据2 小时前
构建新闻数据爬虫:自动化提取与数据清洗技巧
运维·爬虫·python·自动化