基于PyQT6从零开始做一个计时器

发布时间:2026/10/11 14:43:17

基于PyQT6从零开始做一个计时器 前言PyQt6 是 Qt 6 的 Python 绑定属于第三方库要pip install PyQt6才能用本机没有安装环境所以本文代码只能逐行推演。官方文档写明 PyQt6 要求Python 3.9 或更高如果你还在用 3.8就只能退回 PyQt5。本文用「做一个秒表」这条主线把 PyQt6 的窗口、布局、信号槽、定时器串一遍。具体 API 与参数以 PyQt6 官方文档为准。这里必须先泼一盆冷水网上绝大多数 PyQt 教程是照着 PyQt5 写的直接抄到 PyQt6 上会报错。这不是玄学而是 PyQt6 和 PyQt5 之间有一批真实的破坏性变更。官方文档《Differences Between PyQt6 and PyQt5》明确列出所有具名枚举named enum现在都是 Python 标准库Enum的子类exec_()和print_()被移除qApp被移除Qt模块被移除。所以本文的示例会刻意写成 PyQt6 的写法并对照指出 PyQt5 的老写法错在哪里。一、从零开始一个窗口的最小骨架Qt 程序的最小骨架是四步创建 QApplication → 创建窗口QWidget / QMainWindow→ show() → app.exec()# 适用于 Python 3.9需 pip install PyQt6第三方库import sysfrom PyQt6.QtWidgets import QApplication, QLabeldef main():app QApplication(sys.argv) # 一个进程只需要一个label QLabel(你好PyQt6)label.setWindowTitle(最小窗口)label.resize(280, 120)label.show()sys.exit(app.exec()) # PyQt6exec()不是 exec_()if __name__ __main__:main()QApplication负责初始化 Qt、管理事件队列是app.exec()里那个事件循环的宿主。sys.exit(app.exec())把exec()的返回码当作进程退出码——官方文档说明exec()会一直阻塞到exit()被调用并返回传给exit()的值。QWidget既能当独立窗口也能当容器基类QMainWindow自带菜单栏、工具栏、状态栏的框架做复杂程序时用。二、信号与槽Qt 的事件绑定方式Qt 不用bind那一套它用信号与槽signals and slots控件发出信号你把自己的可调用对象连上去。# 适用于 Python 3.9需 pip install PyQt6第三方库from PyQt6.QtWidgets import QPushButtonbutton QPushButton(开始)button.clicked.connect(self.start) # clicked 是信号self.start 是槽文档写明QPushButton在鼠标点击、空格键或快捷键激活时发出clicked()信号另外还有pressed()、released()。clicked.connect(...)返回一个连接对象需要断开时可以调用它的disconnect()。信号可以在连接时传参数也可以不传连接的目标既可以是普通函数、lambda也可以是加了装饰器的方法具体重载写法以官方文档为准。三、定时器QTimer 与「不阻塞事件循环」秒表的关键是定时刷新界面。Qt 的做法是QTimer。官方文档说明创建一个QTimer把它的timeout信号连到槽上再调用start()它就会按固定间隔反复发出timeout。间隔用setInterval()设置。# 适用于 Python 3.9需 pip install PyQt6第三方库from PyQt6.QtCore import QTimertimer QTimer(self)timer.setInterval(30) # 毫秒timer.timeout.connect(self.on_tick)timer.start()这里有个跨工具包通用的道理QTimer的回调由事件循环调用所以回调里只要做「读一下时间、改一下标签」这种瞬间完成的事界面就始终畅通。反过来如果在on_tick里做重活事件循环同样会被卡住。四、完整示例带开始 / 暂停 / 重置的秒表把窗口、布局、信号槽、定时器拼起来。计时不用「累加每次 30 毫秒」的办法会累积误差而是记录起点、用time.monotonic()求差。# 适用于 Python 3.9需 pip install PyQt6第三方库import sysimport timefrom PyQt6.QtCore import Qt, QTimerfrom PyQt6.QtWidgets import (QApplication, QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget,)class Stopwatch(QWidget):def __init__(self):super().__init__()self.setWindowTitle(PyQt6 秒表)self._running Falseself._accumulated 0.0 # 之前几段累计的秒数self._started_at 0.0self.time_label QLabel(00:00.00)# PyQt6枚举必须写全限定名 Qt.AlignmentFlag.AlignCenterself.time_label.setAlignment(Qt.AlignmentFlag.AlignCenter)self.time_label.setStyleSheet(font-size: 30px;)self.start_btn QPushButton(开始)self.reset_btn QPushButton(重置)row QHBoxLayout()row.addWidget(self.start_btn)row.addWidget(self.reset_btn)column QVBoxLayout(self)column.addWidget(self.time_label)column.addLayout(row)self.timer QTimer(self)self.timer.setInterval(30)self.timer.timeout.connect(self._on_tick)self.start_btn.clicked.connect(self._toggle)self.reset_btn.clicked.connect(self._reset)def _toggle(self):if self._running:self.timer.stop()self._accumulated time.monotonic() - self._started_atself.start_btn.setText(继续)else:self._started_at time.monotonic()self.timer.start() # 按 setInterval 的间隔开始发 timeoutself.start_btn.setText(暂停)self._running not self._runningdef _on_tick(self):total self._accumulated (time.monotonic() - self._started_at)self.time_label.setText(self._format(total))def _reset(self):self.timer.stop()self._running Falseself._accumulated 0.0self.start_btn.setText(开始)self.time_label.setText(00:00.00)staticmethoddef _format(seconds):minutes, rest divmod(seconds, 60)whole, frac divmod(rest, 1)return f{int(minutes):02d}:{int(whole):02d}.{int(frac * 100):02d}def main():app QApplication(sys.argv)window Stopwatch()window.resize(280, 150)window.show()sys.exit(app.exec())if __name__ __main__:main()几个逐行推演要点。QVBoxLayout(self)直接把布局设为窗口的顶层布局等价于先建布局再self.setLayout(column)addLayout用来把一个子布局塞进布局里形成嵌套。_format用divmod把秒拆成「分」和「余下的秒」再把余下的秒拆成整数部分和小数部分乘 100 取两位百分秒divmod(seconds, 60)里seconds是浮点所以分和秒都是浮点最后统一int()再格式化。计时基准是time.monotonic()它是单调时钟不受系统时间被改动的影响。五、PyQt6 与 PyQt5 的真实差异本文最该记住的一段差异点PyQt5 老写法PyQt6 正确写法枚举访问Qt.AlignCenter不限定枚举名Qt.AlignmentFlag.AlignCenter必须写全限定名事件循环app.exec_()app.exec()打印相关printer.print_()该方法已被移除全局应用对象qApp已移除自己保存QApplication实例模块Qt模块已移除枚举类型基础混用IntEnum与自定义类型全部是 PythonEnum子类可组合的标志位是Flag子类原因在官方文档里写得很清楚PyQt6 把所有具名枚举实现成标准Enum的子类并取消了 Qt5 那种「枚举值可以直接从命名空间拿到」的兼容写法所以Qt.AlignCenter在 PyQt6 里找不到必须写成Qt.AlignmentFlag.AlignCenter。同理Qt 6 本身移除了一批 Qt 5 已废弃的 APIPyQt6 也不提供。照抄 PyQt5 教程时最常见的报错就是AttributeError: type object Qt has no attribute AlignCenter。常见坑点❌ 抄 PyQt5 教程写app.exec_()。✅ PyQt6 里exec_()已被移除要写app.exec()。❌ 写label.setAlignment(Qt.AlignCenter)。✅ PyQt6 具名枚举是Enum子类必须全限定Qt.AlignmentFlag.AlignCenter。❌ 在槽函数里跑长循环或发同步网络请求。✅ 槽由事件循环调用长时间占用会让界面假死耗时任务交给QThread且注意工作线程不能直接碰控件要用信号把结果发回主线程。❌ 定时器里用「每次加一个间隔」来累计时间。✅ 定时器不保证严格准点官方文档也说明系统忙时可能延迟要记起点用time.monotonic()求差避免误差累积。❌ 忘记调用show()或者忘记app.exec()。✅ 不show()窗口不可见不exec()事件循环不启动窗口一闪即没。❌ 程序退出后定时器还在跑界面已销毁导致访问已释放对象。✅ 退出路径里先timer.stop()并把QTimer的父对象设为控件让销毁顺序被 Qt 管理。❌ 把QApplication当成普通对象创建多次。✅ 一个进程通常只需要一个QApplication它是全局状态重复创建会出问题。总结要点结论安装PyQt6 是第三方库需 pip 安装要求 Python 3.9骨架QApplication→ 窗口 →show()→exec()事件绑定信号与槽button.clicked.connect(handler)定时刷新QTimersetInterval()timeout信号计时基准用time.monotonic()求差不要累加间隔最大坑PyQt5 教程不能直抄枚举全限定、exec_改exec从零做一个计时器代码量不大但它把「事件循环、信号槽、定时器、布局」四件事都过了一遍。真正值得你带走的是最后一节那张对照表——它能让你的 PyQt6 代码不在第一行就崩。
延伸阅读

