警告

本节包含从C++自动翻译到Python的代码片段,可能包含错误。

布局管理

标准布局管理器的介绍以及自定义布局的入门。

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

介绍

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

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

  • 子部件的位置

  • 窗口的合理默认大小

  • 窗口的合理最小尺寸

  • 调整大小处理

  • 内容更改时自动更新:

    • 子部件中的字体大小、文本或其他内容

    • 隐藏或显示子部件

    • 移除子部件

Qt的布局类

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

PySide6.QtWidgets.QGraphicsAnchorLayout

QGraphicsAnchorLayout 类提供了一个布局,可以在图形视图中将小部件锚定在一起。

PySide6.QtWidgets.QGraphicsAnchor

QGraphicsAnchor 类表示在 QGraphicsAnchorLayout 中两个项目之间的锚点。

PySide6.QtWidgets.QBoxLayout

QBoxLayout 类将子部件水平或垂直排列。

PySide6.QtWidgets.QHBoxLayout

QHBoxLayout 类将小部件水平排列。

PySide6.QtWidgets.QVBoxLayout

QVBoxLayout 类垂直排列小部件。

PySide6.QtWidgets.QFormLayout

QFormLayout 类管理输入小部件及其相关标签的表单。

PySide6.QtWidgets.QGridLayout

QGridLayout 类以网格形式布局小部件。

PySide6.QtWidgets.QLayout

QLayout 类是几何管理器的基类。

PySide6.QtWidgets.QLayoutItem

QLayoutItem 类提供了一个 QLayout 操作的抽象项。

PySide6.QtWidgets.QSpacerItem

QSpacerItem 类在布局中提供空白空间。

PySide6.QtWidgets.QWidgetItem

QWidgetItem 类是一个表示小部件的布局项。

PySide6.QtWidgets.QSizePolicy

QSizePolicy 类是一个布局属性,描述了水平和垂直调整大小的策略。

PySide6.QtWidgets.QStackedLayout

QStackedLayout 类提供了一个小部件堆栈,其中一次只能看到一个小部件。

PySide6.QtWidgets.QButtonGroup

QButtonGroup 类提供了一个容器来组织按钮小部件组。

PySide6.QtWidgets.QGroupBox

QGroupBox 小部件提供了一个带有标题的组框框架。

PySide6.QtWidgets.QStackedWidget

QStackedWidget 类提供了一个小部件堆栈,其中一次只能看到一个小部件。

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

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

  • 一个 QHBoxLayout 将小部件水平排列,从左到右(或从右到左,适用于从右到左的语言)。

    ../_images/qhboxlayout-with-5-children1.png
  • 一个 QVBoxLayout 将小部件从上到下垂直排列。

    ../_images/qvboxlayout-with-5-children1.png
  • 一个 QGridLayout 在二维网格中布局小部件。小部件可以占据多个单元格。

    ../_images/qgridlayout-with-5-children.png
  • QFormLayout 以两列描述性标签-字段样式布局小部件。

    ../_images/qformlayout-with-6-children.png

在代码中布局小部件

以下代码创建了一个QHBoxLayout,用于管理五个QPushButtons的几何形状,如上图所示:

window = QWidget()
button1 = QPushButton("One")
button2 = QPushButton("Two")
button3 = QPushButton("Three")
button4 = QPushButton("Four")
button5 = QPushButton("Five")

layout = QHBoxLayout(window)
layout.addWidget(button1)
layout.addWidget(button2)
layout.addWidget(button3)
layout.addWidget(button4)
layout.addWidget(button5)
window.show()

QVBoxLayout 的代码是相同的,除了创建布局的那一行。QGridLayout 的代码有点不同,因为我们需要指定子部件的行和列位置:

window = QWidget()
button1 = QPushButton("One")
button2 = QPushButton("Two")
button3 = QPushButton("Three")
button4 = QPushButton("Four")
button5 = QPushButton("Five")

layout = 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列。这是通过在addWidget()的第五个参数中指定2来实现的。

QFormLayout 将在一行上添加两个小部件,通常是 QLabelQLineEdit 来创建表单。在同一行上添加 QLabelQLineEdit 会将 QLineEdit 设置为 QLabel 的伙伴。以下代码将使用 QFormLayout 在一行上放置三个 QPushButtons 和一个相应的 QLineEdit

