简介:这是一份围绕 Qt5 多 UI 界面切换与弹窗显示的小型工程示例,面向正在学习 Qt 界面编程、需要实现窗口跳转与模态对话框的开发者,也可作为课程演示或自测项目参考。压缩包共 29 个文件,约 57KB,以 cpp、h 源码和 ui 界面文件为核心,配合 .pro 工程文件与 Makefile 组织构建流程,并包含少量编译产物,便于直接查看工程结构与类之间的关系。内容覆盖主窗口内多子界面切换、QDialog 自定义弹窗、QMessageBox 提示以及信号与槽的界面管理思路,能帮助读者理解多窗口协作时的显示隐藏、模态交互和基础工程配置。已有 182 人学习或浏览,适合作为 Qt5 界面切换与弹窗实现的入门级参考样例,用于对照自己的工程结构、补充界面管理思路和排查构建配置问题。
1. 从一次页面错位说起:Qt5 多 UI 界面切换与弹窗到底解决什么问题
主窗口里塞了六七个页面,导航栏点第三项,界面却停在第二页;关掉一个"确认删除"的弹窗,程序直接退出。这类问题在 Qt UI 工程里非常典型,根源往往不是控件用错,而是界面没有被统一管理——页面各自 new、各自 show,弹窗各自 exec,谁也不知道当前该显示谁。这份资源围绕 Qt5 多 UI 界面切换和弹窗显示两条主线,把页面收进同一个容器、把弹窗收进同一个出口,切换有明确入口,返回值有明确落点。刚接触 Qt5 的人可以照着搭一遍骨架,手里有老工程想拆出界面层的人,也能从这里拿到一套可用的组织方式。
2. 工程骨架与目录结构:.ui 文件怎么变成可切换的页面
很多人第一次做多界面,习惯在主窗口构造函数里连着写十几个new,每个页面一个成员变量,切换时手动hide()一个show()一个。代码跑到两百行就开始失控,加一个页面要改三处,删一个页面要全局搜。真正省事的做法是把页面当资源来管:目录上分层,构建上让 uic 自动生成代码,运行上用容器统一承载。
2.1 目录结构与构建配置:uic、moc 各管什么
我一般按"窗口 / 页面 / 弹窗"三层拆目录,pages放可切换的主界面,dialogs放弹窗,主窗口只留骨架和导航。这样后面无论加页面还是加弹窗,改动都收敛在一个目录里。
ui-switch-demo/ ├── ui-switch-demo.pro ├── main.cpp ├── mainwindow.h ├── mainwindow.cpp ├── mainwindow.ui ├── pages/ │ ├── homepage.h / homepage.cpp / homepage.ui │ └── settingpage.h / settingpage.cpp / settingpage.ui ├── dialogs/ │ └── confirmdialog.h / confirmdialog.cpp / confirmdialog.ui └── resources/ └── style.qssqmake 工程的.pro要做的就是把这三类文件登记进去,FORMS里列出的.ui会被 uic 在构建期翻译成ui_xxx.h,落到构建目录而不是源码目录。
QT += core gui widgets TARGET = ui-switch-demo TEMPLATE = app SOURCES += \ main.cpp \ mainwindow.cpp \ pages/homepage.cpp \ pages/settingpage.cpp \ dialogs/confirmdialog.cpp HEADERS += \ mainwindow.h \ pages/homepage.h \ pages/settingpage.h \ dialogs/confirmdialog.h FORMS += \ mainwindow.ui \ pages/homepage.ui \ pages/settingpage.ui \ dialogs/confirmdialog.ui RESOURCES += resources.qrcTEMPLATE = app表示产出可执行程序;FORMS是关键,少写一个.ui就会出现"控件明明拖了、编译却提示ui->xxx不存在"的情况。SOURCES/HEADERS的路径相对.pro所在目录,换行用反斜杠续行,末尾不能有反斜杠。
新工程我更倾向 CMake,因为 AUTOUIC 打开之后不用手工维护 uic 规则,新增.ui只要保证和同名.h在同一目录。
cmake_minimum_required(VERSION 3.5) project(ui-switch-demo LANGUAGES CXX) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_AUTOMOC ON) # 处理 Q_OBJECT 的 moc set(CMAKE_AUTOUIC ON) # 处理 .ui -> ui_xxx.h set(CMAKE_AUTORCC ON) # 处理 .qrc find_package(Qt5 COMPONENTS Widgets REQUIRED) add_executable(ui-switch-demo main.cpp mainwindow.cpp mainwindow.h mainwindow.ui pages/homepage.cpp pages/homepage.h pages/homepage.ui pages/settingpage.cpp pages/settingpage.h pages/settingpage.ui dialogs/confirmdialog.cpp dialogs/confirmdialog.h dialogs/confirmdialog.ui ) target_link_libraries(ui-switch-demo PRIVATE Qt5::Widgets)三个AUTOx开关里,AUTOUIC最容易出问题:它要求#include "ui_mainwindow.h"里的名字能和某个mainwindow.ui对上,且该.ui必须出现在add_executable的源文件列表里,漏了就会在编译阶段报找不到头文件。AUTOMOC则要求带Q_OBJECT的类头文件同样被列进去,否则信号槽会静默失效——链接不报错,运行时就是连不上。
2.2 主窗口骨架:QStackedWidget 当容器,导航当入口
界面切换的本体是QStackedWidget,它内部维护一个 widget 列表,同一时刻只显示其中一个,setCurrentIndex()就是切换动作。主窗口不再持有每个页面的指针,只持有一个容器和一份名字到索引的映射。
// mainwindow.h #include <QMainWindow> #include <QHash> QT_BEGIN_NAMESPACE namespace Ui { class MainWindow; } QT_END_NAMESPACE class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = nullptr); ~MainWindow() override; public slots: void switchTo(const QString &pageName); // 按名字切页 private: void buildPages(); // 注册所有页面 Ui::MainWindow *ui; QHash<QString, int> m_pageIndex; // 页面名 -> 容器索引 };QHash<QString, int>的作用是把"第几个"这种脆弱的下标换成稳定的名字,导航栏的userData或文本都可以作为键。这里刻意不存QWidget*,因为控件本身归QStackedWidget所有,重复持有指针在删除时容易变成野指针。
构造函数里做三件事:调ui->setupUi(this)、建容器、注册页面。
// mainwindow.cpp #include "mainwindow.h" #include "ui_mainwindow.h" #include "pages/homepage.h" #include "pages/settingpage.h" MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) { ui->setupUi(this); // 1. 容器接管所有页面,布局交给 ui 文件里的占位区域 m_stack = new QStackedWidget(this); ui->stackLayout->addWidget(m_stack); // stackLayout 是 .ui 里的 QVBoxLayout // 2. 注册页面 buildPages(); // 3. 导航与容器联动 connect(ui->navList, &QListWidget::currentRowChanged, this, [this](int row) { if (row >= 0 && row < m_stack->count()) m_stack->setCurrentIndex(row); }); ui->navList->setCurrentRow(0); } void MainWindow::buildPages() { // 顺序即索引,addWidget 返回值就是该页在容器里的下标 int idx = m_stack->addWidget(new HomePage(this)); m_pageIndex.insert("home", idx); idx = m_stack->addWidget(new SettingPage(this)); m_pageIndex.insert("setting", idx); }addWidget()会把 widget 的父对象改成容器,所以传this只是临时父对象,不会造成双删。返回值就是索引,直接存进 map 最稳妥。导航用currentRowChanged而不是itemClicked,是为了让键盘上下键也能切页,行为一致性比省一行代码重要。
2.3 页面注册:别用 QHash 遍历顺序当索引
一个常见翻车点是先QHash<QString, QWidget*>存页面,再遍历 hash 逐个addWidget,然后假设navList的第 0 行对应home。QHash的遍历顺序由哈希值决定,和插入顺序无关,跑两次结果都可能不同,界面就会"玄学错位"。
正确做法是让顺序只有一个来源。要么用QList<QPair<QString, QWidget*>>显式保序,要么像上面那样以addWidget的返回值作为唯一索引,名字只做查询键。
void MainWindow::switchTo(const QString &pageName) { const int idx = m_pageIndex.value(pageName, -1); if (idx < 0) { qWarning() << "unknown page:" << pageName; return; } m_stack->setCurrentIndex(idx); ui->navList->setCurrentRow(idx); // 保持导航高亮和内容同步 }参数pageName用字符串而不是枚举,是为了后面加插件式页面时不用改头文件;但代价是拼错只能在运行时发现,所以value()给了-1兜底并打一条qWarning,出问题时日志里能直接看到是哪个名字没注册。这套骨架跑起来之后,加一个页面就变成"写一个.ui、写一个类、在buildPages()里加两行",仅此而已。
3. 界面切换的三种实现:QStackedWidget、懒加载与状态保持
骨架搭好之后,切换本身还有讲究。同样叫"切页",预加载和懒加载在启动耗时、内存占用、状态保持上的表现完全不同,选错了后面再做优化就得动大手术。这一章把三种常见做法拆开讲,并说明各自的边界。
3.1 索引切换与信号槽:一次点击走到哪个槽
最直接的方式是把导航控件的信号接到QStackedWidget::setCurrentIndex,Qt 的信号槽支持参数直接匹配,currentRowChanged(int)和setCurrentIndex(int)签名一致,可以直连。
// 导航行变化 -> 容器切页,一行搞定 connect(ui->navList, &QListWidget::currentRowChanged, m_stack, &QStackedWidget::setCurrentIndex); // 反向:容器变了,导航跟着高亮,避免两边打架 connect(m_stack, &QStackedWidget::currentChanged, ui->navList, &QListWidget::setCurrentRow);这两行是双向绑定的基础,但直连有个隐患:当你在代码里主动调switchTo()时,setCurrentIndex会触发currentChanged,进而触发setCurrentRow,setCurrentRow又发currentRowChanged,再回到setCurrentIndex。多数情况下值相同不会重复发信号,但一旦中间有自定义逻辑(比如切页时弹确认框),就会形成回环。
我的做法是只在一边做绑定,另一边用QSignalBlocker临时屏蔽:
void MainWindow::switchTo(const QString &pageName) { const int idx = m_pageIndex.value(pageName, -1); if (idx < 0 || idx == m_stack->currentIndex()) return; QSignalBlocker blocker(ui->navList); // 防止回环 ui->navList->setCurrentRow(idx); m_stack->setCurrentIndex(idx); }QSignalBlocker是 RAII 对象,构造时阻断、析构时恢复,比手动blockSignals(true/false)安全,中途return也不会漏恢复。这类小细节在单页面时看不出差别,页面一多、逻辑一绕,就是省下半天排查时间的后悔药。
currentChanged(int)这个信号还适合挂页面级的初始化动作,比如进入报表页才去拉数据:
connect(m_stack, &QStackedWidget::currentChanged, this, [this](int idx) { QWidget *page = m_stack->widget(idx); qInfo() << "enter page:" << page->objectName(); // 如果页面实现了统一接口,可以在这里调 onEnter() });3.2 懒加载:第一次进页面才 new 出来
预加载的代价是启动时把所有页面的控件树都建一遍。页面少无所谓,一旦某个页面里有QTableWidget、图表或者嵌套布局,启动时间会肉眼可见地变长。懒加载的思路是页面只在第一次被切到时才构造,构造完缓存进容器。
// mainwindow.h 里加一张工厂表 #include <functional> QHash<QString, std::function<QWidget *()>> m_factories; QHash<QString, QWidget *> m_instances;void MainWindow::registerPage(const QString &name, std::function<QWidget *()> factory) { m_factories.insert(name, factory); // 注册时只建占位,真正实例化推迟到首次切换 m_stack->addWidget(new QWidget(this)); // 占位空页 m_pageIndex.insert(name, m_stack->count() - 1); } QWidget *MainWindow::ensurePage(const QString &name) { if (m_instances.contains(name)) return m_instances.value(name); auto it = m_factories.find(name); if (it == m_factories.end()) return nullptr; QWidget *page = it.value()(); // 调用工厂构造 page->setObjectName(name); const int idx = m_pageIndex.value(name); m_stack->insertWidget(idx, page); // 插到占位位置 m_stack->removeWidget(m_stack->widget(idx + 1)); // 移除占位 m_instances.insert(name, page); return page; }insertWidget(index, widget)会把新控件插到指定位置并把原来的往后挤,所以插完要顺手把占位页摘掉。这里用removeWidget而不是delete,是因为占位页没有父对象之外的引用,交给 Qt 的对象树回收更省心。
懒加载的取舍很明确:首屏更快,但第一次切页会有一次构造抖动。如果页面构造里还有网络请求,抖动会更明显,这时候可以在ensurePage()后补一句"加载中"的占位提示,或者把重活挪到页面自己的showEvent()里异步做。
3.3 状态保持与切换动画:hide 之后定时器还在跑
QStackedWidget切走页面时只是hide(),控件不销毁,所以输入框内容、列表滚动位置天然保留——这也是它比deleteLater()+ 重建更适合多界面场景的原因。
但"不销毁"也意味着页面里的定时器、网络轮询、动画还在后台跑。一个典型的血泪经验:某个页面里放了QTimer每秒刷新一次状态,切到别的页面后 CPU 占用不降,用户以为卡死。
// 页面基类里统一处理 void BasePage::showEvent(QShowEvent *e) { QWidget::showEvent(e); if (m_refreshTimer && !m_refreshTimer->isActive()) m_refreshTimer->start(1000); } void BasePage::hideEvent(QHideEvent *e) { QWidget::hideEvent(e); if (m_refreshTimer && m_refreshTimer->isActive()) m_refreshTimer->stop(); // 不可见就停,别空转 }showEvent/hideEvent在QStackedWidget切页时都会被正确触发,这是最省事的挂载点。注意父类同名函数必须调一次,否则样式和焦点行为会出问题。
想要切换有点过渡感,可以对容器做透明度动画。给页面套一层QGraphicsOpacityEffect,在currentChanged里跑QPropertyAnimation:
auto *effect = new QGraphicsOpacityEffect(m_stack); m_stack->setGraphicsEffect(effect); auto *anim = new QPropertyAnimation(effect, "opacity", this); anim->setDuration(180); anim->setStartValue(0.2); anim->setEndValue(1.0); anim->setEasingCurve(QEasingCurve::OutCubic); anim->start(QAbstractAnimation::DeleteWhenStopped);setDuration别超过 250ms,切页动画是辅助不是主角,长了反而显得迟钝。DeleteWhenStopped保证动画对象跑完自删,不然每切一次就泄漏一个QPropertyAnimation。
4. 弹窗体系:QMessageBox、自定义 QDialog 与回传链路
界面切换解决的是"主区域显示谁",弹窗解决的是"临时打断一下并拿到用户的选择"。这两件事在 Qt 里是两套机制:前者靠容器,后者靠模态事件循环。混着用就会出现"弹窗关不掉""返回值拿不到""关窗后主界面白了"这类问题。
4.1 QMessageBox:返回值、默认按钮与阻塞边界
标准确认框用静态函数最省事,一行拿到结果。
const auto ret = QMessageBox::question( this, // 父窗口,决定居中位置和模态范围 tr("删除确认"), // 标题 tr("删除后无法恢复,确定继续吗?"), // 正文 QMessageBox::Yes | QMessageBox::No, // 按钮组合 QMessageBox::No); // 默认聚焦按钮 if (ret == QMessageBox::Yes) { doDelete(); } else { qInfo() << "user canceled"; }五个参数里,parent传错是最常见的翻车点:传nullptr会让弹窗脱离主窗口居中,多屏环境下可能跑到另一块屏上,用户以为程序卡了。defaultButton建议永远设成"取消"或"否",误按回车的代价小得多。
QMessageBox::question内部是exec(),会开一个局部事件循环,阻塞在这一行直到用户点击。这意味着它不能随意用在信号槽的深层调用里——如果此时父窗口正在析构,弹窗持有的父指针就悬空了。判断标准很简单:这个函数执行期间,父窗口有没有可能被关闭?有的话就别用静态版,改成堆上创建 +open()。
auto *box = new QMessageBox(this); box->setAttribute(Qt::WA_DeleteOnClose); box->setIcon(QMessageBox::Warning); box->setText(tr("配置已修改,是否保存?")); box->setStandardButtons(QMessageBox::Save | QMessageBox::Discard); box->open(); // 非阻塞,返回后继续走 connect(box, &QMessageBox::finished, this, [this](int result) { if (result == QMessageBox::Save) saveConfig(); });open()是异步的,函数立刻返回,结果通过finished(int)信号回来。WA_DeleteOnClose让弹窗关闭时自删,避免每次提示都留一个对象。
4.2 自定义 QDialog:.ui 配 exec() 的完整回传链路
QMessageBox只能显示纯文本,遇到"输入名称 + 选择类型"这种表单就得自己写QDialog。流程是固定的四步:拖.ui、继承QDialog、在accept()前校验、调用方拿exec()的返回值。
// confirmdialog.h #include <QDialog> namespace Ui { class ConfirmDialog; } class ConfirmDialog : public QDialog { Q_OBJECT public: explicit ConfirmDialog(QWidget *parent = nullptr); ~ConfirmDialog() override; QString name() const; // 供调用方取输入值 private slots: void onOkClicked(); // 校验通过才 accept private: Ui::ConfirmDialog *ui; };// confirmdialog.cpp ConfirmDialog::ConfirmDialog(QWidget *parent) : QDialog(parent) , ui(new Ui::ConfirmDialog) { ui->setupUi(this); setWindowTitle(tr("新建配置")); setModal(true); // 模态,挡住父窗口 connect(ui->btnOk, &QPushButton::clicked, this, &ConfirmDialog::onOkClicked); connect(ui->btnCancel, &QPushButton::clicked, this, &QDialog::reject); // 取消统一走 reject } void ConfirmDialog::onOkClicked() { const QString text = ui->editName->text().trimmed(); if (text.isEmpty()) { QMessageBox::warning(this, tr("提示"), tr("名称不能为空")); return; // 不关闭,留在弹窗里 } accept(); // 触发 finished(Accepted) } QString ConfirmDialog::name() const { return ui->editName->text().trimmed(); }调用方只有一种写法:
ConfirmDialog dlg(this); if (dlg.exec() == QDialog::Accepted) { qInfo() << "new config:" << dlg.name(); createConfig(dlg.name()); }exec()返回QDialog::Accepted(值为 1)或Rejected(值为 0),分别由accept()和reject()触发。栈上创建dlg意味着生命周期明确,exec()返回后立即析构,不存在泄漏。关键是onOkClicked里校验失败时不能调accept(),否则弹窗关了、数据还是空的,调用方拿到Accepted却取到空字符串,属于典型的"看起来成功实则失败"。
4.3 非模态弹窗:WA_DeleteOnClose 与父子对象树
有些弹窗不该挡住操作,比如"保存成功"的轻提示、悬浮的工具面板。这类要用show()而不是exec(),并且必须解决"谁来 delete"的问题。
void MainWindow::showToast(const QString &text) { auto *toast = new QLabel(text, this); // 父对象是主窗口 toast->setAttribute(Qt::WA_DeleteOnClose); // 关闭即销毁 toast->setWindowFlags(Qt::Tool | Qt::FramelessWindowHint); toast->setAlignment(Qt::AlignCenter); toast->adjustSize(); // 相对主窗口右下角定位 const QPoint pos = mapToGlobal(QPoint(width() - toast->width() - 24, height() - toast->height() - 24)); toast->move(pos); toast->show(); // 2 秒后自动关闭 QTimer::singleShot(2000, toast, &QLabel::close); }WA_DeleteOnClose加close()的组合是标准解法,QTimer::singleShot的第三个参数传toast作为上下文对象,弹窗提前销毁时定时器会自动失效,不会回调到野指针上。这一步很容易漏,漏了就是"关掉弹窗后过两秒程序崩了"。
父对象的作用是兜底:主窗口析构时,所有子对象一起销毁,即使弹窗还开着也不会泄漏。但要注意父子关系会影响坐标——带父对象的QWidget默认是子控件而非顶层窗口,所以这里必须加Qt::Tool或Qt::Dialog让它成为独立窗口,否则move()的坐标基准会变成父窗口内部坐标。
5. 避坑与排查:多界面项目里最容易翻车的五件事
这一章的五条都是我实际调试过的,现象、原因、解决按顺序写,遇到同类问题可以直接对照。
5.1 反复切换后内存一直涨
现象:在两个页面之间来回点二十次,任务管理器里的内存从 80MB 涨到 200MB,且不回落。
原因:切页逻辑里每次都new一个新页面再addWidget,旧页面只是被hide(),既没removeWidget也没deleteLater,越堆越多。
解决:页面实例只创建一次并缓存;确实需要重建的场景,先在removeWidget之后调page->deleteLater(),并且确认没有别的地方持有它的裸指针。定位方法是给页面的构造函数和析构函数各加一行qDebug(),看构造次数和析构次数是否配对。
5.2 弹窗关闭后程序直接退出
现象:点主界面上的"关于"按钮弹出对话框,关闭对话框,整个程序跟着退出了。
原因:把弹窗当成了唯一窗口。Qt 默认在最后一个可见顶层窗口关闭时退出事件循环,如果弹窗没设父对象,它就是一个独立顶层窗口,关掉它触发quitOnLastWindowClosed。
解决:所有弹窗都传父窗口,QDialog dlg(this)或者new Dialog(this);实在需要无父窗口,显式调qApp->setQuitOnLastWindowClosed(false),然后在主窗口的closeEvent里自己控制退出。
5.3 子界面改了数据,主窗口拿不到
现象:设置页里改了用户名,切回首页,首页显示的还是旧名字。
原因:页面之间没有通信,首页的控件只在构造时赋过一次值,QStackedWidget切回来只是显示,不会重新执行赋值逻辑。
解决:两条路选一条。轻量做法是页面重写showEvent,在里面从共享的配置对象重新读一次值;更规范的做法是在主窗口维护一个数据模型,页面通过信号把变更抛上去,主窗口再广播给其他页面。我一般先用showEvent顶住,等页面超过五个再抽模型,避免过早抽象。
5.4 改了 .ui 文件,界面纹丝不动
现象:在 Designer 里调整了按钮位置,重新编译运行,界面上位置没变。
原因:三种可能——.ui没被加进FORMS(qmake)或add_executable(CMake);构建目录里的ui_xxx.h是旧的没重新生成;或者代码里setupUi之后又手动setGeometry覆盖了布局。
解决:先看构建目录下ui_xxx.h的时间戳,比.ui老就是构建系统没识别到变更,执行一次 clean 再 build;时间戳是新的就搜代码里有没有在setupUi之后硬编码坐标。养成习惯:用了布局管理器就别再手动设几何,两套机制打架时以代码为准,这类问题最难查。
5.5 高 DPI 屏上弹窗尺寸错乱
现象:笔记本外接 4K 显示器,主窗口正常,弹窗里的文字被截断,按钮挤在一起。
原因:弹窗里用了固定像素尺寸(setFixedSize(300, 120)),在高缩放比下字体放大了但容器没放大。
解决:弹窗只设最小尺寸setMinimumWidth(),让布局自己撑开;需要固定比例的地方用QFontMetrics按当前字体计算,而不是写死像素。同时在main.cpp里打开高 DPI 缩放,并确认资源里的样式表没有用px写死字体大小,改成pt更稳。
6. 二次开发:把切页和弹窗收进两个统一入口
骨架能跑之后,真正决定这个工程能不能长期维护的,是有没有把"切页"和"弹窗"收成两个统一入口。我的做法是加一个UiRouter,页面用名字注册、用名字跳转,容器索引完全不暴露给业务代码。
// uirouter.h #include <QObject> #include <QStackedWidget> #include <functional> class UiRouter : public QObject { Q_OBJECT public: explicit UiRouter(QStackedWidget *stack, QObject *parent = nullptr) : QObject(parent), m_stack(stack) {} void registerPage(const QString &name, std::function<QWidget *()> factory); void go(const QString &name); // 唯一的跳转入口 QString currentPage() const; signals: void pageChanged(const QString &name); private: QWidget *ensure(const QString &name); QStackedWidget *m_stack; QHash<QString, std::function<QWidget *()>> m_factories; QHash<QString, QWidget *> m_cache; }; inline void UiRouter::go(const QString &name) { QWidget *page = ensure(name); if (!page) { qWarning() << "page not registered:" << name; return; } m_stack->setCurrentWidget(page); // 用 widget 而非索引,更稳 emit pageChanged(name); }setCurrentWidget()比setCurrentIndex()更抗变更——中间插入页面不会影响已有跳转。go()里统一打日志、统一发信号,后面要做埋点、要做返回栈、要拦截未保存提示,都只需要改这一处。
弹窗同理,收一个DialogService,把"确认删除""输入名称""错误提示"三种高频弹窗包成三个函数,业务侧只调函数不碰QMessageBox。
| 改造点 | 改造前 | 改造后 | 收益 |
|---|---|---|---|
| 页面跳转 | 各处setCurrentIndex(3) | router->go("setting") | 插入页面不影响调用方 |
| 页面构造 | 主窗口构造函数里全 new | 工厂注册 + 首次访问构造 | 启动时间可控 |
| 确认弹窗 | 每个类里手写QMessageBox | DialogService::confirm() | 文案和按钮风格统一 |
| 弹窗生命周期 | 栈对象 + 堆对象混用 | 统一WA_DeleteOnClose | 不泄漏也不野指针 |
验证改造是否成功有个笨办法但很有效:把所有页面的注册名打印出来,逐个go()一遍,看日志里pageChanged是否成对出现、有没有重复构造。再对着弹窗做一轮"弹出→关闭→再弹出"的循环,观察内存曲线是否回落。
踩过最疼的一次,是一个浮层弹窗忘了设WA_DeleteOnClose,测试同学连续点了五十次,内存涨上去还不下来,最后靠对象树打印才定位到。从那以后我每次写new出来的弹窗,都会先问自己一句"谁负责删它",答不上来就改成栈对象。希望帮到你。
本文还有配套的精品资源,点击获取