图形视图框架
图形视图框架提供了一个用于大量自定义2D图元的管理和交互界面, 这个界面是一个可视化图元的视图widget, 支持缩放和旋转操作.
图形视图框架包含一套事件分发架构, 它允许对场景元素进行双精度交互操作. 场景元素支持键盘事件, 鼠标(点击, 移动, 释放,双击, 轨迹追踪)事件.
图形视图框架采用BSP(Binary Space Partitioning 二叉空间分割)树提供非常快速的图元查找能力, 因此, 它能实时可视化具有大量元素(百万级别)的场景.
Qt从4.2版本支持图形视图框架, 替代之前版本的QCanvas.
Topics:
图形视图框架结构
图形视图框架提供了一种基于图元, 近似于Model/View的编程方式, 很像InterView类型的类: QTableView, QTreeView 和 QListView. 多个视图可以查看同一个场景, 这个场景包含不同形状的图元.
场景
QGraphicsScene 提供图像视图框架中的场景. 这个场景负责以下职责:
- 提供管理大量图元的快捷接口
- 为每一个图元分发事件
- 管理图元状态, 如选择和焦点
- 提供未转换的渲染功能; 主要用于打印
QGraphicsScene是 QGraphicsItem 的容器类. 通过调用QGraphicsScene::addItem()将图元加入场景, 场景类提供许多图元检索函数. QGraphicsScene::items()及其重载函数返回全部图元或者满足特定条件(与点, 矩形, 多边形或轨迹相交)的图元. QGraphicsScene::itemAt() 返回位于特定点最顶层的图元. 所有图元检索函数以降序方式返回图元 (i.e., 第一个图元位于最顶层, 最后一个图元位于底部).
QGraphicsScene scene; QGraphicsRectItem *rect = scene.addRect(QRectF(0, 0, 100, 100)); QGraphicsItem *item = scene.itemAt(50, 50, QTransform());
QGraphicsScene的事件分发机制传递场景事件给相应图元, 也会管理不同图元间的事件传递. 如果场景在某一个点接收到一个鼠标点击事件, 那么场景会将这个鼠标事件传递给这个位置的图元.
QGraphicsScene 也管理图元状态, 如图元点选和焦点状态. 你能在调用 QGraphicsScene::setSelectionArea()函数时, 传递任意形状来选择图元. 在QGraphicsView, 这个函数也能作为自定义选择区域的基础函数. 调用QGraphicsScene::selectedItems()函数获取所有被选中的图元. QGraphicsScene 中另一个状态是图元是否具备键盘焦点. 你可以调用 QGraphicsScene::setFocusItem 或者 QGraphicsItem::setFocus()()设置图元的焦点, 你可以调用 QGraphicsScene::focusItem()获取当前具有焦点的图元.
最后, QGraphicsScene 允许你调用 QGraphicsScene::render() 函数将部分场景渲染到绘制设备. 你可以在后续章节(打印
)获取更多信息.
视图
QGraphicsView 提供可视化场景的视图widget. 你可以为多个视图设置同一个场景. 这个视图widget是一个滚动区域, 可以为大型场景提供滚动条. 你可以调用 QGraphicsView::setViewport()函数,并传入QOpenGLWidget ,启用OpenGL渲染 .
QGraphicsScene scene; myPopulateScene(&scene); QGraphicsView view(&scene); view.show();
视图从键盘和鼠标接收到输入事件, 会在传递给场景之前转换成场景事件 (从视图坐标系转换为场景坐标系).
通过转换矩阵QGraphicsView::transform(), 视图可以转换 场景坐标系统, 实现缩放和旋转功能. 方便起见, QGraphicsView 也提供视图和场景之间的坐标转换函数: QGraphicsView::mapToScene() 和 QGraphicsView::mapFromScene().
图元
QGraphicsItem 是场景中所有图元的基类. 图形视图提供一些常见形状的图元, 例如矩形图元 (QGraphicsRectItem), 椭圆图元 (QGraphicsEllipseItem) 和文本图元 (QGraphicsTextItem), 当你实现自定义图元类时, QGraphicsItem 提供了一些非常有用的特性, 如下所示:
- 鼠标事件(点击, 移动, 释放, 双击, 悬浮, 滚轮, 菜单).
- 键盘焦点和键盘事件
- 拖拽
- 使用 QGraphicsItemGroup类并设置父子对象进行分组管理
- 碰撞检测
类似QGraphicsView, 图元虽然本身使用局部坐标系, 但是也提供许多转换函数, 实现图元和场景之间转换以及不同图元之间转换. 这些坐标系之间转换使用矩阵: QGraphicsItem::transform(). 这种方式对于独立图元的缩放和旋转是有用的.
图元可以包含其他图元(父子关系). 子图元继承父图元的转换操作. 这些函数 (e.g., QGraphicsItem::contains(), QGraphicsItem::boundingRect(), QGraphicsItem::collidesWith()) 不受图元转换影响, 仍然运行在局部坐标系.
QGraphicsItem 通过 QGraphicsItem::shape() 函数和QGraphicsItem::collidesWith()函数支持碰撞检测, 这两个函数都是虚函数. 使用QGraphicsItem::shape()返回局部坐标系 QPainterPath , QGraphicsItem 计算所有的碰撞检测. 如果你想实现自己的碰撞检测, 你可以实现 QGraphicsItem::collidesWith().
图形视图框架中的类
下面这些类为实现交互式应用程序提供一个框架.
所有路径图元的基类 | |
Represents an anchor between two items in a QGraphicsAnchorLayout | |
Layout where one can anchor widgets together in Graphics View | |
所有图形显示效果的基类 | |
可以加入 QGraphicsScene的椭圆图元 | |
图形视图中管理widget的网格布局 | |
QGraphicsScene中所有图元的基类 | |
一组简单图元的容器 | |
图形视图中所有布局的基类 | |
Can be inherited to allow your custom items to be managed by layouts | |
线条图元 | |
Horizontal or vertical layout for managing widgets in Graphics View | |
Base class for all graphics items that require signals, slots and properties | |
路径图元 | |
图像图元 | |
多边形图元 | |
Proxy layer for embedding a QWidget in a QGraphicsScene | |
矩形图元 | |
Surface for managing a large number of 2D graphical items | |
Context menu events in the graphics view framework | |
Events for drag and drop in the graphics view framework | |
图形视图相关的事件基类 | |
Events when a tooltip is requested | |
Hover events in the graphics view framework | |
Mouse events in the graphics view framework | |
Events for widget moving in the graphics view framework | |
Events for widget resizing in the graphics view framework | |
Wheel events in the graphics view framework | |
简单文本图元 | |
渲染SVG文件的图元 | |
富文本图元 | |
扩展图元转换的基类 | |
显示场景内容的widget | |
widget图元的基类 | |
绘制图元所需的参数 |
图形视图坐标系
图形视图基于笛卡尔坐标系; 场景中图元的位置和几何形状由2个数字组表示: X坐标, Y坐标. 当使用未转换的视图观察场景时, 屏幕上的一个像素代表场景的一个单元.
注意: 图形视图使用Qt的坐标系, 不支持Y轴反向的坐标系 ( y
向上增长).
图形视图有三种坐标系: 图元坐标系, 场景坐标系, 视图坐标系. 为了便于使用, 图形视图提供三种坐标系之间相互转换函数.
当渲染时, 图形视图的场景坐标系相当于 QPainter的 局部 坐标系, 视图坐标系是 设备 坐标系. 在 Coordinate System 文档, 你可以阅读到更多关于局部坐标系和设备坐标系之间的关系.
图元坐标系
图元使用局部坐标系. 坐标点(0,0)位于图元中心, 也是坐标变换中心. 坐标系中的几何图元通常称为点图元, 线图元或者矩形图元.
自定义图元时, 你需要关注图元坐标; QGraphicsScene 和 QGraphicsView 将执行所有的转换. 这种方式使实现自定义图元非常容易. 例如, 如果你收到一个鼠标点击事件或拖拽进入事件, 那么这个事件的点是图元坐标系. QGraphicsItem::contains() 虚函数返回 true
表示当前点时位于你的图元, 否则返回false
. 类似地, 图元的边框和形状也是图元坐标系.
图元的 位置 是图元中心点在父坐标系中的位置. 如果图元不指定父对象, 那么它的父是场景. 所有顶层图元的坐标是场景坐标系.
Child coordinates are relative to the parent's coordinates. If the child is untransformed, the difference between a child coordinate and a parent coordinate is the same as the distance between the items in parent coordinates. For example: If an untransformed child item is positioned precisely in its parent's center point, then the two items' coordinate systems will be identical. If the child's position is (10, 0), however, the child's (0, 10) point will correspond to its parent's (10, 10) point.
Because items' position and transformation are relative to the parent, child items' coordinates are unaffected by the parent's transformation, although the parent's transformation implicitly transforms the child. In the above example, even if the parent is rotated and scaled, the child's (0, 10) point will still correspond to the parent's (10, 10) point. Relative to the scene, however, the child will follow the parent's transformation and position. If the parent is scaled (2x, 2x), the child's position will be at scene coordinate (20, 0), and its (10, 0) point will correspond to the point (40, 0) on the scene.
QGraphicsItem::pos() 是为数不多的例外, QGraphicsItem的函数运行在自身的坐标系, 父坐标系变换不需要考虑子图元. 例如, 一个图元的边框 (i.e. QGraphicsItem::boundingRect()) 总是图元坐标系中的点坐标.
场景坐标系
场景坐标系表示所有图元的基本坐标系. 场景坐标系描述每个顶层图元的位置, 也构成所有从视图到场景的事件基础. 场景中每个图元都有场景位置和边框 (QGraphicsItem::scenePos() 和 QGraphicsItem::sceneBoundingRect()). 场景位置描述图元在场景中的位置, 图元的场景边框决定 QGraphicsScene 如何确认哪些区域发生改变. 场景中变动是通过信号 QGraphicsScene::changed() 通知, 这个信号的参数是场景矩形区域的列表.
视图坐标系
视图坐标系是widget坐标系. 视图中的每个单元对应一个像素. 这个坐标系特殊的地方是相对于widget或视口, 不受场景影响. QGraphicsView视口的左上角坐标总是 (0, 0), 右下角的坐标总是 (视口宽度, 视口高度). 所有的鼠标事件和拖拽事件是视图坐标系, 当与图元交互时, 你需要将这些坐标转换到场景坐标系中.
坐标映射
当处理场景中的图元时, 不同图元间, 场景和视图间, 场景和图元间的坐标映射是有用的. 例如, 当你在 QGraphicsView的视口点击鼠标时, 你能调用 QGraphicsView::mapToScene() 及 QGraphicsScene::itemAt()函数查询位于场景光标下的图元. 如果你想知道视口中图元的位置, 你能调用图元函数 QGraphicsItem::mapToScene() , 然后再调用视口函数 QGraphicsView::mapFromScene() . 最后, 如果你想查找视口椭圆形区域中的图元, 你能传递 QPainterPath 到 mapToScene()函数, 然后传递这个返回的映射路径到 QGraphicsScene::items()函数实现查找功能.
你能调用 QGraphicsItem::mapToScene() 和 QGraphicsItem::mapFromScene()函数实现图元坐标和形状的场景映射. 你也能调用 QGraphicsItem::mapToParent() 和 QGraphicsItem::mapFromParent()函数实现图元和父对象间坐标映射, 调用 QGraphicsItem::mapToItem() 和 QGraphicsItem::mapFromItem()函数实现图元间的坐标映射. 所以的映射函数都可以映射点, 矩形, 多边形和路径.
视图中也有相同的映射函数: 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 对象.
当你变换视图时, QGraphicsView 保持视图居中.
参见 Elastic Nodes 示例: 如何实现基本的缩放功能.
打印
图形视图提供单行打印功能的渲染函数 QGraphicsScene::render() 和 QGraphicsView::render(). 这些函数提供相同的 API: 你可以使用这些函数中的一个, 通过传递参数 QPainter 打印场景或视图全部或部分内容.
QGraphicsScene scene; QPrinter printer; scene.addRect(QRectF(0, 0, 100, 200), QPen(Qt::black), QBrush(Qt::green)); if (QPrintDialog(&printer).exec() == QDialog::Accepted) { QPainter painter(&printer); painter.setRenderHint(QPainter::Antialiasing); scene.render(&painter); }
场景和视图渲染函数的不同之处是一个使用场景坐标, 一个使用视图坐标. 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 , 所以QGraphicsView也提供 QWidget 支持的拖拽功能. 除此之外, 图形视图框架支持对图元的场景拖拽. 对于视图接收的拖拽事件, 图形视图框架将其转换为 QGraphicsSceneDragDropEvent, 然后传递给场景. 场景接收到这个事件, 会将其传递给鼠标光标下接收这个事件的第一个图元.
在开始拖拽一个图元时, 图形视图框架会创建一个 QDrag 对象, 并传递一个widget指针, 开始拖拽. 如果图元同时被许多视图观察, 那么仅有一个视图可以开始拖拽. 在多数情况下, 拖拽是由于鼠标按下或移动开始, 所以在mousePressEvent() 或 mouseMoveEvent()函数中, 你能从这个事件中获取原始widget. 例如:
void CustomItem::mousePressEvent(QGraphicsSceneMouseEvent *event) { QMimeData *data = new QMimeData; QDrag *drag = new QDrag(event->widget()); drag->setMimeData(data); drag->exec(); }
为了在场景中拦截拖拽事件, 你可以定义一个QGraphicsScene子类, 然后重新实现 QGraphicsScene::dragEnterEvent() 函数并判断是否所需事件. 在 QGraphicsScene文档, 你能读到更多有关拖拽的内容.
调用 QGraphicsItem::setAcceptDrops()函数可以设置图元是否支持拖拽. 为了操作这个进入的拖拽事件, 你可以重新实现 QGraphicsItem::dragEnterEvent(), QGraphicsItem::dragMoveEvent(), QGraphicsItem::dragLeaveEvent(), 和 QGraphicsItem::dropEvent().
参见 Drag and Drop Robot 示例: 图形视图框架如何支持拖拽操作.
光标和提示
像QWidget, QGraphicsItem 也支持设置光标 (QGraphicsItem::setCursor()), 和提示 (QGraphicsItem::setToolTip()). 当鼠标进入图元区域时, 光标和提示被 QGraphicsView 激活 (通过调用 QGraphicsItem::contains()).
你也能调用 QGraphicsView::setCursor()直接设置默认的光标.
参见 Drag and Drop Robot 示例: 如何设置提示和光标形状.
动画
图形视图支持多个级别的动画. 你可以使用动画框架轻松组合动画. 为了实现动画效果, 你需要自定义继承自 QGraphicsObject 子类, 并关联 QPropertyAnimation . QPropertyAnimation 允许任意 QObject 属性动画.
另一种方式是自定义继承自 QObject 和 QGraphicsItem子类. 然后设置一个定时器, 在 QObject::timerEvent()函数中控制动画.
第三种方式是调用 QGraphicsScene::advance()函数(兼容Qt 3中的QCanvas), 或者调用 QGraphicsItem::advance()函数.
OpenGL 渲染
为了开启OpenGL 渲染, 你可以调用QGraphicsView::setViewport()设置一个新的 QOpenGLWidget 作为 QGraphicsView的视口. 如果你想设置OpenGL的抗锯齿效果, 你可以创建一个 QSurfaceFormat 对象, 并设置合适的采样数 (参见 QSurfaceFormat::setSamples()).
Example:
QGraphicsView view(&scene); QOpenGLWidget *gl = new QOpenGLWidget(); QSurfaceFormat format; format.setSamples(4); gl->setFormat(format); view.setViewport(gl);
图元分组
将一个图元设置成另一个图元的子图元, 你能实现图元分组最基本的特性: 一组图元可以一起移动, 变换.
此外, QGraphicsItemGroup 是一个特殊的图元, 一组图元可以增加同时处理所有图元事件的接口. 在 QGraphicsItemGroup 中增加图元不改变图元的原始位置和变换, 但是将图元设置一个新的父对象会使图元重新计算相对父对象的位置. 方便起见, 你可以调用QGraphicsScene::createItemGroup()创建 QGraphicsItemGroup.
widget和布局
Qt 4.4添加 QGraphicsWidget , 支持对几何形状和布局的感知. 这个特殊的图元类似于 QWidget, 但是没用继承自 QPaintDevice; 而是继承自 QGraphicsItem . 你可以编写带有事件, 信号, 槽函数, 大小提示和策略的完整widget, 你也可以使用 QGraphicsLinearLayout and QGraphicsGridLayout管理QGraphicsWidget.
QGraphicsWidget
基于 QGraphicsItem 的功能, QGraphicsWidget 提供了最好的方案: 不仅包含QWidget的能力, 例如样式, 字体, 调色板, 布局和几何形状, 还包含 QGraphicsItem 独立于设备的分辨率和转换. 由于图形视图框架采用真实的坐标, 因此QGraphicsWidget的几何形状函数也接受参数 QRectF 和 QPointF. 这也适用于矩形, 边距和间距. 例如, 使用(0.5, 0.5, 0.5, 0.5)设置 QGraphicsWidget 的边距很常见. 使用QGraphicsWidget, 你不仅能创建子widget, 也能创建 "顶层" 窗口; 某些情况下, 你也能将图形视图框架用于高级 MDI 应用程序.
QGraphicsWidget 不能支持 QWidget的所有属性. 你应该参考 QGraphicsWidget的文档了解不支持哪些属性. 例如, 你可以在构造QGraphicsWidget时, 传入窗口标志 Qt::Window创建装饰窗口, 但是在mocOS上, 图形视图不支持 Qt::Sheet 和 Qt::Drawer 标志.
QGraphicsLayout
QGraphicsLayout 是为 QGraphicsWidget设计的二代布局框架的一部分. 它的API与 QLayout 非常相似. 你可以使用 QGraphicsLinearLayout 和 QGraphicsGridLayout管理widget和子布局. 你也可以子类化 QGraphicsLayout 轻松地实现自定义布局, 或者 编写合适的QGraphicsLayoutItem子类管理 QGraphicsItem .
支持嵌入式widget
图形视图框架支持将任意widget嵌入场景. 你不仅能嵌入简单widget(如, QLineEdit , QPushButton), 也能嵌入复杂widget(如 QTabWidget), 甚至更完整的主窗口. 调用 QGraphicsScene::addWidget()向场景嵌入widget, 或创建 QGraphicsProxyWidget 实例嵌入widget.
通过 QGraphicsProxyWidget, 图形视图框架能支持widget的各种功能, 如光标, 工具提示, 鼠标, 平板和键盘事件, 子widget, 动画, 弹出widget (如., QComboBox 或 QCompleter), 及widget焦点和激活. QGraphicsProxyWidget 还支持嵌入widget的tab属性. 你还能在你的场景嵌入新的 QGraphicsView 实现复杂的嵌套场景.
在转换嵌入式widget时, 图形视图框架保证widget进行与设备无关的分辨率转换, 以便在放大时使字体和样式保持清晰. (注意 分辨率的影响取决于样式.)
性能
浮点指令
为了准确, 快速地完成图元转换, 图形视图假定用户的硬件对浮点指令提供较好的性能支持.
许多工作站和桌面计算机配备了合适的硬件用于加速浮点指令运算, 但是, 一些嵌入式设备只支持软件层面的数学计算或浮点计算.
因此, 不同的设备上某些效果可能更慢. 你可以通过其他方面的优化弥补这种损失; 例如, 使用 OpenGL 渲染场景. 但是, 如果这些优化依赖支持浮点指令的硬件, 那么也可能降低性能.