window = QWidget()
button1 = QPushButton("One")
lineEdit1 = QLineEdit()
button2 = QPushButton("Two")
lineEdit2 = QLineEdit()
button3 = QPushButton("Three")
lineEdit3 = QLineEdit()

layout = QFormLayout(window)
layout.addRow(button1, lineEdit1)
layout.addRow(button2, lineEdit2)
layout.addRow(button3, lineEdit3)
window.show()

使用布局的提示

当你使用布局时,在构建子部件时不需要传递父部件。布局会自动重新设置部件的父部件(使用setParent()),使它们成为安装布局的部件的子部件。

注意

布局中的小部件是安装布局的小部件的子部件,不是布局本身的子部件。小部件只能有其他小部件作为父部件,不能有布局。

你可以使用addLayout()在布局中嵌套布局;内部布局随后成为插入布局的子布局。

向布局添加小部件

当你向布局添加小部件时,布局过程如下:

  1. 所有小部件最初将根据它们的sizePolicy()sizeHint()分配一定量的空间。

  2. 如果任何小部件设置了拉伸因子,且值大于零,则它们将按照其拉伸因子的比例分配空间(如下所述)。

  3. 如果任何小部件的拉伸因子设置为零,它们只有在没有其他小部件需要空间时才会获得更多空间。在这些情况下,空间首先分配给具有Expanding大小策略的小部件。

  4. 任何分配到的空间小于其最小尺寸(或未指定最小尺寸时的最小尺寸提示)的小部件将被分配它们所需的最小尺寸。(小部件不一定需要有最小尺寸或最小尺寸提示,在这种情况下,拉伸因子是它们的决定因素。)

  5. 任何被分配的空间超过其最大尺寸的小部件将被分配它们所需的最大尺寸空间。(小部件不一定需要有一个最大尺寸,在这种情况下,拉伸因子是它们的决定因素。)

拉伸因子

小部件通常在没有任何拉伸因子的情况下创建。当它们被布局在一个布局中时,小部件会根据它们的sizePolicy()或它们的最小尺寸提示(以较大者为准)获得空间份额。拉伸因子用于改变小部件之间获得的空间比例。

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

../_images/layout1.png

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

../_images/layout2.png

布局中的自定义小部件

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

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

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

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

有关实现这些功能的进一步指导,请参阅Qt Quarterly文章Trading Height for Width

布局问题

在标签小部件中使用富文本可能会对其父小部件的布局引入一些问题。当标签进行自动换行时,由于Qt的布局管理器处理富文本的方式,会出现这些问题。

在某些情况下,父布局被设置为QLayout::FreeResize模式,这意味着它不会调整其内容的布局以适应小尺寸窗口,甚至可能阻止用户将窗口调整得过小以至于无法使用。这可以通过子类化有问题的部件,并实现适当的sizeHint()minimumSizeHint()函数来克服。

在某些情况下,向小部件添加布局时是相关的。当你设置QDockWidgetQScrollArea的小部件时(使用setWidget()setWidget()),布局必须已经设置在小部件上。如果没有,小部件将不可见。

手动布局

如果您正在制作一个独一无二的特殊布局,您也可以按照上述方法制作一个自定义小部件。重新实现 resizeEvent() 来计算所需的大小分布,并在每个子项上调用 setGeometry()

当布局需要重新计算时,小部件将收到一个类型为 QEvent::LayoutRequest 的事件。重新实现 event() 以处理 QEvent::LayoutRequest 事件。

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

手动布局的替代方法是通过子类化QLayout来编写自己的布局管理器。Flow Layout示例展示了如何做到这一点。

这里我们详细展示一个例子。CardLayout 类的灵感来源于同名的Java布局管理器。它将项目(小部件或嵌套布局)一个接一个地排列,每个项目通过 spacing() 进行偏移。

要编写自己的布局类,您必须定义以下内容:

  • 一种数据结构,用于存储布局处理的项。每个项是一个QLayoutItem。在这个例子中,我们将使用QList。

  • addItem(),如何向布局中添加项目。

  • setGeometry(),如何执行布局。

  • sizeHint(),布局的首选大小。

  • itemAt(),如何遍历布局。

  • takeAt(),如何从布局中移除项目。

在大多数情况下,您还将实现 minimumSize()

头文件 ( `` card.h``

)

#ifndef CARD_H
#define CARD_H
from PySide6 import QtWidgets

class CardLayout(QLayout):

