ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Qt中QStackedWidget的实现示例

Qt中QStackedWidget的实现示例 前言做桌面应用的时候在同一块区域里切换不同内容几乎是刚需登录页和主界面之间切换、设置对话框左侧点一下右边换一页、安装向导的上一步下一步、多标签页工具……如果你每换一页就new一个窗口或者手动hide()/show()一堆 widget代码很快就变成一团乱麻而且很容易出现窗口闪烁、尺寸跳动、信号重复连接这些经典问题。QStackedWidget堆叠窗口部件就是 Qt 给出的标准答案。它本质上是一叠卡片同一时刻只露出最上面那一张。本文会讲清它的工作原理、核心接口、和QStackedLayout及QTabWidget的关系并给出一个可以编译运行的多步骤向导示例。一、QStackedWidget 是什么用一句话概括QStackedWidget是一个容器它把多个 widget 叠在一起通过索引index控制哪个可见。先纠正一个常见误解很多人以为QStackedWidget内部用的是隐藏/显示来切换页面。实际上它用的是更底层的机制——它继承自QFrame内部维护一个子 widget 列表通过QLayout的setCurrentIndex语义只让一个子 widget 参与布局和绘制其余的处于未显示状态。这带来两个直接后果所有页面在添加时就已经被创建了切换只是换个索引非常快不可见的页面依然占内存如果某个页面很重比如嵌了个大表格需要考虑懒加载。它与 QTabWidget 的关系这是一个面试常问的点QTabWidget的视图部分就是一个QStackedWidget。QTabWidget只是在QStackedWidget外面套了一层标签栏QTabBar并把标签的选中状态和堆叠索引绑定起来。类是否带切换控件适用场景QStackedWidget否需要自己造切换入口自定义导航、向导、登录/主界面切换QTabWidget是自带QTabBar标准多标签界面开箱即用QStackedLayout否是布局而非 widget需要把堆叠能力嵌进别的容器布局如果你能接受 Qt 原生标签栏的样子用QTabWidget更省事如果你要的是侧边栏点一下右边换页这种自定义外观那QStackedWidget 自己的按钮组才是正解。二、核心 API 一览接口作用备注addWidget(QWidget*)追加页面返回索引会自动设置父对象insertWidget(int, QWidget*)在指定位置插入索引会顺延removeWidget(QWidget*)移除页面不删除对象对象仍需自己 deletesetCurrentIndex(int)切换页面越界不报错但无效果setCurrentWidget(QWidget*)按对象切换比索引更直观currentIndex()当前索引无页面时返回 -1currentWidget()当前页面指针无页面时返回 nullptrcount()页面数量—indexOf(QWidget*)查页面索引找不到返回 -1widget(int)按索引取页面越界返回 nullptrcurrentChanged(int)当前页变化信号参数是新索引widgetRemoved(int)页面被移除信号参数是被移除的索引这里有个非常重要的细节removeWidget()只是把 widget 从堆叠里摘出来并不会销毁它也不会自动解除父子关系——准确地说Qt 会把该 widget 的父对象设回nullptr。如果你removeWidget之后就把它忘了那就是内存泄漏。正确做法是移除后手动delete或者用deleteLater()。QWidget *page stack-widget(2); stack-removeWidget(page); // 从堆叠中摘出 page-deleteLater(); // 必须补这一刀三、原理切换时到底发生了什么理解currentChanged的触发时机能帮你避开一半的坑。看下面的时序QStackedWidget *stack new QStackedWidget; stack-addWidget(new QLabel(A)); // 索引自动成为 0并成为当前页 stack-addWidget(new QLabel(B)); // 索引 1但不改变当前页 connect(stack, QStackedWidget::currentChanged, [](int idx){ qDebug() 切换到 idx; }); stack-setCurrentIndex(1); // 打印 切换到 1 stack-setCurrentIndex(1); // 不打印索引没变信号不发出关键点setCurrentIndex()传入与当前相同的索引时currentChanged不会触发。这个特性既是优点避免重复刷新也是坑你指望靠它做重新加载当前页结果啥也没发生。另外addWidget()时如果堆叠是空的新页面会自动成为当前页此时也会发出currentChanged(0)。所以如果你在构造函数里先connect再addWidget会收到一个初始化信号逻辑里要能容忍这种情况反之先addWidget再connect就不会收到。这个顺序差异经常导致首次进入页面时初始化没执行的诡异 bug。四、实战多步骤向导 侧边导航下面这个例子实现一个多步骤表单顶部是导航按钮下面用QStackedWidget承载页面并在切页时做数据校验。完整可编译。CMakeLists.txt 关键片段cmake_minimum_required(VERSION 3.16) project(StackedDemo LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) find_package(Qt6 REQUIRED COMPONENTS Widgets) add_executable(StackedDemo main.cpp wizarddialog.cpp wizarddialog.h ) target_link_libraries(StackedDemo PRIVATE Qt6::Widgets)如果用 qmakewizarddemo.proQT widgets CONFIG c17 TARGET StackedDemo SOURCES main.cpp wizarddialog.cpp HEADERS wizarddialog.h头文件// wizarddialog.h #ifndef WIZARDDIALOG_H #define WIZARDDIALOG_H #include QDialog #include QVector class QStackedWidget; class QPushButton; class QLabel; class WizardDialog : public QDialog { Q_OBJECT public: explicit WizardDialog(QWidget *parent nullptr); private slots: void goNext(); void goBack(); void onPageChanged(int index); private: QWidget *createPage(const QString title, const QString hint); QStackedWidget *m_stack nullptr; QPushButton *m_btnBack nullptr; QPushButton *m_btnNext nullptr; QLabel *m_stepTip nullptr; QVectorQString m_titles; }; #endif // WIZARDDIALOG_H实现文件// wizarddialog.cpp #include wizarddialog.h #include QStackedWidget #include QPushButton #include QLabel #include QVBoxLayout #include QHBoxLayout #include QMessageBox #include QDebug WizardDialog::WizardDialog(QWidget *parent) : QDialog(parent) { setWindowTitle(QStringLiteral(多步骤向导演示)); resize(520, 340); // 1. 先建好堆叠容器和三个页面 m_stack new QStackedWidget(this); m_titles QStringLiteral(第一步基本信息) QStringLiteral(第二步详细配置) QStringLiteral(第三步确认提交); for (const QString t : m_titles) m_stack-addWidget(createPage(t, QStringLiteral(这里是「%1」的内容区域。).arg(t))); // 2. 底部按钮区 m_btnBack new QPushButton(QStringLiteral(上一步), this); m_btnNext new QPushButton(QStringLiteral(下一步), this); m_stepTip new QLabel(this); auto *btnLayout new QHBoxLayout; btnLayout-addWidget(m_stepTip); btnLayout-addStretch(); btnLayout-addWidget(m_btnBack); btnLayout-addWidget(m_btnNext); auto *mainLayout new QVBoxLayout(this); mainLayout-addWidget(m_stack, 1); mainLayout-addLayout(btnLayout); // 3. 信号槽连接——放在所有 addWidget 之后避免收到初始化信号 connect(m_btnNext, QPushButton::clicked, this, WizardDialog::goNext); connect(m_btnBack, QPushButton::clicked, this, WizardDialog::goBack); connect(m_stack, QStackedWidget::currentChanged, this, WizardDialog::onPageChanged); onPageChanged(0); // 手动初始化一次按钮状态 } QWidget *WizardDialog::createPage(const QString title, const QString hint) { auto *page new QWidget; auto *layout new QVBoxLayout(page); auto *titleLabel new QLabel(title, page); titleLabel-setStyleSheet(QStringLiteral(font-size:18px;font-weight:bold;)); auto *hintLabel new QLabel(hint, page); hintLabel-setWordWrap(true); layout-addWidget(titleLabel); layout-addWidget(hintLabel); layout-addStretch(); return page; } void WizardDialog::onPageChanged(int index) { const int last m_stack-count() - 1; m_btnBack-setEnabled(index 0); m_btnNext-setText(index last ? QStringLiteral(完成) : QStringLiteral(下一步)); m_stepTip-setText(QStringLiteral(第 %1 / %2 步) .arg(index 1).arg(m_stack-count())); qDebug() 已切换到页面索引: index; } void WizardDialog::goNext() { const int cur m_stack-currentIndex(); if (cur m_stack-count() - 1) { // 最后一页这里可以放真正的提交逻辑 QMessageBox::information(this, QStringLiteral(完成), QStringLiteral(向导已完成)); accept(); return; } m_stack-setCurrentIndex(cur 1); } void WizardDialog::goBack() { m_stack-setCurrentIndex(m_stack-currentIndex() - 1); }入口// main.cpp #include wizarddialog.h #include QApplication int main(int argc, char *argv[]) { QApplication app(argc, argv); WizardDialog dlg; dlg.show(); return app.exec(); }这个例子里有两个值得注意的设计onPageChanged()被复用既作为信号槽响应切换又在构造末尾手动调一次做初始化。这样就不依赖addWidget时是否发信号这种顺序细节逻辑更稳。页面创建抽成createPage()实际项目里每个页面应该是独立的类比如PageBasic : public QWidget这里为了示例紧凑才用工厂函数。五、QStackedWidget 与 QStackedLayout 的取舍两者能力几乎一样区别在于维度QStackedWidgetQStackedLayout类型widget继承 QFramelayout能否直接放进布局能当普通 widget 用不能要setLayout能否加边框/样式表能本身就是 QFrame需套个容器切换接口setCurrentIndexsetCurrentIndex实践建议99% 的场景直接用QStackedWidget。只有当你需要把堆叠作为一个更大的自定义 widget 的内部布局、且不想多一层QFrame时才考虑QStackedLayout。常见坑点坑点 1以为切换页面会销毁旧页面内存误区❌ 错误认知// 以为切走就释放了 stack-setCurrentIndex(1); // 页面 0 依然活着占着内存✅ 正确做法不用的页面要主动移除并删除。QWidget *old stack-widget(0); stack-removeWidget(old); old-deleteLater(); // 注意removeWidget 不会 delete反过来也要注意如果页面是new出来的务必让它有父对象。addWidget()会自动把父对象设成QStackedWidget所以通常不会泄漏但如果你先new QLabel又没有父对象、又因为某种原因没addWidget成功那就漏了。坑点 2页面尺寸不一致导致窗口跳动每个页面的sizeHint()不同时QStackedWidget的推荐尺寸会随着当前页变化外层窗口跟着一抽一抽地缩放。✅ 解决办法有三种按推荐程度排// 方案 A固定堆叠的最小尺寸取所有页面里最大的 int w 0, h 0; for (int i 0; i stack-count(); i) { const QSize s stack-widget(i)-sizeHint(); w qMax(w, s.width()); h qMax(h, s.height()); } stack-setMinimumSize(w, h); // 方案 B让所有页面用同一个 QSizePolicy::Preferred 相同的 minimumSize // 方案 C外层窗口设置足够大的固定/最小尺寸坑点 3setCurrentIndex传相同索引信号不发出❌ 错误写法想刷新当前页void reload() { m_stack-setCurrentIndex(m_stack-currentIndex()); // 不会触发 currentChanged }✅ 正确写法把刷新逻辑抽成独立函数直接调用void reload() { refreshPage(m_stack-currentIndex()); // 直接调别指望信号 }坑点 4删页面后索引错乱removeWidget()之后后面的页面索引会整体前移。如果你把索引硬编码在别处比如按钮数组就会错位。❌ 错误写法m_stack-removeWidget(page); m_btnGroup-button(2)-setChecked(true); // 索引 2 已经不是原来那个页面了✅ 正确写法用indexOf()动态查或切到安全的相邻页const int removed m_stack-indexOf(page); m_stack-removeWidget(page); page-deleteLater(); if (m_stack-count() 0) m_stack-setCurrentIndex(qBound(0, removed, m_stack-count() - 1));坑点 5跨线程操作堆叠窗口QStackedWidget是 UI 对象只能在主线程访问。工作线程跑完了想切页面必须用信号槽把结果传回来。❌ 错误写法void Worker::run() { // 工作线程里直接碰 UI —— 未定义行为可能崩溃 m_stack-setCurrentIndex(1); }✅ 正确写法发信号回主线程// Worker 里 emit finished(/* 结果 */); // 主线程里用 QueuedConnection跨线程时自动就是排队连接 connect(worker, Worker::finished, this, [this](/* ... */){ m_stack-setCurrentIndex(1); // 在主线程执行安全 });补充一点跨线程时连接类型默认是Qt::AutoConnectionQt 会检测发送者线程与接收者线程是否相同不同则自动走QueuedConnection所以上面的代码不用显式写连接类型也安全——但前提是接收者对象this属于主线程。总结QStackedWidget是 Qt 里区域切换的基础设施QTabWidget也只是它的一个封装。使用时抓住四条要点说明生命周期addWidget会设父对象removeWidget不删对象要自己deleteLater信号时机索引不变不发信号空堆叠首次addWidget会发currentChanged(0)尺寸跳动统一各页面sizeHint或给堆叠设最小尺寸线程安全只能在主线程操作跨线程用信号槽回主线程另外如果你的页面内容很重记得考虑懒加载先addWidget(new QWidget)占位等真正切到该页时再往里面填内容这样启动速度会明显好于一次性把所有页面都建出来。
返回列表