Graphics View 架构

The Graphic View 提供了一个 Surface,用于管理和交互大量定制的 2D 图形化 Item;同时还提供了一个用于可视化这些 Items 的 View 组件,这个 View 支持缩放和旋转。

该框架包括事件传播架构,可以为场景中的 Item 提供精确的双精度交互能力。 Item 可以处理键盘事件,鼠标的按压、释放、移动和双击事件,也可以跟踪鼠标移动。

Graphics View 使用 BSP(二叉搜索树)提供非常快速的 Item 查找,因此,它可以实时显示大型场景,即使包含了数百万个 Item。

Graphics View 在 Qt 4.2 中引入,取代了其前身 QCanvas

Graphic View 的架构

Graphics View 提供了一种基于 Item 的 Model-View 编程方法,和 InterView 中的便捷类 QTableViewQTreeView QListView 一样,多个 View 可以观察单个 Scene,Scene 中包含不同几何形状的 Item。

The Scene

QGraphicsScene 提供了 Graphic View 的场景,场景有以下职责:

  • 提供一个高性能的接口来管理大量的 Items
  • 将事件传播到每个 Item
  • 管理 Item 状态,如选择和焦点处理
  • 提供未被变换的渲染能力,主要用于打印

Scene 作为 QGraphicsItem 对象的容器,通过调用 QGraphicsScene::addItem() 将 Item 添加到 Scene 中。还有许多 Item 查找函数,QGraphicsScene::items() 有多个重载版本,可以返回返回由点、矩形、多边形或向量路径包含或相交的所有 Items 。 QGraphicsScene::itemAt() 返回特定点的最上面的 Item 。 所有 Item 查找函数以降序堆叠顺序返回找到的 Item(即,第一个返回的 Item 是最上面的,最后一个 Item 是最底部的)。

QGraphicsScene scene;
QGraphicsRectItem *rect = scene.addRect(QRectF(0, 0, 100, 100));

QGraphicsItem *item = scene.itemAt(50, 50);
// item == rect

QGraphicsScene 的事件传播架构调度 Scene 事件并传递给 Item,并且也管理 Item 之间的事件传播。 如果 Scene 在特定位置接收到鼠标按压事件,则 Scene 将传递事件到该位置的那个 Item。

QGraphicsScene 还管理某些 Item 状态:如 Item 的选择和焦点。 您可以通过调用 QGraphicsScene::setSelectionArea() 传递一个任意形状来选择 Scene 中的 Item。 这个函数也是在 QGraphicsView 中进行橡皮筋选择的基础。 要获取所有当前选中的 Item,可以调用 QGraphicsScene::selectedItems()QGraphicsScene 处理的另一个状态是 Item 是否具有键盘输入焦点,可以通过调用 QGraphicsScene::setFocusItem()QGraphicsItem::setFocus() 来为 Item 设置焦点,或通过调用 QGraphicsScene::focusItem() 获取当前的有焦点的 Item。

最后,QGraphicsScene 允许您通过 QGraphicsScene::render() 函数将 Scene 的一部分渲染到绘图设备中。 您可以在本文档后面的“打印”部分中阅读更多信息。

The View

QGraphicsView 提供了 View 组件,用于可视化场景中的内容。 您可以将多个 View 附加到同一个 Scene 上,相当于将同一个数据集提供给多个视口。View 组件也是一个滚动区域,并提供用于浏览大型场景的滚动条。 要启用 OpenGL 支持,可以通过调用 QGraphicsView::setViewport()QGLWidget 设置为视口。

QGraphicsScene scene;
myPopulateScene(&scene);

QGraphicsView view(&scene);
view.show();

视图可以从键盘和鼠标接收事件,并将这些事件转换为场景事件(同时将坐标转换为场景坐标),然后将其发送给场景。

使用变换矩阵和 QGraphicsView::transform() ,视图可以变换场景的坐标系。这样可以实现一些高级导航功能:如缩放和旋转。为方便起见,QGraphicsView 还提供了视图和场景之间的坐标映射函数:QGraphicsView::mapToScene()QGraphicsView::mapFromScene()

image

The Item

QGraphicsItem 是场景中图形化 Item 的基类。Graphic View 提供了几个典型形状的标准 Item,如矩形(QGraphicsRectItem),椭圆(QGraphicsEllipseItem)和文本项(QGraphicsTextItem)。当您编写自定义 Item 时,QGraphicsItem 的强力特性都是可用的。除此之外,QGraphicsItem 还支持以下功能:

  • 鼠标按下,移动,释放和双击事件,以及鼠标悬停事件,滚轮事件和上下文菜单事件。
  • 键盘输入焦点和按键事件
  • 拖放
  • 通过父子关系和 QGraphicsItemGroup 进行分组
  • 碰撞检测

