.ui 保存界面的声明,uic 把它转换成 C++ 辅助类,窗口类再调用 setupUi(this) 建立控件树。理解这一层分工,才能回答两个常见问题:Ui::YOLOWINDOW 与 YOLOWINDOW 为什么能同名,以及窗口析构时究竟要释放什么。
从 .ui 到窗口对象
跳转到“从 .ui 到窗口对象”在典型的组合写法中,三类对象承担不同职责:
| 对象 | 内容 | 生命周期 |
|---|---|---|
.ui 文件 | 控件类型、布局、属性、连接等 XML 声明 | 构建输入,不是运行中的窗口 |
Ui::Form | 生成的普通 C++ 辅助类,包含控件指针以及 setupUi、retranslateUi 等方法 | 由业务类选择值成员、智能指针或裸指针管理 |
Form : public QWidget | 业务窗口,承接界面事件和应用逻辑 | 遵循 C++ 生命周期和 Qt 对象树规则 |
常规生成的 Ui::Form 不继承 QWidget,也不是 QObject,所以不能指望 ui->layout()、ui->show() 这类窗口方法存在。需要访问窗口自身的 layout(),或者访问生成类中的具名成员,例如 ui->verticalLayout。Qt 官方介绍了值成员、指针成员及多重继承等集成方式;本文使用组合与智能指针,保持生成代码和业务代码分离。Qt:在应用中使用 UI 文件
名字相近不表示它们来自同一处。以本节后面的例子为准:
| 名字 | 来源 | 本例 |
|---|---|---|
.ui 文件名 | 项目文件系统 | panel_form.ui |
| 常规生成头文件名 | 构建工具调用 uic 时选择的输出名 | ui_panel_form.h |
| 生成辅助类名 | XML 的 <class> 元素 | Ui::DifferentForm |
| 根控件类型 | XML 根 <widget class="…"> | QWidget,决定 setupUi 接收的类型 |
| 根控件对象名 | XML 根 <widget name="…"> | rootPanel,用于对象命名 |
| 业务窗口类名 | 自己编写的 C++ 类 | Panel |
因此,重命名 .ui 文件不会自动把 XML 中的 <class> 改成新名字;业务类也不必与辅助类同名。原先的 YOLOWINDOW 和 Ui::YOLOWINDOW 只是位于不同命名空间的两个类型。头文件中的 namespace Ui { class YOLOWINDOW; } 是前向声明;QT_BEGIN_NAMESPACE、QT_END_NAMESPACE 则兼容使用命名空间构建的 Qt,不能把它们理解成产生 Ui 命名空间的原因。
不要直接修改生成的 ui_*.h。下次运行 uic 就可能覆盖它;界面布局改 .ui,行为改自己的 .h/.cpp。Qt:uic
delete ui 释放的是什么
跳转到“delete ui 释放的是什么”setupUi(this) 会创建控件和布局,并按生成代码设置父子关系。辅助类保存这些对象的指针,但普通指针成员自身不会在辅助类析构时执行 delete。在这里,销毁 Ui::DifferentForm 只销毁辅助对象;已经创建的控件继续由窗口对象树管理。
这解释了常见模板中的两步:如果构造函数写了 ui(new Ui::Form),业务析构函数通常写 delete ui;;随后窗口基类的析构会回收其子对象。前一句用于配对这个裸指针分配,不等于“删除界面上所有控件”。若改用值成员或 std::unique_ptr,就不应再手写同一对象的 delete。也不存在“所有 Qt 程序都必须先删除 ui,再删除其他一切”的普遍顺序规则;顺序取决于成员之间是否仍有访问依赖。Qt:对象树与所有权
原来的 SettingScreen 阴影例子还涉及另一条所有权路径:QWidget::setGraphicsEffect(effect) 接管效果对象。即便创建效果时没有传入 parent,也不能因此断言它必然泄漏。局部变量 shadow 只是指针,离开函数不会销毁它指向的堆对象;谁最终释放它,要看对象树或接管它的 API 契约。替换一个已有的图形效果时,旧效果也会被删除。Qt:setGraphicsEffect
一个可构建的 Designer 项目
跳转到“一个可构建的 Designer 项目”以下五个文件放在同一目录。示例刻意让文件名、XML 类名和业务类名不同,并保留原阴影参数:偏移 (5, 5)、颜色 (43, 43, 43)、模糊半径 8。正常启动显示窗口;传入 --check 执行生命周期断言后退出。
panel_form.ui:
<?xml version="1.0" encoding="UTF-8"?><ui version="4.0"> <class>DifferentForm</class> <widget class="QWidget" name="rootPanel"> <property name="windowTitle"><string>Designer ownership</string></property> <layout class="QVBoxLayout" name="rootLayout"> <item> <widget class="QFrame" name="cardFrame"> <layout class="QVBoxLayout" name="cardLayout"> <item> <widget class="QPushButton" name="pushButton"> <property name="text"><string>Apply</string></property> </widget> </item> </layout> </widget> </item> </layout> </widget> <resources/> <connections/></ui>panel.h:
#pragma once#include <QWidget>#include <memory>
QT_BEGIN_NAMESPACEnamespace Ui { class DifferentForm; }QT_END_NAMESPACE
class Panel final : public QWidget {public: explicit Panel(QWidget *parent = nullptr); ~Panel() override;private: std::unique_ptr<Ui::DifferentForm> ui;};panel.cpp:
#include "panel.h"#include "ui_panel_form.h"#include <QColor>#include <QGraphicsDropShadowEffect>
Panel::Panel(QWidget *parent) : QWidget(parent), ui(std::make_unique<Ui::DifferentForm>()) { ui->setupUi(this); auto *shadow = new QGraphicsDropShadowEffect; shadow->setOffset(5, 5); shadow->setColor(QColor(43, 43, 43)); shadow->setBlurRadius(8); ui->cardFrame->setGraphicsEffect(shadow);}
Panel::~Panel() = default;std::unique_ptr 的成员析构需要看到完整的辅助类定义,因此析构函数定义放在包含生成头文件的 .cpp 中,不能在只有前向声明的头文件里随意内联默认析构。
main.cpp:
#include "panel.h"#include "ui_panel_form.h"#include <QApplication>#include <QFrame>#include <QGraphicsEffect>#include <QPointer>#include <QPushButton>#include <cassert>#include <memory>
int main(int argc, char *argv[]) { QApplication app(argc, argv); if (app.arguments().contains("--check")) { QPointer<QPushButton> child; { QWidget host; auto helper = std::make_unique<Ui::DifferentForm>(); helper->setupUi(&host); child = helper->pushButton; assert(child && host.objectName() == "rootPanel"); helper.reset(); assert(child); // 辅助类没了,控件仍在宿主对象树中 } assert(child.isNull());
QPointer<QFrame> frame; QPointer<QGraphicsEffect> effect; { Panel panel; frame = panel.findChild<QFrame *>("cardFrame"); assert(frame); effect = frame->graphicsEffect(); assert(effect); } assert(frame.isNull() && effect.isNull()); return 0; } Panel panel; panel.resize(360, 180); panel.show(); return app.exec();}CMakeLists.txt:
cmake_minimum_required(VERSION 3.20)project(DesignerOwnership LANGUAGES CXX)set(CMAKE_CXX_STANDARD 17)set(CMAKE_CXX_STANDARD_REQUIRED ON)set(CMAKE_AUTOUIC ON)find_package(Qt6 REQUIRED COMPONENTS Widgets)add_executable(designer_ownership main.cpp panel.cpp panel.h panel_form.ui)target_link_libraries(designer_ownership PRIVATE Qt6::Widgets)在配置好的 Qt 开发环境里构建并运行:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debugcmake --build build根据所用生成器到构建输出目录运行 designer_ownership --check;多配置生成器需选择 Debug。若 CMake 无法找到 Qt,给 CMAKE_PREFIX_PATH 指定对应 Qt 安装目录。AUTOUIC 处理这里的 #include "ui_panel_form.h" 与 .ui 输入,不需要手工维护生成头文件。使用 qmake 的项目则通常把文件加入 FORMS += panel_form.ui,并配置 QT += widgets。Qt:CMake 与 Designer UI 文件
排查时沿所有权逐层确认
跳转到“排查时沿所有权逐层确认”遇到泄漏或崩溃,先确认 ui 是辅助类指针还是实际窗口指针,再检查控件的 parent()、是否交给 setWidget、setGraphicsEffect 等接管 API,最后确认回调是否还会访问已销毁对象。QPointer 可以观察 QObject 是否已经销毁;它不拥有对象,也不能替代所有权设计。布局移除是否隐藏或销毁控件,另见 [[10-knowledge/computing/software-development/application-development/desktop/qt/gui/layout-ownership-and-removal|布局、Spacer 与移除控件]]。