写作目的

本文阐述在Qt中实现多页连续的布局管理器的一种方式。

写作本篇文章是因为我一开始希望寻找一篇介绍Qt中多页连续布局的相关文章,但是网络上关于多页连续布局的文章寥寥无几。

仅有的几篇文章都是教我如何将图片绘制在同一个窗口上,也就是将图片或者其他什么Pixmap,Pdf文档渲染出来的东西的多页连续布局。而这些东西不是一个可操作单位,我当然可以用QPainter将其随意绘制在同一个widget上。

但是,当多页连续布局管理的单位变成widget后,会出现很多问题,比如:

  1. 你无法再将widget当做一个QPainter可以绘制出来的项,因为widget没有实体,它自己是实时使用QPainter绘制在窗口上的,我们的父widget获得不了子widget画出来的东西。
  2. 就算我们获得了子widget画出来的东西,那么如何解决它们之间事件传递的逻辑?难道重写一套事件传递的逻辑?

为了解决上述问题,我想到了使用布局管理器。因为布局管理器可以调整子widget的绘制位置的同时,保持事件传递的逻辑。

本篇文章是讨论实现这种布局管理器的一种实现思路,并利用这个思路实现一个极其简单的多页连续布局管理器(如果要真正具有泛用性,那还需要经过详细的设计)。

多页连续布局管理器演示视频

多页连续布局管理器有什么作用?

在阐述这个问题之前呢,我们首先需要了解:我这里所描述的多页连续布局管理器是什么?

以Microsoft Word为例,Word的文本编辑区就可以当做一个使用多页连续布局管理器实现的一个东西:

在这里插入图片描述
它可以进行页与页之间的连续滑动。并且每个页还是一个可以操作的widget。


再比如:PowerPoint中左边的预览栏

在这里插入图片描述
这种可以在一个窗口中,展示多个子widget,能操作子widget并且能滚动展示的东西,我个人认为也是多页连续布局的一种应用。


你也可以用多页连续布局管理器实现一个pdf阅读器。

网上的一些文章实现pdf查看器的时候,他会告诉你直接操作pdf每个页的大小和坐标,将其绘制在画布上,而不是自己造一个多页连续布局管理器。使用多页连续布局管理器,你可以直接将pdf页图片塞到widget中,然后让多页连续布局管理器管理这个widget,这样你就不需要关心坐标,你只需要关心这个图片如何适配widget大小从而得到完美的展示。


总之,多页连续布局管理器能够让你像平常滚动PDF文档一样,滚动你添加到它里面的widget,并且你还可以正常操纵这些widget。

实现原理探讨

布局管理器

我们如果要在Qt中实现多页连续的布局管理器,就必须先对Qt中布局管理器的存在目的和工作原理作一定的了解。

坐标系

对于一个widget来说,它具有自己的坐标系,这个坐标系是一个二维的坐标系,y轴正方向垂直向下,x轴正方向水平向右。

同时一个widget拥有大小,如果一个widget的大小为QSize(w,h),那么这样它就在它自己的坐标系上拥有一个展示它自己和它儿子的区域。这个区域是一个矩形,其左上点为(0,0),右下点为(w,h)。具象化的就是这样。

在这里插入图片描述
一个widget可以通过管理自身在这个蓝色矩形的展示形态从而具备某些实际的功能。

父子关系

我们假设这样一种情况:有一个widget它叫做parentWidget,它旗下有一个儿子widget叫做childWidget

childWidget依附于parentWidget之上,parentWidget会给childWidget分配一个坐标点(起名)。然后childWidget根据这个父亲分配的坐标,以这个坐标点为原点,建立它自己的一个坐标系(以名字为基点,建立自己的社会关系),我们称childWidget通过分配得来的坐标建立的坐标系叫做相对坐标系,因为childWidget坐标系上的所有坐标都是相对于parentWidget分配的坐标点得来的。

childWidget会在相对坐标系上建立自己的领域,具象地表现为:

在这里插入图片描述

