Qt Designer 与 UI 文件:.ui 原理、uic 编译、动态加载
引言
前几章我们一直用 C++ 创建控件、设置属性、添加布局。这样做能把 Qt 的基础 API 练熟,但界面一复杂,代码就会迅速变成长长的"控件清单"。Qt Designer 解决的正是这个问题:把界面交给可视化编辑器,把行为和数据留在 C++。
本文面向 Qt 6 Widgets,按"先会用,再理解原理"的顺序学习。读完后应能:
- 用 Qt Designer 设计一个带布局的窗口,并知道
objectName为什么重要; - 读懂
.ui文件中最常见的 XML 节点; - 解释
uic如何把.ui变成ui_*.h,以及setupUi(this)做了什么; - 在 CMake 中开启 AUTOUIC,让构建系统自动生成并编译 UI 代码;
- 用
QUiLoader在运行时加载.ui,理解这种"动态"的边界、优点与代价。

图 1:同一个 .ui 文件可以走编译期路径,也可以走运行时路径。初学时先掌握左侧的设计流程,再理解右侧两条加载路线。
一、先会用:在 Qt Designer 里做出一个登录窗口
1.1 新建表单并选择基类
在 Qt Creator 中右键项目,选择"添加新文件 → Qt → Qt Designer Form"。常见模板有:
- Dialog without Buttons:适合自定义按钮的对话框;
- Main Window:带菜单栏、工具栏、中央窗口区域的主窗口;
- Widget :最普通的
QWidget,适合作为自定义页面或嵌入式面板。
这里选择 Dialog without Buttons,文件名保存为 login_dialog.ui。文件名会影响生成头文件名:
text
login_dialog.ui → ui_login_dialog.h
1.2 拖控件、设属性、先放布局
从左侧 Widget Box 拖入:两个 QLineEdit、一个 QPushButton 和若干 QLabel。在 Object Inspector 中把对象名objectName改成:
text
accountEdit
passwordEdit
loginButton
objectName 不是显示给用户看的文字,而是 Qt 对象的程序标识。在编译期 UI 中,它常用于 findChild()、QSS、自动连接和测试等场景;在 QUiLoader 动态加载中,它尤其重要,因为业务代码通常需要通过 findChild() 根据对象名找到控件。
显示文字应该设置在 text 属性中,例如"登录"。
选中这些控件,点击工具栏中的"垂直布局"或"网格布局"。布局要尽早设置,不要先用鼠标把控件摆到 "看起来合适" 的位置再补布局;那样容易留下固定几何尺寸,窗口缩放时就会变形。
可在 Property Editor 中设置:
windowTitle:窗口标题;placeholderText:输入框提示;echoMode = Password:密码输入框显示圆点;minimumSize/sizePolicy:控件的尺寸约束;layoutSpacing、layoutMargin:布局内部间距和边缘间隔。
1.3 预览,而不是运行
按 Ctrl+R(或菜单"Form → Preview")可以直接预览当前表单。预览只验证界面描述和布局,不会执行你的业务代码。按钮点击没有反应是正常的,因为还没有连接 C++ 槽函数。
可以在 Designer 中编辑信号槽连接,但入门阶段更推荐在 C++ 中连接:这样编译器能够帮助检查函数签名,业务逻辑也不会隐藏在 XML 里。
二、.ui 文件到底是什么
2.1 它是 XML,不是"二进制界面"
用文本编辑器打开 login_dialog.ui,会看到类似结构:
xml
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>LoginDialog</class>
<widget class="QDialog" name="LoginDialog">
<property name="windowTitle">
<string>用户登录</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLineEdit" name="accountEdit">
<property name="placeholderText">
<string>账号</string>
</property>
</widget>
</item>
</layout>
</widget>
</ui>
可以把它读成一棵对象树:
text
QDialog(LoginDialog)
└── QVBoxLayout(verticalLayout)
├── QLineEdit(accountEdit)
└── QPushButton(loginButton)
几个高频节点的含义如下:
| XML 节点 | 作用 | Designer 中对应的概念 |
|---|---|---|
class |
生成代码时使用的类名提示 | Form 的类名 |
widget |
创建一个 QWidget 对象 | 控件和父子关系 |
layout |
描述布局类型和布局对象 | 水平、垂直、网格布局 |
property |
设置对象属性 | Property Editor 中的一行 |
item |
把控件或子布局放入布局 | 布局中的一个项目 |
connection |
描述信号到槽的连接 | Signal/Slot Editor |
resources |
引用资源集合 | .qrc 资源文件 |
.ui 的价值在于"描述",而不是保存运行时对象。打开文件不会得到一个已经存在的 QPushButton;程序必须经过后面的生成或解析步骤,才能创建真正的 C++ 对象。
2.2 .ui、Ui::LoginDialog、LoginDialog 三者是什么关系?
写 Designer 表单时,初学者常会同时看到这三个名字。它们名字相近,但处在三个不同阶段:

