布局负责在已有空间中安放控件,页面容器负责切换内容。原笔记中的 Designer 参数、两按钮伸展示例,以及四页动态界面,可以沿这两个职责组织起来。
本文 C++ 示例的 --self-test 使用 assert,应在 Debug 构建或未定义 NDEBUG 的情况下运行;Release 构建若禁用了断言,退出成功不代表这些检查已经执行。
Designer 参数与 C++ 接口对应
跳转到“Designer 参数与 C++ 接口对应”QVBoxLayout、QHBoxLayout 是布局管理器,不是可直接显示的 QWidget。Designer 可能把布局属性展示在承载控件的属性面板中,界面标签也会随版本变化;编写 C++ 时应使用实际 API。
| 原参数名称或界面含义 | C++ 对应 | 解释 |
|---|---|---|
layoutName | setObjectName() | 名称是 QObject 的对象名;变量名 vBaseLayout 不会自动设置对象名 |
layoutLeftMargin、layoutTopMargin、layoutRightMargin、layoutBottomMargin | setContentsMargins(left, top, right, bottom) | 四周留白;原文 layoutleftMargin 的大小写不能当成实际 C++ 成员 |
layoutSpacing | setSpacing() | 相邻布局项之间的间距;与四边留白不同 |
layoutStretch | setStretch(index, value) 或 setStretchFactor(widget, value) | 沿布局主方向分配空间的相对因子;三个项目可分别设置为 1、2、3 |
layoutSizeConstraint | setSizeConstraint() | 约束承载控件的尺寸;默认、固定、最小、最大等策略有不同语义 |
这里的整数几何尺寸采用 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。