Qt 布局管理

Qt 主要提供了 QLayout 类及其子类来作为布局管理器。QLayout 类继承于 QObject 类与 QLayoutItem 类。

[TOC]

1 布局管理系统

所有 QWidget 的子类对象都可以使用布局管理器管理位于它们之中的子部件,QWidget::setLayout
可以在一个部件上应用布局管理器。

在一个部件上设置了布局管理器,可以完成如下功能:

1
2
3
4
5
定位子部件
感知窗口默认大小
感知窗口最小大小
窗口大小变化时进行处理
当内容改变时自动更新

1.1 简介

QLayout 是布局管理器的基类,它是一个抽象类,继承于 QObject 类和 Q LayoutItem 类。QLayout 有几个子类:
QBoxLayout (基本布局管理器)
QGridLayout (栅格布局管理器)
QFormLayout (窗体布局管理器)
QStackedLayout (栈布局管理器)
其中QStackedLayout与上一讲的QStackedWidget用法相似,不再讲述。

1.2 基本布局管理器

QBoxLayout可以使子部件在水平方向或者垂直方向排成一列。
它将所有空间分成一行box,然后把每个部件放入一个box中。

  • 它有两个子类:QHBoxLayout(水平布局管理器)QVBoxLayout(垂直布局管理器)
    布局管理器有如下常用属性:
属性 名称
layoutName 布局管理器名称
layoutLeftMargin 布局管理器到左边界距离
。layoutTopMargin 布局管理器到左上边界距离
layoutRightMargin 布局管理器到右边界距离
layoutBottomMargin 布局管理器到下边界距离
layoutSpacing 布局管理器中各个子部件之间的距离
layoutStretch 伸缩因子
layoutSizeConstraint 设置大小的约束条件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 设置布局和父窗口边缘的空白距离:左、上、右、下
void setContentsMargins(int left, int top, int right, int bottom);

// 获取布局边距
void getContentsMargins(int *left, int *top, int *right, int *bottom) const;

// 设置控件之间的间距
void setSpacing(int spacing);

// 获取控件之间的间距
int spacing() const;

// 设置布局的尺寸约束
void setSizeConstraint(QLayout::SizeConstraint constraint);

// 获取布局的尺寸约束
QLayout::SizeConstraint sizeConstraint() const;

// 设置布局所在区域的几何位置
void setGeometry(const QRect &rect);

// 获取布局当前占用的区域
QRect geometry() const;

// 使布局重新计算
void invalidate();

// 强制激活布局
bool activate();

// 添加一个控件
void addWidget(QWidget *widget);

// 添加一个子布局
void addLayout(QLayout *layout);

示例:

1
2
3
4
5
QVBoxLayout *layout = new QVBoxLayout(this);
layout->setContentsMargins(10, 10, 10, 10);
layout->setSpacing(8);
layout->addWidget(new QLabel("用户名", this));
layout->addWidget(new QLineEdit(this));

表示的含义:

1
2
3
整个布局距离窗口边缘 10px
控件之间间隔 8px
控件按垂直方向排列

1.3 栅格布局管理器

QGridLayout将部件在网格中布局,将所有的空间分为行和列,交叉处形成单元格,将部件放入一个特定的单元格中。

核心概念

1
2
3
4
5
6
row / column      控件所在的行和列
spacing 行列之间的间距
stretch 窗口放大时的空间分配比例
minimum size 行或列允许的最小尺寸
rowSpan 跨越的行数
columnSpan 跨越的列数

常用属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// 设置水平间距
void setHorizontalSpacing(int spacing);

// 获取水平间距
int horizontalSpacing() const;

// 设置垂直间距
void setVerticalSpacing(int spacing);

// 获取垂直间距
int verticalSpacing() const;

// 同时设置水平和垂直间距
void setSpacing(int spacing);

// 设置布局边距(左、上、右、下)
void setContentsMargins(int left, int top, int right, int bottom);

// 获取布局边距
void getContentsMargins(int *left, int *top, int *right, int *bottom) const;

// 设置某一行的拉伸因子
void setRowStretch(int row, int stretch);

