跳转到内容
新建笔记

Qt 布局参数与页面切换:边距、伸展和 QStackedWidget

布局负责在已有空间中安放控件,页面容器负责切换内容。原笔记中的 Designer 参数、两按钮伸展示例,以及四页动态界面,可以沿这两个职责组织起来。

本文 C++ 示例的 --self-test 使用 assert,应在 Debug 构建或未定义 NDEBUG 的情况下运行;Release 构建若禁用了断言,退出成功不代表这些检查已经执行。

Designer 参数与 C++ 接口对应

跳转到“Designer 参数与 C++ 接口对应”

QVBoxLayout、QHBoxLayout 是布局管理器,不是可直接显示的 QWidget。Designer 可能把布局属性展示在承载控件的属性面板中,界面标签也会随版本变化;编写 C++ 时应使用实际 API。

原参数名称或界面含义C++ 对应解释
layoutNamesetObjectName()名称是 QObject 的对象名;变量名 vBaseLayout 不会自动设置对象名
layoutLeftMargin、layoutTopMargin、layoutRightMargin、layoutBottomMarginsetContentsMargins(left, top, right, bottom)四周留白;原文 layoutleftMargin 的大小写不能当成实际 C++ 成员
layoutSpacingsetSpacing()相邻布局项之间的间距;与四边留白不同
layoutStretchsetStretch(index, value) 或 setStretchFactor(widget, value)沿布局主方向分配空间的相对因子;三个项目可分别设置为 1、2、3
layoutSizeConstraintsetSizeConstraint()约束承载控件的尺寸;默认、固定、最小、最大等策略有不同语义

这里的整数几何尺寸采用 Qt 的逻辑坐标;高 DPI 下不能直接等同于显示器的物理像素。默认间距与边距由样式、父布局等决定。需要零边距、零间距时应明确设置,而不是把默认值当成恒定的 9 或 11。

伸展因子受最小尺寸、最大尺寸、sizeHint()、sizePolicy() 和对齐方式共同约束。特别是普通 QPushButton 常采用不随垂直方向扩张的尺寸策略,仅写 1:2 不能保证按钮实际高度永远是 1:2。有关空间分配规则,参见 QBoxLayout 和 布局管理。

两个按钮的完整伸展示例

跳转到“两个按钮的完整伸展示例”

保留原例的两个按钮、零边距、零间距和 1:2 因子,并显式允许按钮在竖直方向扩张。示例先给足窗口高度;它演示这一组约束下的分配,不承诺在任意窗口尺寸或任意按钮限制下仍有精确比例。

下面两个 C++ 程序各自保存为一个独立项目的 main.cpp,用 Qt 6 Widgets、C++17 构建。传入 --self-test 运行检查后退出。

#include <QApplication>
#include <QPushButton>
#include <QSizePolicy>
#include <QVBoxLayout>
#include <QWidget>
#include <cassert>
#include <cstdlib>
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
QWidget window;
auto *vBaseLayout = new QVBoxLayout(&window);
vBaseLayout->setObjectName("vBaseLayout");
vBaseLayout->setContentsMargins(0, 0, 0, 0);
vBaseLayout->setSpacing(0);
auto *button1 = new QPushButton("Button 1");
auto *button2 = new QPushButton("Button 2");
for (QPushButton *button : {button1, button2}) {
button->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
vBaseLayout->addWidget(button);
}
const bool found1 = vBaseLayout->setStretchFactor(button1, 1);
const bool found2 = vBaseLayout->setStretchFactor(button2, 2);
assert(found1 && found2);
vBaseLayout->setSizeConstraint(QLayout::SetDefaultConstraint);
window.resize(360, 360);
window.show();
if (app.arguments().contains("--self-test")) {
app.processEvents();
assert(window.layout() == vBaseLayout);
assert(vBaseLayout->stretch(0) == 1 && vBaseLayout->stretch(1) == 2);
assert(button1->parentWidget() == &window);
assert(button2->height() > button1->height());
assert(std::abs(button2->height() - 2 * button1->height()) <= 2);
return 0;
}
return app.exec();
}

