ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Qt5多UI页面切换与弹窗实战:QStackedWidget与QDialog

Qt5多UI页面切换与弹窗实战:QStackedWidget与QDialog

简介:面向Qt5初学者与桌面界面开发者的多UI切换与弹窗示例工程,围绕主窗口内嵌多个子界面、按钮触发切换以及模态弹窗展示等常见需求,提供可参考的实现组织方式。压缩包共29个文件,以cpp源文件、h头文件、ui界面描述和o编译产物为主,并含pro工程配置与Makefile,整体约57KB,便于直接打开对照编译与运行。已有182人学习,适合需要理解窗口管理、界面切换和弹窗交互的读者。内容涉及QWidget的显示隐藏切换、QDialog自定义弹窗、QMessageBox提示、信号与槽通信及窗口过渡动画等要点,也保留了多窗口协作与事件传递的工程结构,可帮助读者快速定位界面切换逻辑、弹窗调用位置和编译配置差异,作为课程设计或小型桌面项目的参考底稿较实用。

1. Qt5 多 UI 页面切换与弹窗显示:从一个用 hide/show 手动切换的老项目说起

接手过一个 Qt5 的配置工具,登录、设备列表、参数配置、日志四块界面全挤在主窗口里,原作者用的是 hide 一个、show 一个的办法,切几次之后窗口尺寸开始乱,弹窗还盖不住父窗口的工具栏。后来把四份 .ui 拆成独立页面类交给 QStackedWidget 托管,确认框和输入框统一走 QDialog,界面层的代码从八百多行降到三百行出头。

这套 qt ui 示例就是按这个思路整理的:多份 .ui 各自独立编译、主窗口只负责调度、弹窗负责取值和确认,一步一步都能跑起来。适合刚上手 Qt5、正被多界面切换和弹窗返回值折腾的开发者,也适合手上有个界面层写乱了想重做一遍的人。

2. 工程骨架:.pro、ui/ 与 src/ 怎么对齐

先把目录定下来,后面所有问题都好排查。Qt5 里多 UI 项目的坑,一大半不在代码逻辑,而在“文件放哪、FORMS 里写没写、生成的头文件在哪”。这三个地方对齐了,编译期就不会莫名其妙报ui_xxx.h: No such file。

2.1 目录结构与 qmake 配置

我一般把界面描述和业务代码分开放:.ui 统一扔 ui/,页面类放 src/,主窗口只管调度。这样设计师改完界面重新保存,不会和手写代码混在一个目录里互相覆盖。

