☰
基于PyQT6从零开始做一个计时器
2026/10/11 14:43:01 网站建设 项目流程

前言


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 sys
from PyQt6.QtWidgets import QApplication, QLabel


def main():
app = QApplication(sys.argv) # 一个进程只需要一个
label = QLabel("你好,PyQt6")
label.setWindowTitle("最小窗口")
label.resize(280, 120)
label.show()
sys.exit(app.exec()) # PyQt6:exec(),不是 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 QPushButton

button = 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 QTimer

timer = 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 sys
import time

from PyQt6.QtCore import Qt, QTimer
from PyQt6.QtWidgets import (
QApplication, QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget,
)


class Stopwatch(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("PyQt6 秒表")

self._running = False
self._accumulated = 0.0 # 之前几段累计的秒数
self._started_at = 0.0

self.time_label = QLabel("00:00.00")
# PyQt6:枚举必须写全限定名 Qt.AlignmentFlag.AlignCenter
self.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_at
self.start_btn.setText("继续")
else:
self._started_at = time.monotonic()
self.timer.start() # 按 setInterval 的间隔开始发 timeout
self.start_btn.setText("暂停")
self._running = not self._running

def _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 = False
self._accumulated = 0.0
self.start_btn.setText("开始")
self.time_label.setText("00:00.00")

@staticmethod
def _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 已废弃的 API,PyQt6 也不提供。照抄 PyQt5 教程时,最常见的报错就是AttributeError: type object 'Qt' has no attribute 'AlignCenter'。


常见坑点



  1. ❌ 抄 PyQt5 教程,写app.exec_()。


✅ PyQt6 里exec_()已被移除,要写app.exec()。



  1. ❌ 写label.setAlignment(Qt.AlignCenter)。


✅ PyQt6 具名枚举是Enum子类,必须全限定:Qt.AlignmentFlag.AlignCenter。



  1. ❌ 在槽函数里跑长循环或发同步网络请求。


✅ 槽由事件循环调用,长时间占用会让界面假死;耗时任务交给QThread(且注意工作线程不能直接碰控件,要用信号把结果发回主线程)。



  1. ❌ 定时器里用「每次加一个间隔」来累计时间。


✅ 定时器不保证严格准点(官方文档也说明系统忙时可能延迟),要记起点用time.monotonic()求差,避免误差累积。



  1. ❌ 忘记调用show(),或者忘记app.exec()。


✅ 不show()窗口不可见;不exec()事件循环不启动,窗口一闪即没。



  1. ❌ 程序退出后定时器还在跑,界面已销毁导致访问已释放对象。


✅ 退出路径里先timer.stop(),并把QTimer的父对象设为控件,让销毁顺序被 Qt 管理。



  1. ❌ 把QApplication当成普通对象创建多次。


✅ 一个进程通常只需要一个QApplication,它是全局状态,重复创建会出问题。


总结




要点结论



安装PyQt6 是第三方库,需 pip 安装,要求 Python 3.9+

骨架QApplication→ 窗口 →show()→exec()

事件绑定信号与槽:button.clicked.connect(handler)

定时刷新QTimer+setInterval()+timeout信号

计时基准用time.monotonic()求差,不要累加间隔

最大坑PyQt5 教程不能直抄:枚举全限定、exec_改exec



从零做一个计时器,代码量不大,但它把「事件循环、信号槽、定时器、布局」四件事都过了一遍。真正值得你带走的是最后一节那张对照表——它能让你的 PyQt6 代码不在第一行就崩。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询