更多相关文章

2026/10/11 14:43:17

一次 Type-C 显示器连接的背后:LDR6020 协议时序全拆解

一、从插线到出图,到底发生了什么 用户把一根 Type-C 线插进显示器,通常 1~2 秒内屏幕亮起。这背后不是 “插上就通”,而是 LDR6020 在 CC 线上完成了至少 5 个阶段、十余轮报文交互。本文按时间顺序拆解每一步。 二、阶段 1:物理…

2026/10/11 14:43:17

Linux内核do_signal深入解析:信号递送、sigreturn与寄存器魔法

内核开发里有个很有意思的函数,叫 do_signal 。很多人第一次听到它是在看系统调用返回路径的代码时,被 exit_to_user_mode_loop() 里那个显眼的调用点吸引住,然后就一头雾水:为什么一个“发信号”的函数,要在每次系…

2026/10/11 14:43:17

在Python中操作MongoDB的详细教程和案例分享

前言 先把最容易搞错的一点说清楚:MongoDB 不是关系型数据库。 它没有 SQL、没有表、没有「行」和「列」、没有 JOIN、没有固定表结构。它是文档型数据库:数据以 BSON 文档(一种类似 JSON 的二进制格式)存储,一层层嵌套…

2026/10/11 15:58:22

跨江桥梁病害检测与资产标定:YOLOv8数据集构建与训练调参实战

