Qt 布局管理系统:QHBoxLayout、QVBoxLayout、QGridLayout
引言
第 8 章的对话框示例里,控件已经开始交给 QVBoxLayout 排列。这样做并非只是"代码更短":窗口变宽、系统字体变大、翻译后的文字更长或用户使用高 DPI 屏幕时,布局会重新计算每个控件的位置和尺寸。若用 move()、resize() 把坐标写死,这些变化都要由应用代码逐一修补。
本文面向 Qt 6 Widgets,按"先会用,再理解为什么"的顺序学习。读完后应能:
- 用
QHBoxLayout、QVBoxLayout与QGridLayout写出可伸缩窗口; - 说清边缘间隔(margins)、控件间距(spacing)和空白弹簧(stretch)的区别;
- 用 stretch、
QSizePolicy和网格行列权重控制多余空间; - 知道三种布局的选择边界,并能把它们嵌套为一个完整界面;
- 从少量 Qt 源码理解"设置参数后为何会重新排版"。
一、为什么不要把控件坐标写死
下面的写法可以让窗口出现一个按钮,但它没有描述"按钮和窗口边缘之间的关系":
cpp
auto *button = new QPushButton(tr("保存"), this);
button->move(260, 180);
button->resize(90, 32);
只要窗口尺寸、按钮文字或字体变了,(260, 180) 就不一定合理。布局管理器保存的是规则:谁在前、谁在后、谁可以拉伸、彼此隔多远。窗口的可用区域改变时,Qt 按这些规则重新为控件设置几何尺寸。
最小可运行例子如下。把布局的父对象直接设为窗口,Qt 会把它安装为该窗口的顶层布局:
cpp
class LoginWidget : public QWidget
{
public:
LoginWidget()
{
auto *title = new QLabel(tr("欢迎登录"));
auto *account = new QLineEdit;
account->setPlaceholderText(tr("账号"));
auto *password = new QLineEdit;
password->setPlaceholderText(tr("密码"));
password->setEchoMode(QLineEdit::Password);
auto *login = new QPushButton(tr("登录"));
auto *layout = new QVBoxLayout(this);
layout->addWidget(title);
layout->addWidget(account);
layout->addWidget(password);
layout->addWidget(login);
}
};
这里的 this 很关键:它既是布局的父对象,也让布局成为窗口的顶层布局。不要先写 new QVBoxLayout(this),又调用 setLayout() 把另一个布局装到同一个窗口;一个 QWidget 同一时间只能有一个顶层布局。

