跳转到内容
新建笔记

QStackedLayout 与 QStackedWidget:页面切换与所有权

两者都可以管理多页界面,并把其中一页设为当前页。QStackedLayout 是布局,适合嵌入已有布局结构;QStackedWidget 是封装好的页面容器控件,适合直接放入 Designer 或其他布局。选择取决于需要布局还是控件,而不是哪一个才有“添加、删除页面”的能力。

方面QStackedLayoutQStackedWidget
基类与形态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)