布局管理
Qt 布局系统提供了一种简单而强大的方法,可自动在控件内部排列子控件,以确保它们充分利用可用空间。
简介
Qt 包含一组布局管理类,用于描述小部件在应用程序用户界面中的布局方式。当可用空间发生变化时,这些布局会自动调整小部件的位置和大小,从而确保它们始终保持一致的排列方式,并保证整个用户界面的可用性。
所有QWidget 的子类均可使用布局来管理其子控件。QWidget::setLayout()函数用于将布局应用于控件。当以这种方式为控件设置布局时,布局将负责执行以下任务:
- 子控件的定位
- 为窗口设定合理的默认尺寸
- 为窗口设置合理的最小尺寸
- 调整大小的处理
- 内容变化时的自动更新:
- 子控件的字体大小、文本或其他内容
- 隐藏或显示子控件
- 移除子控件
Qt 的布局类
Qt 的布局类专为手动编写的 C++ 代码设计,为简便起见,允许以像素为单位指定尺寸,因此易于理解和使用。 使用Qt Widgets Designer 创建的表单所生成的代码也使用了这些布局类。在尝试设计表单时,使用Qt Widgets Designer 非常有用,因为它避免了用户界面开发中通常涉及的编译、链接和运行循环。
将子控件水平或垂直对齐 | |
用于组织按钮小部件组的容器 | |
管理输入控件及其关联标签的布局 | |
表示 QGraphicsAnchorLayout 中两个项目之间的锚点 | |
可在图形视图中将小部件锚定在一起的布局 | |
将控件以网格形式布局 | |
带有标题的组框框架 | |
将控件水平对齐 | |
几何管理器的基类 | |
由 QLayout 操作的抽象项 | |
描述水平和垂直调整大小策略的布局属性 | |
布局中的空白区域 | |
一次仅显示一个小部件的小部件堆栈 | |
小部件堆栈,其中每次仅显示一个小部件 | |
将小部件垂直排列 | |
代表小部件的布局项 |
水平、垂直、网格和表单布局
要为小部件设计良好的布局,最简单的方法是使用内置的布局管理器:QHBoxLayout 、QVBoxLayout 、QGridLayout 和QFormLayout 。这些类继承自QLayout ,而该类又继承自QObject (而非QWidget )。它们负责管理一组小部件的几何布局。若要创建更复杂的布局,可以将布局管理器相互嵌套。
- QHBoxLayout 将小部件按水平行排列,从左到右(对于从右到左书写的语言,则从右到左)。

- QVBoxLayout 将小部件以从上到下的顺序排列在垂直列中。

- QGridLayout 将小部件以二维网格形式排列。小部件可以占据多个单元格。

- QFormLayout 将控件以双栏的“描述性标签-字段”样式进行布局。

