10_Qt Designer 与 UI 文件(.ui 原理、uic 编译、动态加载)

📅 发布时间:2026/8/28 19:11:50
10_Qt Designer 与 UI 文件(.ui 原理、uic 编译、动态加载)
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。文件名会影响生成头文件名login_dialog.ui → ui_login_dialog.h1.2 拖控件、设属性、先放布局从左侧 Widget Box 拖入两个QLineEdit、一个QPushButton和若干QLabel。在 Object Inspector 中把对象名objectName改成accountEdit passwordEdit loginButtonobjectName不是显示给用户看的文字而是 Qt 对象的程序标识。在编译期 UI 中它常用于findChild()、QSS、自动连接和测试等场景在QUiLoader动态加载中它尤其重要因为业务代码通常需要通过findChild()根据对象名找到控件。显示文字应该设置在text属性中例如“登录”。选中这些控件点击工具栏中的“垂直布局”或“网格布局”。布局要尽早设置不要先用鼠标把控件摆到 “看起来合适” 的位置再补布局那样容易留下固定几何尺寸窗口缩放时就会变形。可在 Property Editor 中设置windowTitle窗口标题placeholderText输入框提示echoMode Password密码输入框显示圆点minimumSize/sizePolicy控件的尺寸约束layoutSpacing、layoutMargin布局内部间距和边缘间隔。1.3 预览而不是运行按CtrlR或菜单“Form → Preview”可以直接预览当前表单。预览只验证界面描述和布局不会执行你的业务代码。按钮点击没有反应是正常的因为还没有连接 C 槽函数。可以在 Designer 中编辑信号槽连接但入门阶段更推荐在 C 中连接这样编译器能够帮助检查函数签名业务逻辑也不会隐藏在 XML 里。二、.ui文件到底是什么2.1 它是 XML不是“二进制界面”用文本编辑器打开login_dialog.ui会看到类似结构?xml version1.0 encodingUTF-8?uiversion4.0classLoginDialog/classwidgetclassQDialognameLoginDialogpropertynamewindowTitlestring用户登录/string/propertylayoutclassQVBoxLayoutnameverticalLayoutitemwidgetclassQLineEditnameaccountEditpropertynameplaceholderTextstring账号/string/property/widget/item/layout/widget/ui可以把它读成一棵对象树QDialog(LoginDialog) └── QVBoxLayout(verticalLayout) ├── QLineEdit(accountEdit) └── QPushButton(loginButton)几个高频节点的含义如下XML 节点作用Designer 中对应的概念class生成代码时使用的类名提示Form 的类名widget创建一个 QWidget 对象控件和父子关系layout描述布局类型和布局对象水平、垂直、网格布局property设置对象属性Property Editor 中的一行item把控件或子布局放入布局布局中的一个项目connection描述信号到槽的连接Signal/Slot Editorresources引用资源集合.qrc资源文件.ui的价值在于“描述”而不是保存运行时对象。打开文件不会得到一个已经存在的QPushButton程序必须经过后面的生成或解析步骤才能创建真正的 C 对象。2.2.ui、Ui::LoginDialog、LoginDialog三者是什么关系写 Designer 表单时初学者常会同时看到这三个名字。它们名字相近但处在三个不同阶段图 2uic不会替你写业务窗口类它只生成一个负责创建控件、布局和静态属性的Ui::LoginDialog辅助类。对应到代码LoginDialog是你亲手声明并实现的窗口类classLoginDialog:publicQDialog{Q_OBJECTpublic:explicitLoginDialog(QWidget*parentnullptr);privateslots:voidtryLogin();private:std::unique_ptrUi::LoginDialogui;};它在构造函数中调用ui-setupUi(this)。Ui::LoginDialog随后根据.ui的描述创建accountEdit、passwordEdit、loginButton等子控件并把它们安装到这个真正的LoginDialog窗口上。.ui是设计文件Ui::LoginDialog是uic生成的辅助类LoginDialog才是我们真正编写业务逻辑的窗口类。2.3.ui与业务代码应该分工建议把职责分成三层login_dialog.ui → 控件、布局、静态属性 LoginDialog 类 → 读取输入、校验、发起登录 LoginService 类 → 网络请求、缓存、业务规则如果把网络请求、数据库操作直接塞进setupUi()生成代码下一次在 Designer 中保存表单就可能覆盖你的修改。ui_*.h是构建产物不要手工编辑。三、编译期路径uic 如何把.ui变成 C3.1 手动运行一次uicuicUser Interface Compiler是 Qt 提供的命令行工具。最小调用方式是uic login_dialog.ui-oui_login_dialog.h生成的头文件大致如下省略细节namespaceUi{classLoginDialog{public:QVBoxLayout*verticalLayout;QLineEdit*accountEdit;QPushButton*loginButton;voidsetupUi(QDialog*LoginDialog){if(LoginDialog-objectName().isEmpty())LoginDialog-setObjectName(LoginDialog);verticalLayoutnewQVBoxLayout(LoginDialog);accountEditnewQLineEdit(LoginDialog);verticalLayout-addWidget(accountEdit);loginButtonnewQPushButton(LoginDialog);verticalLayout-addWidget(loginButton);retranslateUi(LoginDialog);}voidretranslateUi(QDialog*LoginDialog){LoginDialog-setWindowTitle(QCoreApplication::translate(LoginDialog,用户登录));loginButton-setText(QCoreApplication::translate(LoginDialog,登录));}};}这里setupUi()本质上就是把Designer中的操作翻译成CDesigner 操作.uisetupUi()拖入 QPushButtonwidget classQPushButtonnew QPushButton()修改标题property nametextsetText()设置布局layout classQVBoxLayoutnew QVBoxLayout()放入布局itemlayout-addWidget()设置对象名nameloginButtonsetObjectName()真正的窗口类只需要持有一个Ui::LoginDialog对象#includelogin_dialog.h#includeui_login_dialog.hLoginDialog::LoginDialog(QWidget*parent):QDialog(parent),ui(std::make_uniqueUi::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。两种写法都可以值成员更简单指针写法便于前置声明和延迟构造。关键点是窗口类负责拥有 UIUI 负责搭建控件业务代码负责使用控件。3.2 Designer 中的自动连接QMetaObject::connectSlotsByName()在 Designer 的“信号/槽编辑器”里创建连接或给槽函数采用约定命名时生成的setupUi()末尾通常会出现QMetaObject::connectSlotsByName(LoginDialog);它会从传入的窗口对象开始查找子控件并按下面的命名规则自动建立连接on_对象名_信号名(参数)例如 Designer 中按钮的objectName是loginButton希望响应它的clicked()信号就在LoginDialog中声明并实现classLoginDialog:publicQDialog{Q_OBJECTprivateslots:voidon_loginButton_clicked();};voidLoginDialog::on_loginButton_clicked(){// 处理登录}不需要再手写connect(ui-loginButton, QPushButton::clicked, ...)setupUi(this)调用的connectSlotsByName()会根据对象名和槽函数名找到这对关系并使用 Qt 元对象系统连接它们。要注意它省去的是connect()调用不是槽函数本身槽仍应在窗口类中声明并实现且对象名、信号名和参数必须匹配。自动连接的优点是上手快但它依赖字符串式命名约定控件改名或槽函数改名时问题通常到运行时才暴露。现代 Qt C 开发中更推荐显式connect()因为函数签名清晰、重构友好自动连接更适合了解 Qt Designer 生成代码时作为知识点掌握。3.3 CMake 自动调用 uicQt 6 项目推荐使用 CMakecmake_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 也可以使用qt_standard_project_setup()它会为 Qt 项目打开常用的自动处理能力通常包括AUTOMOC、AUTOUIC和AUTORCC。为了让初学者清楚看到开关本文仍保留显式写法。图 3CMake 负责发现和安排任务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/等独立目录可补充搜索路径set(CMAKE_AUTOUIC_SEARCH_PATHS ${CMAKE_CURRENT_SOURCE_DIR}/forms )也可以使用目标级属性避免影响其他目标set_property(TARGET LoginDemo PROPERTY AUTOUIC ON)3.5 显式调用 uic适合排错和特殊构建大多数项目不需要手写自定义命令但知道显式写法有助于排错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 启动可能得到不同结果。推荐把表单加入.qrcRCCqresourceprefix/formsfilelogin_dialog.ui/file/qresource/RCCCMakeqt_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 最小动态加载代码#includeQFile#includeQMessageBox#includeQUiLoaderQWidget*loadLoginForm(QWidget*parent){QFilefile(:/forms/login_dialog.ui);if(!file.open(QIODevice::ReadOnly)){qWarning()open ui failed:file.errorString();returnnullptr;}QUiLoader loader;QWidget*rootloader.load(file,parent);file.close();if(!root){qWarning()load ui failed:loader.errorString();returnnullptr;}auto*loginButtonroot-findChildQPushButton*(loginButton);auto*accountEditroot-findChildQLineEdit*(accountEdit);if(!loginButton||!accountEdit){root-deleteLater();returnnullptr;}QObject::connect(loginButton,QPushButton::clicked,root,[accountEdit]{QMessageBox::information(nullptr,QObject::tr(提示),accountEdit-text());});returnroot;}调用if(QWidget*formloadLoginForm(nullptr))form-show();图 4动态加载时没有ui_*.h程序在运行中读取 XML并通过 Qt 元对象系统创建控件。4.3 “动态”不等于“完全不写代码”动态加载改变的是界面创建时机不是业务逻辑的写法。QUiLoader并不是把.ui编译成 C 后再运行而是在程序运行过程中直接解析 XML 描述并根据其中的信息创建对象。因为没有ui-loginButton这种编译期成员通常通过objectName查找auto*buttonroot-findChildQPushButton*(loginButton);这带来一个明显边界如果 Designer 中把loginButton改名编译器不会报错程序只会在运行时找不到对象。因此动态 UI 更需要对关键对象名做集中常量定义加载后立即检查必需控件为.ui做版本兼容或自动化加载测试。自定义控件还要额外处理。QUiLoader只认识 Qt 内置控件遇到自定义类通常需要继承QUiLoader并重写createWidget()或者在加载前注册可用的自定义控件。否则会出现“未知控件”或根对象为空。五、编译期与运行时该怎么选对比项编译期uic运行时QUiLoaderUI 变化后重新构建替换.ui即可生效错误暴露许多问题在编译期发现很多问题推迟到运行期访问控件ui-button类型明确findChild()依赖名称启动性能通常更好需要解析 XML 和创建对象发布方式.ui可不随程序发布.ui必须随程序或进资源适合场景稳定产品界面、强类型开发插件化、主题替换、可配置页面入门阶段默认选择编译期路径。它的代码更容易被 IDE 补全、编译器检查和重构工具理解。只有当“无需重新编译就要换界面”确实是需求时再引入QUiLoader。六、从一个表单扩展到可维护项目6.1 头文件与源文件的推荐结构forms/ login_dialog.ui src/ login_dialog.h login_dialog.cpp login_service.h login_service.cpp CMakeLists.txtlogin_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 配置重新得到它。七、常见问题速查问题 1ui_xxx.h找不到优先检查CMAKE_AUTOUIC、.ui是否加入目标源文件以及是否重新配置 CMake。不要直接复制一份旧的ui_xxx.h到源码目录这只会掩盖构建依赖问题。问题 2控件指针为空编译期路径中通常是objectName或类名写错动态路径中则是findChild()名字不匹配、类型不匹配或.ui根对象加载失败。问题 3布局在预览中正常运行时却挤压检查是否真的给父容器设置了布局是否残留了setGeometry()以及控件的sizePolicy、最小尺寸和字体是否与预览环境不同。问题 4Designer 里能放自定义控件动态加载却失败Designer 的“提升为Promoted to”只是设计期信息QUiLoader运行时仍需要能创建这个类。为自定义类提供工厂、插件或createWidget()实现。八、一句话总结Qt Designer 负责把界面画出来.ui负责把界面描述成 XMLuic负责在构建期把 XML 转成ui_*.h而QUiLoader则在运行期直接解析 XML 创建 QWidget。默认用编译期路径获得更早的错误检查只有确实需要“替换界面而不重新编译”时才选择动态加载。现在我们已经基本了解Qt环境的组成下一章可以在此基础上对常用UI控件进行了解学习下一篇预告《常用控件速查上QPushButton、QLabel、QLineEdit、QComboBox》