3、PyQt5基础:Qt Designer介绍、信号与槽机制、事件循环、主窗口与部件

好,咱们进入第三章。说实话,很多初学者学PyQt5,一上来就被各种类和方法搞晕了。我当年也是。但后来我发现,只要搞懂四个核心概念——Qt Designer、信号与槽、事件循环、主窗口与部件——剩下的就是拼积木。这一章,我就带你把这四块积木摸透。

3.1 Qt Designer:别手写界面了,拖拽它不香吗?

我个人习惯,做工业HMI界面,从来不会纯手写代码去布局。为什么?因为效率太低。你想想看,一个按钮的位置调来调去,改代码、运行、看效果,再改……浪费时间。

Qt Designer 就是解决这个问题的。它是一个可视化的界面设计工具。你拖拽控件,调整大小,设置属性,最后生成一个 .ui 文件。这个文件本质上是XML格式,PyQt5可以直接加载它。

我的习惯:先用Qt Designer把界面搭好,再用代码去控制逻辑。界面和逻辑分离,后期维护起来特别爽。

怎么用?很简单。你安装好PyQt5后,在Python的 Scripts 目录下就能找到 designer.exe。双击打开,选择“Main Window”模板,然后就可以拖拽左侧的控件到画布上了。

生成 .ui 文件后,有两种方式加载:

  • 方法一:uic.loadUi() 直接加载,适合快速原型。
  • 方法二:pyuic5 工具把 .ui 转成 .py 文件,适合正式项目。

我个人推荐方法一,尤其是做工业HMI时,界面经常要改,直接加载 .ui 文件,改完界面不用重新编译代码。

# 方法一:直接加载 .ui 文件
from PyQt5 import uic
from PyQt5.QtWidgets import QApplication

app = QApplication([])
window = uic.loadUi('main_window.ui')
window.show()
app.exec_()
注意:如果你用 pyuic5 转成 .py 文件,每次改完界面都要重新转一次。我曾经有个项目,就因为忘了重新转换,调试了半天才发现界面没更新。嗯,血的教训。

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

好,界面搭好了,怎么让按钮点击后执行某个函数?这就引出了PyQt5最核心的机制——信号与槽

说白了,信号就是一个事件发生的通知。比如你点击按钮,按钮会发出一个 clicked 信号。槽就是一个函数,专门用来响应这个信号。你把信号和槽连接起来,事件发生时就自动调用函数。

为什么会这样设计?因为GUI程序是事件驱动的。你不能像写控制台程序那样顺序执行。你想想看,用户什么时候点击按钮,你根本不知道。所以你需要一个机制,让程序“监听”事件,然后响应。

# 信号与槽的经典例子
from PyQt5.QtWidgets import QPushButton, QApplication, QWidget

app = QApplication([])
window = QWidget()
button = QPushButton('点击我', window)

def on_button_clicked():
    print('按钮被点击了!')

# 连接信号与槽
button.clicked.connect(on_button_clicked)

window.show()
app.exec_()

这里 button.clicked 就是信号,on_button_clicked 就是槽。用 connect 方法把它们连起来。

关键点:信号可以携带数据。比如 QLineEdit.textChanged 信号会携带当前文本内容。槽函数可以接收这个参数。

我在项目中遇到过一个问题:多个按钮连接同一个槽函数,怎么区分是哪个按钮触发的?答案是使用 lambda 表达式或者 sender() 方法。

# 使用 sender() 区分信号来源
def on_any_button_clicked():
    sender = window.sender()
    print(f'{sender.text()} 被点击了')

button1.clicked.connect(on_any_button_clicked)
button2.clicked.connect(on_any_button_clicked)

3.3 事件循环:程序为什么不会自己退出?

你有没有想过一个问题:为什么 app.exec_() 之后,程序就一直运行,直到你关闭窗口?这就是事件循环的功劳。

事件循环,说白了就是一个无限循环。它不停地从系统消息队列里取事件,然后分发到对应的对象去处理。比如鼠标移动、键盘按下、窗口重绘,这些都是事件。