控制欲极强的父母

parentWidget在刚生下childWidget的时候,childWidget的范围可能很长一段时间都在parentWidget的管辖之内(即上图的绿色框处于蓝色框之内)。但是,或许是parentWidget的管辖范围变窄,或许是childWidget的活动范围变广,也或许是两者皆有?childWidget最终超出了parentWidget的范围。childWidget一部分范围得不到parentWidget的监管了:

在这里插入图片描述
如果你经常看短视频,你可能会看到这样一种父母:他们有着极强的控制欲,孩子的任何动作都要在父母的监管范围内,你不能关房门,即使关了门他们也可能将孩子房间的房门给拆了…这是一种控制欲变态的表现。具体什么原因导致的并不在我的讨论范围之内,我这里想说的是这种现象映射到Qt的widget中是怎样的。

对于一个widget来说,我本来要展示给用户看的,你怎么能超出范围而展示不全呢?childWidget必须完全处于parentWidget内!

为了满足parentWidget这种极强的控制欲,Qt提供了布局管理器。那么如何做到childWidget的范围严格处于parentWidget的范围之内呢?可以调整childWidget的w和h,让它处于parentWidget的管辖之内。

但有些情况下,比如childWidget“离家出走”了之后,你再怎么缩小childWidget的w和h,它都不可能归parentWidget的范围之内:

在这里插入图片描述
于是,布局管理器为了满足parentWidget的控制欲,它会调整childWidget被分配的坐标(多远都给你抓回来),其次调整childWidget的范围(装小黑屋里,这里说玻璃屋可能更形象[狗头])。

如果有parentWidget有多个子widget,那么布局管理器会干什么?它首先会根据孩子的数量,知道需要多少个小黑屋,然后在parentWidget的范围内设计好小黑屋的排列,当parentWidget的范围发生或孩子数量发生变化时,它调整这些小黑屋的排列。同时,当parentWidget变得小时,它会压缩小黑屋空间。

如果parentWidget太小,而小黑屋没办法再压缩时,布局管理器会警告parentWidget甚至显示parentWidget告诉它不能再减小范围了,再减小的话你的控制欲我没法满足。而parentWidget就纯纯是一个控制欲仙人,为了控制欲他放弃一切,因此它当然接受。

上述行为具象到实际的布局管理器对象,比如QVBoxLayout,就会是:当parentWidget雇佣一个QVBoxLayout来管理N个孩子的时候,QVBoxLayout会在雇主的范围内规划N个小黑屋,这些小黑屋的排列设计为:

在这里插入图片描述
然后将雇主交给它的孩子按照递交的顺序一个个抓进去。


Qt中大部分的布局管理器都是为了满足控制欲极强的widget而存在的,这些布局管理器不会让任何一个雇主交给它的孩子逃出parent的范围。

QLayout这个体系内所有关于坐标管理的接口和类对象,都几乎是给控制欲极强的widget服务的。

说了那么多,我就像明确一点:我们接下来要完成的“多页连续布局管理器”并非为了满足控制欲极强的widget而存在的,不能随便套用原QLayout体系中的大部分接口(因为这不符合那些控制欲不强的父母的本意)。

开明的父母

与控制欲极强的父母不同,开明的父母在孩子超出自己的范围之内会说:走吧走吧,出去走走,看更广阔的世界,有空闲就多回来看看我们就可以啦。

而我们要完成的“多页连续布局管理器”就是为了满足这种parentWidget的需求而存在的。

在这里插入图片描述
parentWidget中的孩子大部分羽翼丰满去往花花世界,但是这些孩子总会留下那么几个陪伴父母,现在是孩子1和2,那下一刻布局管理器可能根据父母对孩子的思念(其实就是滚轮事件wheelEvent移动视角),把1放出去,然后3放进来。

在这里插入图片描述
这样循环往复,每个孩子都能轮流陪伴父母。

