警告

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

布局管理#

标准布局管理程序的简要介绍和自定义布局的介绍。

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

简介#

Qt包含一组布局管理类,用于描述如何在应用程序的用户界面中布局小窗口。这些布局在可用空间发生变化时自动定位和调整小窗口的大小,确保它们始终整齐排列,并且整体用户界面保持可用。

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

  • 子小窗口的位置

  • 窗口的合理默认大小

  • 窗口的合理最小大小

  • 调整大小处理

  • 内容更改时的自动更新

    • 子小窗口的字体大小、文本或其他内容

    • 隐藏或显示子小窗口

    • 删除子小窗口

Qt的布局类#

Qt的布局类是为手写的C++代码设计的,可以简单地将像素指定为度量单位,因此它们易于理解和使用。使用Qt Designer创建的表单的代码也使用布局类。当试验表单设计时,Qt 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类提供了一个小部件堆栈,一次只显示一个小部件。

水平、垂直、网格和窗体布局#

为您的小部件获取良好布局的最简单方法就是使用内置的布局管理器:QHBoxLayoutQVBoxLayoutQGridLayout ,和 QFormLayout 。这些类继承自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 将小部件排布在2列描述标签-字段样式。

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

在代码中布局小部件#

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

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

QFormLayout 将在一行中添加两个小部件,通常是一个 QLabel 和一个 QLineEdit,以创建表单。在同一行上添加一个 QLabel 和一个 QLineEdit 将设置 QLineEditQLabel 的联朋。下面的代码将使用 QFormLayout 在一行中放置三个 QPushButton 和相应的 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() 的多次连续调用只会导致一次布局重新计算。

如果你的部件最优高度取决于其实际宽度(例如,一个具有自动换行的标签),请在部件的sizePolicy中的height-for-width标志,并重写heightForWidth()

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

关于实现这些函数的更多指导,请参阅《Qt Quarterly》文章Trading Height for Width

布局问题#

在标签部件中使用丰富文本可能会给它父部件的布局引入一些问题。问题在于标签执行单词换行时,Qt布局管理器处理丰富文本的方式。

在某些情况下,父布局被置于QLayout::FreeResize模式,这意味着它将不会适应其内容以适应小窗口或甚至防止用户将窗口缩小到不可用。可以通过对有问题的部件进行子类化,并实现合适的sizeHint()minimumSizeHint()函数来解决。

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

手动布局#

如果您的布局是独一无二的特殊布局,您也可以像上述那样创建自定义部件。重写resizeEvent()来计算所需的大小分布,并对每个子部件调用setGeometry()

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

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

手动布局的替代方案是编写自己的布局管理器,通过继承 QLayout流布局 示例展示了如何实现这一功能。

以下将详细展示一个示例。本例中的 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 = 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 = 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 = i + 1

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

更多说明

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

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

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

  • 调用 `sizeHint()` 等操作可能是昂贵的。因此,如果您需要在同一函数中稍后再次使用该值,请将其存储在局部变量中。

  • 您不应在同一个函数中同一个项上两次调用 `setGeometry()`。如果项有多个子部件,则此调用可能非常昂贵,因为每次都需要进行完整的布局。相反,先计算几何形状,然后再设置它。(这不仅仅适用于布局,如果实现了自己的 resizeEvent() 等,也应该这样做。)

布局示例

许多 Qt Widgets 例子已经使用布局,但是还有一些示例专门展示各种布局。

计算器示例

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

日历小部件示例

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

流动布局示例

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

图像合成示例

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

菜单示例

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

简单树模型示例

简单树模型示例显示了如何使用 Qt 的标准视图类使用层次化模型。