两者都可以管理多页界面,并把其中一页设为当前页。QStackedLayout 是布局,适合嵌入已有布局结构;QStackedWidget 是封装好的页面容器控件,适合直接放入 Designer 或其他布局。选择取决于需要布局还是控件,而不是哪一个才有“添加、删除页面”的能力。
功能与结构对照
跳转到“功能与结构对照”| 方面 | QStackedLayout | QStackedWidget |
|---|---|---|
| 基类与形态 | QLayout,管理宿主中的页面布局 | QFrame,本身是控件,内部使用堆叠布局 |
| 页面接口 | addWidget、insertWidget、widget、count,并继承 removeWidget | 同样提供添加、插入、访问、计数与移除接口 |
| 当前页面 | setCurrentIndex、setCurrentWidget | 同样提供这两个接口 |
| 默认可见性 | StackOne,只显示当前页 | 只显示当前页 |
| 叠加显示 | StackAll 显示所有页,并把当前页抬到最上层 | 没有公开的 stackingMode 属性 |
| 页面父控件 | 安装布局的宿主控件 | 堆叠容器本身 |
| 导航按钮、标签栏 | 由应用提供 | 同样由应用提供 |
QStackedWidget 不自动产生标签栏,也不会自动提供向导的“下一步”流程;这类现成交互分别可考虑 QTabWidget 或 QWizard。普通堆叠容器常与按钮、列表或组合框连接。Qt:QStackedLayout、Qt:QStackedWidget
方案一:把 QStackedLayout 嵌入外层布局
跳转到“方案一:把 QStackedLayout 嵌入外层布局”下面只有 outer 安装到 host,没有宿主的 pages 通过 addLayout() 嵌入。不能先 new QStackedLayout(&host),再在同一宿主安装另一个 QVBoxLayout,最后把已经安装的布局当作无主子布局加入。
#include <QApplication>#include <QLabel>#include <QPushButton>#include <QStackedLayout>#include <QVBoxLayout>#include <QWidget>#include <cassert>
int main(int argc, char *argv[]) { QApplication app(argc, argv); QWidget host; auto *outer = new QVBoxLayout(&host); auto *pages = new QStackedLayout; auto *first = new QLabel("Page 1"); auto *second = new QLabel("Page 2"); auto *next = new QPushButton("Switch page"); pages->addWidget(first); pages->addWidget(second); outer->addLayout(pages); outer->addWidget(next); QObject::connect(next, &QPushButton::clicked, &host, [pages, first, second] { pages->setCurrentWidget(pages->currentWidget() == first ? second : first); }); host.resize(400, 240); host.show(); app.processEvents(); if (app.arguments().contains("--check")) { assert(host.layout() == outer); assert(pages->parentWidget() == &host); assert(first->parentWidget() == &host); assert(pages->count() == 2 && pages->currentWidget() == first); next->click(); assert(pages->currentWidget() == second); assert(first->isHidden() && !second->isHidden()); pages->setStackingMode(QStackedLayout::StackAll); assert(!first->isHidden() && !second->isHidden()); pages->setStackingMode(QStackedLayout::StackOne); assert(first->isHidden() && !second->isHidden()); return 0; } return app.exec();}连接中的 [pages, first, second] 捕获了实际使用的局部指针。原例在成员函数里只写 [this],却在 lambda 中使用局部 stackedLayout,不能因此访问该局部变量。若把指针作为窗口成员保存,可通过 this 访问成员;若仍是局部变量,就要正确捕获。连接使用 &host 作为上下文,使窗口销毁时连接随之失效。
StackAll 适合确实需要叠加绘制的界面;上层控件是否透明、鼠标事件如何分配,还要由具体控件的绘制与事件规则决定,切换枚举并不自动完成这些设计。Qt:StackingMode
方案二:直接使用 QStackedWidget
跳转到“方案二:直接使用 QStackedWidget”这里页面的父控件变为 pages。切换时使用页面指针,可避免代码把某个会变化的索引误当作永久身份。
#include <QApplication>#include <QLabel>#include <QPointer>#include <QPushButton>#include <QStackedWidget>#include <QVBoxLayout>#include <QWidget>#include <cassert>
int main(int argc, char *argv[]) { QApplication app(argc, argv); QWidget host; auto *outer = new QVBoxLayout(&host); auto *pages = new QStackedWidget; auto *first = new QLabel("Page 1"); auto *second = new QLabel("Page 2"); auto *next = new QPushButton("Switch page"); pages->addWidget(first); pages->addWidget(second); outer->addWidget(pages); outer->addWidget(next); QObject::connect(next, &QPushButton::clicked, &host, [pages, first, second] { pages->setCurrentWidget(pages->currentWidget() == first ? second : first); }); host.resize(400, 240); host.show(); app.processEvents(); if (app.arguments().contains("--check")) { next->click(); assert(pages->currentWidget() == second); assert(first->parentWidget() == pages); QPointer<QLabel> observed = first; pages->removeWidget(first); assert(observed && first->parentWidget() == pages); assert(first->isHidden()); assert(pages->indexOf(first) == -1); assert(pages->currentWidget() == second); assert(pages->currentIndex() == 0); // 原来的第 1 页变成第 0 页 pages->addWidget(first); pages->setCurrentWidget(first); assert(pages->currentWidget() == first); return 0; } return app.exec();}页面移除、销毁与索引变化
跳转到“页面移除、销毁与索引变化”QStackedWidget::removeWidget() 会把页面从内部布局移除并隐藏,但不会删除页面;它的父对象仍是原堆叠容器。要转移到另一个容器,就在新容器中重新安装并安排所有权;已经不再需要时,选择合适时机销毁。不要把“移出页面列表”当成“已脱离原父对象”。Qt:removeWidget
QStackedLayout 的页面也不会因为删除布局而一并销毁,它们仍属于宿主。普通布局与堆叠布局的可见性行为不可混用;做临时移除时,显式安排页面是否隐藏,并确认后续回调不再访问被删除的页面。详见 [[10-knowledge/computing/software-development/application-development/desktop/qt/gui/layout-ownership-and-removal|布局与控件移除]]。
两种容器的索引都可能随插入或删除变化。在当前页前面插入页面,当前控件可以保持相同,但索引会增加;空容器的 currentIndex() 为 -1,currentWidget() 为 nullptr。生产代码还应处理页面在运行时被关闭的情况;本例的切换 lambda 假设两页都保持存活,测试移除后会把页面重新加入,没有永久删除它。
两个程序分别放入独立目录的 main.cpp,均可用下面的构建文件。Debug 构建后传入 --check 执行断言;不传参数可点击按钮切换页面。
cmake_minimum_required(VERSION 3.20)project(StackedPagesExample LANGUAGES CXX)set(CMAKE_CXX_STANDARD 17)set(CMAKE_CXX_STANDARD_REQUIRED ON)find_package(Qt6 REQUIRED COMPONENTS Widgets)add_executable(stacked_pages main.cpp)target_link_libraries(stacked_pages PRIVATE Qt6::Widgets)