new QVBoxLayout(&window) 已将布局安装到 window。同一控件只能有一个顶层布局;嵌套布局使用 addLayout(),不能反复对同一个 QWidget 安装多个布局。

QStackedWidget 存放 QWidget 页面,每个页面可以各自有布局。原文两段重复代码都创建四页,分别包含 2、3、4、5 个标签;下面保留这个场景,用循环减少重复,并用按钮组 ID 连接页面索引。

#include <QApplication>
#include <QButtonGroup>
#include <QHBoxLayout>
#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 window;
auto *mainLayout = new QVBoxLayout(&window);
auto *navigation = new QHBoxLayout;
auto *stack = new QStackedWidget;
auto *group = new QButtonGroup(&window);
mainLayout->addLayout(navigation);
mainLayout->addWidget(stack);
for (int pageNumber = 0; pageNumber < 4; ++pageNumber) {
auto *page = new QWidget;
auto *pageLayout = new QVBoxLayout(page);
for (int labelNumber = 0; labelNumber < pageNumber + 2; ++labelNumber)
pageLayout->addWidget(new QLabel(
QString("Layout %1 - Graphic %2")
.arg(pageNumber + 1).arg(labelNumber + 1)));
const int pageIndex = stack->addWidget(page);
auto *button = new QPushButton(QString("Show Layout %1").arg(pageNumber + 1));
button->setCheckable(true);
group->addButton(button, pageIndex);
navigation->addWidget(button);
}
group->button(0)->setChecked(true);
QObject::connect(group, &QButtonGroup::idClicked,
stack, &QStackedWidget::setCurrentIndex);
window.setWindowTitle("Four independent pages");
window.resize(560, 320);
if (app.arguments().contains("--self-test")) {
assert(stack->count() == 4);
for (int i = 0; i < 4; ++i) {
group->button(i)->click();
assert(stack->currentIndex() == i);
assert(stack->widget(i)->layout()->count() == i + 2);
assert(stack->widget(i)->parentWidget() == stack);
}
QPointer<QWidget> removed = stack->widget(3);
stack->removeWidget(removed);
assert(stack->count() == 3 && removed);
assert(removed->parentWidget() == stack);
delete removed.data();
assert(removed.isNull());
return 0;
}
window.show();
return app.exec();
}

addWidget() 把页面所有权交给容器。removeWidget() 将页面移出显示管理并隐藏它,但不会删除页面,也不会自动解除原 parent。之后要么明确删除,要么给它重新设置合适的父对象并复用。上面删除页的操作只在自测分支执行;真实可变页面还需要同步删除导航按钮,并维护 ID 与当前索引之间的映射。页面插入和移除会改变索引,长期业务标识不宜直接等同于易变索引。参见 QStackedWidget。

如何选择其他动态界面方案

跳转到“如何选择其他动态界面方案”
需求选择与边界
同一位置一次显示一页QStackedWidget;如需要现成标签栏,可用 QTabWidget
用户拖动分隔条调整两侧大小QSplitter,它是 QWidget,不是 QLayout;通过 addWidget() 管理面板
在当前页新增或移除少量控件QVBoxLayout、QHBoxLayout、QGridLayout 等;布局和 QWidget 生命周期分别处理
特殊空间排列算法自定义 QLayout;要实现布局项管理、尺寸提示和几何分配,不能只改某次坐标
声明式界面、状态、动画Qt Quick/QML;这是另一种界面体系,不是 Widgets 布局的直接替代类

从布局取出 QLayoutItem 与删除其 QWidget 是两件事。动态删除时先确定对象之后是否复用;removeWidget() 不负责销毁控件,takeAt() 交出的布局项也需要调用方处理。控件若仍在使用中,先隐藏、再在合适的事件边界调用 deleteLater(),不要把布局移除理解成对象已经不存在。

QMainWindow 自带专门布局,通常给它设置 centralWidget,再向中央控件安装自己的布局。页面模块保留自己的对象、状态和信号,导航只负责选择页面;这样调整窗口大小、增加页面和改变页面内容时,各自的责任都清楚。

补充参考:QLayout、QSplitter、QMainWindow。