跳转到内容
新建笔记

Qt Designer 生成类与界面对象的生命周期

.ui 保存界面的声明,uic 把它转换成 C++ 辅助类,窗口类再调用 setupUi(this) 建立控件树。理解这一层分工,才能回答两个常见问题:Ui::YOLOWINDOW 与 YOLOWINDOW 为什么能同名,以及窗口析构时究竟要释放什么。

在典型的组合写法中,三类对象承担不同职责:

对象内容生命周期
.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

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_NAMESPACE
namespace 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=Debug
cmake --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 与移除控件]]。