1. 五种布局到底在解决什么问题
刚接触 Qt 那会儿,我总觉得布局这东西有点多余。明明可以用setGeometry或者move把每个控件的位置和大小写死,为什么还要费劲去学什么水平布局、网格布局?直到我接手了一个需要适配多种分辨率屏幕的项目,才发现硬编码坐标的界面在 1280×720 上看着还行,换到 1920×1080 就满屏空白,缩到 1024×768 又挤成一团。那一刻我才真正理解,布局管理器不是可选项,而是 Qt 界面开发的必修课。
Qt 的布局系统本质上是一套自动排版引擎。你把控件丢进布局里,布局负责计算每个控件应该占多大、放在哪里。窗口大小变了,布局重新算一遍,控件跟着调整。这套机制把开发者从繁琐的坐标计算中解放出来,也让界面具备了响应式能力。Qt 提供了五种常用的布局类:QHBoxLayout(水平布局)、QVBoxLayout(垂直布局)、QGridLayout(网格布局)、QFormLayout(表单布局)和QStackedLayout(堆叠布局)。这五种布局各有各的适用场景,搭配使用几乎能覆盖日常开发中 90% 以上的界面需求。
这篇文章面向的是已经了解 Qt 基础控件用法、但对布局系统还不够熟悉的开发者。我会从每种布局的核心机制讲起,拆解它们的适用场景和常见坑点,然后给出可直接复现的代码示例。最后还会分享一些我在实际项目中踩过的坑和总结出来的经验技巧。读完你应该能做到:拿到一个界面原型,脑子里能快速判断该用哪种布局组合来实现。
2. 五种布局的核心机制与适用场景拆解
2.1 QHBoxLayout 与 QVBoxLayout:线性排列的基本功
QHBoxLayout和QVBoxLayout都继承自QBoxLayout,区别只在于排列方向。水平布局从左到右依次排列控件,垂直布局从上到下依次排列。它们是最基础、使用频率最高的两种布局。
这两个布局的核心机制可以用一句话概括:按顺序排列,按策略分配空间。每个被添加到布局中的控件都有一个尺寸策略(QSizePolicy),布局根据这些策略来决定如何分配可用空间。比如一个按钮的默认策略是Fixed,它只占自己需要的大小;而一个文本框的策略可能是Expanding,它会尽可能占满剩余空间。
我刚开始用的时候犯过一个典型错误:把所有控件的尺寸策略都设成默认值,然后纳闷为什么窗口拉大之后控件不跟着变大。后来才明白,默认策略下很多控件是Preferred,它们会保持自己的理想尺寸,不会主动去抢占额外空间。如果你希望某个控件在窗口变大时跟着拉伸,需要显式设置它的尺寸策略。
// 水平布局示例:一个标签加一个输入框加一个按钮 QWidget *window = new QWidget; QHBoxLayout *hLayout = new QHBoxLayout(window); QLabel *label = new QLabel("用户名:"); QLineEdit *input = new QLineEdit; QPushButton *btn = new QPushButton("搜索"); // 让输入框占据剩余空间 input->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); hLayout->addWidget(label); hLayout->addWidget(input); hLayout->addWidget(btn);这段代码里,input的尺寸策略被设为水平方向Expanding,所以当窗口变宽时,标签和按钮保持原大小,输入框会吸收所有多出来的宽度。这是搜索栏类界面的标准做法。
垂直布局的用法完全对称,只是方向变成从上到下。实际项目中,垂直布局经常用来组织一组相关的控件,比如一个设置面板里的多个选项行。
注意:
QBoxLayout还提供了addStretch()方法,可以在控件之间插入一个可伸缩的空白。这个技巧在需要把某些控件推到一边时特别有用,比如让按钮靠右对齐。
2.2 QGridLayout:二维排布的万能选手
网格布局把界面划分成行和列的二维网格,每个控件占据一个或多个单元格。它的灵活性远超线性布局,适合实现计算器键盘、仪表盘、参数配置面板这类需要精确对齐的界面。
QGridLayout的核心方法签名是addWidget(widget, row, column, rowSpan, columnSpan)。前两个参数指定控件左上角所在的行和列,后两个参数指定控件跨越多少行和多少列。行号和列号都从 0 开始计数。
网格布局有一个容易被忽视的特性:行列的尺寸由内容决定,但可以通过setColumnStretch和setRowStretch来调整比例。默认情况下,网格布局会尽量让每列宽度适应其中最宽的控件,但如果你希望某列占据更多空间,就需要设置拉伸因子。
// 网格布局示例:一个简单的登录表单 QWidget *window = new QWidget; QGridLayout *grid = new QGridLayout(window); grid->addWidget(new QLabel("用户名:"), 0, 0); grid->addWidget(new QLineEdit, 0, 1); grid->addWidget(new QLabel("密码:"), 1, 0); grid->addWidget(new QLineEdit, 1, 1); grid->addWidget(new QPushButton("登录"), 2, 0, 1, 2); // 跨两列 // 让第二列占据更多空间 grid->setColumnStretch(1, 1);这里按钮跨越了两列,所以它会在水平方向上填满整个表单宽度。setColumnStretch(1, 1)让第二列(输入框所在列)获得所有额外的水平空间,第一列(标签列)保持最小宽度。
网格布局最常见的坑是行列号写错导致控件重叠。因为addWidget不会报错,两个控件放在同一个单元格时,后添加的会覆盖先添加的。调试时如果发现某个控件不见了,先检查一下是不是行列号冲突了。
2.3 QFormLayout:专为表单而生的布局
表单布局是网格布局的一个特化版本,专门用于实现“标签-输入框”成对出现的界面。它自动处理标签对齐、输入框拉伸、跨平台样式差异等问题,比手动用网格布局实现要省事得多。
QFormLayout的典型用法是addRow(label, field),一行代码就能添加一对标签和输入控件。它还支持addRow(QString, QWidget)的重载版本,直接传入标签文本,布局会自动创建一个QLabel。
// 表单布局示例:用户信息录入 QWidget *window = new QWidget; QFormLayout *form = new QFormLayout(window); form->addRow("姓名:", new QLineEdit); form->addRow("邮箱:", new QLineEdit); form->addRow("年龄:", new QSpinBox); form->addRow("备注:", new QTextEdit); // 设置标签对齐方式 form->setLabelAlignment(Qt::AlignRight); // 设置字段增长策略 form->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow);表单布局有一个很实用的策略设置:setFieldGrowthPolicy。默认情况下,输入框不会主动拉伸,表单右侧会留出空白。设为ExpandingFieldsGrow后,带有Expanding尺寸策略的输入框会占满剩余宽度,界面看起来更饱满。
setRowWrapPolicy控制标签和输入框的换行行为。在窄屏幕上,可以设为WrapLongRows,让长标签自动换到输入框上方,避免界面被撑得过宽。这个特性在需要适配移动端或小尺寸屏幕时特别有用。
2.4 QStackedLayout:多页面切换的隐形容器
堆叠布局比较特殊,它同时管理多个控件(通常是一组页面容器),但同一时间只显示其中一个。你可以把它想象成一摞卡片,每次只把最上面那张翻给用户看。
QStackedLayout的核心方法是setCurrentIndex(int)和setCurrentWidget(QWidget*),用来切换当前显示的页面。它本身不提供切换按钮,通常需要配合QComboBox、QListWidget或自定义按钮来驱动页面切换。
// 堆叠布局示例:简单的设置向导 QWidget *window = new QWidget; QVBoxLayout *mainLayout = new QVBoxLayout(window); QStackedLayout *stack = new QStackedLayout; QWidget *page1 = new QWidget; // 第一页内容 QWidget *page2 = new QWidget; // 第二页内容 QWidget *page3 = new QWidget; // 第三页内容 stack->addWidget(page1); stack->addWidget(page2); stack->addWidget(page3); QPushButton *nextBtn = new QPushButton("下一步"); QObject::connect(nextBtn, &QPushButton::clicked, [stack]() { int next = (stack->currentIndex() + 1) % stack->count(); stack->setCurrentIndex(next); }); mainLayout->addLayout(stack); mainLayout->addWidget(nextBtn);堆叠布局的一个常见误区是把它当成QTabWidget的替代品。QTabWidget自带标签栏,适合用户主动切换的场景;而QStackedLayout更适合由程序逻辑控制的流程式界面,比如安装向导、多步骤表单。选择哪个取决于交互模式,而不是功能强弱。
2.5 布局的嵌套组合:1+1>2 的关键
单独使用一种布局能解决的问题有限,真正强大的地方在于布局可以嵌套。一个垂直布局里可以放多个水平布局,一个网格布局的单元格里可以放另一个网格布局。通过嵌套,你能搭建出任意复杂度的界面结构。
嵌套布局的核心原则是:先分块,再细化。拿到一个界面原型,先把它拆成几个大的区域(比如顶部工具栏、中间内容区、底部状态栏),用外层布局把这些区域框起来。然后针对每个区域,再选择合适的布局来排列内部控件。
// 嵌套布局示例:一个典型的编辑器界面 QWidget *window = new QWidget; QVBoxLayout *mainLayout = new QVBoxLayout(window); // 顶部工具栏:水平布局 QHBoxLayout *toolbar = new QHBoxLayout; toolbar->addWidget(new QPushButton("新建")); toolbar->addWidget(new QPushButton("打开")); toolbar->addWidget(new QPushButton("保存")); toolbar->addStretch(); // 把后面的控件推到右边 toolbar->addWidget(new QPushButton("设置")); // 中间内容区:水平布局,左边列表右边编辑区 QHBoxLayout *contentLayout = new QHBoxLayout; QListWidget *list = new QListWidget; QTextEdit *editor = new QTextEdit; contentLayout->addWidget(list, 1); // 列表占 1 份宽度 contentLayout->addWidget(editor, 3); // 编辑区占 3 份宽度 // 底部状态栏:水平布局 QHBoxLayout *statusBar = new QHBoxLayout; statusBar->addWidget(new QLabel("就绪")); // 组装 mainLayout->addLayout(toolbar); mainLayout->addLayout(contentLayout, 1); // 内容区占据所有剩余垂直空间 mainLayout->addLayout(statusBar);这段代码展示了嵌套布局的典型写法。contentLayout中的addWidget第二个参数是拉伸因子,列表和编辑区按 1:3 的比例分配水平空间。mainLayout中contentLayout的拉伸因子设为 1,意味着它会吸收窗口垂直方向上的所有额外空间,而工具栏和状态栏保持固定高度。
提示:嵌套层级不宜过深。一般来说,三层以内的嵌套足够应对绝大多数界面。如果发现需要四层甚至五层嵌套,可能是界面结构设计得不够合理,考虑用自定义控件来封装重复的布局模式。
3. 从零搭建一个完整界面的实操过程
3.1 需求拆解与布局选型
假设我们要做一个简单的图片查看器界面,需求是这样的:顶部有一排工具按钮(打开、上一张、下一张、缩放),中间左侧是缩略图列表,右侧是大图预览区,底部显示当前图片的文件名和尺寸信息。
拿到这个需求,我的拆解思路是这样的:整体是一个垂直结构,从上到下依次是工具栏、内容区、状态栏。工具栏内部是水平排列的按钮,用QHBoxLayout。内容区左右分栏,用QHBoxLayout配合拉伸因子控制比例。状态栏也是水平排列,用QHBoxLayout。内容区左侧的缩略图列表用QListWidget就够了,右侧预览区用QLabel配合setScaledContents来显示图片。
这个界面没有用到网格布局和表单布局,因为它的结构本质上是线性的。布局选型的关键是先判断结构是线性还是二维。线性结构优先用QHBoxLayout或QVBoxLayout,二维对齐需求才考虑QGridLayout,表单类界面直接用QFormLayout,多页面切换用QStackedLayout。
3.2 代码实现与关键参数说明
#include <QApplication> #include <QWidget> #include <QVBoxLayout> #include <QHBoxLayout> #include <QPushButton> #include <QListWidget> #include <QLabel> #include <QFileDialog> #include <QPixmap> class ImageViewer : public QWidget { public: ImageViewer(QWidget *parent = nullptr) : QWidget(parent) { // 外层垂直布局 QVBoxLayout *mainLayout = new QVBoxLayout(this); // 顶部工具栏 QHBoxLayout *toolbar = new QHBoxLayout; QPushButton *openBtn = new QPushButton("打开"); QPushButton *prevBtn = new QPushButton("上一张"); QPushButton *nextBtn = new QPushButton("下一张"); QPushButton *zoomBtn = new QPushButton("缩放"); toolbar->addWidget(openBtn); toolbar->addWidget(prevBtn); toolbar->addWidget(nextBtn); toolbar->addWidget(zoomBtn); toolbar->addStretch(); // 按钮靠左,右侧留空 // 中间内容区 QHBoxLayout *contentLayout = new QHBoxLayout; QListWidget *thumbList = new QListWidget; QLabel *preview = new QLabel("预览区"); preview->setAlignment(Qt::AlignCenter); preview->setMinimumSize(400, 300); preview->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); contentLayout->addWidget(thumbList, 1); contentLayout->addWidget(preview, 3); // 底部状态栏 QHBoxLayout *statusBar = new QHBoxLayout; QLabel *statusLabel = new QLabel("就绪"); statusBar->addWidget(statusLabel); statusBar->addStretch(); // 组装到主布局 mainLayout->addLayout(toolbar); mainLayout->addLayout(contentLayout, 1); // 内容区占据剩余空间 mainLayout->addLayout(statusBar); // 连接信号槽 connect(openBtn, &QPushButton::clicked, [this, preview, statusLabel]() { QString file = QFileDialog::getOpenFileName(this, "选择图片"); if (!file.isEmpty()) { QPixmap pix(file); preview->setPixmap(pix.scaled(preview->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); statusLabel->setText(file); } }); } }; int main(int argc, char *argv[]) { QApplication app(argc, argv); ImageViewer viewer; viewer.resize(900, 600); viewer.show(); return app.exec(); }这段代码有几个关键点值得展开说。preview的尺寸策略设为Expanding,这样它会尽可能占据内容区的空间。contentLayout中缩略图列表和预览区的拉伸因子是 1:3,意味着水平方向上预览区占 75% 的宽度。mainLayout中contentLayout的拉伸因子是 1,工具栏和状态栏没有设置拉伸因子(默认为 0),所以垂直方向上的额外空间全部给内容区。
图片缩放那里用了Qt::KeepAspectRatio和Qt::SmoothTransformation,前者保证图片不变形,后者保证缩放质量。这两个参数在实际项目中几乎是必选项,默认的快速变换在缩小图片时会有明显的锯齿。
3.3 布局效果的验证与微调
代码写完之后,运行起来拖动窗口边缘,观察各个区域的变化。工具栏和状态栏的高度应该保持不变,内容区的高度随窗口变化。水平方向拖动时,缩略图列表和预览区的宽度按 1:3 的比例变化。
如果发现某个区域没有按预期伸缩,检查三个地方:该区域的拉伸因子是否设置正确、内部控件的尺寸策略是否允许伸缩、是否有setFixedSize或setMinimumSize限制了范围。我遇到过好几次“布局不生效”的情况,最后发现是某个控件设了固定尺寸,把整个布局卡死了。
微调阶段可以用setSpacing和setContentsMargins来控制控件之间的间距和布局四周的留白。默认间距通常是 6 像素,边距是 9 像素。对于紧凑型界面,我会把间距调到 4,边距调到 6;对于需要呼吸感的界面,间距调到 10 以上。这些数值没有绝对标准,根据视觉感受调整就行。
4. 布局开发中的常见问题与排查技巧
4.1 控件不显示或显示不全
这是新手最常遇到的问题。控件明明添加到了布局里,运行起来却看不到,或者只显示了一部分。原因通常有以下几种:
第一种是控件被其他控件覆盖。在网格布局中,两个控件放在同一个单元格,后添加的会盖住先添加的。排查方法是检查addWidget的行列号是否有重复。在堆叠布局中,只有当前索引对应的控件可见,其他控件默认隐藏,这是正常行为。
第二种是控件尺寸被压缩到 0。当布局空间不足时,Preferred策略的控件会被压缩到最小尺寸。如果最小尺寸也是 0,控件就完全看不见了。解决办法是给控件设置setMinimumSize,或者调整拉伸因子让空间分配更合理。
第三种是父控件没有设置布局。一个QWidget如果没有设置布局,它的子控件不会自动排列。你需要用setLayout或者把布局的父对象设为该控件。用new QVBoxLayout(this)这种写法时,布局会自动成为this的布局,不需要额外调用setLayout。
4.2 窗口缩放时布局错乱
窗口拉大拉小时界面变得乱七八糟,通常是因为拉伸因子和尺寸策略配合不当。我整理了一个排查表,遇到类似问题时可以逐项检查:
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 控件不随窗口拉伸 | 尺寸策略为 Fixed 或 Preferred | 改为 Expanding |
| 控件拉伸过度 | 多个控件都设了 Expanding | 只保留需要拉伸的控件为 Expanding |
| 比例不对 | 拉伸因子设置不合理 | 调整 addWidget 的 stretch 参数 |
| 出现滚动条 | 最小尺寸之和超过窗口 | 减小最小尺寸或使用 QScrollArea |
| 控件重叠 | 网格布局行列号冲突 | 检查 addWidget 的行列参数 |
拉伸因子的分配逻辑是:所有拉伸因子之和为分母,每个控件的因子为分子,按比例分配额外空间。比如三个控件的拉伸因子分别是 1、2、3,它们分到的额外空间比例就是 1/6、2/6、3/6。拉伸因子为 0 的控件不参与额外空间分配,只保持自己的理想尺寸。
4.3 嵌套布局的层级管理
嵌套布局用多了容易乱,尤其是当界面结构复杂时,代码里一堆addLayout调用,过两天自己都看不懂了。我的经验是给每个布局起个有意义的名字,并且在添加子布局时写注释说明这个区域是什么。
// 好的做法:布局变量名体现用途 QVBoxLayout *mainLayout = new QVBoxLayout(this); QHBoxLayout *topToolbarLayout = new QHBoxLayout; QHBoxLayout *contentAreaLayout = new QHBoxLayout; QHBoxLayout *bottomStatusLayout = new QHBoxLayout; // 添加时写清楚 mainLayout->addLayout(topToolbarLayout); // 顶部工具栏 mainLayout->addLayout(contentAreaLayout, 1); // 中间内容区,占据剩余空间 mainLayout->addLayout(bottomStatusLayout); // 底部状态栏另一个技巧是把复杂的子布局封装成自定义控件。比如上面图片查看器的内容区,如果后续要在多个地方复用,可以把它抽成一个ImageContentWidget类,内部管理自己的布局,外部只需要把它当成一个普通控件添加到布局里。这样既降低了主界面的布局复杂度,也提高了代码的可维护性。
注意:自定义控件内部设置布局后,外部不要再对它调用
setLayout,否则会报“already has a layout”的警告。一个控件只能有一个顶层布局。
4.4 布局与样式表的配合问题
Qt 样式表(QSS)和布局系统有时候会打架。比如你给一个控件设了margin或padding,布局计算尺寸时会把它们算进去,导致实际显示效果和预期有偏差。我遇到过给按钮设了padding: 10px之后,按钮在布局里显得特别大,把其他控件挤没了。
解决办法是尽量用布局的setSpacing和setContentsMargins来控制间距,而不是用样式表的 margin 和 padding。布局提供的间距控制更符合布局系统的计算逻辑,不会产生意外的尺寸偏差。如果确实需要用样式表来控制外观,建议在样式表里只设置颜色、边框、圆角这些不影响尺寸的属性。
还有一个坑是样式表的继承。给父控件设置的字体、颜色等属性会传递给子控件,但布局本身不是控件,不会继承样式。如果你给一个QWidget设了背景色,它的子控件默认是透明的,会透出父控件的背景。这个特性有时候能做出不错的效果,但如果不了解的话,会困惑为什么子控件“没有背景”。
5. 布局进阶技巧与性能优化建议
5.1 动态增删控件的正确姿势
实际项目中经常需要动态添加或删除控件,比如根据用户操作往列表里插入一行。动态操作布局时,删除控件不能只调用delete,还要从布局中移除。否则布局里会留下一个悬空指针,后续计算尺寸时可能崩溃。
正确的删除流程是这样的:
// 从布局中移除并删除一个控件 void removeWidgetFromLayout(QLayout *layout, QWidget *widget) { layout->removeWidget(widget); widget->deleteLater(); // 用 deleteLater 而不是 delete }用deleteLater而不是delete的原因是,如果删除操作是在控件的信号槽里触发的,直接delete可能导致程序在信号发射过程中访问已释放的内存。deleteLater会把删除操作推迟到事件循环的下一个周期,避开这个风险。
动态添加控件时,如果新控件需要出现在特定位置,用insertWidget而不是addWidget。insertWidget的第一个参数是插入位置的索引,0 表示插到最前面。插入后布局会自动重新计算,不需要手动调用update或adjustSize。
5.2 布局性能的优化要点
布局计算本身的开销很小,但在包含大量控件的界面中,频繁的布局重算可能成为性能瓶颈。我做过一个测试,在一个垂直布局里放 500 个按钮,每次窗口缩放都会触发所有按钮的重新计算,肉眼能感觉到卡顿。
优化的思路是减少布局重算的频率和范围。具体做法有几种:一是用setUpdatesEnabled(false)在批量操作前暂停界面更新,操作完成后再恢复;二是把大量控件放在QScrollArea里,滚动区域外的控件不参与布局计算;三是用QListView或QTableView代替手动添加大量控件,这些视图类内部有虚拟化机制,只渲染可见区域。
还有一个容易被忽视的点是避免在布局中嵌套过深。每多一层嵌套,布局计算就多一轮递归。三层嵌套和五层嵌套在控件数量少时差别不大,但控件数量上去之后,差距会明显放大。能用一层布局解决的问题,不要用两层。
5.3 跨平台布局的适配经验
Qt 的布局系统在不同平台上的表现基本一致,但有一些细节差异需要注意。比如 macOS 上按钮的默认高度比 Windows 上大,如果你在代码里写死了按钮高度,在 macOS 上可能会显得很挤。解决办法是尽量用布局的自动尺寸计算,避免硬编码尺寸。
字体差异也会影响布局。不同平台的默认字体不同,同一个标签在不同系统上占的宽度可能差 10% 到 20%。如果你的界面里有并排的标签和输入框,在 Windows 上对齐得好好的,到 macOS 上可能就错位了。用QFormLayout可以自动处理这个问题,它会根据标签的实际宽度来对齐输入框。
还有一个跨平台差异是高 DPI 屏幕的缩放。Qt 5.6 之后默认启用了高 DPI 缩放,布局会自动按缩放比例调整。但如果你在代码里用了像素值来设置间距或尺寸,在高 DPI 屏幕上可能会显得偏小。建议用QApplication::setAttribute(Qt::AA_EnableHighDpiScaling)开启自动缩放,然后用逻辑像素而不是物理像素来思考尺寸。
5.4 布局调试的实用工具
Qt Creator 自带了一个布局调试的利器:Qt Designer 的预览功能。你可以在 Designer 里拖拽控件、设置布局,然后按 Ctrl+R 预览效果,拖动窗口边缘观察布局变化。这比每次改代码再编译运行要快得多。
对于代码写的布局,可以用QWidget::dumpObjectTree()在控制台打印控件树,查看布局的层级结构。这个方法会输出每个控件的类名、对象名和父子关系,排查嵌套布局问题时很有用。
还有一个技巧是临时给控件设置不同颜色的背景,用样式表就能做到:
// 调试用:给不同区域的控件设置半透明背景色 widget1->setStyleSheet("background-color: rgba(255, 0, 0, 50);"); widget2->setStyleSheet("background-color: rgba(0, 255, 0, 50);"); widget3->setStyleSheet("background-color: rgba(0, 0, 255, 50);");这样运行起来就能直观地看到每个控件实际占据的区域,布局有没有按预期分配空间一目了然。调试完成后把样式表去掉就行。
6. 我踩过的坑和总结的经验
布局这块我踩过的坑不算少,挑几个印象深刻的说说。有一次做一个设置对话框,用了QFormLayout,标签和输入框死活对不齐,折腾了半天才发现是标签文本里混了全角空格和半角空格,导致宽度计算不一致。从那以后我养成了一个习惯:界面上的文本统一用半角字符,需要对齐的地方用布局的setLabelAlignment来控制,而不是靠空格凑。
还有一次做自适应界面,窗口拉大之后某个面板变得特别宽,里面的按钮却还是原来的大小,看起来很不协调。排查后发现是按钮的尺寸策略默认是Fixed,不会随容器拉伸。改成Expanding之后按钮会跟着变宽,但单个按钮变得太宽也不好看。最后的方案是给按钮外面套一个水平布局,按钮之间加addStretch,让按钮保持合理宽度,多余空间由空白吸收。
关于布局的嵌套,我的经验是能用一层布局解决的,绝不用两层。嵌套层级越少,代码越清晰,性能也越好。如果发现需要多层嵌套才能实现某个效果,先想想是不是可以用QGridLayout或者QFormLayout来简化。这两种布局的表达能力比线性布局强很多,很多时候一层网格布局就能替代两三层嵌套的线性布局。
最后说一个关于拉伸因子的心得。拉伸因子不是越大越好,也不是所有控件都需要设置。我的习惯是:只给需要吸收额外空间的控件设置拉伸因子,其他控件保持默认的 0。比如一个典型的“列表+详情”界面,列表和详情的拉伸因子设为 1:2 或 1:3,工具栏和状态栏不设拉伸因子。这样窗口缩放时,只有列表和详情区域会变化,其他区域保持稳定。这个原则帮我避免了很多布局错乱的问题。