具体的实现方式就是当我们需要将原有的一个编号低的页面退出父母范围,让下一个编号高的页面进入父母范围时,我们将所有分配给孩子的坐标减去一个页面的高度。这样就能达到整体上移的效果,下移其实就是所有孩子加上一个页面的高度(或者你想要换人的过程流畅点,那就减小偏移的高度)。

请记住,实现这种布局管理器的时候,对于QLayout中所给QLayoutItem之类的对象的方法最好不使用,因为它们是用来满足控制欲极强的widget的,你搬过来很可能水土不服。最好的方式是与孩子的widget直接交流,也就是直接调用QLayoutItem::widget()获得直接交流的接口进行QWidget原生方法的调用。

即兴表演

接下来,我们按照上面定义的抽象概念,实现一个极其简单的“多页连续布局管理器”。

虽然我们的理念和其他布局管理器不同,但我们的服务目标都是widget,所以继承了QLayout来实现该布局管理器(你不是这个Widget也雇佣不了你QWidget::addLayout()没法调用)。

按照Qt官方文档,或者你使用的是Visual Studio,你可以直接一键实现QLayout的这几个纯虚函数:

    // 通过 QLayout 继承
    QSize sizeHint() const override;

    void addItem(QLayoutItem*) override;

    QLayoutItem* itemAt(int index) const override;

    QLayoutItem* takeAt(int index) override;

    int count() const override;

对于孩子的坐标调整逻辑,你应该重写setGeometry()方法。

    void setGeometry(const QRect& rect); // 这个rect是parentWidget的范围。

由于widget最终会被封装成QLayoutItem(实际上是用QLayoutItem*指向的QWidgetItem),并且该item会传入addItem(),因此我们还需要一个容器来记录这些我们应该管理的widget,这里采用QList<>来完成这个任务:

    QList<QLayoutItem*> items;

与这个容器配套的是以下四个方法:

void MultiPageLayout::addItem(QLayoutItem* item)
{
	items.push_back(item);
}

QLayoutItem* MultiPageLayout::itemAt(int index) const
{
	if (index >= 0 && index < items.size())
	{
		return items.value(index);
	}
	return nullptr;
}

QLayoutItem* MultiPageLayout::takeAt(int index)
{
	if (index >= 0 && index < items.size())
	{
		return items.takeAt(index);
	}
	return nullptr;
}

int MultiPageLayout::count() const
{
	return items.size();
}

重点在于setGeometry(),我们将前面介绍的概念实现在该方法中,因为widget在渲染时会自动调用这个方法来对子widget进行布局:

void MultiPageLayout::moveY(int dy)
{
	// offsetY是一个初值为0的成员变量
	offsetY += dy;
	doLayout();
}

void MultiPageLayout::setGeometry(const QRect& rect)
{
	QLayout::setGeometry(rect);
	doLayout();
}

void MultiPageLayout::doLayout()
{
	int pageY = offsetY;
	for (QLayoutItem* pItem : items)
	{
		QWidget* w = pItem->widget();
		if (w)
		{
			w->move(QPoint(0, pageY));
			pageY += w->size().height() + 5;
		}
	}
}

然后我们定义一个Widget,让它往MultiPageLayout中加100个QPushButton,并定义wheelEvent()用来移动Y轴的偏移坐标, 看看效果如何:

MultiPageWidget::MultiPageWidget(QWidget* parent)
	: QWidget(parent)
	, pageLayout(new MultiPageLayout(this))
{
	int n = 100;
	for (int i = 0; i < n; i++) 
	{
		QPushButton* btn = new QPushButton;
		btn->setText(QString("%1").arg(i));
		btn->resize(300, 300);
		pageLayout->addWidget(btn);
	}
}

MultiPageWidget::~MultiPageWidget()
{
}

void MultiPageWidget::wheelEvent(QWheelEvent* event)
{
	if (event->angleDelta().y() < 0) 
	{
		pageLayout->moveY(-20);
	}
	else 
	{
		pageLayout->moveY(20);
	}
	update();
}

最终效果如下:

在这里插入图片描述

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