每个 Item 都有自己的本地坐标系,像 QGraphicsView 一样,它还提供许多函数,用于在 Item 和 Scene 之间,以及在 Item 和 Item 之间映射坐标。此外,像 QGraphicsView 一样,它也有使用矩阵来变换坐标系的函数:QGraphicsItem::transform(),这对于旋转和缩放各个独立的 Item 很有用。

Item 可以包含其他子 Item。父 Item 的变换会被其所有子Item 继承。除了变换会被累积, Item 的所有其他函数(例如,QGraphicsItem::contains()QGraphicsItem::boundingRect()QGraphicsItem :: collidesWith() 仍然在本地坐标中运行。

Item 可以通过 QGraphicsItem::shape() 函数进行碰撞检测,这个函数和 QGraphicsItem::collidesWith() 都是虚函数。利用 QGraphicsItem::shape() 返回的 Item 形状作为 QPainterPath 的局部坐标系,Item 可以处理所有的碰撞检测。但是,如果要提供自己的碰撞检测,可以重新实现 QGraphicsItem::collidesWith()

The Graphics View 的坐标系

Graphics View 基于笛卡尔坐标系; Item 在场景中的位置和形状由两组数字表示: x 坐标和 y 坐标。 当使用没有进行过坐标变换的视图观察场景时,场景上的一个单位就是屏幕上的一个像素。

注意:Graphics VIew 使用 Qt 的坐标系,不支持倒置的Y轴坐标系(即 y 向上增长)。

这样,Graphics View 中就有三套的坐标系:Item 坐标、Scene 坐标和 View 坐标。 为了简化您的实现,Graphics View 提供了便于三个坐标系之间进行映射的函数。

渲染时,Scene 坐标对应于 QPainter 的逻辑坐标,View 坐标相当于设备坐标。 在《坐标系统》文档中,您可以阅读有关逻辑坐标和设备坐标之间的关系。

Item 坐标系

Item 有自己的本地坐标系,坐标原点为 (0, 0) ,这也是所有变换的原点。 Item 坐标系中的几何图元通常被称为点、线或矩形。

当创建自定义 Item 时,唯一需要操心的就是 Item 坐标系; QGraphicsSceneQGraphicsView 会执行所有变换,这使得实现自定义项目很容易。例如,如果您接收到鼠标点击或拖动事件,事件位置将以 Item 坐标系给出。 QGraphicsItem::contains() 函数接收一个 Item 坐标系中的坐标参数,如果这个坐标在 Item 上则返回 true,否则返回 false 。类似地,Item 的边界和形状也是在 Item 坐标系中描述的。

Item 的位置是 Item 原点在其父系坐标系中的坐标,称为父坐标。Scene 被认为是所有无父元素的 Item 的 Parent,因此,顶层 Item 的位置在 Scene 坐标系中描述。

要将子元素的座标映射到父元素的坐标系中,需要经过一番计算,例如:没有变换的子元素的原点如果精确位于其父元素的原点上,则父子两个 Item 的坐标系将相同;然而,如果子元素原点的位置在父坐标系中是(10,0) ,则子元素坐标系中的 (0,10) 在父坐标系中为 (10,10)

由于子元素的坐标系只参照父元素,因此子元素的坐标系不受父元素变换的影响。在上面的例子中,即使父元素被旋转和缩放,子元素中的(0,10) 点映射到父元素坐标系中仍然是 (10,10) 点。然而,在 Scene 坐标系中,如果 Scene 坐标系发生了变换和重新定位。如 Scene 被缩放 (2x,2x) ,则子元素的 (0, 0) 位置在 Scene 坐标系中变为 (20,0) ,且其 (10,0) 点在 Scene 坐标系中变为 (40,0) 。Scene 坐标系可以看做是世界坐标系。

除了 QGraphicsItem::pos() 等少数例外,其他 QGraphicsItem 的函数都是在 Item 坐标系中操作,而不考虑 Item 自身或者其父元素的变换。例如,Item 的边界矩形(即 QGraphicsItem::boundingRect())总是在 Item 坐标坐标系中。

Scene 坐标系

Scene 坐标系是 Item 的基准坐标系,每个顶层 Item 都是定位在 Scene 坐标系中,从 View 传递到 Scene 的所有事件也在 Scene 中定位。 Scene 中的每个 Item 除了在本地坐标系中有定位和边界矩形之外,在场景坐标系中也具有场景位置 "scene position" 和边界矩形(QGraphicsItem::scenePos()QGraphicsItem::sceneBoundingRect())。场景位置描述 Item 在场景坐标系中的位置,而场景边界矩可以让 Scene 确定场景的哪些区域已经发生了改变。 Scene 中的变化通过 QGraphicsScene::changed() 信号传递,参数是场景矩形的列表。

View 坐标系

View 坐标系是组件的坐标系,View 坐标系中的每个单位对应于一个像素。 View 坐标系的全部控件就是组件窗口或者视口,它不受所观察的 Scene 影响。GraphicsView 视口的左上角始终为 (0,0) ,右下角始终为 (视口宽度,视口高度) 。 所有的鼠标事件和拖放事件最初发生在 View 坐标系中,您需要将这些坐标映射到 Scene 中以便与 Item 进行交互。

坐标系映射

在处理场景中的 Item 时,常常需要将坐标和形状从 Scene 映射到 Item、或者从一个 Item 映射到另一个 Item,或者从 View 映射到 Scene 。例如,当您在 QGraphicsView 的视口中单击鼠标时,您可以通过调用 QGraphicsView::mapToScene() ,然后再调用 QGraphicsScene::itemAt() 向场景查询该 Item 。如果想确定 Item 所在视口中的位置,可以在 Item 上调用 QGraphicsItem::mapToScene() ,然后再调用视图的 QGraphicsView::mapFromScene()。最后,如果您想要查看 View 中某个椭圆内部的内容,可以将 QPainterPath 传递给mapToScene() ,然后将映射路径传递给 QGraphicsScene::items()

您可以通过调用 QGraphicsItem::mapToScene()QGraphicsItem::mapFromScene() 来映射 Scene 和 Item 之间的坐标和形状。也通过调用 QGraphicsItem::mapToParent()QGraphicsItem::mapFromParent() 或通过调用 QGraphicsItem::mapToItem()QGraphicsItem::mapFromItem() 在父子元素或者不同元素之间映射坐标。所有映射函数都可以映射点、矩形、多边形和路径。

视图中提供了相同的映射功能,用于 View 与 Scene 之间的相互映射: QGraphicsView::mapFromScene()QGraphicsView::mapToScene() 。要从视图映射到项目,您首先映射到场景,然后从场景映射到项目。

关键特征

缩放和旋转

QGraphicsView 通过 QGraphicsView::setMatrix() 支持与 QPainter 相同的仿射变换能力。 通过对视图应用变换,可以轻松添加对缩放和旋转的支持。

以下是在 QGraphicsView 子类中实现缩放和旋转槽的示例:

class View : public QGraphicsView
  {
  Q_OBJECT
      ...
  public slots:
      void zoomIn() { scale(1.2, 1.2); }
      void zoomOut() { scale(1 / 1.2, 1 / 1.2); }
      void rotateLeft() { rotate(-10); }
      void rotateRight() { rotate(10); }
      ...
  };

这些槽可以被 QToolButtons 连接,并开启 autoRepeat 属性

当变换 View 时, QGraphicsView 保持原点的位置

打印

Graphics View 通过其渲染函数 QGraphicsScene::render()QGraphicsView::render() 提供一行打印能力。 这些渲染函数提供相同的接口:即将 QPainter 传递给渲染函数,场景或视图即会将他们的全部或部分内容绘制到绘画设备中。 此示例显示如何使用 QPrinter 将整个场景打印到一个页面中。

QGraphicsScene scene;
  scene.addRect(QRectF(0, 0, 100, 200), QPen(Qt::black), QBrush(Qt::green));

  QPrinter printer;
  if (QPrintDialog(&printer).exec() == QDialog::Accepted) {
      QPainter painter(&printer);
      painter.setRenderHint(QPainter::Antialiasing);
      scene.render(&painter);
  }

场景和视图渲染功能之间的区别在于:一个在 Scene 坐标中操作,另一个在 View 坐标中。 QGraphicsScene::render() 常常用来打印未变换过的场景,例如绘制几何数据或打印文本文档; 另一方面,QGraphicsView::render() 更适合打印屏幕截图,其默认行为是使用提供的绘制设备来精确呈现视口中的内容。

QGraphicsScene scene;
scene.addRect(QRectF(0, 0, 100, 200), QPen(Qt::black), QBrush(Qt::green));

QPixmap pixmap;
QPainter painter(&pixmap);
painter.setRenderHint(QPainter::Antialiasing);
scene.render(&painter);
painter.end();

pixmap.save("scene.png");

当源和目标区域的大小不匹配时,源内容被拉伸以适应目标区域。 通过将 Qt::AspectRatioMode 传递给正在使用的渲染函数,您可以选择在内容拉伸时保持或忽略场景的宽高比。

拖拽

因为 QGraphicsView 间接继承了QWidget ,所以它已经提供了与QWidget 相同的拖放功能。 此外,为了方便起见,Graphics View 框架为Scene 以及 Item 都提供了拖放支持。 当 View 接收到拖拽时,它将拖放事件转换为 QGraphicsSceneDragDropEvent ,然后将其转发给 Scene ,Scene 接管此事件的调度,并将其发送给接受区域的鼠标下的第一个 Item。

要拖拽 Item,需要先创建一个 QDrag 对象,然后将这个对象传递给启动拖拽的 Widget 。Item 可以被多个 View 观察,但只有一个 View 可以开始拖动。 在大多数情况下,拖放是由于按住或移动鼠标而启动的,因此可以在mousePressEvent()mouseMoveEvent() 中,获取启动的 Widget。

void CustomItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
 {
      QMimeData *data = new QMimeData;
      data->setColor(Qt::green);

      QDrag *drag = new QDrag(event->widget());
      drag->setMimeData(data);
      drag->start();
 }

要拦截场景的拖放事件,需要在 QGraphicsItem 子类中重新实现QGraphicsScene::dragEnterEvent() 以及其他场景需要的事件处理程序。 可以在 QGraphicsScene 的每个事件处理程序的文档中阅读更多关于 Graphics View 中拖放的信息。

Item 可以通过调用 QGraphicsItem::setAcceptDrops() 来启用拖放支持。 要处理传入的拖动,需要重新实现 QGraphicsItem::dragEnterEvent()QGraphicsItem::dragMoveEvent()QGraphicsItem::dragLeaveEvent()QGraphicsItem::dropEvent()

另请参阅《拖放机器人》示例,以显示 Graphic View 对拖放操作的支持。

光标和提示信息

QWidget 一样,QGraphicsItem 也支持游标(QGraphicsItem :: setCursor())和工具提示(QGraphicsItem :: setToolTip())。 当鼠标光标进入 Item 的区域(通过调用 QGraphicsItem :: contains()进行检测,游标和工具提示将被 QGraphicsView 激活,。

您还可以通过调用 QGraphicsView :: setCursor() 直接在视图上设置默认光标。

另请参阅拖放机器人示例,用于实现工具提示和光标形状处理的代码。

动画

图形视图支持多个级别的动画。 可以使用动画框架轻松组合各种动画。 为此, Item 需要继承 QGraphicsObjectQPropertyAnimationQPropertyAnimation 允许将任何 QObject 的属性动画化。

另一个选项是创建一个继承自 QObjectQGraphicsItem 的自定义Item, 该 Item 可以设置自己的定时器,并在 QObject :: timerEvent() 中控制动画。

第三个选项,主要用于与 Qt 3 中的 QCanvas 兼容,通过调用QGraphicsScene :: advance() 来进一步调用 QGraphicsScene :: advance() ,这又调用了 QGraphicsItem :: advance()

OpenGL 渲染

要启用 OpenGL 渲染,您只需通过调用 QGraphicsView :: setViewport() 将新的 QGLWidget 设置为 QGraphicsView 的视口。 如果您希望 OpenGL 具有抗锯齿,则需要 OpenGL 示例缓冲区支持(请参阅QGLFormat :: sampleBuffers() )。

QGraphicsView view(&scene);
view.setViewport(new QGLWidget(QGLFormat(QGL::SampleBuffers)));

Item 组

通过使 Item 成为另一个 Item 的孩子,可以实现基本的分组功能:所有 Item 将一起移动,所有变换都会从父级到子级传播。

除此之外,QGraphicsItemGroup 是一个特殊的 Item,它将子事件处理与一个有用的界面相结合,用于向组中添加和删除项目。 将项目添加到QGraphicsItemGroup 将保留项目的原始位置和转换,而一般来说,重新启动项目将导致该子项相对于其新父项重新定位。 为方便起见,您可以通过调用 QGraphicsScene :: createItemGroup() 通过场景创建QGraphicsItemGroups

Widget 与布局

Qt 4.4 通过 QGraphicsWidget 引入了对几何和布局敏感的 Item 的支持。这个特殊的基本 Item 类似于 QWidget,但它不继承自 QPaintDevice,而是继承自 QGraphicsItem 。这允许您编写具有事件、信号和插槽、大小提示和策略的完整 Widget ,还可以通过QGraphicsLinearLayoutQGraphicsGridLayout 布局来管理 Widget 几何属性。

QGraphicsWidget

QGraphicsWidget 基于 QGraphicsItem ,提供了两个最好的功能:QWidget 的额外功能,如样式,字体,调色板,布局方向及其几何特性,以及 QGraphicsItem 的分辨率独立性和变换支持。因为Graphic View 使用实际坐标而不是整数,所以 QGraphicsWidget 的几何特性函数可以处理QRectFQPointF ,这也适用于框架矩形,边距和间距。因此,使用 QGraphicsWidget ,例如指定内容边距 (0.5,0.5,0.5,0.5) 并不罕见。您可以创建子窗口和“顶级”窗口;在某些情况下,甚至可以使用 Graphics View 创建高级 MDI 应用程序。

支持一些 QWidget 的属性,包括窗口标志和属性,但不是全部。您应该参考 QGraphicsWidget 的类文档,以了支持以及不支持属性的完整概述。例如,您可以通过将 Qt :: Window 窗口标志传递给 QGraphicsWidget 的构造函数来创建装饰窗口,但是 Graphics View 目前不支持在 macOS 上常见的 Qt :: SheetQt :: Drawer标志

QGraphicsLayout

QGraphicsLayout 是专为 QGraphicsWidget 设计的第二代布局框架的一部分。它的 API 非常类似于 QLayout 。您可以在 QGraphicsLinearLayoutQGraphicsGridLayout 内部管理 Widgets 和子布局。您也可以通过子类化 QGraphicsLayout 来轻松的编写自己的布局,或者通过 QGraphicsLayoutItem 的适配器子类将自己的 Item 项添加到布局中。

对嵌入常规 Widget 的支持

Graphics View 提供了将常规 Widget 嵌入到 Scene 中的无缝支持。您可以嵌入简单的 Widget ,例如 QLineEditQPushButton ,也可以嵌入复杂的 Widget,如 QTabWidget ,甚至是一个完整的 main window。要将 Widget 嵌入到 Scene 中,只需调用 QGraphicsScene :: addWidget() ,或者创建一个 QGraphicsProxyWidget 的实例来手动嵌入。

通过 QGraphicsProxyWidget ,Graphics View 能够深入集成客户 Widget 的各种功能:包括其光标、工具提示、鼠标、平板电脑和键盘事件、子窗口小部件、动画、弹出窗口(例如 QComboBoxQCompleter )以及窗口小部件的输入焦点和激活。QGraphicsProxyWidget 甚至集成了嵌入式小部件的 tab order,以便您可以用 tab 键进入和退出嵌入 widget。您甚至可以在您的场景中嵌入一个新的 QGraphicsView ,以提供复杂的嵌套场景。

在转换嵌入式窗口小部件时,Graphics View 确保窗口小部件独立转换分辨率,允许字体和样式在放大时保持清晰。(请注意,独立性的影响取决于样式。)

性能

浮点指令

为了准确快速地将变换和效果应用于项目,Graphics View 的构建假设是用户的硬件能够为浮点指令提供合理的性能。

许多工作站和台式计算机都配备了适当的硬件来加速这种计算,但是一些嵌入式设备只能提供库来处理数学运算或模拟软件中的浮点指令。

因此,某些设备的某些效果可能比预期的慢。 可以通过在其他领域进行优化来弥补这种性能的影响; 例如,通过使用OpenGL渲染场景。 然而,如果任何此类优化还依赖于浮点硬件的存在,本身可能会导致性能下降。

推荐阅读更多精彩内容

  • 简述 图形视图(Graphics View)提供了一个平台,用于大量自定义2D图元的管理与交互,并提供了一个视图部...
    YBshone阅读 3,676评论 0 3
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 144,064评论 18 619
  • View Geometry and Coordinate Systems 二、视图几何和坐标系统 The defa...
    大灰很阅读 541评论 0 1
  • 2016-03-04 宝木大仙 浅白轩 午后阳光正好,照在窗前的地上明晃晃的一片光亮,小女孩甚至觉得,那可能是一片...
    宝木大仙阅读 113评论 0 1
  • 前言 首先我要感谢粉丝,读者和王小二读书写作俱乐部成员给予我的支持!给了我继续写作下去的勇气…… 文化基础方面的弱...
    杨殿国阅读 287评论 1 4