Qt Graphics View Framework 预研

该架构涉及到的最主要的三个类是QGraphicsSceneQGraphicsViewQGraphicsItem

整体流程

  1. 绘制流程(QGraphicsItem->QGraphicsScene->QGraphicsView

    QGraphicsItem当中保存了自身的“场景坐标”供QGraphicsScene进行管理。在绘制时,由QGraphicsView对象调用渲染方法,根据自身所设置的可视化相关属性,基于“视图坐标”确定将要绘制的QGraphicsScene当中有哪一部分“场景坐标”内的图元需要渲染,随后通过QGraphicsScene提供的方法,将属于这部分“场景坐标”内的图元(也就是QGraphicsItem)全部找出,并渲染这些图元到可视化viewport中。

  1. 事件流转(QGraphicsView->QGraphicsScene->QGraphicsItem

    QGraphicsView绘制出的viewport是与用户直接交互的对象,用户发起的UI事件都由QGraphicsView首先接收,它在接收到事件以后对其中的部分参数进行适当的处理(如鼠标事件的坐标进行转换),随后将事件转发给QGraphicsScene对象,由QGraphicsScene确定事件发送到哪个具体的图元(如鼠标事件发送到符合坐标位置的图元,键盘事件发送到当前焦点所在的图元),图元在接收到事件以后作自行处理。

  2. QGraphicsScene的查询图元的BSP树相关算法

    在实际的工程场景中,极有可能会出现一个QGraphicsScene管理非常多个QGraphicsItem,例如几百万个,当用户与QGraphicsView交互的时候,需要经过QGraphicsScene将本事件传递到某个或者某些具体的图元,如果挨个遍历每个图元,并且判断是否触发事件,那么耗费的代价就太大了。我们希望做到的效率是几百万条数据在几毫秒以内能够确定目标图元,因此需要上相应的算法。

  3. 图元实现优先级以及顺序和事件实现优先级以及顺序

    QGraphicsItem本质上是一个抽象父类,为子类规定了各种需要覆写的方法,子类通过重写这些方法能够很好的被QGraphicsScene所管理,最终呈现出来。因此不仅支持官方提供的矩形、椭圆、文本框等,还能处理自定义类型的图元。

    在现阶段,先不考虑纯虚的抽象类,先把QGraphicsItem当成矩形框来做,先和QGraphicsScene把流程跑通,后续在扩展的时候也能非常快就完成工作。

    事件处理是一个大块。经讨论,事件处理的优先级是鼠标事件大于键盘事件大于拖拽事件。因此第一步实现的时候,先实现最基本的handleMousePressEvent,当这个流程通了以后,后续的所有操作都是一模一样的了。

  4. 整体实现顺序

    先把QGraphicsView剔出来,QGraphicsView实际上只负责可视化的绘制,真正做处理的操作在QGraphicsScene这一层,因此管理类QGraphicsScene和图元类QGraphicsItem是第一步需要实现的。

    目前的工作计划经商讨以后,决定先调研QGraphicsSceneQGraphicsItem的源码,明确如何以最小的代价实现我们目前需要的功能,包括事件转发、绘制逻辑、坐标系统等,这样明确需求和设计以后再实现初版,搭好整体架构。

重要功能总结

QGraphicsScene

QGraphicsScene类提供了一个管理大量QGraphicsItem的容器。

QGraphicsScene类只管理所有的图元,若想要可视化场景,需要配合QGraphicsView

QGraphicsItem作为QGraphicsScene类的图元,自然有很多接口都是围绕着QGraphicsItem来的,例如:

  1. 添加图元:除了最基本的addItem(),还有更多针对特殊类型图元的add*()函数

    image-20240719141156412

  2. 查找图元:在大规模数据的情况下保证在极短的时间内查询到对应的图元,Qt使用的是索引算法,使用的是BSP(二进制空间分区)树。能够在极短的时间内在极大的数据规模中确定某个图元的位置,这也是QGraphicsScene的最大优势之一。

  3. 移除图元:removeItem()

  4. 管理图元状态,处理图元选择和焦点处理等

    • setSelectionArea():可以传递一个形状范围来选择图元项目
    • clearSelection():清除当前选择
    • selectedItems():获取被选择的图元的列表
    • setFocusItem()、setFocus():设置图元获取焦点
    • focusItem():获取当前焦点
  5. 接受事件:用户通过QGraphicsView触发事件,并通过QGraphicsScene将事件转发到对应的图元:例如鼠标按下、移动、释放和双击事件,鼠标悬停(LarkSDK目前好像没有)、滚轮事件,键盘输入焦点和按键事件,拖拽事件等

    image-20240719141220363

    QGraphicsScene另一个重要的功能就是转发QGraphicsView的事件,通过一系列操作,例如通过鼠标点击的坐标计算出到底是选中了哪个图元,键盘事件对应的哪些图元具有焦点等,能够将这些事件转发给对应的图元,最后进入真正的事件循环进行处理。

QGraphicsView

QGraphicsView类真正提供可视化QGraphicsScene的内容的功能,它在一个可滚动的viewport之内将一个QGraphicsScene中的内容实现可视化。

QGraphicsView主要功能包括但不限于:

  1. 设置可视化操作的属性:QGraphicsView中提供了大量可设置的属性用以指示在实现可视化操作时的各种具体事项,如RenderHints提供参数初始化用于绘制的QPainterAlignment提供当前视图中所绘制的场景的对齐方式。

    image-20240719141236742

  2. 对场景(Scene)进行可视化与视觉效果调整:QGraphicsView对象的成员方法render对场景进行可视化的绘制呈现在viewport中,并提供了一系列方法对viewport整体的视觉效果进行调整,如centerOn方法将滚动viewport中的内容以确保场景坐标pos在视图居中,fitInView方法将缩放并滚动viewport中的内容使得场景内的矩形区域rect铺满当前viewport

    image-20240719141250017

  3. 管理“场景(Scene)坐标”与“视图(View)坐标”之间的数学关系,并提供方法对视图内容施行各种坐标变换。QGraphicsScene对象当中的各个图元有其在QGraphicsScene中的坐标即“场景坐标”,它们代表了各个图元在QGraphicsScene中的位置信息;而QGraphicsView对各个图元进行绘制以及调整变换时则是通过由自身管理的“视图坐标”,它们代表了各个要绘制的图形在viewport中的位置信息。QGraphicsView可以由用户设置“场景坐标”向“视图坐标”变换的方式,对viewport实现旋转、伸缩等坐标变换,同时由于QGraphicsView的绘图使用“视图坐标”,因此这个过程不会干扰图元自身的“场景坐标”。此外还提供mapToScene/mapFromScene方法供用户调用实现这两种坐标之间的数学换算。

  4. 接受鼠标和键盘的事件,并通过处理传递给QGraphicsScene对象,进而通过索引算法转发给对应的图元。

QGraphicsItem

QGraphicsItem是所有图元的基类,可以派生出各种典型的形状(例如矩形、椭圆、文本等)和自定义的形状。

QGraphicsItem主要功能包括但不限于:

  1. 接受QGraphicsScene传递的事件:进行事件处理,例如鼠标按下、移动、释放和双击事件,鼠标悬停、滚轮事件,键盘输入焦点和按键事件,拖拽事件等。

    image-20240719141308564

  2. 坐标系统

    每个Item都有自己的本地坐标系,一般以自身的中心为(0, 0),自身的方向作为基准方向建立,多个Item的情况就如图。因此需要通过某些机制将不同Item的坐标联系在一起。

    为了统一方便的管理,引入parent-child的关系。每个对象的变换都依赖于其父对象的坐标。子对象的pos()接口返回的是其在父对象坐标系统中的坐标。子对象的坐标处理是首先通过父对象不断向上传递,最终得到一个真实的坐标。同理,父对象的坐标变换也会同理影响到子对象的真实位置(批量处理),但是注意子对象存储的坐标没有变化,这样就非常好维护了。

    image-20240719141330634

  3. 坐标变换

    QGraphicsItem除了提供基本位置pos()以外,还支持坐标变换,例如setRotation()旋转,setScale()缩放等。

    同理,父对象的变换会影响子对象,例如父对象顺时针转90度,子对象会跟着一起转90度。

  4. 提供分组功能(调研是否需要当前考虑)

    通过前面的坐标系统可以知道,每个QGraphicsItem都有一个父对象,也可以有一系列子对象,这是类似于对象树机制的在构造的时候确定的关系。 当然也可以手动分组。QGraphicsItemGroup是一个特殊的派生类,该类记录了一系列的图元为一个组,在该组的所有图元通过Group调用的时候移动、事件处理等都会进行批量处理。

  5. 提供编写自定义图元的接口(后续考虑)

    创建一个QGraphicsItem的子类,然后覆写两个纯虚函数boundingRect()返回该图元项目所绘制区域的估计值,paint()实现实际绘图。

    boundingRect()返回的是一个QRectF类型,将该图元的外部估计边界定义为矩形,这个方法也可以为不同Item的范围做大致的估算,可以被其他的方法所调用,省去一些暴力查找的过程。当然对于真正的矩形boundingRect()可以返回精确的范围,对于其他的曲线或者不规则的形状只能做大致的范围。

  6. 碰撞检测(后续考虑)

    通过shape()函数和collidesWith()这两个虚拟函数,可以支持碰撞检测。shape()函数返回一个局部的坐标QPainterPath。目前没有细节调研QPainterPath,只知道QPainterPath记录了绘图的路径,比如2D图形的形状是由某些直线、曲线等构成的,通过这个能够确定图形的形状。

    QGraphicsItem会根据默认的shape()函数自动处理碰撞检测,实现合理的效果,比如在碰撞区域应该如何进行绘制。如果用户想要定义自己的碰撞检测,可以通过collidesWith()实现。

  7. 图元顺序(后续考虑)

    难免会发生两个图元的范围出现重叠的情况。合理处理顺序决定了鼠标点击的时候哪些场景会接受鼠标事件。一个比较合理的想法是子对象位于父对象的顶部,而同级对象之间按照定义的顺序进行堆叠。例如添加对象A、B,那么对象B位于A的顶部。这是比较符合自然逻辑的,Qt也是这样做的。

    Qt提供了一些可以更改项目的排序方式的接口。例如可以在一个图元项目上调用 setZValue(),以将其显式堆叠在其他同级项目之上或之下。项的默认Z值为0,具有相同Z值的项按插入顺序堆叠。还可以设置ItemStacksBehindParent标志以将子项堆叠在其父项之后。

2D BSP 树在 QGraphicsScene 中的应用

BSP树构造一个n维空间到凸子空间的分层细分(a BSP tree is a heirarchical subdivisions of n dimensional space into convex subspaces)。每个节点都有一个前叶子节点和后叶子节点。从根节点开始,所有后续插入都由当前节点的超平面划分。在二维空间中,超平面是一条线。 在3维空间中,超平面是一个平面。BSP树的最终目标是让超平面的分布情况满足“每个叶节点都在父节点超平面的前面或后面”(The end goal of a BSP tree if for the hyperplanes of the leaf nodes to be trivially "behind" or "infront" of the parent hyperplane.)。

BSP树对于与静态图像的显示进行实时交互非常有用。 在渲染静态场景之前,需要计算BSP树。 可以非常快地(线性时间)遍历BSP树,以去除隐藏的表面和投射阴影。 通过一些工作,可以修改BSP树以处理场景中的动态事件。

下面是在对象空间构建BSP树的过程:

  1. 首先,确定要划分的世界区域以及其中包含的所有多边形。为了方便讨论,我们将使用一个二维世界。

  2. 创建一个根节点L,该节点本身对应一个分区超平面(在二维世界中,分区超平面就是直线);同时这个节点维护一个多边形列表,在节点刚刚创建时,多边形列表中保存着它对应的超平面所划分的目标区域(对 L 而言,它对应的直线所划分的目标区域就是整个二维世界)当中的所有多边形。

  3. 使用L对世界进行分区,假设分为了两个区域AB。在根节点上创建两个叶子节点分别对应AB,并将世界中的所有多边形移动到AB的多边形列表中。遵循以下规则:

    对于世界中的每个多边形:

    • 如果该多边形完全位于A区域,请将该多边形移动到A区域的节点列表中。
    • 如果该多边形完全位于B区域,请将该多边形移动到B区域的节点列表中。
    • 如果该多边形与L相交,则将其拆分为两个多边形,并将它们移动到AB的相应多边形列表中。这种情况下,算法必须找到多边形与分割线L的交点,以确定多边形的哪一部分位于哪个区域。
    • 如果该多边形与L重合(也就是说,恰好是一条位于L上的线段),请保持其仍位于节点L处的多边形列表中。

    在上述步骤完成后,节点AB的多边形列表已经生成,但是AB尚未完成创建分区超平面并划分的步骤,也就是说,对于当前的节点A,它的状态与步骤1中的“整个二维世界”一致(区域确定,包含的多边形确定),只不过区域大小有差别,B同理。

  4. AB上继续划分其所对应的二维区域,并将上述算法递归地应用于AB的多边形列表。

    image-20240719113430085

更多细节和代码实现请参考博客:一些关于空间数据结构的简单研究与实现 | GilbertNewtonLewis

参考文档

  1. https://doc.qt.io/qt-6/graphicsview.html
  2. https://www.cnblogs.com/q735613050/p/13413866.html
  3. https://blog.csdn.net/keilert/category_12595359.html
  4. https://blog.csdn.net/weixin_43925768/article/details/129138298
  5. https://blog.csdn.net/ZJU_fish1996/article/details/52554620
  6. https://blog.csdn.net/packdge_black/article/details/114681992
使用搜索:必应百度