布局管理

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

简介

Qt 包含一组用于描述应用程序用户界面中控件布局的布局管理类。这些布局在可用空间量变化时自动定位和调整控件大小,确保布局始终如一,并且整个用户界面保持可用。

所有 QWidget 子类都可以使用布局来管理其子控件。函数 QWidget::setLayout() 将布局应用于一个控件。以这种方式在 Widget 上设置布局时,它将负责以下任务

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

Qt 的布局类

Qt 的布局类是为手写的 C++ 代码设计的,允许以像素为单位指定度量为简便起见,因此它们容易理解和使用。用于使用 Qt Designer 创建的表单生成的代码也使用了布局类。在尝试表单设计时,使用 Qt Designer 非常有用,因为它避免了通常涉及用户界面开发的人员编译、链接和运行周期。

QBoxLayout

水平或垂直排列子控件

QButtonGroup

用于组织按钮控件的容器

QFormLayout

管理输入控件和其关联标签的表单

QGraphicsAnchor

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

QGraphicsAnchorLayout

在图形视图中可以将控件锚定的布局

QGridLayout

以网格形式布置控件

QGroupBox

带标题的组框框架

QHBoxLayout

水平排列控件

QLayout

几何管理器的基础类

QLayoutItem

QLayout 动操作的对象的抽象项

QSizePolicy

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

QSpacerItem

布局中的空白空间

QStackedLayout

只有一次一个控件可见的控件堆叠

QStackedWidget

只有一次一个控件可见的控件堆叠

QVBoxLayout

垂直排列控件

QWidgetItem

表示控件的布局项

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

为小部件布局提供良好布局的最简单方法是使用内置的布局管理器: QHBoxLayoutQVBoxLayoutQGridLayoutQFormLayout。这些类继承自 QLayout,而 QLayout 又继承自 QObject(而不是 QWidget)。它们会处理一组小部件的几何管理。要创建更复杂的布局,您可以在布局管理器内部嵌套其他布局管理器。

  • QHBoxLayout 将小部件水平排列成一行,从左到右(对于从右到左的语言则相反)。

  • QVBoxLayout 将小部件垂直排列成一列,从上到下。

  • QGridLayout 将小部件排列成二维网格。小部件可以占用多个单元格。

  • QFormLayout 将小部件排列成两列的描述性标签-字段样式。

在代码中布局小部件

以下代码创建了一个管理五个 QPushButton 几何形状的 QHBoxLayout,如图所示的第一张截图。

    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 的代码与其相同,只是在创建布局的那一行不同。因为我们需要指定子小部件的行和列位置,《a href="qgridlayout.html" translate="no">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 作为 QGridLayout::addWidget() 的第五个参数来做到这一点。

QFormLayout 将在同一行添加两个小部件,通常是一个 QLabel 和一个 QLineEdit 以创建表单。在同一行添加 QLabelQLineEdit 将设置 QLineEditQLabel 的好友。以下代码使用 QFormLayout 将三个 QPushButton 和相应的一个 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 排列三个小部件,布局将像这样

Three widgets in a row

如果我们对每个小部件应用拉伸因子,它们将以比例排列(但永远不小于最小尺寸提示),例如

Three widgets with different stretch factors in a row

布局中的自定义小部件

当你创建自己的小部件类时,也应该传达其布局属性。如果小部件使用 Qt 的布局之一,这已经得到照顾。如果没有子小部件的小部件或使用手动布局,你可以通过以下任何一种或所有机制来更改小部件的行为

在大小提示、最小大小提示或大小策略更改时调用 QWidget::updateGeometry()。这将导致布局重新计算。连续多次调用 QWidget::updateGeometry() 只会导致一次布局重新计算。

如果你的小部件的首选高度取决于其实际宽度(例如,使用自动单词折行的标签),在小部件的 size policy 中设置 height-for-width 标志,并重新实现 QWidget::heightForWidth()。

即使你实现了 QWidget::heightForWidth(),仍然提供合理的 sizeHint() 是一个好主意。

关于实现这些函数的进一步指导,请参阅 Qt 季度刊 文章 以高度交换宽度

布局问题

在标签小部件中使用富文本可能会引入一些问题给其父小部件的布局。由于当标签进行单词折行时,Qt 布局管理器处理富文本的方式,导致问题发生。

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

在某些情况下,当一个布局被添加到一个小部件(Widget)上时是有相关性的。当您设置QDockWidgetQScrollArea(通过QDockWidget::setWidget() 和 QScrollArea::setWidget())的小部件时,布局必须已在小部件上设置。如果没有设置,小部件将不可见。

手动布局

如果您正在创建一个独一无二的特殊布局,您也可以创建一个定制的 widgets,如上所述。重写QWidget::resizeEvent() 来计算所需的尺寸分布,并对每个子部件调用setGeometry()。

当布局需要重新计算时,小部件将获得一个类型为 QEvent::LayoutRequest的事件。重写QWidget::event() 来处理QEvent::LayoutRequest事件。

如何编写自定义布局管理器

手动布局的替代方法是通过派生QLayout来自定义布局管理器。例如,流布局显示了如何这样做的方法。

在此,我们详细介绍了示例。被称作 CardLayout 的类受到同名 Java 布局管理器的启发。它使项目(widgets 或嵌套布局)垂直排列,每个项目与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()。这些函数内部由布局系统用来处理 widgets 的删除操作。它们也向应用程序程序员提供。

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 小部件 示例 已经使用了布局,但是存在几个示例来展示各种布局。

计算器示例

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

日历小部件示例

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

流式布局示例

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

图像合成示例

展示 QPainter 中的组合模式如何工作。

菜单示例

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

简单的树模型示例

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

© 2024 The Qt Company Ltd. 本文档的贡献包括各自的版权。本中的文档是根据自由软件基金会发布的 GNU 自由文档许可证版本 1.3 的条款许可的。Qt 及相关商标是芬兰的 The Qt Company Ltd. 和/或在世界上其他国家的商标。所有其他商标均为各自所有者的财产。