布局管理

Qt 布局系统提供了一种简单而强大的方法,可自动安排 widget 中的子 widget,确保它们充分利用可用空间。

布局管理

Qt 包含一组布局管理类,用于描述部件在应用程序用户界面中的布局方式。当窗口部件的可用空间发生变化时,这些布局会自动定位和调整它们的大小,从而确保它们的布局一致,并使整个用户界面保持可用性。

所有QWidget 子类都可以使用布局来管理它们的子类。QWidget::setLayout() 函数会将布局应用到 widget 上。当以这种方式在部件上设置布局时,它将负责以下任务:

  • 定位子部件
  • 窗口的合理默认尺寸
  • 窗口的合理最小尺寸
  • 调整大小处理
  • 内容变化时自动更新
    • 子部件的字体大小、文本或其他内容
    • 隐藏或显示子部件
    • 删除子窗口小部件

Qt 的布局类

Qt Designer 的布局类专为手写 C++ 代码而设计,允许以像素为单位指定测量值,以简化操作,因此易于理解和使用。使用Qt Widgets Designer 创建的表单生成的代码也使用布局类。在尝试设计表单时,使用Qt Widgets Designer 非常有用,因为它避免了用户界面开发中通常涉及的编译、链接和运行周期。

QBoxLayout

水平或垂直排列子部件

QButtonGroup

组织按钮部件组的容器

QFormLayout

管理输入部件的表格及其相关标签

QGraphicsAnchor

代表 QGraphicsAnchorLayout 中两个项目之间的锚点

QGraphicsAnchorLayout

可在图形视图中将部件锚定在一起的布局

QGridLayout

在网格中布局部件

QGroupBox

带有标题的组框

QHBoxLayout

水平排列部件

QLayout

几何图形管理器的基类

QLayoutItem

QLayout 可操作的抽象项

QSizePolicy

描述水平和垂直大小调整策略的布局属性

QSpacerItem

布局中的空白空间

QStackedLayout

一次只能看到一个部件的部件堆栈

QStackedWidget

同时只有一个部件可见的部件堆栈

QVBoxLayout

垂直排列部件

QWidgetItem

表示部件的布局项

水平、垂直、网格和表单布局

为部件设计良好布局的最简单方法是使用内置的布局管理器:QHBoxLayout,QVBoxLayout,QGridLayout, 和QFormLayout 。这些类继承自QLayout ,而 又派生自QObject (而非QWidget )。它们负责管理一组 widget 的几何图形。要创建更复杂的布局,可以将布局管理器相互嵌套。

  • 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 跨两列。这可以通过在QGridLayout::addWidget() 的第五个参数中指定 2 来实现。

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() 嵌套布局;这样,内部布局就会成为插入布局的子布局。

在布局中添加部件

在布局中添加部件时,布局流程如下:

  1. 所有部件最初都将根据其QWidget::sizePolicy() 和QWidget::sizeHint() 分配一定的空间。
  2. 如果有任何部件设置了拉伸因子,且值大于零,那么它们将按照拉伸因子的比例分配空间(解释如下)。
  3. 如果有任何部件的伸展系数设置为零,只有在没有其他部件需要空间的情况下,它们才会获得更多空间。其中,空间会首先分配给采用Expanding 大小策略的部件。
  4. 任何被分配的空间小于其最小尺寸(如果没有指定最小尺寸,则为最小尺寸提示)的部件都会被分配到所需的最小尺寸。(部件不一定要有最小尺寸或最小尺寸提示,在这种情况下,拉伸因子就是它们的决定因素)。
  5. 任何被分配的空间大于其最大尺寸的部件都会被分配到所需的最大尺寸空间。(部件不一定要有最大尺寸,在这种情况下,拉伸系数是其决定因素)。

拉伸系数

创建小工具时通常不会设置任何伸展系数。在布局中布置部件时,会根据其QWidget::sizePolicy() 或最小尺寸提示(以较大者为准)分配空间。拉伸因子用于改变部件之间的空间比例。

如果我们使用QHBoxLayout 布局三个部件,且未设置拉伸因子,我们将得到如下布局:

连续三个小部件

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

一排三个具有不同拉伸系数的部件

布局中的自定义部件

当你制作自己的 widget 类时,你也应该传达它的布局属性。如果 widget 使用 Qt 的布局之一,这一点已经得到了处理。如果 widget 没有任何子 widget,或者使用的是手动布局,则可以使用以下任何或所有机制来更改 widget 的行为:

每当尺寸提示、最小尺寸提示或尺寸策略发生变化时,请调用QWidget::updateGeometry() 。这将导致布局重新计算。多次连续调用QWidget::updateGeometry() 只会导致一次布局重新计算。

如果部件的首选高度取决于其实际宽度(例如,具有自动分词功能的标签),请在部件的size policy 中设置height-for-width 标志,并重新实现QWidget::heightForWidth()。

即使您实现了QWidget::heightForWidth(),提供合理的 sizeHint() 也不失为一个好主意。

有关实现这些函数的进一步指导,请参阅《Qt 季刊》文章《用高度换宽度》。

布局问题

在标签部件中使用富文本会给其父部件的布局带来一些问题。出现问题的原因是 Qt 的布局管理器在对标签进行文字包装时处理富文本的方式。

在某些情况下,父布局会进入 QLayout::FreeResize 模式,这意味着它不会调整其内容布局以适应小尺寸窗口,甚至不会阻止用户将窗口设置得太小而无法使用。可以通过对有问题的部件进行子类化,并实现合适的sizeHint() 和minimumSizeHint() 函数来解决这个问题。

在某些情况下,向部件添加布局时也会出现这种情况。当您设置QDockWidgetQScrollArea (使用QDockWidget::setWidget() 和QScrollArea::setWidget()) 的部件时,必须已经在该部件上设置了布局。否则,该 widget 将不可见。

手动布局

如果要制作独一无二的特殊布局,也可以按上述方法制作自定义 widget。重新实现QWidget::resizeEvent() 以计算所需的尺寸分布,并在每个子控件上调用setGeometry() 。

当需要重新计算布局时,该 widget 将收到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 示例已经使用了布局,不过还有几个示例展示了各种布局。

Calculator Example

该示例展示了如何使用信号和槽来实现计算器部件的功能,以及如何使用 QGridLayout 将子部件放置在网格中。

Calendar Widget Example

日历部件示例展示了 QCalendarWidget 的使用。

Flow Layout Example

展示如何根据不同的窗口大小排列部件。

Image Composition Example

展示了 QPainter 中的合成模式是如何工作的。

Menus Example

菜单示例演示了如何在主窗口应用程序中使用菜单。

Simple Tree Model Example

简单树模型示例展示了如何使用 Qt 的标准视图类来建立分层模型。

© 2025 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.