第 5 讲 | Qt Designer 工作流与 .ui 文件
本讲目标 :搞清
.ui文件到底是什么、两种加载方式怎么选,以及"该用 Designer 还是手写代码"这个老争论。
配套代码 :ui/student_form.ui(界面文件)+cases/c05_designer_form.py(逻辑代码)
一、先破除一个迷信:.ui 就是 XML
很多人把 Qt Designer 当成黑盒,觉得"用它拖出来的界面很神秘"。其实 .ui 文件打开就是普通的 XML:
xml
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>StudentForm</class>
<widget class="QWidget" name="StudentForm">
<property name="geometry">
<rect><x>0</x><y>0</y><width>560</width><height>400</height></rect>
</property>
<property name="windowTitle">
<string>学生信息录入</string>
</property>
<layout class="QVBoxLayout" name="rootLayout">
<item>
<widget class="QLabel" name="titleLabel">
<property name="text"><string>学生信息录入</string></property>
</widget>
</item>
...
看懂这段,你就明白三件事:
- Designer 只是这个 XML 的可视化编辑器 ,它生成的东西和手写
QVBoxLayout完全等价 name属性(也就是 objectName)就是控件的"变量名",加载后靠它访问控件.ui文件可以手写、可以版本管理、可以 diff,本质是个配置文件
所以 Designer 一点不神秘,它就是个写 XML 的图形工具。理解这一点之后,"用不用 Designer"就变成一个纯粹的效率问题,而不是技术路线问题。
二、Designer 装在哪、怎么打开
PyQt6 的 pip 安装包里不含 Designer(这点和 PySide6 不同)。三个办法:
办法 1(推荐) :装 pyqt6-tools(已停更,新 Python 版本可能装不上)
bash
pip install pyqt6-tools
pyqt6-tools designer
办法 2(最稳):装 PySide6 只为拿它的 Designer,然后用它设计 PyQt6 的界面
bash
pip install PySide6
pyside6-designer
生成的 .ui 文件是同一套格式,PyQt6 完全能加载。这是目前最省事的方案。
办法 3:直接用 Qt 官网的独立版 Qt Designer。
本讲的案例工程里已经附了一份写好注释的
ui/student_form.ui,你可以直接打开它体验加载流程,不必先装 Designer。
三、Designer 界面导览
打开后是四个区域,记住它们的作用就行:
| 区域 | 名字 | 作用 |
|---|---|---|
| 左侧 | Widget Box(控件箱) | 拖控件的地方,按类别分组 |
| 右上 | Object Inspector(对象查看器) | 最关键:控件树的层级关系 + objectName |
| 右下 | Property Editor(属性编辑器) | 改属性(文字、尺寸、字体、样式表) |
| 底部 | Signal/Slot Editor | 可视化连信号槽(不推荐,逻辑写在代码里更好) |
新手最容易忽略的是 Object Inspector :它显示的不是"控件的样子",而是"控件属于谁"。
你的 QLabel 为什么没显示?看看它在树里是不是挂在正确的位置、有没有被布局管着。
布局要用"右键"来做
Designer 里的布局不是拖出来的,是选中若干控件后右键设置的:
- 按住 Ctrl 选中同一行的几个控件
- 右键 →
Layouts → Lay Out Horizontally(横向)或Vertically(纵向) - 整块区域选好后,再右键外层 → 套一层新的布局
更省事的做法 :先往窗体里拖一个布局容器(Vertical Layout),再把控件拖进去。
或者拖完所有控件后,在窗体的空白处右键 → Lay Out Vertically,一步给整个窗口套布局。
必须加 Spacer
想让某些控件靠左、某些靠右,就在中间拖一个 Horizontal Spacer(横向弹簧)。
它对应代码里的 addStretch()。没有 Spacer,Designer 排出来的界面会"看起来怪怪的" ,
通常就是控件被均匀拉开导致的。
四、加载 .ui 的两种方式
方式 A:运行时加载(开发期推荐)
python
from PyQt6 import uic
from PyQt6.QtWidgets import QWidget
class StudentFormWindow(QWidget):
def __init__(self):
super().__init__()
# ★ 一行代码,把 .ui 里所有控件挂到 self 上
uic.loadUi("ui/student_form.ui", self)
# 之后 self.editName、self.comboClass、self.btnSubmit 全都能直接用
self.btnSubmit.clicked.connect(self.on_submit)
self.btnReset.clicked.connect(self.on_reset)
关键点 :loadUi(path, self) 的第二个参数是 self,这表示"把 .ui 里的控件
统统挂到当前对象上"。于是:
.ui里 objectName 叫editName的 QLineEdit → 变成self.editName- objectName 叫
btnSubmit的按钮 → 变成self.btnSubmit
这就是为什么 objectName 必须认真起名 。叫 lineEdit、lineEdit_2、pushButton_3
的界面,写业务逻辑时会疯掉。
优点 :改界面直接改 .ui 文件,保存后重新运行就能看到效果,不用重新生成任何代码 。
界面和逻辑的迭代是解耦的。
方式 B:预编译成 Python 模块(发布期推荐)
bash
pyuic6 ui/student_form.ui -o ui/student_form_ui.py
生成的 student_form_ui.py 长这样(截取核心):
python
class Ui_StudentForm(object):
def setupUi(self, StudentForm):
StudentForm.setObjectName("StudentForm")
StudentForm.resize(560, 400)
self.rootLayout = QVBoxLayout(StudentForm)
self.titleLabel = QLabel(StudentForm)
self.titleLabel.setObjectName("titleLabel")
...
然后这样用:
python
from ui.student_form_ui import Ui_StudentForm
class StudentFormWindow(QWidget, Ui_StudentForm):
def __init__(self):
super().__init__()
self.setupUi(self) # ★ 界面构建交给生成的代码
self.btnSubmit.clicked.connect(self.on_submit)
优点:
- 不需要在运行时解析 XML,启动稍微快一点
- 不需要把 .ui 文件一起打包(打包 exe 时少一份数据文件,少一类"找不到文件"的坑)
- 代码可读、可 diff,出问题能直接看生成结果
缺点 :改界面后必须重新执行一次 pyuic6,忘了就会"改了没反应"。
怎么选?
| 阶段 | 推荐 |
|---|---|
| 开发期、界面还在反复改 | 方式 A(loadUi) |
| 功能稳定准备发布 | 方式 B(pyuic6) |
| 团队协作、要进 Git | 方式 B(生成的 .py 能 diff,.ui 也能,但 B 更直观) |
本工程用方式 A,因为读者拿到代码就能跑、改界面立刻见效。
五、案例讲解:学生信息录入
界面(ui/student_form.ui):
┌──────────────────────────────────────┐
│ 学生信息录入(titleLabel) │
├──────────────────────────────────────┤
│ 姓名: [editName ] │
│ 班级: [comboClass ] │
│ 入学成绩: [spinScore ] │
│ 备注: [textRemark ] │
├──────────────────────────────────────┤
│ [重置] [提交] │
├──────────────────────────────────────┤
│ listResult(结果列表) │
└──────────────────────────────────────┘
逻辑代码只有四十行,全部是业务,一行界面代码都没有:
python
def on_submit(self) -> None:
name = self.editName.text().strip()
if not name:
self.listResult.addItem("⚠ 姓名不能为空")
return
cls = self.comboClass.currentText()
score = self.spinScore.value()
self.listResult.addItem(f"✓ 已录入:{name} / {cls} / {score} 分")
self.on_reset()
self.listResult.scrollToBottom()
def on_reset(self) -> None:
self.editName.clear()
self.comboClass.setCurrentIndex(0)
self.spinScore.setValue(600)
self.textRemark.clear()
self.editName.setFocus()
这就是"界面与逻辑分离"的真实样子 :界面在 .ui 里,逻辑在一个个方法里,
中间靠 objectName 连接。后面不管界面怎么调整(把表单从两列改成三列、加个字段),
只要 objectName 不变,逻辑代码一行都不用改。