图 1:布局项可以是控件,也可以是另一个布局,因此复杂界面通常由多个小布局递归组成。
二、认识三位主角
三者都继承自 QLayout,都会管理布局项(QLayoutItem)。区别只在于"怎样把项目排开"。
2.1 布局到底管理什么?
前面说过,QHBoxLayout、QVBoxLayout 和 QGridLayout 都继承自 QLayout,而 QLayout 管理的是一组 **QLayoutItem**。
这句话看起来有些抽象。
我们平时写布局时,最常见的是:
scss
layout->addWidget(button);
好像布局管理的就是 QPushButton。
但实际上,QLayout 内部并不是直接把所有 QWidget 当成自己的布局项,而是通过 QLayoutItem 对这些对象进行统一抽象。
可以简单理解成:
markdown
QLayout
│
管理 QLayoutItem
│
┌─────────────────┼─────────────────┐
│ │ │
▼ ▼ ▼
QWidgetItem QSpacerItem 子 QLayout
│ │ │
▼ ▼ ▼
QWidget 空白空间 QVBoxLayout
│
▼
QWidget...
也就是说,一个布局中可以同时存在三种东西:
- 一个控件;
- 一块空白空间;
- 另一个布局。
QWidget:通过 QWidgetItem 加入布局
当我们写:
ini
auto *button = new QPushButton(tr("确定"));
layout->addWidget(button);
从使用层面看,是把 button 加入了布局。
但从布局系统的角度,可以理解为:
markdown
QPushButton
│
▼
QWidgetItem
│
▼
QLayout
QWidgetItem 是 Qt 用来把 QWidget 包装成 QLayoutItem 的对象。
这样布局系统就不需要针对 QPushButton、QLineEdit、QLabel 等不同控件分别处理,而是统一通过 QLayoutItem 获取它们的尺寸信息,并最终设置它们的几何区域。
例如布局在计算空间时,需要知道一个控件:
你希望多大?
最小能多小?
最大能多大?
是否允许扩张?
这些信息最终都会参与布局计算。
Stretch:实际上是一种空白布局项
再看我们经常使用的:
scss
layout->addStretch(1);
它并不是创建了一个看不见的 QWidget。
更准确地说,它向布局中加入了一个可以扩张的空白布局项,可以理解为 QSpacerItem。
例如:
scss
auto *layout = new QHBoxLayout;
layout->addWidget(new QLabel(tr("状态正常")));
layout->addStretch(1);
layout->addWidget(new QPushButton(tr("确定")));
可以把它想象成:
scss
QHBoxLayout
│
├── QLabel
│
├── QSpacerItem ← addStretch()
│
└── QPushButton
当窗口还有多余宽度时,中间的 QSpacerItem 就会不断吸收这些空间:
css
窗口较窄:
[状态正常] ───── [确定]
窗口变宽:
[状态正常] ───────────────────── [确定]
因此:
scss
layout->addStretch(1);
本质上不是"把按钮移动到右边",而是:
在两个布局项之间放入一个可以占据多余空间的布局项。
这也是为什么 addStretch() 不需要知道窗口当前到底有多宽。
子布局:布局本身也可以成为布局项
还有一种情况可能更容易让人疑惑:
ini
auto *root = new QVBoxLayout;
auto *buttons = new QHBoxLayout;
buttons->addWidget(new QPushButton(tr("取消")));
buttons->addWidget(new QPushButton(tr("确定")));
root->addLayout(buttons);
这里 root 管理的并不是一个 QWidget,而是另一个 QHBoxLayout。
可以理解成:
markdown
QVBoxLayout
│
├── 其他布局项
│
└── QHBoxLayout
│
├── QPushButton
└── QPushButton
这就是 Qt 布局能够不断嵌套的原因。
例如一个真实的设置页面,可以形成这样的结构:
markdown
QVBoxLayout
│
├── 标题
│
├── QGridLayout
│ ├── 标签
│ ├── 输入框
│ ├── 标签
│ └── 输入框
│
└── QHBoxLayout
├── Stretch
├── 取消
└── 确定
因此,Qt 布局并不是简单地"把几个控件排列起来"。
更准确的理解是:
布局管理器维护一组
**QLayoutItem**,这些布局项可以代表控件、空白空间或者子布局。布局系统根据这些项目提供的尺寸约束和布局规则,计算每个项目最终应该占据的几何区域。
这也解释了为什么 Qt 的布局可以递归嵌套。
一层布局解决一个空间关系,下一层布局继续解决更细的空间关系,最终形成一棵布局树。
2.2 布局和控件分别负责什么?
理解了 QLayoutItem 后,还需要把一个容易混淆的问题区分开:
布局负责"放在哪里、占多大",控件负责"显示什么、怎么交互"。
例如:
ini
auto *button = new QPushButton(tr("保存"));
auto *layout = new QHBoxLayout(this);
layout->addWidget(button);
这里可以简单理解成:
markdown
QHBoxLayout
│
│ 决定
▼
位置 + 尺寸
│
▼
QPushButton
│
│ 负责
▼
文字 + 外观 + 事件 + 绘制
布局并不负责绘制按钮。
它只负责根据当前窗口大小以及各个布局项的约束,计算出按钮应该放在哪里、应该多大。
真正的按钮绘制、鼠标点击、文字显示等工作,仍然由 QPushButton 及 Qt 的 Widget/Style 系统完成。
因此可以把整个过程简单理解成:
markdown
窗口尺寸发生变化
│
▼
布局系统重新计算空间
│
▼
得到每个控件的 geometry
│
▼
QWidget 获得新的位置和尺寸
│
▼
控件按照自己的绘制机制显示
这也是布局管理和前面 move()、resize() 最大的区别。
使用:
scss
button->move(260, 180);
button->resize(90, 32);
是应用程序直接告诉控件:
"你现在就放在这个坐标,尺寸就是这么大。"
而使用布局时,我们描述的是:
"这个按钮属于这一组控件,它应该位于这里,并根据整个布局的空间规则参与尺寸分配。"
窗口尺寸发生变化时,Qt 可以重新计算这些关系,而不需要我们手动修改每个控件的坐标。
2.3 布局控件对比
| 布局 | 排列方式 | 最适合的第一选择 | 典型例子 |
|---|---|---|---|
QHBoxLayout |
从左到右(受布局方向影响)的一行 | 一行工具栏、标签和输入框、底部按钮 | "名称: 输入框 浏览" |
QVBoxLayout |
从上到下的一列 | 页面、对话框、侧栏 | 标题、表单区域、按钮栏纵向堆叠 |
QGridLayout |
行 × 列的单元格 | 多行字段需要标签对齐 | 设置页、登录/注册表单、参数面板 |
QHBoxLayout 和 QVBoxLayout 都是 QBoxLayout 的便捷子类:前者方向为 LeftToRight,后者方向为 TopToBottom。它们的 stretch 写在布局项的位置 上;QGridLayout 没有单一主轴,需要分别设置行和列的 stretch。
入门时先记一个选择规则:一条线用 Box Layout(HBox/VBox),反复出现的行列对齐用 Grid Layout;实际窗口通常两者嵌套。
三、QHBoxLayout:把一行关系写清楚
下面是一行"路径输入 + 浏览按钮"。输入框获得所有可扩张的多余空间,按钮保持自然宽度:
cpp
auto *pathEdit = new QLineEdit;
auto *browseButton = new QPushButton(tr("浏览..."));
auto *row = new QHBoxLayout;
row->addWidget(new QLabel(tr("保存位置:")));
row->addWidget(pathEdit, 1); // stretch = 1:优先获得剩余宽度
row->addWidget(browseButton); // 默认 stretch = 0
addWidget() 的常用形态是:
cpp
layout->addWidget(widget, stretch, alignment);
widget:要放入的控件;stretch:沿布局主轴分配额外空间 的权重,默认是0;alignment:可选对齐方式。指定Qt::AlignLeft或Qt::AlignTop后,控件不会再填满单元格的可用空间,而倾向按自身建议尺寸放置。
3.1 用 addStretch() 把一组控件推到右边
"左侧状态文字,右侧两个按钮"是 addStretch() 的典型用途:
cpp
auto *footer = new QHBoxLayout;
footer->addWidget(new QLabel(tr("已自动保存")));
footer->addStretch(1); // 一个可伸缩的空白项
footer->addWidget(new QPushButton(tr("取消")));
footer->addWidget(new QPushButton(tr("确定")));
它并不把按钮移动到某个固定 x 坐标,而是把中间的空白项设置为可扩张项。窗口越宽,这块空白越大,按钮仍贴在右侧。addSpacing(16) 则不同:它插入的是恒定 16 像素的空白,不会吸收额外空间。
3.2 两个控件怎样按 1:2 拉伸
cpp
auto *left = new QTextEdit;
auto *right = new QTextEdit;
auto *split = new QHBoxLayout;
split->addWidget(left, 1);
split->addWidget(right, 2);
当两个控件都允许在水平方向扩张、且满足各自最小尺寸后,剩余空间按约 1:2 分配。不要把它理解成"右边永远是左边的两倍":控件的 minimumSize、maximumSize、sizeHint() 和 QSizePolicy 都是约束条件。一个最大宽度被限制为 200 的控件,到达上限后,剩余空间会继续交给别的可扩张项。
3.3 sizeHint() 是什么?
sizeHint() 是控件向布局系统提供的"建议尺寸",表示在没有特殊限制的情况下,这个控件希望多大。
它不是最终尺寸,也不是强制尺寸。
布局系统会综合 sizeHint()、最小尺寸、最大尺寸、QSizePolicy 和 stretch 等因素,最终决定控件的实际几何尺寸。
cpp
qDebug() << button->sizeHint();
qDebug() << button->size();
你会发现 sizeHint() ≠ size()
四、QVBoxLayout:把页面按区块从上到下组织
QVBoxLayout 的使用与 QHBoxLayout 完全相同,只是主轴换成垂直方向。一个稳定的页面常按"标题---内容---按钮栏"来组织:
cpp
auto *title = new QLabel(tr("导出设置"));
title->setStyleSheet("font-weight: 600; font-size: 18px;");
auto *content = new QWidget;
auto *contentLayout = new QGridLayout(content);
contentLayout->addWidget(new QLabel(tr("文件名:")), 0, 0);
contentLayout->addWidget(new QLineEdit, 0, 1);
auto *buttons = new QDialogButtonBox(
QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
auto *page = new QVBoxLayout(this);
page->addWidget(title);
page->addWidget(content, 1); // 中间内容优先拿到额外高度
page->addWidget(buttons);
常见误解是"给按钮栏的 stretch 写 0,就一定贴到底部"。实际上,只有页面还有多余高度、且中间内容允许扩张时,content 才会吸收高度。若只是想把一组固定高度控件顶到上方,也可以在最后加:
cpp
page->addStretch(1);
这和在 HBox 中把控件推向右侧是同一条规则,只是主轴变成了竖直方向。
五、QGridLayout:多行字段对齐的正确工具
如果每一行都手写一个 HBox,虽然也能做出表单,但"标签列统一右对齐、输入列统一拉伸、某个控件跨两列"会越来越麻烦。QGridLayout 用 (row, column) 指定单元格位置,左上角是 (0, 0):
cpp
auto *grid = new QGridLayout;
auto *nameEdit = new QLineEdit;
auto *emailEdit = new QLineEdit;
auto *newsCheck = new QCheckBox(tr("订阅产品更新"));
grid->addWidget(new QLabel(tr("姓名:")), 0, 0);
grid->addWidget(nameEdit, 0, 1);
grid->addWidget(new QLabel(tr("邮箱:")), 1, 0);
grid->addWidget(emailEdit, 1, 1);
grid->addWidget(newsCheck, 2, 0, 1, 2); // 从 (2,0) 开始,跨 1 行 2 列
grid->setColumnStretch(1, 1); // 第 1 列得到多余宽度
跨行跨列版本的参数为:
cpp
grid->addWidget(widget, fromRow, fromColumn, rowSpan, columnSpan);
例如 addWidget(newsCheck, 2, 0, 1, 2) 的意思是:从第 2 行第 0 列开始,占 1 行、2 列。注意它不是"结束行和结束列"。
5.1 让网格列与行按比例获得空间
cpp
grid->setColumnStretch(0, 0); // 标签列按内容宽度即可
grid->setColumnStretch(1, 1); // 输入列吸收横向多余空间
grid->setRowStretch(3, 1); // 若第 3 行放 QTextEdit,它吸收纵向多余空间
若有两列内容区,写 setColumnStretch(1, 1)、setColumnStretch(2, 2),两列在满足最小尺寸之后按约 1:2 分配多余宽度。与 Box Layout 一样,stretch 是权重,不是固定百分比。没有项目的行或列也能预设 stretch;当以后向其中加入控件时,该规则会生效。
5.2 让标签列保持统一对齐
cpp
auto *nameLabel = new QLabel(tr("用户名:"));
auto *emailLabel = new QLabel(tr("邮箱地址:"));
grid->addWidget(nameLabel, 0, 0, Qt::AlignRight | Qt::AlignVCenter);
grid->addWidget(emailLabel, 1, 0, Qt::AlignRight | Qt::AlignVCenter);
Grid 负责"放在哪一行哪一列",alignment 负责"控件在自己的布局单元里怎么对齐"。
六、间隔的三层含义:margins、spacing 与 stretch
这是布局最容易混淆的部分。可以按"边缘---邻居---可变空白"三个问题来区分:

图 2:布局先处理边缘与控件间固定距离,再把可用的剩余空间交给可扩张项目。
| 想控制什么 | API | 含义 | 是否随窗口变大 |
|---|---|---|---|
| 布局内容离容器四边的距离 | setContentsMargins(left, top, right, bottom) |
外边缘留白 | 否 |
| 相邻布局项之间的距离 | setSpacing(pixels) |
项间固定间距 | 否 |
| 某处的固定空白 | addSpacing(pixels) |
插入固定空白项 | 否 |
| 某处可吃掉所有剩余空间 | addStretch(factor) |
插入可扩张空白项 | 是 |
| 控件/行/列之间分配剩余空间的权重 | addWidget(..., stretch)、setColumnStretch() |
权重 | 是 |
6.1 推荐先使用风格默认值
新建布局时,如果没有手动设置 contentsMargins 和 spacing,Qt 会使用默认值。其中顶层布局的默认边缘留白与当前 QStyle 有关,因此不同平台、不同样式可能存在差异。
对于嵌套布局,则不要简单认为每一级都会自动产生一圈相同的外边距。实际项目中通常由顶层布局负责页面外边距,再通过 spacing 控制内部区块之间的距离。
这正是默认值比"到处写 9 像素"更可靠的原因。只有在设计规范明确要求时再覆盖:
cpp
layout->setContentsMargins(24, 20, 24, 20);
layout->setSpacing(12);
嵌套布局时尤其要小心"双重边缘"。通常只有页面的顶层布局 设置 contentsMargins;嵌入其中的子布局多使用 setContentsMargins(0, 0, 0, 0),由外层 spacing 保持区块距离:
cpp
auto *root = new QVBoxLayout(this);
root->setContentsMargins(24, 20, 24, 20);
root->setSpacing(16);
auto *row = new QHBoxLayout;
row->setContentsMargins(0, 0, 0, 0);
row->setSpacing(8);
不要把 setContentsMargins(0, 0, 0, 0) 当作所有布局的默认选择。对话框、页面的外层留白被移除后,控件会贴边,视觉和可点击性都变差。
6.2 stretch 与 QSizePolicy:谁决定"能不能长"
stretch 回答的是"谁优先拿到多余空间 ";QSizePolicy 回答的是"控件在该方向能否变大或变小"。大多数输入控件默认适合水平扩张,而按钮更接近按内容宽度显示。需要显式调整时:
cpp
auto *editor = new QPlainTextEdit;
editor->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
auto *save = new QPushButton(tr("保存"));
save->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Fixed);
不要一开始就给每个控件调用 setFixedSize()。固定尺寸会绕开布局的弹性,长文本、缩放和系统字体变化都可能造成截断。更常见的需求是设下限,例如 setMinimumWidth(240),然后让布局决定其余空间。
七、把三种布局组合成一个完整的设置页
下面的例子展示实际页面最常见的结构:顶层 VBox 管页面区块,表单用 Grid 对齐,底部按钮用 HBox 靠右。重点不在控件数量,而在每层布局只负责一种清晰的关系。
cpp
class NetworkSettingsPage : public QWidget
{
public:
explicit NetworkSettingsPage(QWidget *parent = nullptr) : QWidget(parent)
{
auto *title = new QLabel(tr("网络设置"));
title->setStyleSheet("font-weight: 600; font-size: 18px;");
auto *hostEdit = new QLineEdit;
auto *portSpin = new QSpinBox;
portSpin->setRange(1, 65535);
portSpin->setValue(443);
auto *form = new QGridLayout;
form->addWidget(new QLabel(tr("服务器:")), 0, 0);
form->addWidget(hostEdit, 0, 1);
form->addWidget(new QLabel(tr("端口:")), 1, 0);
form->addWidget(portSpin, 1, 1);
form->setColumnStretch(1, 1);
auto *testButton = new QPushButton(tr("测试连接"));
auto *saveButton = new QPushButton(tr("保存"));
auto *buttons = new QHBoxLayout;
buttons->addStretch();
buttons->addWidget(testButton);
buttons->addWidget(saveButton);
auto *root = new QVBoxLayout(this);
root->setContentsMargins(24, 20, 24, 20);
root->setSpacing(16);
root->addWidget(title);
root->addLayout(form);
root->addStretch(); // 多余高度留在表单和按钮之间
root->addLayout(buttons);
}
};
这里有一个容易被忽略的细节:form 和 buttons 没有 new QWidget 包起来,也能加入 root。布局本身可以作为另一布局的项目;只有需要独立背景、边框、事件处理,或希望把一组控件封装成可复用组件时,才额外创建容器 QWidget。
八、从本地源码看:设置后为什么会重新布局
下面直接阅读本机安装的 Qt 6.8.3 源码。本文只摘取公开 API 附近的少量实现;QLayoutPrivate、QBoxLayoutPrivate 等私有类用于理解数据流即可,应用代码不要包含或依赖它们。
源码入口:
text
C:\Qt6\6.8.3\Src\qtbase\src\widgets\kernel\qlayout.cpp
C:\Qt6\6.8.3\Src\qtbase\src\widgets\kernel\qboxlayout.cpp
C:\Qt6\6.8.3\Src\qtbase\src\widgets\kernel\qgridlayout.cpp
8.1 margins 和 spacing:记录新值,再标记几何信息失效
QLayout::setContentsMargins() 的核心逻辑可缩写为:
cpp
void QLayout::setContentsMargins(int left, int top, int right, int bottom)
{
// 值没有变化时直接返回
d->userLeftMargin = left;
d->userTopMargin = top;
d->userRightMargin = right;
d->userBottomMargin = bottom;
invalidate();
}
在 Qt 6.8.3 中,这段代码位于 qlayout.cpp 的 288--301 行。invalidate() 是这里最有价值的线索:它不是马上逐个控件 resize(),而是使此前缓存的几何计算失效,之后由布局更新流程重新计算。QBoxLayout::setSpacing() 同样只是保存 d->spacing 后调用 invalidate()(qboxlayout.cpp 的 579--584 行)。
8.1.1 invalidate() 之后发生了什么
理解了 invalidate() 后,还需要注意一个问题:
调用
invalidate()并不等于 Qt 在这一行代码里立即把所有控件重新排列完毕。
例如:
scss
layout->setSpacing(20);
可以把过程简化成:
scss
setSpacing(20)
│
▼
修改布局参数
│
▼
invalidate()
│
▼
原有布局计算结果失效
│
▼
布局系统在后续更新过程中重新计算
│
▼
得到新的布局几何信息
│
▼
更新子控件 geometry
因此,布局 API 和我们手动调用:
scss
widget->move(...);
widget->resize(...);
思路并不一样。
手动调用 move() / resize() 是直接修改某个控件的几何信息:
scss
代码
│
├── move()
└── resize()
│
▼
直接修改 QWidget geometry
而布局系统更像是在修改一组"规则":
scss
代码
│
▼
修改 margins / spacing / stretch
│
▼
布局规则发生变化
│
▼
invalidate()
│
▼
重新进行布局计算
│
▼
统一更新子控件 geometry
这也是布局系统能够适应窗口大小变化的关键。
例如窗口从:
800 × 600
变成:
yaml
1200 × 800
应用程序并不需要重新计算每一个按钮的坐标。
布局系统会根据新的可用空间,以及:
contentsMarginsspacingstretchsizeHint()minimumSizemaximumSizeQSizePolicy
等信息重新计算布局项应该占据的空间。
最终才会把计算结果反映到各个 QWidget 的 geometry 上。
所以,从更高的层次看:
Qt Layout 并不是一组"自动调用 move() 和 resize() 的快捷 API",而是一套根据布局规则和尺寸约束进行几何计算的管理机制。
这也正是前面"为什么不要把控件坐标写死"这一节的最终答案:
固定坐标:
"我告诉控件应该在哪里。"
布局系统:
"我告诉 Qt 控件之间是什么关系,
Qt 根据当前空间计算它们应该在哪里。"
Qt 6 还增加了 QLayout::unsetContentsMargins()(qlayout.cpp 的 326--329 行),它内部调用 setContentsMargins(-1, -1, -1, -1),把布局恢复为由当前 QStyle 提供的默认边缘。只有在运行时确实需要撤销自定义边缘时才使用它;普通页面直接保留风格默认值或一次性设置设计规范即可。
这解释了两个实践结论:布局参数可以在运行时调整;但不应在高频动画或每次鼠标移动中反复修改层层嵌套的布局参数,否则会造成不必要的重新计算。
8.2 Box Layout:stretch 存在每个布局项上
QBoxLayout::setStretch() 的主体很短:
cpp
void QBoxLayout::setStretch(int index, int stretch)
{
if (index >= 0 && index < d->list.size()) {
QBoxLayoutItem *box = d->list.at(index);
if (box->stretch != stretch) {
box->stretch = stretch;
invalidate();
}
}
}
这段实现位于 Qt 6.8.3 的 qboxlayout.cpp 1101--1111 行,说明两件事:stretch 按项目索引 保存,而不是按控件名称保存;值改变后也要重新计算。addStretch(int stretch) 则只是转发给 insertStretch(-1, stretch)(974--977 行),所以它本质上是添加一个可扩张的布局项,并没有任何"右对齐"的特殊魔法。
8.3 Grid Layout:控件先变成一个网格项,再写入坐标范围
单元格版本的 QGridLayout::addWidget() 核心过程如下:
cpp
addChildWidget(widget);
QWidgetItem *item = QLayoutPrivate::createWidgetItem(this, widget);
addItem(item, row, column, 1, 1, alignment);
在 Qt 6.8.3 的 qgridlayout.cpp 1399--1413 行,单元格版本会先检查控件和坐标是否有效,再调用 addChildWidget(),把控件包装成 QWidgetItem 后传给 addItem()。跨行跨列的重载会计算结束行、结束列,把控件包装为 QGridBox 后交给内部网格。setColumnStretch() 则转发给私有网格数据并调用 invalidate()(1556--1561 行)。因此你在 API 中写的 row、column、span、stretch 并不是立即绘制坐标,而是一组参与下一次几何求解的约束。
源码阅读的目标不是复制 Qt 的私有算法,而是确认公开 API 的语义:QLayout 管边缘和失效通知,Box Layout 管一维项目列表,Grid Layout 管二维行列约束。
九、常见问题与排查顺序
9.1 为什么控件没有跟着窗口变大?
依次检查:控件是否真的已加入布局;布局是否已经安装到窗口;控件的 QSizePolicy 是否允许扩张;是否被 setFixedWidth() 或最大尺寸限制;以及你是否传了会阻止填充的 alignment。需要观察建议尺寸时可打印:
cpp
qDebug() << widget->sizeHint()
<< widget->minimumSizeHint()
<< widget->sizePolicy();
9.2 为什么两列没有严格 1:2?
stretch 只分配扣除 margins、spacing、最小尺寸后的额外空间;它不是百分比布局。先检查控件的最小/最大宽度和 QSizePolicy,再判断是否真的需要严格比例。严格视觉比例通常还需要在 resizeEvent() 中计算,或改用更专门的组件;普通表单不要为此牺牲对长文字和 DPI 的适配。
9.3 为什么出现 "already has a layout" 警告?
同一个 QWidget 被安装了第二个顶层布局。保留一个根布局,然后用 addLayout() 嵌套子布局;不要对同一个窗口重复 setLayout()。
9.4 为什么窗口最小尺寸很大、甚至无法缩小?
布局会汇总子项的最小尺寸。排查大型图片、过长的固定文本、setFixedSize()、过高的 minimumHeight,以及是否把多个布局的 margins 叠加了。必要时用 QScrollArea 承载内容,而不是强行压缩无法阅读的控件。
总结
布局管理器让界面描述"关系"而非"坐标"。QHBoxLayout 解决一行内的左右关系,QVBoxLayout 组织上下区块,QGridLayout 保证多行多列对齐;它们可以嵌套,形成可维护的布局树。边缘留白用 contentsMargins,相邻控件距离用 spacing,可变空白用 stretch;stretch 是权重,需要结合 QSizePolicy、建议尺寸和最小/最大尺寸一起理解。
下一章将继续深入 Qt Widgets 的视觉体系重要一环:QtDesigner拖拽出来的UI控件,如何被编译加载成我们看到的实际渲染界面。
下一篇预告:《Qt Designer 与 UI 文件(.ui 原理、uic 编译、动态加载)》