Qt实现多层布局

Question / 问题

按照需求,需要在一些底层控件上叠加一些UI。比如底层是CAD的绘制页面或者视频播放页面,上面要放一些Label或者Button。上层需要添加的小空间往往不好直接加在最底层的控件上(底层已经有特殊的布局了)

Search / 研究过程

中英文都搜索了不少页面,大多是下面几种方法

  • 建立两个控件,最上层设置透明背景,属性设置ToolTip一类来实现
    • 属性设置的过程很复杂,先后顺序有特定要求
    • 跨平台有问题
  • 利用QGridLayout布局,两个控件按先后顺序添加在同一个格子里
  • 使用QStackLayout
  • 使用类似与Popup Menu的方法
    • Popup属性的窗口失去焦点后就会自动隐藏

除了特别列出来的问题,这些方法都有一个根本的问题————所有的消息都会被最上层的Widget拦截。例如鼠标指针跨过透明背景点击了底层的控件,消息仍会被最上层的控件拦截。

  • 有些热心的开发者指出可以在最顶层的控件安装事件过滤器和转发器来实现消息可以合理的转发到底层。
    • 过滤和转发所有的事件会带来巨大的工作量,代码复用性几乎没有
  • 也可以通过直接添加上层控件到父窗口来实现————使用setParent()而不是layout()->addWidget()
    • 没有布局功能,需要在父类实现resizeEvent()moveEvent()的转发来手动计算布局
    • 工作量也比较大,代码复用性也不好

Solution / 解决方案

最后Qt的官方文档Layout Management给了我启发。示例实现了个Qt没有自带而Java中有的布局CardLayout

The CardLayout class is inspired by the Java layout manager of the same name.

其中设置子控件geometry部分的代码可以看到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
}
}

其中最关键的一句是o->setGeometry(geom);。这表明Layout的本质是自动设置子控件的大小和位置。而我直接利用Qt原有的布局功能来计算每一层控件的大小和位置,再根据计算的结果来手动设置每个控件的大小的位置。不就能优雅地达到目的了吗。

实现起来比想象中的要更简单。先贴出代码
OverlapLayout.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#pragma once

#include <QLayout>
#include <QList>

class OverlapLayout : public QLayout
{
public:
OverlapLayout(QWidget* parent = nullptr);
~OverlapLayout() = default;

/// <summary>
/// 新增的Item会转发到最后一层layout的addItem()
/// </summary>
/// <param name="item"></param>
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;

void AddLayout(QLayout*);

private:
QList<QLayout*> things;
};

OverlapLayout.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
#include "OverlapLayout.h"

#include <QWidget>

OverlapLayout::OverlapLayout(QWidget* parent)
:QLayout(parent)
{

}

void OverlapLayout::addItem(QLayoutItem* item)
{
if(!things.empty())
{
things.last()->addItem(item);
}
}

QSize OverlapLayout::sizeHint() const
{
QSize s(0, 0);
for (auto e : things)
{
s = s.expandedTo(e->sizeHint());
}

return s;
}

QSize OverlapLayout::minimumSize() const
{
QSize s(0, 0);
for (auto e : things)
{
s = s.expandedTo(e->minimumSize());
}

return s;
}

int OverlapLayout::count() const
{
int num = 0;
for (auto e : things)
{
num += e->count();
}

return num;
}

QLayoutItem* OverlapLayout::itemAt(int index) const
{
int num = 0;
int next = 0;
for (auto e : things)
{
next = num + e->count();
if (index < next)
{
return e->itemAt(index - num);
}
num = next;
}

return nullptr;
}

QLayoutItem* OverlapLayout::takeAt(int index)
{
invalidate();

int num = 0;
int next = 0;
for (auto e : things)
{
next = num + e->count();
if (index < next)
{
e->invalidate();
return e->takeAt(index - num);
}
num = next;
}

return nullptr;
}

void OverlapLayout::setGeometry(const QRect& r)
{
if (things.size() == 0)
return;

for (auto e : things)
{
e->invalidate();
e->setGeometry(r);
}
}

void OverlapLayout::AddLayout(QLayout* layout)
{
if(layout)
{
invalidate();
things.append(layout);
addChildLayout(layout);
layout->invalidate();
}
}

实现的思路也非常简单。储存多层QLayout*, 在setGeometry()中直接把整体的QRect传入到每一个层中计算就好。这样就可以优雅的使用添加多层布局和使用Qt中原有的功能。

Usage / 使用方法

使用的方法也非常简单。父窗口的layout设置为OverlapLayout对象,把每一层的布局设置好,按顺序使用OverlapLayout::AddLayout()添加到多层布局里就好

Doubts / 疑点

目前存在的一个疑问是————如何保证每一层的顺序呢。从官方例子中的CardLayout::setGeometry中看到的表现是,最后设置geometry的QLayoutItem在最上层,在OverlapLayout实际体验中也是如此表现,但是目前还不确定一定会如此

使用中发现和层序有关的问题,在QScrollBar第一次触发hover事件的时候,QScrollBar会提到最上面刷新一下,又回到原来的层次,但是有些控件会被挡住。触发下被挡住的控件将恢复正常,且不再出现异常

Update 1 / 更新1

在使用中发现了控件改变后不能做出有效更新的问题。翻看了QBoxLayout等的源代码后发现里面多处使用到了invalidate()来标记重新计算布局。在源代码中增加其调用后表现正常