// 设置某一列的拉伸因子
void setColumnStretch(int column, int stretch);

// 获取某一行的拉伸因子
int rowStretch(int row) const;

// 获取某一列的拉伸因子
int columnStretch(int column) const;

// 设置某一行的最小高度
void setRowMinimumHeight(int row, int minSize);

// 设置某一列的最小宽度
void setColumnMinimumWidth(int column, int minSize);

// 添加控件到指定行列
void addWidget(QWidget *widget, int row, int column);

// 添加控件并跨行跨列
void addWidget(QWidget *widget, int row, int column, int rowSpan, int columnSpan);

// 在指定位置添加子布局
void addLayout(QLayout *layout, int row, int column);

// 在指定位置添加跨行跨列子布局
void addLayout(QLayout *layout, int row, int column, int rowSpan, int columnSpan);

// 获取布局中的行数
int rowCount() const;

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
QWidget *window = new QWidget;

QLabel *userLabel = new QLabel("用户名:", window);
QLineEdit *userEdit = new QLineEdit(window);

QLabel *passwordLabel = new QLabel("密码:", window);
QLineEdit *passwordEdit = new QLineEdit(window);
passwordEdit->setEchoMode(QLineEdit::Password);

QPushButton *loginButton = new QPushButton("登录", window);
QPushButton *cancelButton = new QPushButton("取消", window);

QGridLayout *layout = new QGridLayout(window);

layout->addWidget(userLabel, 0, 0);
layout->addWidget(userEdit, 0, 1);

layout->addWidget(passwordLabel, 1, 0);
layout->addWidget(passwordEdit, 1, 1);

layout->addWidget(loginButton, 2, 0);
layout->addWidget(cancelButton, 2, 1);

window->setLayout(layout);
window->show();

上述代码的流程是:

  1. 创建一个顶级窗口 window
  2. 创建一个布局 layout
  3. 往 layout 里面添加控件
  4. 把 layout 安装到 window 上
  5. window 显示时,layout 负责摆放控件

1.4 窗体布局管理器

eg.

1
2
3
4
5
用户名: [____________]

密码: [____________]

邮箱: [____________]

写法:

1
2
3
4
5
QFormLayout *layout = new QFormLayout;

layout->addRow("用户名:", new QLineEdit);
layout->addRow("密码:", new QLineEdit);
layout->addRow("邮箱:", new QLineEdit);

常用函数有:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 添加一行(文本标签 + 控件)
void addRow(const QString &labelText,
QWidget *field);

// 添加一行(标签控件 + 控件)
void addRow(QWidget *label,
QWidget *field);

// 添加单独一行控件
void addRow(QWidget *widget);

// 添加单独一行布局
void addRow(QLayout *layout);

常用属性和枚举量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// ---------- 1. 标签对齐方式 ----------

// 设置左侧 label 的对齐方式
void setLabelAlignment(Qt::Alignment alignment);

// 常用枚举 / flags
Qt::AlignLeft; // 左对齐
Qt::AlignRight; // 右对齐
Qt::AlignHCenter; // 水平居中
Qt::AlignTop; // 顶部对齐
Qt::AlignVCenter; // 垂直居中
Qt::AlignCenter; // 水平 + 垂直居中


// ---------- 2. 整个表单对齐方式 ----------

// 设置整个 form layout 在可用空间中的对齐方式
void setFormAlignment(Qt::Alignment alignment);

// 常用枚举 / flags
Qt::AlignLeft; // 整体靠左
Qt::AlignRight; // 整体靠右
Qt::AlignHCenter; // 整体水平居中
Qt::AlignTop; // 整体靠上
Qt::AlignBottom; // 整体靠下
Qt::AlignCenter; // 整体居中


// ---------- 3. 行换行策略 ----------

// 设置一行太长时如何换行
void setRowWrapPolicy(QFormLayout::RowWrapPolicy policy);

// 枚举常量
QFormLayout::DontWrapRows; // 不换行,label 和 field 始终在同一行
QFormLayout::WrapLongRows; // 行太长时换行
QFormLayout::WrapAllRows; // 所有行都换行,label 在上,field 在下