在代码中布局控件
以下代码创建了一个QHBoxLayout ,用于管理五个QPushButtons 的几何布局,如上图第一张截图所示:
QWidget *window = new QWidget;
QPushButton *button1 = new QPushButton("One");
QPushButton *button2 = new QPushButton("Two");
QPushButton *button3 = new QPushButton("Three");
QPushButton *button4 = new QPushButton("Four");
QPushButton *button5 = new QPushButton("Five");
QHBoxLayout *layout = new QHBoxLayout(window);
layout->addWidget(button1);
layout->addWidget(button2);
layout->addWidget(button3);
layout->addWidget(button4);
layout->addWidget(button5);
window->show();QVBoxLayout 的代码完全相同,只有创建布局的那一行有所不同。QGridLayout 的代码则略有不同,因为我们需要指定子控件的行和列位置:
QWidget *window = new QWidget;
QPushButton *button1 = new QPushButton("One");
QPushButton *button2 = new QPushButton("Two");
QPushButton *button3 = new QPushButton("Three");
QPushButton *button4 = new QPushButton("Four");
QPushButton *button5 = new QPushButton("Five");
QGridLayout *layout = new QGridLayout(window);
layout->addWidget(button1, 0, 0);
layout->addWidget(button2, 0, 1);
layout->addWidget(button3, 1, 0, 1, 2);
layout->addWidget(button4, 2, 0);
layout->addWidget(button5, 2, 1);
window->show();第三个QPushButton 占用了 2 列。这是通过将 2 作为QGridLayout::addWidget() 的第五个参数来实现的。
QFormLayout 将在一行中添加两个控件,通常是一个QLabel 和一个QLineEdit 来创建表单。在同一行中添加QLabel 和QLineEdit 会将QLineEdit 设为QLabel 的伙伴。以下代码将使用QFormLayout 在一行中放置三个QPushButtons 和一个相应的QLineEdit 。
QWidget *window = new QWidget;
QPushButton *button1 = new QPushButton("One");
QLineEdit *lineEdit1 = new QLineEdit();
QPushButton *button2 = new QPushButton("Two");
QLineEdit *lineEdit2 = new QLineEdit();
QPushButton *button3 = new QPushButton("Three");
QLineEdit *lineEdit3 = new QLineEdit();
QFormLayout *layout = new QFormLayout(window);
layout->addRow(button1, lineEdit1);
layout->addRow(button2, lineEdit2);
layout->addRow(button3, lineEdit3);
window->show();布局使用技巧
使用布局时,在创建子控件时无需传入父控件。布局会自动重新设定控件的父子关系(使用QWidget::setParent()),使其成为安装了该布局的控件的子控件。
注意: 布局中的控件 是该布局所安装的控件的子控件,而不是布局本身的子控件。控件只能将其他控件作为父控件,而不能将布局作为父控件。
您可以通过在布局上调用addLayout() 来嵌套布局;此时,内部布局将成为其被插入的布局的子控件。
将小部件添加到布局中
当您向布局添加控件时,布局过程如下:
- 所有小部件最初都会根据其QWidget::sizePolicy()和QWidget::sizeHint()函数的设置,被分配相应的空间。
- 如果任何小部件设置了拉伸因子且其值大于零,则会根据其拉伸因子按比例分配空间(详见下文)。
- 如果任何小部件的拉伸因子设置为零,则只有在没有其他小部件需要该空间时,它们才会获得更多空间。在这些情况下,系统会优先将空间分配给采用Expanding 大小策略的小部件。
- 任何被分配的空间小于其最小尺寸(或未指定最小尺寸时的最小尺寸提示)的小部件,将被分配其所需的最小尺寸。(小部件不必具有最小尺寸或最小尺寸提示,在这种情况下,拉伸因子是决定性因素。)
- 任何被分配的空间超过其最大大小的控件,将被分配其所需的最大大小空间。(控件不必具有最大大小,在这种情况下,拉伸因子将成为其决定因素。)
拉伸因子
小部件通常在创建时未设置任何拉伸因子。当它们在布局中进行排版时,会根据其QWidget::sizePolicy() 返回值或最小尺寸提示(以较大者为准)分配相应的空间份额。拉伸因子用于调整各小部件之间空间分配的比例关系。
如果我们有三个控件使用QHBoxLayout 进行布局,且未设置拉伸因子,则会得到如下布局:

如果对每个小部件应用拉伸因子,它们将按比例进行布局(但绝不会小于其最小尺寸提示),例如:

布局中的自定义控件
在创建自己的控件类时,还应指定其布局属性。如果该控件使用 Qt 的某种布局,则此问题已得到解决。如果该控件没有子控件,或者使用手动布局,则可以通过以下任一或全部机制来更改该控件的行为:
- 重写 `QWidget::sizeHint()` 方法,以返回小部件的首选尺寸。
- 重写 `QWidget::minimumSizeHint()` 方法,以返回控件可有的最小尺寸。
- 调用 `QWidget::setSizePolicy()` 方法来指定小部件的空间需求。
每当尺寸提示、最小尺寸提示或尺寸策略发生变化时,请调用 `QWidget::updateGeometry()`。这将触发布局重新计算。连续多次调用 `QWidget::updateGeometry()` 仅会触发一次布局重新计算。
如果小部件的首选高度取决于其实际宽度(例如,具有自动断词功能的标签),请在小部件的size policy 中设置height-for-width 标志,并重写QWidget::heightForWidth()方法。
即使您实现了QWidget::heightForWidth()方法,提供一个合理的sizeHint()值仍然是个好主意。
有关实现这些函数的更多指导,请参阅《Qt Quarterly》杂志上的文章《用高度换取宽度》。
布局问题
在标签控件中使用富文本可能会对其父控件的布局造成一些问题。当标签发生换行时,由于 Qt 布局管理器处理富文本的方式,就会引发这些问题。
在某些情况下,父布局会被置于 QLayout::FreeResize 模式,这意味着它不会调整内容布局以适应小尺寸窗口,甚至会阻止用户将窗口缩小到无法使用的程度。可以通过继承存在问题的控件,并实现适当的sizeHint() 和minimumSizeHint() 函数来解决此问题。
在某些情况下,布局何时被添加到控件中是关键的。当您通过QDockWidget::setWidget() 和QScrollArea::setWidget() 设置QDockWidget 或QScrollArea 的控件时,该控件必须已经设置了布局。否则,该控件将不可见。
手动布局
如果您要制作独一无二的特殊布局,也可以按照上述方法创建自定义小部件。重写QWidget::resizeEvent() 方法以计算所需的大小分配,并对每个子项调用setGeometry() 方法。
当需要重新计算布局时,该小部件将收到类型为QEvent::LayoutRequest 的事件。请重写QWidget::event()方法来处理QEvent::LayoutRequest 事件。
如何编写自定义布局管理器
除了手动布局外,另一种方法是通过继承 `QLayout` 类来编写自己的布局管理器。Flow Layout示例展示了具体实现方法。
下面我们将详细介绍一个示例。CardLayout 类的设计灵感来源于同名的Java布局管理器。它将项目(控件或嵌套布局)层叠排列,每个项目通过QLayout::spacing()进行偏移。
要编写自己的布局类,必须定义以下内容:
- 一种用于存储布局所处理项的数据结构。每个项都是一个 `QLayoutItem`。在本示例中,我们将使用 `QList `。
- addItem() 方法,用于向布局中添加项目。
- setGeometry(),说明如何向布局中添加项目。(),说明如何执行布局。
- sizeHint(),布局的首选大小。
- itemAt(), 如何遍历布局。
- takeAt(), 如何从布局中移除项目。
在大多数情况下,你还需要实现minimumSize()方法。
头文件(card.h )
#ifndef CARD_H
#define CARD_H
#include <QtWidgets>
#include <QList>
class CardLayout : public QLayout
{
public:
CardLayout(int spacing): QLayout()
{ setSpacing(spacing); }
CardLayout(int spacing, QWidget *parent): QLayout(parent)
{ setSpacing(spacing); }
~CardLayout();
void addItem(QLayoutItem *item) override;
QSize sizeHint() const override;
QSize minimumSize() const override;
int count() const override;
QLayoutItem *itemAt(int) const override;
QLayoutItem *takeAt(int) override;
void setGeometry(const QRect &rect) override;
private:
QList<QLayoutItem *> m_items;
};
#endif实现文件(card.cpp )
//#include "card.h"首先,我们定义count() 函数来获取列表中的项目数量。
int CardLayout::count() const
{
// QList::size() returns the number of QLayoutItems in m_items
return m_items.size();
}然后,我们定义两个用于遍历布局的函数:itemAt() 和takeAt() 。这些函数由布局系统在内部使用,用于处理小部件的删除操作。应用程序开发人员也可以使用这些函数。
itemAt() 返回指定索引处的项目。takeAt() 删除指定索引处的项目,并返回该项目。在此情况下,我们使用列表索引作为布局索引。在其他涉及更复杂数据结构的情况下,我们可能需要花费更多精力来为项目定义一个线性顺序。
QLayoutItem *CardLayout::itemAt(int idx) const
{
// QList::value() performs index checking, and returns nullptr if we are
// outside the valid range
return m_items.value(idx);
}
QLayoutItem *CardLayout::takeAt(int idx)
{
// QList::take does not do index checking
return idx >= 0 && idx < m_items.size() ? m_items.takeAt(idx) : 0;
}addItem() 实现布局项的默认放置策略。此函数必须实现。它会被 QLayout::add() 以及将布局作为父布局的QLayout 构造函数调用。如果您的布局具有需要参数的高级放置选项,则必须提供额外的访问函数,例如QGridLayout::addItem()、QGridLayout::addWidget() 和QGridLayout::addLayout() 的跨行和跨列重载版本。
void CardLayout::addItem(QLayoutItem *item)
{
m_items.append(item);
}布局将接管所添加项的责任。由于QLayoutItem 未继承QObject ,因此我们必须手动删除这些项。在析构函数中,我们使用takeAt() 将每个项从列表中移除,然后将其删除。
CardLayout::~CardLayout()
{
QLayoutItem *item;
while ((item = takeAt(0)))
delete item;
}setGeometry() 函数实际上执行了布局操作。作为参数传入的矩形不包含margin() 。如有必要,请将spacing() 作为项与项之间的距离。
void CardLayout::setGeometry(const QRect &r)
{
QLayout::setGeometry(r);
if (m_items.size() == 0)
return;
int w = r.width() - (m_items.count() - 1) * spacing();
int h = r.height() - (m_items.count() - 1) * spacing();
int i = 0;
while (i < m_items.size()) {
QLayoutItem *o = m_items.at(i);
QRect geom(r.x() + i * spacing(), r.y() + i * spacing(), w, h);
o->setGeometry(geom);
++i;
}
}sizeHint() 和minimumSize() 的实现通常非常相似。这两个函数返回的尺寸应包含spacing() ,但不包含margin() 。
QSize CardLayout::sizeHint() const
{
QSize s(0, 0);
int n = m_items.count();
if (n > 0)
s = QSize(100, 70); //start with a nice default size
int i = 0;
while (i < n) {
QLayoutItem *o = m_items.at(i);
s = s.expandedTo(o->sizeHint());
++i;
}
return s + n * QSize(spacing(), spacing());
}
QSize CardLayout::minimumSize() const
{
QSize s(0, 0);
int n = m_items.count();
int i = 0;
while (i < n) {
QLayoutItem *o = m_items.at(i);
s = s.expandedTo(o->minimumSize());
++i;
}
return s + n * QSize(spacing(), spacing());
}补充说明
- 此自定义布局不处理高度与宽度的关系。
- 我们忽略QLayoutItem::isEmpty(); 这意味着布局将把隐藏的小部件视为可见。
- 对于复杂的布局,通过缓存计算出的值可以大大提高速度。在这种情况下,请实现QLayoutItem::invalidate() 来标记缓存数据已发生变化。
- 调用 `QLayoutItem::sizeHint()` 等方法可能会消耗大量资源。因此,如果您稍后在同一函数内还需要该值,应将其存储在局部变量中。
- 不应在同一个函数中对同一项调用两次 `QLayoutItem::setGeometry()`。如果该项有多个子控件,此调用可能非常耗时,因为布局管理器每次都必须执行完整的布局。 相反,应先计算几何尺寸,然后进行设置。(这不仅适用于布局,例如,如果您实现了自定义的 resizeEvent() 方法,也应采取同样的做法。)
布局示例
虽然许多Qt Widgets 示例已经使用了布局,但仍有若干示例专门用于展示各种布局。
该示例展示了如何使用信号和槽来实现计算器小部件的功能,以及如何使用 QGridLayout 将子小部件排列在网格中。 | |
“日历控件”示例展示了 QCalendarWidget 的使用方法。 | |
演示了如何根据不同的窗口大小排列小部件。 | |
演示了 QPainter 中的合成模式如何工作。 | |
“菜单”示例演示了如何在主窗口应用程序中使用菜单。 | |
“简单树模型”示例展示了如何将分层模型与 Qt 的标准视图类结合使用。 |
© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.