我刚开始学的时候,总觉得事件循环很玄乎。后来我把它想象成一个邮局:事件就是信件,事件循环就是邮递员,不停地送信到各个窗口。

# 事件循环的伪代码
while True:
    event = get_next_event()  # 获取下一个事件
    if event == QUIT:
        break
    dispatch_event(event)     # 分发事件

嗯,这里要注意:如果你的槽函数里写了死循环或者耗时操作,事件循环就会被阻塞。界面会卡死,因为没空处理鼠标点击和重绘事件了。

避坑指南:我曾经在工业HMI里,直接在槽函数里写了一个读取PLC数据的循环,结果界面卡得动不了。后来改用 QTimer 定时器,或者把耗时操作放到子线程里,问题就解决了。

3.4 主窗口与部件:搭积木的艺术

PyQt5里,主窗口QMainWindow,它提供了菜单栏、工具栏、状态栏等标准框架。部件就是各种控件,比如按钮、标签、输入框。

主窗口和部件的关系,就像画布和画上去的元素。主窗口提供一个容器,部件放在里面。

常用的部件有这些:

部件类 用途 我在项目中常用场景
QLabel 显示文本或图片 显示设备状态、报警信息
QPushButton 按钮 启动、停止、复位操作
QLineEdit 单行文本输入 输入IP地址、参数值
QComboBox 下拉选择框 选择配方、模式切换
QTableWidget 表格 显示生产数据、历史记录
QGroupBox 分组框 把相关控件分组,界面更清晰

布局管理也很重要。你总不能把按钮随便扔在窗口上吧?PyQt5提供了 QHBoxLayout(水平布局)、QVBoxLayout(垂直布局)、QGridLayout(网格布局)等。

# 使用布局管理器
from PyQt5.QtWidgets import QVBoxLayout, QPushButton, QWidget

layout = QVBoxLayout()
layout.addWidget(QPushButton('按钮1'))
layout.addWidget(QPushButton('按钮2'))
layout.addWidget(QPushButton('按钮3'))

window = QWidget()
window.setLayout(layout)
我的建议:在Qt Designer里用布局管理器来排列控件,而不是手动设置坐标。手动设置坐标在窗口缩放时会乱掉,布局管理器会自动适应。

3.5 综合示例:一个简单的HMI启动界面

好了,理论讲完了,咱们来点实战。下面是一个简单的工业HMI启动界面,包含一个启动按钮、一个停止按钮、一个状态显示标签。

import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton, QLabel, QVBoxLayout, QWidget

class HMIWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle('工业HMI - 启动界面')
        self.setGeometry(100, 100, 400, 300)

        # 创建部件
        self.status_label = QLabel('设备状态:停止')
        self.start_button = QPushButton('启动')
        self.stop_button = QPushButton('停止')

        # 布局
        layout = QVBoxLayout()
        layout.addWidget(self.status_label)
        layout.addWidget(self.start_button)
        layout.addWidget(self.stop_button)

        container = QWidget()
        container.setLayout(layout)
        self.setCentralWidget(container)

        # 信号与槽
        self.start_button.clicked.connect(self.start_device)
        self.stop_button.clicked.connect(self.stop_device)

    def start_device(self):
        self.status_label.setText('设备状态:运行中')
        print('设备已启动')

    def stop_device(self):
        self.status_label.setText('设备状态:停止')
        print('设备已停止')

if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = HMIWindow()
    window.show()
    sys.exit(app.exec_())

这个例子虽然简单,但包含了主窗口、部件、信号与槽、事件循环的全部要素。你运行一下,点击按钮,标签文本会变化,控制台也会输出信息。

总结一下:Qt Designer帮你搭界面,信号与槽让界面动起来,事件循环保证程序不死,主窗口和部件就是你的积木块。把这四个搞明白,PyQt5你就入门了。

下一章,咱们会深入讲布局管理,以及如何用样式表让HMI界面更好看。嗯,到时候见。