路径职责容易出问题的地方
src/main.cpp入口,设置高 DPI 属性后拉起主窗口属性必须在 QApplication 构造之前设置
src/mainwindow.*主窗口,持有 QStackedWidget,做页面调度构造函数里别做耗时的数据初始化
src/pagebase.h页面基类,统一 onEnter / onLeave 与跳转信号忘了 Q_OBJECT,信号连不上且不报错
src/loginpage.*登录页,独立 .ui控件 objectName 改了,代码要同步改
src/homepage.*主页/列表页数据刷新放在 onEnter,别放构造函数
ui/*.ui各页面与弹窗的界面描述文件名大小写要和 FORMS 里完全一致

对应的 .pro 写起来并不复杂,关键是 FORMS 和 HEADERS/SOURCES 三处要能对得上:

QT += core gui widgets greaterThan(QT_MAJOR_VERSION, 4): QT += widgets TARGET = MultiPageDemo TEMPLATE = app CONFIG += c++11 # 一份 .ui 对应一个页面,漏写哪一行,编译期就会报 ui_xxx.h 找不到 FORMS += ui/mainwindow.ui \ ui/loginpage.ui \ ui/homepage.ui \ ui/settingpage.ui \ ui/confirmdialog.ui HEADERS += src/mainwindow.h \ src/pagebase.h \ src/loginpage.h \ src/homepage.h \ src/settingpage.h \ src/confirmdialog.h SOURCES += src/main.cpp \ src/mainwindow.cpp \ src/loginpage.cpp \ src/homepage.cpp \ src/settingpage.cpp \ src/confirmdialog.cpp INCLUDEPATH += src

QT += widgets这行是 Qt5 相对 Qt4 最直观的变化,Qt4 时代 QtGui 里就有 QWidget,Qt5 把它拆出去了,漏写会得到一堆“QMainWindow 未定义”的报错。greaterThan(QT_MAJOR_VERSION, 4)是为了让同一份 .pro 在 Qt4 环境下也能编过,纯 Qt5 项目里其实可以直接删掉。

.ui 放在 ui/ 子目录时,uic 生成的头文件会落到构建目录的同名子目录下,qmake 会自动把它加进 include path,所以代码里照常写#include "ui_loginpage.h"就行。但这里有个前提:改过 FORMS 之后必须重新执行一次 qmake。只点构建按钮,qmake 的工程文件缓存不会刷新,你会对着一个明明写对了的 FORMS 条目怀疑人生。

入口文件里顺手把高 DPI 打开,不然后面每个页面都要单独处理缩放:

#include "mainwindow.h" #include <QApplication> int main(int argc, char *argv[]) { // 高 DPI 属性必须在 QApplication 构造前设置,构造之后再设是无效的 QApplication::setAttribute(Qt::AA_EnableHighDpiScaling, true); QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps, true); QApplication a(argc, argv); a.setApplicationName(QStringLiteral("MultiPageDemo")); MainWindow w; w.show(); return a.exec(); }

AA_EnableHighDpiScaling打开后,Qt 会按屏幕的 devicePixelRatio 缩放控件和字体;AA_UseHighDpiPixmaps决定 @2x 这类多倍图能不能被自动选中。两个都开,1080p 和 4K 屏上界面的观感才不会差太多。注意这两个是静态函数,写在 QApplication 构造之后就完全无效,属于那种“代码看着没问题、现象却对不上”的情况。

2.2 两种加载 .ui 的写法:单继承还是多继承

Qt Designer 生成的ui_xxx.h里其实就一个类,里面全是控件指针和setupUi()。用它有两种姿势,选错了后期改界面会很别扭。

单继承(has-a)是把 Ui 对象当成员持有,页面类本身还是干干净净的 QWidget:

// 单继承:Ui 对象是成员,需要自己管理生命周期 class LoginPage : public PageBase { Q_OBJECT public: explicit LoginPage(QWidget *parent = nullptr); signals: void loginSuccess(const QString &user); private slots: void onLoginClicked(); private: Ui::LoginPage *ui; // 用 new 创建,父对象析构时要 delete };
LoginPage::LoginPage(QWidget *parent) : PageBase(parent) , ui(new Ui::LoginPage) { ui->setupUi(this); // 这一行之后,ui->xxx 才不是野指针 // 控件在 setupUi 之后才存在,connect 必须写在这后面 connect(ui->btnLogin, &QPushButton::clicked, this, &LoginPage::onLoginClicked); } LoginPage::~LoginPage() { delete ui; }

多继承(is-a)是让页面类同时继承 QWidget 和 Ui 生成的那个类,好处是 Ui 里的控件名直接变成页面类的成员,写起来短:

// 多继承:Ui 里的控件直接成为自己的成员,短但容易撞名 class SettingPage : public PageBase, private Ui::SettingPage { Q_OBJECT public: explicit SettingPage(QWidget *parent = nullptr) : PageBase(parent) { setupUi(this); // 不需要 ui-> 前缀,直接用 btnSave } };

两种都能跑,我一般选单继承。原因是多继承下ui_settingpage.h会跟着settingpage.h一起被别的文件包含,改一次界面就触发一大片重编译;而且多继承时 Ui 里的控件名字直接进了类作用域,页面一复杂就容易和自己的成员重名,编译器报的错还挺绕。单继承多写一个ui->,换来的隔离性值这个价。

还有一个隐形的顺序问题:setupUi(this)之前ui->btnLogin是未初始化的指针,这时候 connect 不会报错,运行时才崩。血泪经验是——页面构造函数里凡是出现ui->的地方,先确认它在setupUi之后。

2.3 容器选型:QStackedWidget、QTabWidget 还是手动 show/hide

多界面切换至少有四种常见做法,选之前先想清楚页面之间是平级还是分步、用户能不能直接跳。

方案适合场景代价
手动 hide/show页面只有两三个、切换逻辑极简尺寸跟着当前页乱跳,重叠区域容易漏刷
QTabWidget页面平级、用户需要在标签间自由来回标签栏样式受限,不适合全屏页面流
QStackedWidget有明确流程(登录→主页→设置),页面全屏切换页面常驻内存,需要自己管刷新时机
QML StackView界面动效多、动画要求高和 Widgets 混用成本高,得另起一套

这套示例用的是 QStackedWidget。它内部只显示currentIndex指向的那一个子控件,其余全部隐藏但对象还在,所以页面状态天然保留——用户从设置页切回主页,滚动位置、输入内容都还在。这个特性用好了很省事,用不好就是“页面数据永远停在第一次进来时的样子”,后面 3.3 会专门处理。

手动 hide/show 的问题在窗口尺寸上最明显:隐藏的控件不参与布局计算,主窗口会随着当前页“缩”成不同的形状,用户拖一下窗口大小,切个页又变回去。QStackedWidget 的尺寸取所有子页的最大值,窗口大小稳定,代价是空页面也会把最小尺寸撑大,这一点在 5.1 里会讲怎么处理。

3. 页面切换核心:QStackedWidget 与信号槽的完整链路

容器选好之后,真正要解决的是“页面怎么告诉主窗口我要跳转”。让页面直接持有主窗口指针是最省事也最容易失控的写法,页面之间会互相耦合,最后变成一团。这套示例的做法是:页面只发信号,主窗口决定跳到哪。

3.1 主窗口里搭一个页面栈

主窗口只做三件事:创建页面、加进栈、连信号。页面的构造参数一律传this,让 Qt 的父子对象树接管生命周期,不用手写 delete。

MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) { ui->setupUi(this); // 1. 页面各自独立构造,父对象先挂主窗口 m_loginPage = new LoginPage(this); m_homePage = new HomePage(this); m_settingPage = new SettingPage(this); // 2. 按顺序入栈,索引就是添加顺序,别写死枚举值 ui->stackedWidget->addWidget(m_loginPage); // 0 ui->stackedWidget->addWidget(m_homePage); // 1 ui->stackedWidget->addWidget(m_settingPage); // 2 ui->stackedWidget->setCurrentIndex(0); // 3. 页面只负责喊一声,跳不跳、跳到哪由主窗口说了算 connect(m_loginPage, &LoginPage::loginSuccess, this, &MainWindow::switchToPage); connect(m_homePage, &PageBase::requestPage, this, &MainWindow::switchToPage); connect(m_settingPage, &PageBase::requestPage, this, &MainWindow::switchToPage); // 4. 切换完成后再做收尾,不要拦截 currentChanged 去改索引 connect(ui->stackedWidget, &QStackedWidget::currentChanged, this, &MainWindow::onPageChanged); }

addWidget会自动把页面的父对象改成栈容器,所以前面传this只是为了构造阶段有个归属,最终归属还是栈。这一步不用纠结,Qt 会处理好。

真正值得写清楚的是switchToPage,索引越界和重复切换这两个边界不处理,早晚会出问题:

void MainWindow::switchToPage(int index) { // 越界直接吞掉:setCurrentIndex 传非法值不会崩溃,但界面会停在原地, // 上层逻辑以为切过去了,后面查起来很难受 if (index < 0 || index >= ui->stackedWidget->count()) { qWarning() << "invalid page index:" << index; return; } // 同一页重复切换会触发两次 currentChanged,onEnter 里的耗时操作会跑两遍 if (index == ui->stackedWidget->currentIndex()) { return; } ui->stackedWidget->setCurrentIndex(index); }

count()是当前栈里的页面数量,setCurrentIndex越界时 Qt 只会打印一条警告然后什么都不做,返回值是 void,你没法判断成没成功。所以在入口处主动挡一道,比事后查“为什么点了没反应”省事得多。

3.2 页面基类 PageBase:把“我要跳转”抽成信号

如果每个页面都自己定义一套跳转信号,主窗口里就要写 N 个 connect,加一个页面改一次主窗口。抽一个基类出来,信号统一,主窗口只连一次。

// pagebase.h class PageBase : public QWidget { Q_OBJECT public: explicit PageBase(QWidget *parent = nullptr) : QWidget(parent) {} ~PageBase() override = default; // 被切进来 / 切走时由主窗口调用,子类按需重写 virtual void onEnter() {} virtual void onLeave() {} signals: // 页面想跳转时统一发这个信号,参数是目标页索引 void requestPage(int index); };

页面里发信号就一行:

void HomePage::onSettingClicked() { emit requestPage(2); // 2 是设置页在栈里的位置 }

这里有个设计取舍:信号参数用 int 索引,写起来最直接,但页面必须知道自己在栈里的位置,加一个页面就要回头核对所有魔法数字。第 6 章的改造会把 int 换成 QString 路由名,彻底解耦。

用索引也不是不行,前提是把索引集中定义成枚举,别散落在各个 .cpp 里:

// 集中在一个头文件里,改顺序时只需要动这里 namespace PageId { enum Index { Login = 0, Home = 1, Setting = 2 }; }

Q_OBJECT宏不能省。省了之后类里声明的 signal 只是一个普通函数声明,connect编译期能过,运行时打印 “QObject::connect: signal not found”,属于隐蔽性很强的问题。

3.3 页面进出时机:onEnter / onLeave 与数据回填

QStackedWidget 的页面是常驻的,构造之后就一直活着。这意味着构造函数只跑一次,如果把“拉取最新数据”写在构造函数里,用户第一次进来看到的是数据,切走再切回来,看到还是一小时前的。

所以页面数据刷新要挂在“被切进来”这个时机上。Qt 本身有showEvent/hideEvent,但栈里的页面在切换时不一定触发这两个事件(取决于具体实现和窗口状态),用自定义的 onEnter 更可控:

void MainWindow::onPageChanged(int index) { QWidget *w = ui->stackedWidget->widget(index); if (!w) { return; } // 先把上一页的 onLeave 走完,再走新页的 onEnter if (m_currentPage && m_currentPage != w) { m_currentPage->onLeave(); } if (auto *page = qobject_cast<PageBase *>(w)) { page->onEnter(); // 数据刷新、定时器启动都放这里 } m_currentPage = qobject_cast<PageBase *>(w); ui->statusBar->showMessage(tr("当前页面:%1").arg(w->objectName())); }

对应的页面实现:

void HomePage::onEnter() { // 每次进来都刷新,避免展示过期数据 m_model->reload(); m_timer->start(5000); // 只有当前页需要轮询 } void HomePage::onLeave() { m_timer->stop(); // 切走一定要停,否则后台一直在查库 }

onLeave里停定时器这一步很容易被忽略。页面常驻内存,定时器也跟着一直活着,如果每页都开一个 5 秒轮询,四个页面就是后台四路查询同时跑,在嵌入式板子上很直观地表现为“切到哪页都卡”。

4. 弹窗显示:QMessageBox、自定义 QDialog 与非模态窗口

多页面结构里,弹窗往往比页面切换更容易出状况,因为它的生命周期短、返回值要马上用,指针一不小心就野了。这块先分清三种用法,再看清父子关系和内存归属。

4.1 QMessageBox 够用的场景,别急着写 QDialog

只提示、只确认、只报错这几种需求,QMessageBox完全够,静态函数一行搞定,还会自动处理模态和本地化按钮。

void HomePage::onDeleteClicked() { // 静态函数是模态的,返回值就是用户点了哪个按钮 auto ret = QMessageBox::question( this, tr("确认删除"), tr("删除后不可恢复,是否继续?"), QMessageBox::Yes | QMessageBox::No, QMessageBox::No); // 默认焦点放在 No 上,防误触 if (ret == QMessageBox::Yes) { doDelete(); } }

最后一个参数是默认按钮。删除这种不可逆操作,默认焦点一定给取消项,这是产品层面的基本要求,代码里也就多写一个参数。

QMessageBox::warning/information用起来更简单,但它们只有“确定”一个按钮,拿不到用户的选择,只能做提示。需要用户做决定的场合还是得用question或者自定义对话框。

还有一种情况值得单独提:错误提示如果是在exec()的对话框内部弹出的,不要用QMessageBox::exec()套着弹,Qt 允许,但栈会越来越深,调试时容易迷失。用open()让它是窗口模态、不阻塞当前调用栈,会舒服很多。

4.2 自定义 QDialog:返回值、校验与数据回传

需要用户输入内容的时候,QMessageBox 就不够用了。自定义 QDialog 的关键在于:数据通过构造函数进去,通过成员函数出来,中间用accept()/reject()决定结果。

// confirmdialog.h class ConfirmDialog : public QDialog { Q_OBJECT public: explicit ConfirmDialog(QWidget *parent = nullptr); ~ConfirmDialog() override; QString value() const { return m_value; } // 外部通过它取结果 private slots: void onOkClicked(); private: Ui::ConfirmDialog *ui; QString m_value; };
ConfirmDialog::ConfirmDialog(QWidget *parent) : QDialog(parent) , ui(new Ui::ConfirmDialog) { ui->setupUi(this); setWindowTitle(tr("新建条目")); setModal(true); // 显式声明模态,含义比 setWindowModality 清楚 connect(ui->btnOk, &QPushButton::clicked, this, &ConfirmDialog::onOkClicked); connect(ui->btnCancel, &QPushButton::clicked, this, &QDialog::reject); } void ConfirmDialog::onOkClicked() { m_value = ui->lineEditName->text().trimmed(); // 校验不过就别 accept,弹窗留在原地让用户改 if (m_value.isEmpty()) { QMessageBox::warning(this, tr("提示"), tr("名称不能为空")); ui->lineEditName->setFocus(); return; } accept(); // 关闭弹窗,并把 exec() 的返回值置为 QDialog::Accepted }

外面调用的时候,栈上分配就够了:

void HomePage::onNewItemClicked() { ConfirmDialog dlg(this); // 栈上对象,离开作用域自动析构,不存在泄漏 if (dlg.exec() == QDialog::Accepted) { addItem(dlg.value()); // 只有用户确认过才取数据 } }

exec()会阻塞在调用处,内部起一个局部事件循环,直到accept()或reject()才返回。这就是模态的本质——不是把父窗口“锁住”,而是让调用处停下来等结果。反过来说,如果exec()里面又调了另一个exec(),就会出现嵌套事件循环,关闭顺序一旦搞错,主界面会失去响应。这种情况在“弹窗里再弹确认框”时最容易出现,注意检查。

accept()和reject()除了关闭窗口,还会设置result()。要返回更丰富的结果,可以在 accept 前把数据存进成员,或者用QDialog::setResult(int)自定义返回码,外面按码分支。

4.3 非模态弹窗的父子关系与内存归属

有些弹窗不该阻塞操作,比如“实时预览”“详细日志”这类窗口,用户希望它开着的时候还能点主界面。这时候用show(),问题就从“返回值”变成了“什么时候销毁”。

void HomePage::onDetailClicked() { // 已经开着就提到前台,别每次都 new 一个新的 if (m_detailDlg) { m_detailDlg->raise(); m_detailDlg->activateWindow(); return; } m_detailDlg = new DetailDialog(this); // 父对象是主窗口,跟着主窗口一起销毁 m_detailDlg->setAttribute(Qt::WA_DeleteOnClose); // 关闭时自动 deleteLater // 关键:对象被销毁后把指针置空,否则下一次点击就是野指针 connect(m_detailDlg, &QObject::destroyed, this, [this]() { m_detailDlg = nullptr; }); m_detailDlg->show(); }

这里有两个点必须同时做,缺一个都会出问题。

一是父对象。传了this,弹窗的父对象就是主窗口,主窗口析构时会连带删掉弹窗;不传父对象,弹窗就是独立顶层窗口,主窗口关掉它还留着,程序退不干净。

二是WA_DeleteOnClose。开了这个属性,弹窗关闭时 Qt 会调deleteLater()而不是立刻删,这在槽函数里关闭自身时特别重要——立刻 delete 会把正在执行的栈帧连根拔掉。但开了属性之后,m_detailDlg就变成了悬空指针,必须靠destroyed信号清掉。少这一步,第二次点“详情”就是必崩。

还有一点:如果弹窗是用new出来又传了父对象,同时又手动delete,那就是双重释放。父对象析构时会再删一次。规则很简单——要么栈上分配,要么 new + 父对象 + 不手动删,别混着来。

5. 避坑与排查:切换和弹窗环节最容易翻车的五个地方

这些是我在实际项目里真的遇到过的,按“现象 → 原因 → 解决”整理,对号入座基本能定位。

5.1 切换环节的三个坑

现象一:切到某个页面后一片空白,窗口尺寸也跟着变了。原因通常是这个页面没有真正加进 QStackedWidget,而是在外面单独new出来直接show()了;另一种情况是addWidget之后又把这个页面设了独立父窗口,从栈里被“抢”走了。排查方法是在switchToPage里打印ui->stackedWidget->count()和widget(i)->objectName(),数量对不上就是没加进去。解决就是统一走addWidget,别手动改父对象。另外,如果某个页面本身内容为空,它的sizeHint很小,栈的尺寸会被其他大页面决定,看着像是“白屏”,实际上是页面没内容,这个要靠setMinimumSize给个下限。

现象二:点一次按钮,槽函数执行了两次甚至多次。原因是页面在每次进入时重新connect了一遍,而旧的连接从来没断开,信号一发就触发 N 个槽。这种情况在没有用基类、各页面自己连主窗口的项目里特别常见。解决有两个方向:一是把 connect 全部收进构造函数,只在构造时连一次,之后不再重复连;二是必须动态连接时,connect的最后一个参数加Qt::UniqueConnection,重复连接会被吞掉。注意UniqueConnection对 lambda 写法不生效,要用成员函数指针那种写法才行。定位手段是QObject::receivers(),或者直接在槽里打日志看执行次数。

现象三:编译时报ui_loginpage.h: No such file or directory。三种可能:FORMS 里漏了这份 .ui;.ui 文件名在磁盘上是小写、在 FORMS 里写成了大写(Linux 下大小写敏感,Windows 下不报错,换环境就出问题);改过 FORMS 之后没有重新执行 qmake。解决顺序是——先在 .pro 里核对文件名,再删掉构建目录重来一次,最后检查构建目录下有没有生成ui_loginpage.h。这里没有捷径,构建目录里的中间文件是判断问题出在 qmake 还是代码的唯一依据。

5.2 弹窗环节的两个坑

现象四:弹窗关掉之后再点同一个按钮,程序直接闪退。这是野指针的典型表现:弹窗设了WA_DeleteOnClose,关闭时对象已经销毁,但成员指针还指着原来那块内存,下一次raise()或者show()就崩了。解决就是前面 4.3 那套——连destroyed信号把指针置空,或者在每次使用前判断if (m_dlg && m_dlg->isVisible())。注意isVisible()判断不了已销毁的对象,指针本身野了照样崩,所以清空指针这一步不能省。

现象五:弹窗没有模态效果,父窗口还能点,用户连点两下开了两个窗。原因通常是该用exec()的地方用了show(),或者构造时传的父对象是nullptr。show()本身不阻塞,模态属性是在QDialog里通过setModal(true)或exec()体现的,只调show()的话默认是非模态。解决是:需要等结果的用exec();确实要非模态的,就按 4.3 那套做单例拦截,重复点击时raise()已有窗口。还有一种隐蔽情况——弹窗是exec()了,但父对象传错成了另一个窗口,模态范围就不对,表现为“这个窗口点不动了,那个还能点”,排查时把父子关系打印出来最快。

6. 二次开发:把写死的索引换成一张路由表

跑通之后最先想改的,多半是那些散落各处的emit requestPage(2)。加一个页面就要回头数一遍索引,改一次顺序全网崩,这种写法撑不住三轮需求变更。我的做法是加一层极薄的路由表:页面用名字注册,按名字跳转,第一次访问才构造。

// router.h:一条路由 = 名字 + 懒创建工厂 class Router : public QObject { Q_OBJECT public: explicit Router(QStackedWidget *stack, QObject *parent = nullptr); void addRoute(const QString &name, std::function<PageBase *(QWidget *)> factory); bool go(const QString &name); // 未注册返回 false,别静默失败 QString current() const; private: QStackedWidget *m_stack; QHash<QString, std::function<PageBase *(QWidget *)>> m_routes; QHash<QString, PageBase *> m_cache; // 访问过的常驻,没访问过的不创建 };
bool Router::go(const QString &name) { if (!m_routes.contains(name)) { qWarning() << "route not found:" << name; return false; } PageBase *page = m_cache.value(name, nullptr); if (!page) { // 第一次访问才构造,启动阶段不用把七八个页面全建一遍 page = m_routes.value(name)(m_stack); page->setObjectName(name); // 日志、样式表、自动化测试都靠它定位 m_stack->addWidget(page); m_cache.insert(name, page); connect(page, &PageBase::requestPage, this, &Router::go); } m_stack->setCurrentWidget(page); return true; }

配套改两处:PageBase::requestPage(int)改成requestPage(const QString &),页面里写emit requestPage("setting");主窗口构造时集中注册一遍,页面和索引彻底解绑。加页面只动注册表,不动任何已有页面。

改造前改造后收益
emit requestPage(2)emit requestPage("setting")增删页面不影响已有调用
构造时全部 new首次访问才创建启动时间随页面数线性下降
索引散落在各 .cpp注册表集中一处路由关系一眼看全

验证方法很简单:启动后打印所有注册的路由名,去掉一个页面看警告有没有正常打出来;再给go()加个耗时日志,确认二次进入同一页面没有重新构造——如果每次都新建,m_cache那行就写错了。另外setCurrentWidget传的是指针,页面被误删时它会静默失败,所以哪怕路由表兜住了,也建议在go()里加一句Q_ASSERT(page)。

从那以后我每次新建页面,都是先把路由名和注册行写完,再去拖界面控件。中途返工过一次——页面做完了才发现名字和别处重了,十几个 connect 全要改,那种感觉不想再体验第二遍。这套骨架不复杂,但顺序错了就要重来,希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进