图 2:uic 不会替你写业务窗口类;它只生成一个负责创建控件、布局和静态属性的 Ui::LoginDialog 辅助类。
对应到代码,LoginDialog 是你亲手声明并实现的窗口类:
cpp
class LoginDialog : public QDialog
{
Q_OBJECT
public:
explicit LoginDialog(QWidget *parent = nullptr);
private slots:
void tryLogin();
private:
std::unique_ptr<Ui::LoginDialog> ui;
};
它在构造函数中调用 ui->setupUi(this)。Ui::LoginDialog 随后根据 .ui 的描述创建 accountEdit、passwordEdit、loginButton 等子控件,并把它们安装到这个真正的 LoginDialog 窗口上。
.ui 是设计文件,Ui::LoginDialog 是 uic 生成的辅助类,LoginDialog 才是我们真正编写业务逻辑的窗口类。
2.3 .ui 与业务代码应该分工
建议把职责分成三层:
text
login_dialog.ui → 控件、布局、静态属性
LoginDialog 类 → 读取输入、校验、发起登录
LoginService 类 → 网络请求、缓存、业务规则
如果把网络请求、数据库操作直接塞进 setupUi() 生成代码,下一次在 Designer 中保存表单就可能覆盖你的修改。ui_*.h 是构建产物,不要手工编辑。
三、编译期路径:uic 如何把 .ui 变成 C++
3.1 手动运行一次 uic
uic(User Interface Compiler)是 Qt 提供的命令行工具。最小调用方式是:
bash
uic login_dialog.ui -o ui_login_dialog.h
生成的头文件大致如下(省略细节):
cpp
namespace Ui {
class LoginDialog
{
public:
QVBoxLayout *verticalLayout;
QLineEdit *accountEdit;
QPushButton *loginButton;
void setupUi(QDialog *LoginDialog)
{
if (LoginDialog->objectName().isEmpty())
LoginDialog->setObjectName("LoginDialog");
verticalLayout = new QVBoxLayout(LoginDialog);
accountEdit = new QLineEdit(LoginDialog);
verticalLayout->addWidget(accountEdit);
loginButton = new QPushButton(LoginDialog);
verticalLayout->addWidget(loginButton);
retranslateUi(LoginDialog);
}
void retranslateUi(QDialog *LoginDialog)
{
LoginDialog->setWindowTitle(QCoreApplication::translate("LoginDialog", "用户登录"));
loginButton->setText(QCoreApplication::translate("LoginDialog", "登录"));
}
};
}
这里setupUi()本质上就是把Designer中的操作翻译成C++:
| Designer 操作 | .ui |
setupUi() |
|---|---|---|
| 拖入 QPushButton | <widget class="QPushButton"> |
new QPushButton() |
| 修改标题 | <property name="text"> |
setText() |
| 设置布局 | <layout class="QVBoxLayout"> |
new QVBoxLayout() |
| 放入布局 | <item> |
layout->addWidget() |
| 设置对象名 | name="loginButton" |
setObjectName() |
真正的窗口类只需要持有一个 Ui::LoginDialog 对象:
cpp
#include "login_dialog.h"
#include "ui_login_dialog.h"
LoginDialog::LoginDialog(QWidget *parent)
: QDialog(parent), ui(std::make_unique<Ui::LoginDialog>())
{
ui->setupUi(this);
connect(ui->loginButton, &QPushButton::clicked,
this, &LoginDialog::tryLogin);
}
这里发生了三件事:
new Ui::LoginDialog只是创建"界面描述对象";setupUi(this)按.ui中的顺序创建控件、布局并设置属性、设置可翻译文本;connect()把业务行为接到生成好的控件上。
Ui::LoginDialog 本身不是窗口,也不是 QWidget,它只是 uic 生成的辅助类。真正的控件是在 setupUi(this) 执行过程中通过 new 创建出来的。
有些项目把 Ui::LoginDialog ui; 作为值成员,而不是 std::unique_ptr。两种写法都可以;值成员更简单,指针写法便于前置声明和延迟构造。关键点是:窗口类负责拥有 UI,UI 负责搭建控件,业务代码负责使用控件。
3.2 Designer 中的自动连接:QMetaObject::connectSlotsByName()
在 Designer 的"信号/槽编辑器"里创建连接,或给槽函数采用约定命名时,生成的 setupUi() 末尾通常会出现:
cpp
QMetaObject::connectSlotsByName(LoginDialog);
它会从传入的窗口对象开始查找子控件,并按下面的命名规则自动建立连接:
text
on_<对象名>_<信号名>(参数)
例如 Designer 中按钮的 objectName 是 loginButton,希望响应它的 clicked() 信号,就在 LoginDialog 中声明并实现:
cpp
class LoginDialog : public QDialog
{
Q_OBJECT
private slots:
void on_loginButton_clicked();
};
void LoginDialog::on_loginButton_clicked()
{
// 处理登录
}
不需要再手写 connect(ui->loginButton, &QPushButton::clicked, ...):setupUi(this) 调用的 connectSlotsByName() 会根据对象名和槽函数名找到这对关系,并使用 Qt 元对象系统连接它们。要注意,它省去的是 connect() 调用,不是槽函数本身;槽仍应在窗口类中声明并实现,且对象名、信号名和参数必须匹配。
自动连接的优点是上手快,但它依赖字符串式命名约定:控件改名或槽函数改名时,问题通常到运行时才暴露。现代 Qt C++ 开发中,更推荐显式 connect(),因为函数签名清晰、重构友好;自动连接更适合了解 Qt Designer 生成代码时作为知识点掌握。
3.3 CMake 自动调用 uic
Qt 6 项目推荐使用 CMake:
cmake
cmake_minimum_required(VERSION 3.21)
project(LoginDemo LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)
find_package(Qt6 REQUIRED COMPONENTS Widgets)
qt_add_executable(LoginDemo
main.cpp
login_dialog.cpp
login_dialog.h
login_dialog.ui
)
target_link_libraries(LoginDemo PRIVATE Qt6::Widgets)
CMAKE_AUTOUIC ON 的含义不是"把 .ui 编译成机器码",而是让 CMake 在生成构建规则时识别 .ui,在需要时调用 uic 生成 ui_*.h,再让 C++ 编译器编译包含它的源文件。
Qt 6 也可以使用:
cmake
qt_standard_project_setup()
它会为 Qt 项目打开常用的自动处理能力,通常包括 AUTOMOC、AUTOUIC 和 AUTORCC。为了让初学者清楚看到开关,本文仍保留显式写法。

