Qt中利用GraphicsView实现可部分擦除的Item的思路解析

可部分擦除Item的抽象概念解析

数学意义上来说,一条线其实并没有宽度的概念,它是由无数的点连接而成的。而面积,是由无数的线所构成的。

在现实中却并非如此,无论你用什么笔,点都会具有面积。这就意味着我们的线也有面积。这就意味着,我们的白板上的笔所留下来的痕迹也需要“模拟”这一特性。

比如说,我们画矩形的时候,矩形的边并非是由一条线组成,而是另一些有着面积的矩形构成的,只不过这个矩形的宽非常窄。就像下面这样:

只是我平常没有注意到,下意识地认为自己所连的一条线,真的就是数学意义上的线。因此,我们使用Qt中有关连线的接口,所画出来的线Line,也不是我们所想要的“线”。我们所想要的线,应该是Qt所称的填充区域的东西!

我们所要完成的笔画,其实就是一种填充区域,这个填充区域模仿了现实中的笔画。比如,我们想要在程序中画一条直线,它并不是一条直线,而是经过看起来像是直线的填充区域,就像下面这张图所示:

因为其宽度足够小,所以人在感官上就觉得这是一条直线,但实际上不是,它是一个有面积的填充区域。

这样抽象的好处是什么?

假如,我们将Qt中所提供的线,当做我们所想要模拟的现实中的“线”,情况怎么样?

Qt中的线,完全是按照数学中的定义来进行处理的。

这样做的话,假如我们有一条Qt中的线line,和一个橡皮擦所覆盖的区域S。我们规定:与橡皮擦S碰撞的所有线的部分,都必须被擦除。

line与S确实发生了碰撞,然后我们试图使用S(用QPainterPath表示)的intersected(line)来获取碰撞的区域,你什么也不会得到,因为Qt中碰撞产生的基础是碰撞的双方必须具有面积,而line没有面积,因此它不会和任何东西产生碰撞!这就与我们现实中的抽象产生了冲突,在我们的认知中line和S它们确实发生了碰撞。

这个问题产生的根本原因就在于,数学意义上的“线”它没有面积,它本不应该有任何现实意义的呈现方式。但是Qt为了表现出线是一条线,它在渲染的时候给线添加了宽度,让我们擅自认为Qt所渲染的线,其实和现实中的线一样是有宽度的,但其实并非如此。

Qt中渲染的线的宽度并非是线本身的概念,而是渲染它的QPen所赋予的。

可部分擦除的Item的行为逻辑抽象

正是因为Qt中的线无法模拟现实中的线,所以我们必须要自定义自己的线用来模拟现实中的笔所画的线。

以矩形为例,我们的矩形,并不是由Qt中的线概念形成的矩形,也就是QRect并不能表示我们想要的矩形(QRect认为由矩形围起来的区域都是矩形本身)。

我们的矩形仅包括模拟的线的填充区域本身。也就是这张图的红色区域。

这两者是完全不同的概念,举个例子:对于QRect来说,S(橡皮擦)只要和矩形内部有相交区域,那么就可以被当做出现了碰撞;然而,对于我们的矩形来说,S可以和红色框内部的区域相交,且不认为这是碰撞。这对我们简化了我们处理橡皮擦碰撞的处理逻辑,你不用再担心橡皮擦有没有真正碰撞到矩形的边框了(这个边框的宽度会随着QPen而变动,且无法通过Qt中给定的碰撞接口来获取)。

那么我们的矩形在遇到擦除的时候该怎么处理呢?假设矩形名为myRect(用QPainterPath表示),橡皮擦区域为为S(用QPainterPath表示),你就可以直接调用
remain = myRect.subtracted(S);就可以将剩余的矩形轨迹获取出来存放到remain中。剩余的矩形区域可能是什么样的呢?可以是这样的:

擦除之后就不再是矩形了,那用什么表示呢?

我想出了这样一种抽象:

其实不论是矩形,还是圆形,都是使用笔所画出来的填充区域,擦除之后依旧可以认为是一个笔画。而矩形只是使用特殊的笔画顺序所生成的一个整体而已。同时,这个整体在初始化时因碰撞而连接,在初始化完成之后不会因为碰撞而连接(这个初始化阶段类比于我们下笔到笔离开纸的过程),当我们再次下笔,那么新形成的笔画不应该和原来在纸上的笔画有任何形式的联系。

同时,笔画在形成后不会因新笔画的碰撞而连接,但却有可能因为与橡皮擦碰撞而分离。橡皮擦的碰撞不一定导致笔画的分离,比如:

但有的时候,擦除会使得一个笔画分离,比如:

这样的情况下,一个笔画就被分成了两个笔画,我们应该将这原本是一个笔画的部分分成两个笔画。这样做的技术难度也不大,就是使用QPainterPath::toFillPolygons()将返回的填充区域都封装成一个新笔画,然后自己再消失就行了。这样笔画之间的关系就被分离,每个笔画可以被单独移动、擦除。