// ---------- 4. 输入控件增长策略 ----------

// 设置右侧 field 控件如何占用多余空间
void setFieldGrowthPolicy(QFormLayout::FieldGrowthPolicy policy);

// 枚举常量
QFormLayout::FieldsStayAtSizeHint; // field 保持推荐大小
QFormLayout::ExpandingFieldsGrow; // 可扩展 field 占用多余空间
QFormLayout::AllNonFixedFieldsGrow; // 所有非固定大小 field 都占用多余空间


// ---------- 5. 行间距 / 列间距 ----------

// 设置水平间距,也就是 label 和 field 之间的距离
void setHorizontalSpacing(int spacing);

// 设置垂直间距,也就是行与行之间的距离
void setVerticalSpacing(int spacing);

// 同时设置水平和垂直间距
void setSpacing(int spacing);


// ---------- 6. 外边距 ----------

// 设置布局和外部边界之间的距离
void setContentsMargins(int left, int top, int right, int bottom);

可以发现,Qt中很多时候都是传入某个枚举类型的枚举值。

1.5 综合使用

  • 讲解一个例子。
1
2
3
4
5
6
7
8
9
10
11
┌────────────────────────────┐
│ 标题 │
│ │
│ API Key: [____________] │ ← QFormLayout
│ 模型: [____________] │
│ │
│ [选项A] [选项B] │ ← QGridLayout
│ [选项C] [选项D] │
│ │
│ [取消] [保存] │ ← QHBoxLayout
└────────────────────────────┘

如:

1
2
3
4
5
6
7
8
9
10
11
12
┌────────────────────────────────────────────┐
│ LLM 设置 │
│ │
│ API Key: [__________________________] │
│ 模型: [__________________________] │
│ Base URL: [__________________________] │
│ │
│ [ ] 启用搜索 [ ] 流式输出 │
│ [ ] 保存历史 [ ] 开机启动 │
│ │
│ [取消] [保存] │
└────────────────────────────────────────────┘

实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
QWidget *window = new QWidget;

QVBoxLayout *mainLayout = new QVBoxLayout(window); // 外层:从上到下

QLabel *title = new QLabel("LLM 设置", window);
mainLayout->addWidget(title);

QFormLayout *formLayout = new QFormLayout;
formLayout->addRow("API Key:", new QLineEdit(window));
formLayout->addRow("模型:", new QLineEdit(window));
formLayout->addRow("Base URL:", new QLineEdit(window));
mainLayout->addLayout(formLayout);

QGridLayout *gridLayout = new QGridLayout;
gridLayout->addWidget(new QCheckBox("启用搜索", window), 0, 0);
gridLayout->addWidget(new QCheckBox("流式输出", window), 0, 1);
gridLayout->addWidget(new QCheckBox("保存历史", window), 1, 0);
gridLayout->addWidget(new QCheckBox("开机启动", window), 1, 1);
mainLayout->addLayout(gridLayout);

QHBoxLayout *buttonLayout = new QHBoxLayout;
buttonLayout->addStretch();
buttonLayout->addWidget(new QPushButton("取消", window));
buttonLayout->addWidget(new QPushButton("保存", window));
mainLayout->addLayout(buttonLayout);

window->show();

1.6 部件大小

1.6.1 大小提示和最小大小提示

所有继承于QWidget的类都有这两个属性。

  • sizeHint保存了建议大小,不同的部件默认有不同的sizeHint。通过调用sizeHint()可以获取其值。
  • minimumSizeHint保存了最小建议大小。通过调用minimumSizeHint()可以获取其值。

1.6.2 QSizePolicy 大小策略属性

以下是QSizepolicy的所有枚举值:

常量 含义
QSizePolicy::Fixed 只使用sizeHint()提供是值,不能伸缩
QSizePolicy::Preferred sizeHint()提供的是最佳大小,可以伸缩
QSizePolicy::Expanding sizeHint()提供的是最佳大小,可以伸缩,有多余空间就自动被拉伸
QSizePolicy::Ignored sizeHint()值被忽略,可以伸缩,有多少空间全部拉伸
QSizePolicy::Minimum sizeHint()值为最小值,可以伸缩
QSizePolicy::Maximum sizeHint()值为最大值,可以伸缩
QSizePolicy::MinimumExpanding sizeHint()值为最小值,可以伸缩,有多余空间就自动被拉伸
  • 注:Expanding比Ignored更常用

1.6.3 缩放因子 Stretch Factor

缩放因子不是控件自己的大小,而是不同控件之间的比例分配。

例:

1
2
3
4
5
6
7
QHBoxLayout *layout = new QHBoxLayout(this);

QPushButton *btn1 = new QPushButton("A", this);
QPushButton *btn2 = new QPushButton("B", this);

layout->addWidget(btn1, 1);
layout->addWidget(btn2, 2);

12 就是缩放因子。

btn1 : btn2 = 1 : 2,窗口变宽时,多余空间按 1:2 分配。

如果是

1
2
layout->addWidget(btn1);
layout->addWidget(btn2);

相当于

1
2
layout->addWidget(btn1, 0);
layout->addWidget(btn2, 0);

则根据sizeHint()sizePolicy()来划分。

常用函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 添加控件,并设置拉伸因子
void QBoxLayout::addWidget(QWidget *widget, int stretch = 0, Qt::Alignment alignment = Qt::Alignment());

// 添加子布局,并设置拉伸因子
void QBoxLayout::addLayout(QLayout *layout, int stretch = 0);

// 添加弹性空白
void QBoxLayout::addStretch(int stretch = 0);

// 设置指定位置 item 的拉伸因子
void QBoxLayout::setStretch(int index, int stretch);

// 获取指定位置 item 的拉伸因子
int QBoxLayout::stretch(int index) const;

1.7 可扩展窗口

一个窗口的功能是很多的,其中一部分只在需要的时候显示出来或者消失,这就是可扩展窗口。

eg.

1
2
3
4
5
6
7
展开前:

┌────────────────────────┐
│ 搜索内容: [________] │
│ │
│ [更多 >>] [搜索] │
└────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
展开后:

┌────────────────────────┐
│ 搜索内容: [________] │
│ │
│ 大小写敏感: [ ] │
│ 全词匹配: [ ] │
│ 使用正则: [ ] │
│ │
│ [更少 <<] [搜索] │
└────────────────────────┘

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <QDialog>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QFormLayout>
#include <QLineEdit>
#include <QCheckBox>
#include <QPushButton>
#include <QWidget>

class SearchDialog : public QDialog
{
public:
explicit SearchDialog(QWidget *parent = nullptr) : QDialog(parent)
{
setWindowTitle("搜索");

QVBoxLayout *mainLayout = new QVBoxLayout(this);

QFormLayout *formLayout = new QFormLayout;
QLineEdit *searchEdit = new QLineEdit(this);
formLayout->addRow("搜索内容:", searchEdit);

extensionWidget = new QWidget(this);
QVBoxLayout *extensionLayout = new QVBoxLayout(extensionWidget);
extensionLayout->setContentsMargins(0, 0, 0, 0);

QCheckBox *caseCheck = new QCheckBox("大小写敏感", extensionWidget);
QCheckBox *wordCheck = new QCheckBox("全词匹配", extensionWidget);
QCheckBox *regexCheck = new QCheckBox("使用正则", extensionWidget);

extensionLayout->addWidget(caseCheck);
extensionLayout->addWidget(wordCheck);
extensionLayout->addWidget(regexCheck);

extensionWidget->hide();

QHBoxLayout *buttonLayout = new QHBoxLayout;
QPushButton *moreButton = new QPushButton("更多 >>", this);
QPushButton *searchButton = new QPushButton("搜索", this);

buttonLayout->addStretch();
buttonLayout->addWidget(moreButton);
buttonLayout->addWidget(searchButton);

mainLayout->addLayout(formLayout);
mainLayout->addWidget(extensionWidget);
mainLayout->addLayout(buttonLayout);

connect(moreButton, &QPushButton::clicked, this, [=]() {
bool visible = extensionWidget->isVisible();
extensionWidget->setVisible(!visible);
moreButton->setText(visible ? "更多 >>" : "更少 <<");
adjustSize();
});
}

private:
QWidget *extensionWidget = nullptr;
};