图 3:CMake 负责发现和安排任务,uic 负责生成头文件,最终仍由普通 C++ 编译器完成编译。
3.4 怎样确认 AUTOUIC 真的生效
遇到 ui_login_dialog.h: No such file 时,按下面顺序检查:
.ui是否加入了qt_add_executable()或目标的源文件列表;- 是否重新运行了 CMake 配置,而不是只点击编译;
login_dialog.cpp的#include "ui_login_dialog.h"是否与表单文件名匹配;- 查看构建目录(具体放在哪里可以查看专栏04文章第七节),确认存在
ui_login_dialog.h; - 打开详细构建输出,搜索
uic,确认命令实际被执行。
如果 .ui 放在 forms/ 等独立目录,可补充搜索路径:
cmake
set(CMAKE_AUTOUIC_SEARCH_PATHS
${CMAKE_CURRENT_SOURCE_DIR}/forms
)
也可以使用目标级属性,避免影响其他目标:
cmake
set_property(TARGET LoginDemo PROPERTY AUTOUIC ON)
3.5 显式调用 uic:适合排错和特殊构建
大多数项目不需要手写自定义命令,但知道显式写法有助于排错:
cmake
set(GENERATED_UI ${CMAKE_CURRENT_BINARY_DIR}/ui_login_dialog.h)
add_custom_command(
OUTPUT ${GENERATED_UI}
COMMAND Qt6::uic
${CMAKE_CURRENT_SOURCE_DIR}/login_dialog.ui
-o ${GENERATED_UI}
DEPENDS login_dialog.ui
VERBATIM
)
target_sources(LoginDemo PRIVATE ${GENERATED_UI})
自动方式适合常规应用;显式方式适合需要固定生成目录、对生成文件做额外检查,或正在排查构建依赖的场景。两者不要同时对同一个 .ui 生效,否则可能生成两份文件。
四、运行时路径:QUiLoader 如何体现"动态"
4.1 先把 .ui 放进资源系统
动态加载不能依赖当前工作目录,否则从 IDE 启动和双击 exe 启动可能得到不同结果。推荐把表单加入 .qrc:
xml
<RCC>
<qresource prefix="/forms">
<file>login_dialog.ui</file>
</qresource>
</RCC>
CMake:
cmake
qt_add_executable(LoginDynamic
main.cpp
login_dynamic.cpp
forms.qrc
)
find_package(Qt6 REQUIRED COMPONENTS Widgets UiTools)
target_link_libraries(LoginDynamic PRIVATE Qt6::Widgets Qt6::UiTools)
使用 .qrc 的主要价值不是 QUiLoader 的硬性要求,而是避免依赖当前工作目录,并且可以把 .ui 一起编译进程序资源,减少部署时的路径问题。
4.2 最小动态加载代码
cpp
#include <QFile>
#include <QMessageBox>
#include <QUiLoader>
QWidget *loadLoginForm(QWidget *parent)
{
QFile file(":/forms/login_dialog.ui");
if (!file.open(QIODevice::ReadOnly)) {
qWarning() << "open ui failed:" << file.errorString();
return nullptr;
}
QUiLoader loader;
QWidget *root = loader.load(&file, parent);
file.close();
if (!root) {
qWarning() << "load ui failed:" << loader.errorString();
return nullptr;
}
auto *loginButton = root->findChild<QPushButton *>("loginButton");
auto *accountEdit = root->findChild<QLineEdit *>("accountEdit");
if (!loginButton || !accountEdit) {
root->deleteLater();
return nullptr;
}
QObject::connect(loginButton, &QPushButton::clicked,
root, [accountEdit] {
QMessageBox::information(nullptr, QObject::tr("提示"),
accountEdit->text());
});
return root;
}
调用:
cpp
if (QWidget *form = loadLoginForm(nullptr))
form->show();

