Note
本篇回答三个问题:为什么不要用 setGeometry 摆控件?五种布局器(HBox、VBox、Grid、Form、Stacked)各自怎么用?如何用嵌套布局和 addStretch、QSizePolicy 精确控制伸缩行为?文末附完整可编译示例和一张布局结构示意图。适合已认识基本控件的读者。
为什么不能用 setGeometry 固定坐标
setGeometry(x, y, w, h) 能把控件精确钉在窗口的某个位置。但它有个致命问题:坐标是死的,窗口是活的。用户把窗口一拉大,控件留在原地孤零零一小块;窗口一缩小,控件可能被切掉一半。
更别提高分屏缩放、不同操作系统默认字体高度不同——同一个坐标在别人屏幕上可能直接把按钮挤出可视区域。
布局管理器(QLayout)的思路完全不同:你不告诉控件”在哪、多大”,只告诉它排列关系(谁在谁左边、谁占多少比例),剩下的计算全部交给 Qt。窗口缩放时,布局器实时重算每个控件的位置和大小。
Warning
布局器不是控件,没有 show() 函数,不能独立显示,必须依附于某个 QWidget(或作为子布局挂在其他布局器里)。对控件手动 setGeometry 和 setLayout 同时使用时,setGeometry 会被布局器覆盖。
QHBoxLayout 与 QVBoxLayout:线性排布
水平布局器把子项排成一行,垂直布局器排成一列。它们共享同一个基类 QBoxLayout,API 也一致:
QWidget window;
QHBoxLayout *lay = new QHBoxLayout(&window); // 直接以 window 为父
lay->addWidget(new QPushButton("确定"));
lay->addWidget(new QPushButton("取消"));
lay->addStretch(); // 末尾加弹簧:两个按钮靠左,空隙留给弹簧三个最常用的”填充物”函数:
addSpacing(int):塞一段固定像素的空白;addStretch(int):塞一个自动伸缩的弹簧,窗口变大时空隙全被弹簧吃掉,控件不会被拉变形;addLayout(QLayout*):把另一个布局器作为整体塞进来,这是嵌套布局的入口。
两个全局参数要记牢:
lay->setSpacing(10); // 子项之间的间距(像素)
lay->setContentsMargins(12, 12, 12, 12); // 布局与容器四边的边距QGridLayout:网格排布
控件分布有行有列时,网格布局器一步到位。addWidget 直接指定行列坐标,还能跨行跨列:
QGridLayout *grid = new QGridLayout;
grid->addWidget(new QLabel("姓名:"), 0, 0); // 第 0 行第 0 列
grid->addWidget(nameEdit, 0, 1); // 第 0 行第 1 列
grid->addWidget(descEdit, 1, 0, 2, 3); // 从 (1,0) 起占 2 行 3 列
grid->setColumnStretch(1, 1); // 第 1 列吸收全部水平伸缩对应地,行列也有自己的控制函数:setRowStretch()、setColumnStretch() 按比例分配新增空间,setRowMinimumHeight()、setColumnMinimumWidth() 设定最小尺寸,setHorizontalSpacing() / setVerticalSpacing() 分别控制两种间距。
QFormLayout:表单专用双列
“一行标签、一行输入”的界面(登录框、设置页)用 QFormLayout 最省事:它固定两列,第一列标签、第二列输入控件,还自动建立标签与控件的伙伴关系:
QFormLayout *form = new QFormLayout;
form->addRow(tr("&姓名:"), nameEdit); // &N 表示快捷键 Alt+N
form->addRow(tr("&邮箱:"), emailEdit);
form->addRow(tr("&年龄:"), ageSpinBox);第一列标签不参与拉伸,宽度以最长的标签为准;第二列输入控件在窗口拉宽时自动伸展。同样的界面用 QGridLayout 要逐个 new 标签、setBuddy、addWidget,代码量多一倍。
Success
判断标准很简单:标准”标签-输入”表单用 QFormLayout;行列结构更自由(有跨行合并、多列内容)就用 QGridLayout。表单布局在垂直方向上不拉伸各行,这是它与网格布局的重要差别。
QStackedLayout:层叠切换
堆栈布局器把多个页面叠在一起,同一时刻只显示其中一页,配合侧边导航或按钮实现”换页”效果:
QStackedLayout *stack = new QStackedLayout;
stack->addWidget(page1); // index 0
stack->addWidget(page2); // index 1
stack->setCurrentIndex(0); // 显示第 0 页日常开发更常直接使用内置了它的 QStackedWidget——它既是容器又是布局,用 setCurrentIndex() 或 setCurrentWidget() 切换,典型应用是设置窗口左侧分类列表 + 右侧内容页。
嵌套布局:搭积木式的界面设计
复杂的界面没有一种布局器能一步到位,正确姿势是由小到大组装:先按行排,再把行排进列;或反过来。
// 目标:上面一排按钮,下面一块大编辑区
QVBoxLayout *mainLay = new QVBoxLayout(&window);
QHBoxLayout *btnLay = new QHBoxLayout;
btnLay->addWidget(new QPushButton("后退"));
btnLay->addWidget(new QPushButton("前进"));
btnLay->addStretch(); // 按钮挤在左边
btnLay->addWidget(new QPushButton("打开"));
mainLay->addLayout(btnLay); // 按钮行作为整体塞进主布局
mainLay->addWidget(editor); // 编辑区吃掉剩余全部空间QSizePolicy:控件想怎么长
布局器分配空间时,会询问每个控件的尺寸策略(QSizePolicy)。七个枚举值里最常用的是三个:
| 策略 | 含义 |
|---|---|
| Fixed | 固定为建议尺寸,不伸不缩 |
| Preferred | 能屈能伸,但优先保持建议尺寸(QWidget 默认) |
| Expanding | 主动扩张,窗口多出来的空间优先给它 |
// 让按钮不被网格布局拉宽
button->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);另一个分配空间的利器是伸展因子(stretch factor),出现在 addWidget 的重载里或专门的 set 函数中:
lay->addWidget(leftPane, 1); // 伸展因子 1
lay->addWidget(rightPane, 3); // 伸展因子 3:新空间按 1:3 分Success
分工原则:控件”要不要伸缩”用 QSizePolicy;多个控件之间”伸缩比例”用伸展因子。两者配合,几乎能表达所有常见布局需求。
完整可运行示例
下面用嵌套布局搭一个”联系人表单 + 底部按钮栏”的窗口,直接编译运行后拖动窗口边缘,观察控件如何自适应:
#include <QApplication>
#include <QWidget>
#include <QLabel>
#include <QLineEdit>
#include <QTextEdit>
#include <QPushButton>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QFormLayout>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QWidget window;
window.setWindowTitle("嵌套布局示例");
window.resize(420, 320);
// 输入控件
QLineEdit *nameEdit = new QLineEdit;
QLineEdit *phoneEdit = new QLineEdit;
QTextEdit *descEdit = new QTextEdit;
descEdit->setPlaceholderText("个人描述(会随窗口拉伸)");
// 表单布局:两行标签-输入对
QFormLayout *form = new QFormLayout;
form->addRow("姓名:", nameEdit);
form->addRow("电话:", phoneEdit);
// 底部按钮行:弹簧把按钮赶到右侧
QHBoxLayout *btnLay = new QHBoxLayout;
btnLay->addStretch(); // 左侧弹簧
btnLay->addWidget(new QPushButton("确定"));
btnLay->addWidget(new QPushButton("取消"));
// 主垂直布局:表单在上、描述居中、按钮在下
QVBoxLayout *mainLay = new QVBoxLayout(&window);
mainLay->addLayout(form); // 嵌套:表单布局进主布局
mainLay->addWidget(new QLabel("描述:"));
mainLay->addWidget(descEdit, 1); // 伸展因子 1:吃掉垂直方向新增空间
mainLay->addLayout(btnLay); // 嵌套:按钮行进主布局
mainLay->setSpacing(10);
mainLay->setContentsMargins(12, 12, 12, 12);
window.show();
return app.exec();
}布局结构一图流
上图中,QHBoxLayout 把三个控件排成一行;QVBoxLayout 排成一列;QGridLayout 按 (行, 列) 坐标安放控件,蓝框表示跨行列的合并格子;QFormLayout 固定两列,左列是标签、右列是输入控件。实际开发中,这些”积木”通过 addLayout 互相嵌套,构成整棵布局树。
Warning
忘记给主窗口设置主布局器是新手最常见的问题:控件全挤在窗口左上角一小团。记住:每个顶层窗口必须有且只有一个主布局器——要么在构造布局器时把窗口指针传进去,要么调用
window.setLayout(lay)。
自测:三个问题检验掌握程度
- addStretch() 和 addSpacing(20) 有什么本质区别?各适合什么场合?
- QFormLayout 相比 QGridLayout 做表单有哪些便利?它有什么明显局限?
- 想让窗口拉宽时左栏:右栏按 1:3 分配空间,代码怎么写?控件该用哪种SizePolicy? (答案都在上文)