PyQt6-第05讲-QtDesigner工作流与ui文件

第 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>
   ...

看懂这段,你就明白三件事:

  1. Designer 只是这个 XML 的可视化编辑器 ,它生成的东西和手写 QVBoxLayout 完全等价
  2. name 属性(也就是 objectName)就是控件的"变量名",加载后靠它访问控件
  3. .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 里的布局不是拖出来的,是选中若干控件后右键设置的:

  1. 按住 Ctrl 选中同一行的几个控件
  2. 右键 → Layouts → Lay Out Horizontally(横向)或 Vertically(纵向)
  3. 整块区域选好后,再右键外层 → 套一层新的布局

更省事的做法 :先往窗体里拖一个布局容器(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 个可运行案例。

相关推荐
m0_674571973 小时前
力扣 hot 100 python
python·算法·leetcode
小白快快跑哦3 小时前
python-字符串全解(六):正则表达式-元字符与转义
python·正则表达式·字符串·元字符·转义
Want5954 小时前
Python炫酷烟花①
开发语言·python
青少儿编程课堂4 小时前
蓝桥杯+全国青少年信息素养大赛+电子学会PythonC/C++考级:最长公共子序列LCS解析
c++·python·算法·bfs·信息学竞赛
Y3815326624 小时前
印度电商数据采集教程:用 SerpBase 查 Flipkart/Myntra/Ajio 商品(附代码)
数据库·python
阿维的博客日记5 小时前
大白话解析快排Python
python
doujiaozh5 小时前
我在 DOS 6.22 上跑通了 Kimi AI:一场跨越 40 年的硬核接力
python
凛铄linshuo5 小时前
分析业务流程什么时候适合断言?
python·pytest
import_random5 小时前
[python]IDE可以自动检测venv工具创建的虚拟环境.venv吗
python
楠风测开5 小时前
Python学习路线:搭好环境,跑通第一个脚本
python