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 初始化、事件处理、避免卡顿场景中高频使用。

相关推荐
用户8356290780516 小时前
Python 实现 PDF 文件加密与解密方法
后端·python
用户8356290780516 小时前
使用 Python 冻结与拆分 Excel 窗格教程
后端·python
你好潘先生15 小时前
别再记命令了,用 yeero do 说句人话就能跑脚本,而且不烧 token
服务器·python·命令行
Agent_大师15 小时前
WebSocket 行情重连成功,K线缺口不会自动消失
python
荣码15 小时前
LLM结构化输出:让AI返回JSON而不是废话,我踩了4个坑
java·python
copyer_xyf15 小时前
FastAPI 如何连接 MySQL
后端·python
apocelipes1 天前
常用编程语言和库的正则表达式性能对比
c语言·c++·python·性能优化·golang·开发工具和环境
用户8356290780511 天前
使用 Python 在 PDF 中创建与管理书签
后端·python
MeixianAgent1 天前
Python 回测数据入口怎么验?历史 K 线入库前先做 5 个检查
后端·python
咕白m6252 天前
用 Python 实现一键批量查找与替换 Excel 数据
后端·python