六、到底该用 Designer 还是手写代码
我的建议是分情况:
| 界面特征 | 建议 |
|---|---|
| 标准表单、设置页、参数面板 | 用 Designer,快得多 |
| 布局很规则(表格 + 按钮栏) | 手写(代码更短、更可控) |
| 需要动态生成控件(循环建 100 个按钮) | 手写(Designer 做不到) |
| 自定义绘制控件 | 手写(第 17 讲) |
| 界面要频繁微调像素级间距 | 手写(改代码比改 XML 快) |
实践中的最佳组合 :用 Designer 搭出"骨架和静态部分",用代码补"动态部分"。
比如本课程第 10 讲的 PDF 工具箱------框架用代码写,因为参数面板要根据下拉框动态切换,
Designer 做这个很别扭。
一句话 :Designer 是提速工具,不是必须工具。不要因为"听说要用 Designer"
而把简单界面搞复杂。
七、四个坑
坑 1:改了 .ui 文件没生效
用方式 A(loadUi)时,改完保存直接重跑就行;用方式 B 时,必须重新执行 pyuic6 。
忘了重新生成是"改了没反应"的头号原因。
坑 2:objectName 重名
同一个界面里两个控件的 objectName 都叫 lineEdit,加载后 self.lineEdit 只有一个能拿到,
另一个永远访问不了。在 Object Inspector 里给每个要访问的控件起个有意义的名字。
坑 3:控件的父级放错了
在 Designer 里把 Label 拖到了窗体上而不是 GroupBox 里,运行时它就会跑到窗口外面。
解决:看 Object Inspector 的树形结构,用拖拽调整层级。
坑 4:打包 exe 时 .ui 文件没带上(方式 A 特有)
方式 A 是运行时读文件,打包时必须用 --add-data 把 .ui 带进去,而且路径要用
resource_path() 处理(第 22 讲)。嫌麻烦就用方式 B ,把 .ui 预编译成 .py,一劳永逸。
八、完整代码
python
# -*- coding: utf-8 -*-
"""第 5 讲 案例 05:加载 Qt Designer 设计的界面
知识点:
- .ui 文件其实就是一个 XML,Qt Designer 只是它的可视化编辑器
- 两种加载方式:
① uic.loadUi(path, self) ------ 运行时加载,改界面不用重新生成代码(推荐开发期)
② pyuic6 xxx.ui -o xxx.py ------ 预编译成 Python 模块(推荐发布期)
- loadUi 到 self 之后,控件名(objectName)直接变成 self 的属性
- 逻辑代码与界面代码分离的标准写法
生成 py 的命令(在工程根目录执行):
pyuic6 ui/student_form.ui -o ui/student_form_ui.py
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from PyQt6 import uic # noqa: E402
from PyQt6.QtWidgets import QWidget # noqa: E402
from utils.env import UI_DIR, run # noqa: E402
UI_FILE = UI_DIR / "student_form.ui"
class StudentFormWindow(QWidget):
"""界面全部来自 .ui 文件,这里只写业务逻辑"""
def __init__(self) -> None:
super().__init__()
# ★ 一行代码把 .ui 里的所有控件挂到 self 上
# 之后 self.editName、self.comboClass、self.btnSubmit 全都可以直接用
uic.loadUi(str(UI_FILE), self)
# 逻辑只放在这里,不和界面混写
self.btnSubmit.clicked.connect(self.on_submit)
self.btnReset.clicked.connect(self.on_reset)
# 对 .ui 载入的元素再做一次微调(也可以,但优先在 Designer 里改)
self.titleLabel.setStyleSheet("font-size:18px; font-weight:bold; padding:8px;")
self.btnSubmit.setStyleSheet(
"QPushButton { background:#2d7ff9; color:white; border:none;"
" border-radius:4px; padding:6px 18px; }"
"QPushButton:hover { background:#1c6be0; }"
)
# ---------------- 业务逻辑 ----------------
def on_submit(self) -> None:
name = self.editName.text().strip()
if not name:
# 不弹窗,直接用列表区提示,避免打断操作
self.listResult.addItem("⚠ 姓名不能为空")
return
cls = self.comboClass.currentText()
score = self.spinScore.value()
self.listResult.addItem(f"✓ 已录入:{name} / {cls} / {score} 分")
self.on_reset()
self.listResult.scrollToBottom()
def on_reset(self) -> None:
self.editName.clear()
self.comboClass.setCurrentIndex(0)
self.spinScore.setValue(600)
self.textRemark.clear()
self.editName.setFocus()
if __name__ == "__main__":
sys.exit(run(StudentFormWindow, shot="c05_designer", width=560, height=400))
九、小结
.ui文件就是 XML,Designer 只是它的可视化编辑器- 两种加载方式 :
uic.loadUi(path, self)(开发期,改完即生效);
pyuic6(发布期,打包更省事) - objectName 是关键,它是界面和逻辑之间的接口,认真起名
- 界面与逻辑分离 :
.ui管长相,.py管行为
现在你能画出漂亮的表单了,但这些都还是"一个孤零零的窗口"。
真实软件有菜单栏、工具栏、状态栏,有"文件 → 打开 → 另存为"这一整套骨架。
下一讲 :QMainWindow 的五大件,我们做一个完整可用的记事本,
顺便讲清 QAction 这个"一次创建、菜单和工具栏共用"的设计,以及快捷键该怎么定义。
本讲是《PyQt6 客户端编程从入门到实战》专栏的第 5 讲。专栏共 22 讲、22 个可运行案例。