图 4:动态加载时没有 ui_*.h,程序在运行中读取 XML,并通过 Qt 元对象系统创建控件。
4.3 "动态"不等于"完全不写代码"
动态加载改变的是界面创建时机,不是业务逻辑的写法。
QUiLoader 并不是把 .ui 编译成 C++ 后再运行,而是在程序运行过程中直接解析 XML 描述,并根据其中的信息创建对象。
因为没有 ui->loginButton 这种编译期成员,通常通过 objectName 查找:
cpp
auto *button = root->findChild<QPushButton *>("loginButton");
这带来一个明显边界:如果 Designer 中把 loginButton 改名,编译器不会报错,程序只会在运行时找不到对象。因此动态 UI 更需要:
- 对关键对象名做集中常量定义;
- 加载后立即检查必需控件;
- 为
.ui做版本兼容或自动化加载测试。
自定义控件还要额外处理。QUiLoader 只认识 Qt 内置控件;遇到自定义类,通常需要继承 QUiLoader 并重写 createWidget(),或者在加载前注册可用的自定义控件。否则会出现"未知控件"或根对象为空。
五、编译期与运行时:该怎么选
| 对比项 | 编译期 uic |
运行时 QUiLoader |
|---|---|---|
| UI 变化后 | 重新构建 | 替换 .ui 即可生效 |
| 错误暴露 | 许多问题在编译期发现 | 很多问题推迟到运行期 |
| 访问控件 | ui->button,类型明确 |
findChild(),依赖名称 |
| 启动性能 | 通常更好 | 需要解析 XML 和创建对象 |
| 发布方式 | .ui 可不随程序发布 |
.ui 必须随程序或进资源 |
| 适合场景 | 稳定产品界面、强类型开发 | 插件化、主题替换、可配置页面 |
入门阶段默认选择编译期路径。它的代码更容易被 IDE 补全、编译器检查和重构工具理解。只有当"无需重新编译就要换界面"确实是需求时,再引入 QUiLoader。
六、从一个表单扩展到可维护项目
6.1 头文件与源文件的推荐结构
text
forms/
login_dialog.ui
src/
login_dialog.h
login_dialog.cpp
login_service.h
login_service.cpp
CMakeLists.txt
login_dialog.cpp 只关心界面行为:读取输入、显示错误、发出登录请求。login_service.cpp 负责业务和网络。这样 Designer 反复调整布局时,不会牵动业务层。
6.2 翻译与重新载入
uic 生成的 retranslateUi() 会集中设置可翻译文本。程序切换语言时,窗口类可以在 changeEvent(QEvent::LanguageChange) 中再次调用 ui->retranslateUi(this)(或重新实现相同逻辑),而不是到处手动修改标签文字。
动态加载也可以重新读取 .ui,但要先销毁旧根对象,再恢复业务状态;否则容易产生重复连接和悬空指针。换肤通常更适合使用 Qt Style Sheet 或资源系统,不必把整个 UI 改成动态加载。
6.3 生成文件、构建目录和版本控制
通常只提交 .ui、C++ 源码和 CMake 文件,不提交构建目录里的 ui_*.h。生成文件属于可重复产物:任何开发者都可以通过同一 Qt 版本和 CMake 配置重新得到它。
七、常见问题速查
问题 1:ui_xxx.h 找不到
优先检查 CMAKE_AUTOUIC、.ui 是否加入目标源文件,以及是否重新配置 CMake。不要直接复制一份旧的 ui_xxx.h 到源码目录,这只会掩盖构建依赖问题。
问题 2:控件指针为空
编译期路径中通常是 objectName 或类名写错;动态路径中则是 findChild() 名字不匹配、类型不匹配,或 .ui 根对象加载失败。
问题 3:布局在预览中正常,运行时却挤压
检查是否真的给父容器设置了布局,是否残留了 setGeometry(),以及控件的 sizePolicy、最小尺寸和字体是否与预览环境不同。
问题 4:Designer 里能放自定义控件,动态加载却失败
Designer 的"提升为(Promoted to)"只是设计期信息;QUiLoader 运行时仍需要能创建这个类。为自定义类提供工厂、插件或 createWidget() 实现。
八、一句话总结
Qt Designer 负责把界面画出来,.ui 负责把界面描述成 XML,uic 负责在构建期把 XML 转成 ui_*.h,而 QUiLoader 则在运行期直接解析 XML 创建 QWidget。默认用编译期路径获得更早的错误检查;只有确实需要"替换界面而不重新编译"时,才选择动态加载。
现在我们已经基本了解Qt环境的组成,下一章可以在此基础上对常用UI控件进行了解学习
下一篇预告:《常用控件速查(上):QPushButton、QLabel、QLineEdit、QComboBox》