布局管理
Qt布局系统提供了一种简单而强大的方式,可以自动在窗口组件中排列子窗口组件,以确保它们充分利用可用空间。
介绍
Qt包含了一组布局管理类,用于描述窗口组件在应用程序用户界面中的布局方式。当可用空间发生变化时,这些布局会自动定位和调整窗口组件的大小,确保它们的排列一致,并且用户界面作为一个整体保持可用。
所有QWidget子类都可以使用布局来管理它们的子组件。函数QWidget::setLayout()为部件应用布局。当以这种方式在部件上设置布局时,它将负责以下任务:
- 子部件的定位
- 合理的窗口默认大小
- 合理的窗口最小尺寸
- 调整处理
- 内容变更时自动更新:
- 子部件的字体大小、文本或其他内容
- 隐藏或显示子部件
- 移除子部件
Qt的布局类
Qt的布局类是为手写的c++代码设计的,为了简单起见,可以用像素来指定测量值,所以它们很容易理解和使用。使用Qt Designer创建的表单生成的代码也使用了layout类。在尝试设计表单时,Qt Designer非常有用, 因为它避免了用户界面开发中通常涉及的编译、链接和运行循环。
QBoxLayout | 水平或垂直排列子部件 |
QButtonGroup | 容器来组织按钮小部件组 |
QFormLayout | 管理输入小部件的表单及其相关标签 |
QGraphicsAnchor | 表示QGraphicsAnchorLayout中两个项目之间的锚点 |
QGraphicsAnchorLayout | 可以在图形视图中将小部件固定在一起的布局 |
QGridLayout | 在网格中布局小部件 |
QGroupBox | 带标题的组框框架 |
QHBoxLayout | 水平排列小部件 |
QLayout | 几何图形管理器的基类 |
QLayoutItem | QLayout操作的抽象项 |
QSizePolicy | 描述水平和垂直调整大小策略的布局属性 |
QSpacerItem | 布局中的空白空间 |
QStackedLayout | 一次只能看到一个小部件的小部件堆栈 |
QStackedWidget | 一次只能看到一个小部件的小部件堆栈 |
QVBoxLayout | 垂直排列部件 |
QWidgetItem | 表示小部件的布局项 |
使用布局的技巧
使用布局时,在构造子部件时不需要传递父组件。布局将自动重新设置小部件的父部件(使用QWidget::setParent()),使它们成为安装了布局的小部件的子部件。
注意:布局中的部件是安装布局的部件的子部件,而不是布局本身的子部件。部件只能有其他部件作为父部件,而不能有布局。
你可以在布局上使用addLayout()嵌套布局;然后,内部布局成为它插入的布局的子布局。
向布局中添加部件
向布局中添加部件时,布局过程如下所示:
- 所有小部件最初将根据它们的QWidget::sizePolicy()和QWidget::sizeHint()分配一定的空间。
- 如果任何小部件设置了拉伸因子,且其值大于零,则按其拉伸因子的比例为它们分配空间(将在下面解释)。
- 如果任何部件将拉伸因子设置为0,则只有在没有其他部件需要空间的情况下,它们才会获得更多空间。其中,空间首先分配给具有扩展大小策略的部件。
- 任何分配的空间小于其最小大小(如果没有指定最小大小,则为最小大小提示)的部件都会分配其所需的最小大小。(部件不需要有最小尺寸或最小尺寸提示,在这种情况下,拉伸因子是它们的决定因子。)
- 任何分配的空间超过其最大尺寸的部件都会分配它们所需的最大尺寸空间。(部件不需要有最大尺寸,在这种情况下,拉伸因子是它们的决定因子。)
延伸的因素
widget通常在创建时没有设置任何拉伸因子。当它们在布局中被布局时,小部件根据它们的QWidget::sizePolicy()或它们的最小大小提示(以较大的为准)获得一份空间份额。拉伸因子用于改变部件之间的空间比例。
如果我们使用没有设置拉伸因子的QHBoxLayout布局三个窗口组件,我们将得到如下布局:
如果我们对每个小部件应用拉伸因子,它们将按比例布局(但绝不会小于它们的最小尺寸提示),例如:
布局中的自定义部件
在创建自己的窗口组件类时,还应该告知它的布局属性。如果组件使用了Qt的布局,这一点已经解决了。如果部件没有任何子部件,或者使用手动布局,则可以使用以下任何或所有机制更改部件的行为:
- 重新实现QWidget::sizeHint()以返回widget的首选大小。
- 重新实现QWidget::minimumSizeHint(),以返回widget可以拥有的最小尺寸。
- 调用QWidget::setSizePolicy()来指定小部件的空间需求。
每当大小提示、最小大小提示或大小策略发生变化时,调用QWidget::updateGeometry()。这将导致布局重新计算。对QWidget::updateGeometry()的多次连续调用只会导致一次布局重新计算。
如果小部件的首选高度取决于它的实际宽度(例如,具有自动断字功能的标签),则在小部件的大小策略中设置height-for-width标志,并重新实现QWidget::heightForWidth()。
即使您实现了QWidget::heightForWidth(),提供一个合理的sizeHint()仍然是一个好主意。
有关实现这些函数的进一步指导,请参阅Qt季度文章Trading Height For Width。
布局问题
在标签小部件中使用富文本可能会给其父小部件的布局带来一些问题。当标签被换行时,Qt的布局管理器处理富文本的方式会导致问题。
在某些情况下,parent布局被设置为QLayout::FreeResize模式,这意味着它将不能适应其内容的布局以适应小尺寸的窗口,甚至阻止用户使窗口太小而无法使用。这可以通过对有问题的部件进行子类化,并实现适当的sizeHint()和minimumSizeHint()函数来解决。
在某些情况下,当向部件添加布局时,它是相关的。当你设置QDockWidget或QScrollArea
的widget时(使用QDockWidget::setWidget()和QScrollArea::setWidget()), widget上必须已经设置了布局。否则,部件将不可见。
手动布局
如果你正在制作一个独一无二的特殊布局,你也可以像上面描述的那样制作一个自定义部件。重新实现QWidget::resizeEvent()来计算所需的大小分布,并在每个子节点上调用setGeometry()。
当布局需要重新计算时,widget将获得一个类型为QEvent::LayoutRequest的事件。重新实现QWidget::event()来处理QEvent::LayoutRequest事件。
如何编写自定义布局管理器
手动布局的另一种选择是通过继承QLayout来编写自己的布局管理器。Border布局和Flow布局的例子展示了如何做到这一点。
这里我们详细介绍一个例子。CardLayout类的灵感来自于同名的Java布局管理器。它将项目(窗口组件或嵌套布局)置于彼此之上,每个项目通过QLayout::spacing()进行偏移。
要编写自己的布局类,必须定义以下内容:
- 存储由布局处理的项的数据结构。每一项都是一个QLayoutItem。在这个例子中,我们将使用QVector。
- addItem(),如何向布局中添加项。
- setGeometry(),如何执行布局
- sizeHint(),布局的首选大小。
- itemAt(),如何遍历布局
- takeAt():从布局中删除元素的方法。
大多数情况下,还需要实现minimumSize()。
#ifndef CARD_H
#define CARD_H
#include <QtWidgets>
#include <QVector>
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:
QVector<QLayoutItem*> m_items;
};
#endif
首先定义count()来获取列表中的项数。
int CardLayout::count() const
{
// QVector::size() returns the number of QLayoutItems in m_items
return m_items.size();
}
然后定义两个遍历布局的函数:itemAt()和takeAt()。布局系统内部使用这些函数来处理部件的删除。应用程序程序员也可以使用它们。
itemAt()返回指定索引处的元素takeAt()删除给定索引处的元素,并返回它。在这种情况下,我们使用列表索引作为布局索引。在其他数据结构更复杂的情况下,我们可能需要花费更多的精力来定义元素的线性顺序。
QLayoutItem *CardLayout::itemAt(int idx) const
{
// QVector::value() performs index checking, and returns nullptr if we are
// outside the valid range
return m_items.value(idx);
}
QLayoutItem *CardLayout::takeAt(int idx)
{
// QVector::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示例已经使用了布局,但是,存在一些示例来展示各种布局
Address Book Tutorial | 介绍GUI编程,展示如何组合一个简单但功能齐全的应用程序。 |
Border Layout Example | 演示如何沿边框排列子部件。 |
Calculator Example | 该示例展示了如何使用信号和槽来实现计算器小部件的功能,以及如何使用QGridLayout在网格中放置子小部件。 |
Calendar Widget Example | CalendarWidget示例展示了QCalendarWidget的用法。 |
Echo Plugin Example | 这个例子展示了如何创建一个Qt插件。 |
Flow Layout Example | 展示如何为不同的窗口大小排列小部件 |
Image Composition Example | 展示了QPainter中的合成模式是如何工作的。 |
Menus Example | 菜单示例演示了如何在主窗口应用程序中使用菜单。 |
Simple Tree Model Example | 简单树模型示例展示了如何在Qt的标准视图类中使用分层模型。 |
Sub-Attaq | 这个例子展示了Qt结合动画框架和状态机框架来创建游戏的能力。 |
Layout Management | Qt Widgets 5.15.17