3、PyQt5基础:Qt框架概述、信号与槽机制、QWidget与QMainWindow、事件循环

好,咱们开始聊PyQt5。说实话,Qt这套框架在嵌入式界摸爬滚打这么多年,能活下来而且活得这么好,是有道理的。我最早接触Qt是在做一款便携式心电监护仪的时候,那时候选型纠结了很久,最后选了PyQt5——原因很简单:开发快,跨平台,而且信号与槽这套机制简直就是为医疗设备这种实时交互场景量身定做的。

3.1 Qt框架概述:它到底是个什么东西?

Qt本质上是一个C++写的跨平台应用框架。PyQt5呢,就是它的Python绑定。说白了,你用Python写代码,底层跑的却是C++的性能。嗯,这里要注意:PyQt5不是Python原生的,它是把Qt的C++ API翻译成了Python能调用的接口。

我个人习惯把Qt框架分成这么几块:

  • 核心模块(QtCore):事件循环、信号与槽、线程、文件操作等基础功能
  • GUI模块(QtWidgets):按钮、标签、输入框、窗口等所有你能看到的界面元素
  • 绘图模块(QtGui):字体、颜色、图像处理、QPainter绘图
  • 网络模块(QtNetwork):TCP/UDP通信、HTTP请求

做心电监护仪,你最常用的是QtWidgets和QtCore。QtGui偶尔用来画波形图。网络模块嘛,如果你要上传数据到服务器,也会用到。

我的经验:刚开始学PyQt5,别急着把所有模块都啃一遍。先把QtWidgets里的常用控件玩熟,再慢慢深入。我见过太多人一上来就研究QPainter画图,结果连QPushButton怎么用都没搞明白。

3.2 信号与槽机制:PyQt5的灵魂

信号与槽,说白了就是「当A发生了,就通知B去做某事」。这是Qt最核心的设计思想,也是它跟其他GUI框架最大的区别。

举个例子:用户点击了「开始采集」按钮,你需要让心电波形开始绘制。传统做法是写一个回调函数,但回调函数有个问题——它是一对一的,而且耦合度高。信号与槽不一样,它是一对多、多对一的,而且发送者和接收者完全解耦。

from PyQt5.QtWidgets import QPushButton, QApplication
from PyQt5.QtCore import pyqtSignal, QObject

# 自定义信号
class ECGController(QObject):
    start_signal = pyqtSignal()  # 定义一个信号
    data_ready = pyqtSignal(list)  # 带参数的信号

# 使用
controller = ECGController()
button = QPushButton("开始采集")

# 连接信号与槽
button.clicked.connect(controller.start_signal.emit)
controller.start_signal.connect(self.start_acquisition)
controller.data_ready.connect(self.update_waveform)

你看,按钮点击后触发start_signal,然后start_signal又连接了两个槽函数。这就是一对多。而且你随时可以断开连接:controller.start_signal.disconnect()

避坑指南:我曾经在项目中犯过一个低级错误——在子线程里直接更新UI。结果程序动不动就崩溃。后来才意识到,Qt的UI操作必须在主线程进行。正确的做法是用信号把数据从子线程发到主线程,让主线程的槽函数去更新界面。记住:信号与槽是线程安全的,这是它的一大优势。

3.3 QWidget与QMainWindow:窗口的两种形态

QWidget是所有界面组件的基类。按钮、标签、输入框……所有你能看到的控件,都是QWidget的子类。而QMainWindow是一个特殊的QWidget,它自带菜单栏、工具栏、状态栏和中央区域。

做心电监护仪,我建议你用QMainWindow作为主窗口。为什么?因为医疗设备通常需要菜单(设置、校准)、工具栏(开始/停止采集)、状态栏(显示心率、连接状态)这些元素。QMainWindow把这些都给你准备好了。

特性 QWidget QMainWindow
菜单栏 自带
工具栏 自带
状态栏 自带
中央区域 自身就是内容 需要设置中央控件
适用场景 弹窗、自定义控件 主窗口
from PyQt5.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QLabel

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("心电监护仪")
        self.setGeometry(100, 100, 1024, 768)
        
        # 创建中央控件
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        
        # 布局
        layout = QVBoxLayout()
        central_widget.setLayout(layout)
        
        # 添加波形显示区域
        self.waveform_label = QLabel("波形显示区域")
        layout.addWidget(self.waveform_label)
        
        # 状态栏
        self.statusBar().showMessage("就绪")
注意:QMainWindow必须设置中央控件(setCentralWidget),否则你添加的任何子控件都不会显示。我刚开始用的时候,折腾了半天才发现这个问题。

3.4 事件循环:程序为什么能「活着」?

事件循环,说白了就是一个无限循环,不断检查有没有事件发生(鼠标点击、键盘按下、定时器超时……),然后把这些事件分发给对应的处理函数。

你写的PyQt5程序,最后一行通常是:

app = QApplication(sys.argv)
# ... 创建窗口、连接信号 ...
window.show()
sys.exit(app.exec_())  # 进入事件循环

app.exec_()就是启动事件循环。程序会一直卡在这里,直到你关闭窗口。这期间,Qt会不断处理各种事件:

  • 用户点击按钮 → 触发clicked信号
  • 定时器超时 → 触发timeout信号
  • 网络数据到达 → 触发readyRead信号
  • 窗口需要重绘 → 触发paintEvent

你想想看,如果没有事件循环,程序执行完window.show()就直接退出了,你什么都看不到。事件循环就是让程序「活」起来的机制。

我的习惯:在事件循环里,千万别做耗时操作。比如你要从串口读取大量数据,如果直接在槽函数里用while True去读,界面会卡死。正确的做法是用QTimer定时读取,或者把耗时操作放到子线程里。我曾经因为这个问题被测试同事追着骂了一周……

嗯,到这里,PyQt5的基础框架就讲完了。信号与槽让你能灵活地处理交互,QMainWindow给你搭好了主窗口的骨架,事件循环让程序持续运行。下一章,咱们会把这些知识串起来,做一个真正能用的心电监护仪界面原型。