1.8 拆分器 QSplitter

不说概念了,直接上图

1
2
3
4
5
6
7
┌───────────────┬──────────────────────┐
│ 左侧列表 │ 右侧内容区 │
│ │ │
│ │ │
└───────────────┴──────────────────────┘

这里可以拖动

典型用途:

1
2
3
4
文件管理器:左侧目录树 + 右侧文件列表
代码编辑器:左侧项目树 + 右侧编辑区
聊天软件:左侧会话列表 + 右侧聊天内容
设置窗口:左侧分类导航 + 右侧设置页面
  • 事实上,QSplitter类继承于QFrame类即继承于QWidget类。换言之,QSplitter类具有QWidget类的特性,可以像QFrame类一样设置边框。

常用函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 创建水平或垂直拆分器
QSplitter(Qt::Orientation orientation, QWidget *parent = nullptr);

// 添加控件
void addWidget(QWidget *widget);

// 插入控件
void insertWidget(int index, QWidget *widget);

// 设置方向
void setOrientation(Qt::Orientation orientation);

// 获取方向
Qt::Orientation orientation() const;

// 设置各个子控件大小
void setSizes(const QList<int> &list);

// 获取各个子控件大小
QList<int> sizes() const;

// 设置某个子控件是否可以被折叠到 0
void setCollapsible(int index, bool collapse);

// 设置所有子控件是否可以被折叠
void setChildrenCollapsible(bool collapse);

其中,Qt::Orientation有两个枚举值:

1
2
3
4
5
// 水平方向
Qt::Horizontal

// 垂直方向
Qt::Vertical

2 伙伴 buddy

buddy 机制提出于QLabel类。让一个标签 QLabel 和另一个输入控件建立关联,
用户按快捷键时,焦点自动跳到对应控件。

例如:

1
2
用户名(&U): [____________]
密码(&P): [____________]

这里 用户名(&U) 这个 label 的伙伴就是右边的 QLineEdit。

Q: &U是什么?

A:&U 表示快捷键助记符,也叫 mnemonic。用户按:

1
Alt + U

Qt 就会触发这个 label 的快捷键。

实现方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
QWidget *window = new QWidget;

QLabel *userLabel = new QLabel("用户名(&U):", window);
QLineEdit *userEdit = new QLineEdit(window);

QLabel *passwordLabel = new QLabel("密码(&P):", window);
QLineEdit *passwordEdit = new QLineEdit(window);
passwordEdit->setEchoMode(QLineEdit::Password);

userLabel->setBuddy(userEdit);
passwordLabel->setBuddy(passwordEdit);

QGridLayout *layout = new QGridLayout(window);
layout->addWidget(userLabel, 0, 0);
layout->addWidget(userEdit, 0, 1);
layout->addWidget(passwordLabel, 1, 0);
layout->addWidget(passwordEdit, 1, 1);

window->show();

3 设计Tab键顺序

核心函数:

1
static void QWidget::setTabOrder(QWidget *first, QWidget *second);

意思是:按 Tab 时,从 first 跳到 second

例如登录窗口:

1
2
3
4
5
6
7
8
QLineEdit *userEdit = new QLineEdit(this);
QLineEdit *passwordEdit = new QLineEdit(this);
QPushButton *loginButton = new QPushButton("登录", this);
QPushButton *cancelButton = new QPushButton("取消", this);

QWidget::setTabOrder(userEdit, passwordEdit);
QWidget::setTabOrder(passwordEdit, loginButton);
QWidget::setTabOrder(loginButton, cancelButton);

这样按 Tab 的顺序就是:

1
userEdit → passwordEdit → loginButton → cancelButton

设置链式顺序时要按顺序写:setTabOrder(a, b); setTabOrder(b, c); setTabOrder(c, d);,不要乱序。