前言
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'。
常见坑点
- ❌ 抄 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) |
| 定时刷新 | QTimer+setInterval()+timeout信号 |
| 计时基准 | 用time.monotonic()求差,不要累加间隔 |
| 最大坑 | PyQt5 教程不能直抄:枚举全限定、exec_改exec |
从零做一个计时器,代码量不大,但它把「事件循环、信号槽、定时器、布局」四件事都过了一遍。真正值得你带走的是最后一节那张对照表——它能让你的 PyQt6 代码不在第一行就崩。