简介:这份资源面向计算机视觉研究者、桥梁监测工程师及目标检测学习者,提供跨江桥梁路面病害与道路资产标定的专用数据集,用于训练裂缝、破损、积水等病害及桥墩、拉索、桥面等结构元素的识别模型,弥补通用数据集在桥梁场景下的适…

2026/10/11 15:58:22

跨江桥梁病害检测与资产标定:YOLOv8训练全流程与避坑指南

简介:这份资源面向计算机视觉研究者、桥梁工程监测人员及目标检测学习者,提供跨江桥梁路面病害与道路资产标定的专用数据集,用于训练裂缝、破损、积水等病害及桥墩、拉索、桥面等结构元素的识别模型,弥补通用数据集在桥梁场景下的…

2026/10/11 15:58:22

ScriptX打印控件详解:ActiveX安装激活与静默打印实战

简介:ScriptX打印控件安装包是一套面向Windows环境的打印控件部署文件,主要服务于需要在浏览器或桌面应用中调用本地打印机完成票据、报表等文档输出的场景。无论是前端开发、系统集成还是IT运维,都可以借助这份安装包快速解决ScriptX打印组件…

2026/10/11 15:58:22

显示驱动板卡电容触控校准原理与实操指南

1. 从一块“飘移”的触控板说起如果你拆过带触控功能的显示模组,大概率见过这样一块板子:上面密密麻麻排着走线,边缘引出一排FPC座子,中间一颗主控芯片旁边围着几颗电容和电阻。这块板子就是显示驱动板卡,它同时干两件…

2026/10/11 15:58:22

鸿蒙Flutter BLE透传数据错乱?CRC16校验实战与避坑指南

前阵子在鸿蒙设备上调试一个 Flutter 的 BLE 透传模块,遇到一个挺磨人的问题。两块开发板通过串口转发数据,偶尔会多收、漏收或者错一两个字节,设备端的动作就跟着乱套。查了半天链路层,最后发现根本不是蓝牙连接问题,…

2026/10/11 15:53:21

Linux连接跟踪机制解析:从conntrack命令到生产环境排查

排查生产环境里的访问异常时,我做得最多的一个动作不是急着抓包,而是先看一眼防火墙设备上的连接跟踪表:这条连接到底在不在表里?状态是 NEW 还是 ESTABLISHED?有没有回包方向的记录?这个习惯帮我省下过大量…

2026/10/11 0:02:13

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/11 0:02:13

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/11 0:02:13

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

2026/10/11 0:02:13

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/11 0:02:13

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/11 0:02:13

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