# public
    CardLayout(int spacing): QLayout()
    { setSpacing(spacing); }
    CardLayout(int spacing, QWidget parent): QLayout(parent)
    { setSpacing(spacing); }
    ~CardLayout()
    def addItem(item):
    QSize sizeHint() override
    QSize minimumSize() override
    int count() override
    QLayoutItem itemAt(int) override
    QLayoutItem takeAt(int) override
    def setGeometry(rect):
# private
*> = QList<QLayoutItem()

#endif

实现文件 ( `` card.cpp``

)

##include "card.h"

首先我们定义 count() 来获取列表中项目的数量。

def count(self):

    # QList::size() returns the number of QLayoutItems in m_items
    return m_items.size()

然后我们定义了两个遍历布局的函数:itemAt()takeAt()。这些函数由布局系统内部使用,用于处理小部件的删除。它们也对应用程序程序员可用。

itemAt() 返回给定索引处的项目。takeAt() 移除给定索引处的项目,并返回它。在这种情况下,我们使用列表索引作为布局索引。在其他情况下,如果我们有更复杂的数据结构,我们可能需要花费更多精力来定义项目的线性顺序。

QLayoutItem CardLayout.itemAt(int idx)

    # 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 and idx < m_items.size() if m_items.takeAt(idx) else 0

addItem() 实现了布局项的默认放置策略。此函数必须被实现。它被QLayout::add()使用,以及接受布局作为父级的QLayout构造函数使用。如果你的布局有需要参数的高级放置选项,你必须提供额外的访问函数,例如addItem()addWidget()addLayout()的行和列跨越重载。

def addItem(self, item):

    m_items.append(item)

布局接管了添加项的责任。由于QLayoutItem不继承QObject,我们必须手动删除这些项。在析构函数中,我们使用takeAt()从列表中移除每个项,然后删除它。

CardLayout.~CardLayout()

     item = QLayoutItem()
     while (item = takeAt(0)):
         del item

setGeometry() 函数实际上执行布局。作为参数提供的矩形不包括 margin()。如果相关,请使用 spacing() 作为项目之间的距离。

def setGeometry(self, r):

    QLayout.setGeometry(r)
    if m_items.size() == 0:
        return
    w = r.width() - (m_items.count() - 1) * spacing()
    h = r.height() - (m_items.count() - 1) * spacing()
    i = 0
    while i < m_items.size():
        o = m_items.at(i)
        geom = QRect(r.x() + i * spacing(), r.y() + i * spacing(), w, h)
        o.setGeometry(geom)
        i += 1

sizeHint()minimumSize() 在实现上通常非常相似。这两个函数返回的大小应该包括 spacing(),但不包括 margin()

def sizeHint(self):

    s = QSize(0, 0)
    n = m_items.count()
    if n > 0:
        s = QSize(100, 70) #start with a nice default size
    i = 0
    while i < n:
        o = m_items.at(i)
        s = s.expandedTo(o.sizeHint())
        i += 1

    return s + n * QSize(spacing(), spacing())

def minimumSize(self):

    s = QSize(0, 0)
    n = m_items.count()
    i = 0
    while i < n:
        o = m_items.at(i)
        s = s.expandedTo(o.minimumSize())
        i += 1

    return s + n * QSize(spacing(), spacing())

进一步说明

  • 此自定义布局不处理宽度的高度。

  • 我们忽略 isEmpty() ; 这意味着布局会将隐藏的小部件视为可见的。

  • 对于复杂的布局,通过缓存计算值可以大大提高速度。在这种情况下,实现 invalidate() 来标记缓存数据为脏数据。

  • 调用 sizeHint() 等操作可能很耗时。因此,如果你在同一个函数中稍后再次需要该值,应该将其存储在局部变量中。

  • 你不应该在同一个函数中对同一个项目调用两次setGeometry()。如果该项目有几个子部件,这个调用可能会非常昂贵,因为布局管理器每次都必须进行完整的布局。相反,应该先计算几何形状,然后再设置它。(这不仅适用于布局,如果你实现自己的resizeEvent(),也应该这样做,例如。)

布局示例

许多Qt小部件示例已经使用了布局,然而,有几个示例展示了各种布局。

Calculator-Example

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

Calendar-Widget-Example

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

Flow-Layout-Example

展示如何为不同窗口大小排列小部件。

Image-Composition-Example

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

菜单-示例

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

Simple-Tree-Model-Example

简单树模型示例展示了如何将分层模型与Qt的标准视图类一起使用。