一、简述
图形视图(Graphics View)提供了一个平台,用于大量自定义2D图元的管理与交互,并提供了一个视图部件(view widget)来显示可以缩放和旋转的图元。
框架包括一个事件传播架构,支持场景(Scene)中的图元(Item)进行精确的双精度交互功能。图元可以处理键盘事件、鼠标按下、移动、释放和双击事件,同时也能跟踪鼠标移动。
图形视图使用一个BSP(Binary Space Partitioning - 二叉空间分割)树,以提供对图形元素的快速查找,正因为如此,它可以使超大的场景实时地可视化,即使包含数百万的图元。
二、图形视图架构
图形视图提供了一个基于图元的方式来实现模型视图(model-view)编程,很像InterView中的便利类:QTableView、QTreeView和QListView。多个视图可以观察一个单独的场景,场景则包含了不同的几何形状
2.1 场景
QGraphicsScene提供了图形视图场景。
场景有以下职责:
- 提供一个快速的接口,用于管理大量图元
- 向每个图元传递事件
- 管理图元的状态,如:选中、焦点处理
- 提供未进行坐标转换的渲染功能,主要用于打印
场景是QGraphicsItem对象的容器。调用QGraphicsScene::addItem()将图元添加到场景中后,你就可以通过调用场景中的不同的查找函数来查找其中的图元。QGraphicsScene::items()函数及其重载函数可以返回所有图元,包括:点、矩形、多边形、通用矢量路径。
QGraphicsScene::itemAt()返回在特定点上最上面的图元。所有找到的图元按照层叠递减的排列顺序(即:最先返回的图元是最顶层的,最后返回的则是最底层的)。
QGraphicsScene scene;
QGraphicsRectItem *rect = scene.addRect(QRectF(0, 0, 100, 100));
QGraphicsItem *item = scene.itemAt(50, 50);
// item == rect
QGraphicsScene的事件传递机制负责将场景事件传递给图元,同时也管理图元之间的传递。如果场景在某个位置得到一个鼠标按下事件,就将该事件传递给这个位置上的图元。
QGraphicsScene同时还管理某些图元的状态,例如:图元的选中和焦点。可以通过调用QGraphicsScene::setSelectionArea(),传递一个任意形状,来选中场景中的图元。此功能也被用于QGraphicsView中橡皮筋(rubberband)选中的基础。通过调用QGraphicsScene::selectedItems()可以获取当前选中的图元列表。另外一种由QGraphicsScene处理的状态是:一个图元是否有键盘输入焦点。你可以调用QGraphicsScene::setFocusItem()或QGraphicsItem::setFocus()为一个图元设置焦点,或通过QGraphicsScene::focusItem()获取当前的焦点图元。
最后,QGraphicsScene允许通过QGraphicsScene::render()将部分场景绘制到绘图设备(paint device - 例如:QImage、QPrinter、QWidget)上。可以在本文关于“打印”部分了解更多细节。
2.2 视图
QGraphicsView提供了视图部件,将一个场景中的内容显示出来。你可以附加多个视图到同一个场景,从而针对同一数据集提供几个视口(viewport)。该视图部件是一个滚动区域(scroll area),为大型场景浏览提供滚动条。如果要启用OpenGL支持,可通过调用QGraphicsView::setViewport(),将一个QGLWidget设置为视口。
QGraphicsScene scene;
myPopulateScene(&scene);
QGraphicsView view(&scene);
view.show();
视图通过键盘和鼠标接收输入事件,并在事件发送给可视化的场景之前,将它们转换成场景事件(将坐标转化为适当的场景坐标)。
利用变换矩阵QGraphicsView::transform(),视图可以转换场景的坐标系,以便实现高级查看功能,例如:缩放、旋转。为方便起见,QGraphicsView也提供了视图和场景坐标之间转换函数:QGraphicsView::mapToScene()和QGraphicsView::mapFromScene()。
2.3 图元
QGraphicsItem是场景中图元的基类。图形视图提供了一些典型形状的标准图元,例如:矩形 ( QGraphicsRectItem )、椭圆 ( QGraphicsEllipseItem ) 、文本项 ( QGraphicsTextItem )。但当你自定义图元时,QGraphicsItem强大的特性就体现出来了。除此之外,QGraphicsItem还支持以下特性:
- 鼠标按下、移动、释放和双击事件,以及鼠标悬浮事件、滚轮事件和上下文菜单事件。
- 键盘输入焦点和键盘事件。
- 拖放。
- 分组:通过父子关系,或QGraphicsItemGroup。
- 碰撞检测。
和QGraphicsView一样,处于局部坐标系下的图元,也提供了很多函数用于图元和场景之间、图元到图元的坐标映射。此外,和QGraphicsView一样,它可以通过一个矩阵(matrix):QGraphicsItem::transform()来转换其自身的坐标系,这对于旋转和缩放单个图元非常有用。
三、图形视图框架中的类
这些类提供了一种创建交互式应用程序的框架。
类 | 描述 |
---|---|
QAbstractGraphicsShapeItem | 所有路径图元的共同基类 |
QGraphicsAnchor | 表示一个QGraphicsAnchorLayout中两个图元之间的anchor |
QGraphicsAnchorLayout | 布局可以anchor部件到图形视图中 |
QGraphicsEffect | 所有图形特效的基类 |
QGraphicsEllipseItem | 可以添加到QGraphicsScene的椭圆图元 |
QGraphicsGridLayout | 图形视图中管理部件的网格布局 |
QGraphicsItem | QGraphicsScene中所有图元的基类 |
QGraphicsItemGroup | 一个将图元组当做单个图元来看待的容器 |
QGraphicsLayout | 图形视图中所有布局类的基类 |
QGraphicsLayoutItem | 可以被继承,允许布局类管理的自定义图元 |
QGraphicsLineItem | 可以添加到QGraphicsScene的直线图元 |
QGraphicsLinearLayout | 图形视图中管理部件的水平或垂直布局 |
QGraphicsObject | 所有需要信号、槽、属性的图元的基类 |
QGraphicsPathItem | 可以添加到QGraphicsScene的路径图元 |
QGraphicsPixmapItem | 可以添加到QGraphicsScene的图形图元 |
QGraphicsPolygonItem | 可以添加到QGraphicsScene的多边形图元 |
QGraphicsProxyWidget | 代理,用于将一个QWidget对象嵌入到QGraphicsScene中 |
QGraphicsRectItem | 可以添加到QGraphicsScene的矩形图元 |
QGraphicsScene | 管理大量2D图元的管理器 |
QGraphicsSceneContextMenuEvent | 图形视图框架中的上下文菜单事件 |
QGraphicsSceneDragDropEvent | 图形视图框架中的拖放事件 |
QGraphicsSceneEvent | 所有图形视图相关事件的基类 |
QGraphicsSceneHelpEvent | Tooltip请求时的事件 |
QGraphicsSceneHoverEvent | 图形视图框架中的悬停事件 |
QGraphicsSceneMouseEvent | 图形视图框架中的鼠标事件 |
QGraphicsSceneMoveEvent | 图形视图框架中的部件移动事件 |
QGraphicsSceneResizeEvent | 图形视图框架中的部件大小改变事件 |
QGraphicsSceneWheelEvent | 图形视图框架中的鼠标滚轮事件 |
QGraphicsSimpleTextItem | 可以添加到QGraphicsScene的简单文本图元 |
QGraphicsSvgItem | 可以用来呈现SVG文件内容的QGraphicsItem |
QGraphicsTextItem | 可以添加到QGraphicsScene的文本图元,用于显示格式化文本 |
QGraphicsTransform | 创建QGraphicsItems高级矩阵变换的抽象基类 |
QGraphicsView | 显示一个QGraphicsScene内容的部件 |
QGraphicsWidget | QGraphicsScene中所有部件图元的基类 |
QStyleOptionGraphicsItem | 用于描述绘制一个QGraphicsItem所需的参数 |
四、图形视图坐标系
图形视图基于笛卡儿坐标系,场景中图元的位置和几何形状由两组数据来表示:X坐标和Y坐标。当使用一个未转换的视图来观察一个场景,场景中的一个单元将会由场景上的一个像素表示。
注意 :图形视图使用了Qt的坐标系,不支持反转的Y轴坐标系(即Y向上为正方向)。
图形视图中使用了三种有效的坐标系:图元坐标、场景坐标、视图坐标。为了简化实现,图形视图提供了非常方便的函数来进行三个坐标系的映射。
渲染时,图形视图的场景坐标对应于QPainter的逻辑坐标,视图坐标与设备坐标一致。参考:Coordinate System,了解更多关于逻辑坐标和设备坐标关系的内容。
4.1 图元坐标
图元生活在自己的局部坐标系。它们的坐标通常围绕它们的中心点(0, 0),并且这也是所有转换的中心。图元坐标系下的几何元素通常指点、线或矩形。
创建自定义图元时,只需考虑图元坐标即可。QGraphicsScene和QGraphicsView会为你实现所有相关的转换,这样一来,实现自定义图元就容易多了。例如:当你接收到鼠标按下或拖拽事件时,事件位置将由图元坐标给出。如果某一点(传递一个图元坐标作为参数)在图元中,那么GraphicsItem::contains()虚函数将会返回true;否则,返回false。同样的,项绑定的矩形或形状区域也是项坐标系统的。同样的,图元的矩形边界和形状都是基于图元坐标的。
图元的位置是图元的中心点在其父坐标系下的坐标,有时也被称为父坐标。场景从这个意义上说是所有无父图元的“parent”,顶层图元的位置在场景坐标中。
子坐标是相对于父坐标而言的。如果子坐标没有转换,那么子坐标和父坐标的差异就和图元在父坐标中的距离一样。例如:一个未经转换的子图元正好位于父图元的中心点,那么,这两个图元的坐标系是完全一样的。如果子图元的位置是(10, 0),那么子图元的(0, 10)点就对应父图元的(10, 10)点位置。
由于图元的位置和转换是相对于父图元来说的,因此,虽然父图元的转换隐式地转换了子图元,但是子图元的坐标不会因父图元的转换而受到影响。在上述示例中,即使父图元经过了旋转和缩放,子图元的(0, 10)点始终对应父图元的(10, 10)点。不过相对于场景来说,子图元将随着父图元进行转换和偏移 。如果父图元缩放了(2x, 2x),那么子图元在场景中的坐标是(20, 0),并且其(10, 0)点将会对应于场景中的(40, 0)点。
不管图元或父图元进行了怎样的转换,QGraphicsItem的函数操作都在图元坐标内。例如:一个图元的矩形边界(QGraphicsItem::boundingRect())总是在图元坐标下给出。但是QGraphicsItem::pos()是例外之一,该函数表示其在父图元中的位置 。
4.2 场景坐标
场景为所有的图元提供了基础的坐标系。场景坐标系描述了每一个顶层图元的位置,同时构成了从视图传递到场景的所有场景事件的基础。场景中的每个图元都有一个场景位置和矩形边界(QGraphicsItem::scenePos()、QGraphicsItem::sceneBoundingRect());另外,也有其自身的位置和矩形边界。场景位置描述了图元在场景坐标下内的位置,场景矩形边界则提供给QGraphicsScene来决定场景中的哪块区域已经被改变了。场景中的变化通过QGraphicsScene::changed()信号发出,参数是场景矩形列表。
4.3 视图坐标
视图坐标是部件的坐标,视图坐标中的每个单位对应一个像素。对于该坐标系来说比较特殊的一点是:它相对于部件或视口,不会受被观察的场景所影响。QGraphicsView的视口左上角总是(0, 0),右下角总是(viewport width, viewport height)。所有的鼠标事件和拖拽事件都以视图坐标接收到的,你需要将这些坐标映射到场景,以便于和图元进行交互。
4.4 坐标映射
通常处理场景中的图元时,从场景到图元、从图元到图元、从视图到场景的坐标或任意形状转换将会非常有用。例如:当在QGraphicsView视口中点击鼠标,你可以向场景询问当前鼠标下方的是什么图元(调用 QGraphicsView::mapToScene()转换坐标,然后通过QGraphicsScene::itemAt()查询图元)。如果想知道一个图元处于视口中的位置,可以调用图元的函数QGraphicsItem::mapToScene(),然后再调用视图的函数QGraphicsView::mapFromScene()。最后,如果想查找位于一个椭圆区域内的图元,你可以把一个QPainterPath传递给mapToScene(),然后将转换后的path传递给QGraphicsScene::items()。
通过调用QGraphicsItem::mapToScene()将坐标或任意形状映射到图元的场景中去,而调用QGraphicsItem::mapFromScene()将其映射回来;通过调用QGraphicsItem::mapToParent()将图元映射到父图元,而调用QGraphicsItem::mapFromParent()将其映射回来;甚至可以调用QGraphicsItem::mapToItem()和QGraphicsItem::mapFromItem()在不同的图元间进行映射。所有的映射函数均支持点、矩形、多边形和路径。
在视图和场景之间也存在着同样的映射函数:QGraphicsView::mapFromScene()和QGraphicsView::mapToScene()。要从视图映射到图元,第一步是映射到场景,然后才能从场景映射到图元。
五、主要特点
5.1 缩放和旋转
和QPainter一样,QGraphicsView也可以通过QGraphicsView::setMatrix()支持仿射转换。通过将转换应用到视图上,可以很轻松地添加对普通浏览的支持,例如:缩放和旋转。
下面的示例,说明了如何通过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); }
...
};
槽可以关联到启用了“autoRepeat”属性的QToolButtons。
在转换视图过程中,QGraphicsView始终保持与视图中心对齐。
参考:Elastic Nodes Example,了解更多关于缩放的内容。
5.2 打印
图形视图通过其渲染函数QGraphicsScene::render()和QGraphicsView::render(),提供了非常简单的打印功能。这两个函数提供了相同的API:只需要将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);
}
场景和视图绘制函数的区别在于:前者操作的是场景坐标,后者操作的则是视图坐标。QGraphicsScene::render()多用于打印一个未转换的场景各部分,例如:打印几何数据图表或文本文档。 QGraphicsView::render()则比较适合用于抓取屏幕截图,其缺省行为是使用提供的painter来渲染视口中确切的内容。
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参数给你使用的渲染函数,在内容被缩放时,可以选择保持或忽略场景的纵横比。
5.3 拖放
由于QGraphicsView间接继承了QWidget,因此QGraphicsView也提供了和QWidget一样的拖放功能。此外,为方便起见,图形视图框架为场景、每个图元提供了拖放支持。当视图接收到一个拖拽动作,它将拖放事件转换为一个QGraphicsSceneDragDropEvent,然后将其转发给场景。场景则会接管该事件的调度,并将其发送给鼠标下面第一个接受放下动作的图元。
要拖拽一个图元,只需要创建一个QDrag对象,将指针传给开始拖拽的部件。图元可以同时被多个视图观察,但是只有一个视图可以进行拖拽。在大多数情况下,拖拽都从鼠标按下或移动开始,因此在 mousePressEvent()或mouseMoveEvent()事件中,你可以从事件中拿到原始的部件指针,例如:
void CustomItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
QMimeData *data = new QMimeData;
data->setColor(Qt::green);
QDrag *drag = new QDrag(event->widget());
drag->setMimeData(data);
drag->start();
}
要拦截场景中的拖放事件,需要实现QGraphicsScene::dragEnterEvent(),选择你需要处理的事件,然后进行相应处理即可。你可以到 QGraphicsScene的文章中查看更多关于拖放的内容。
图元通过调用QGraphicsItem::setAcceptDrops()来启用对拖放的支持;如果要处理拖动,需要实现 QGraphicsItem::dragEnterEvent()、QGraphicsItem::dragMoveEvent()、QGraphicsItem::dragLeaveEvent()、QGraphicsItem::dropEvent(),这几个事件。
参考:Drag and Drop Robot example,了解更多关于图形视图拖拽的内容。
5.4 光标和tooltip
和QWidget一样,QGraphicsItem也支持设置光标(QGraphicsItem::setCursor())和tooltip(QGraphicsItem::setToolTip())。当鼠标光标进入item区域(由QGraphicsItem::contains()检测)时,光标和tooltip就会被QGraphicsView激活。
你也可以通过调用QGraphicsView::setCursor(),直接为视图设置一个默认的光标。
参考:Drag and Drop Robot example,了解更多关于tooltip和光标形状操作的内容。
5.5 动画
图形视图在几个层面上提供了对动画的支持。你可以用Animation Framework轻松地设置动画:只需要让你的图元从QGraphicsObject继承,然后将QPropertyAnimation绑定到上面。QPropertyAnimation可以为任何QObject属性实现动画效果。
另外一个选择是:创建一个自定义图元,从QObject和QGraphicsItem继承。该图元可以设置自己的定时器,然后在QObject::timerEvent()中控制动画。
第三个选择仅限于与Qt3中的QCanvas兼容。调用QGraphicsScene::advance()从而会依次调用 QGraphicsItem::advance()。
5.6 OpenGL渲染
要启用OpenGL渲染,只要简单地调用QGraphicsView::setViewport()来设置一个新的QGLWidget作为QGraphicsView的视口。如果你希望OpenGL具有反锯齿,则需要OpenGL支持采样缓冲(参考:QGLFormat::sampleBuffers())。
QGraphicsView view(&scene);
view.setViewport(new QGLWidget(QGLFormat(QGL::SampleBuffers)));
5.7 图元组
通过将一个图元设置为另一个图元的子图元,就可以得到图元组最重要的功能:图元会一起移动,所有转换都会从父图元传播到子图元中。
此外,QGraphicsItemGroup是一个特殊的图元,它提供了对子图元事件的支持,同时还提供了用于添加和删除子图元的接口。将一个图元添加到QGraphicsItemGroup将保持图元原始的位置和坐标转换,不过重新设置图元的父图元则会导致图元重新定位到相对于父图元的位置。为了方便起见,你可以调用QGraphicsScene::createItemGroup()来创建QGraphicsItemGroup图元。
5.8 部件和布局
Qt4.4通过QGraphicsWidget引入了对几何体和对布局敏感的图元的支持。这一特殊的基类图元和QWidget类似,但是不像QWidget,它没有从QPaintDevice继承,而是从QGraphicsItem。这样就允许你完全实现能够处理事件、信号与槽、大小调整和策略的部件,同时你还可以通过QGraphicsLinearLayout和QGraphicsGridLayout来管理部件的几何元素。
5.8.1 QGraphicsWidget
QGraphicsWidget建立在QGraphicsItem之上,具有QGraphicsItem的所有功能,保持了较小的资源占用,同时提供了两者的优势:来自QWidget的额外的功能,例如:样式、字体、调色板、布局、几何形状,来自QGraphicsItem的分辨率独立性和坐标转换的支持。由于图形视图使用真实的坐标而不是整数,因此 QGraphicsWidget的几何形状函数可以同时操作QRectF和QPointF。同时也能应用到边框的大小、边距和间距上,例如:对于QGraphicsWidget,规定内容边距为(0.5, 0.5, 0.5, 0.5)是非常常见的。例如:你可以创建子部件,甚至是“顶级”窗口。在某些情况下,你甚至可以将图形视图用于高级多文档界面的应用程序。
QGraphicsWidget支持部分QWidget属性,包括窗口标志位(window flags)和属性,但是并非全部支持。可以参考QGraphicsWidget文档,以获取完整列表来判断哪些支持以及哪些不支持。例如:你可以在创建QGraphicsWidget时赋予Qt::Window标志,从而得到封装的窗口,但是图形视图目标并不支持 Qt::Sheet和Qt::Drawer标志,这两者在Mac Os X上非常常见。
5.8.2 QGraphicsLayout
QGraphicsLayout是第二代布局框架的内容之一,专门为QGraphicsWidget设计。其API和QLayout非常相似。你可以在QGraphicsLinearLayout或QGraphicsGridLayout中对部件或者子布局进行管理,也可以通过派生QGraphicsLayout实现你自己的布局类,还可以通过派生QGraphicsLayoutItem来实现你自己的适配器,从而将QGraphicsItem图元加入到布局中。
5.9 嵌入式部件支持
图形视图对将任何部件嵌入到场景中提供无缝的支持。你可以嵌入简单的部件,例如:QLineEdit或QPushButton,也可以是复杂的部件,例如:QTabWidget,甚至是完整的主窗口。要将部件嵌入场景中,只需要简单地调用QGraphicsScene::addWidget()或者创建一个QGraphicsProxyWidget对象并将部件手工的嵌入其中。
通过QGraphicsProxyWidget图形视图能够完全继承客户端部件特性,包括:它的鼠标光标、tooltip、鼠标事件、平板电和键盘事件、子窗口、动画、弹出(例如:QComboBox或QCompleter),以及部件的输入焦点和激活状态。QGraphicsProxyWidget甚至集成了嵌入式部件的tab切换顺序,这样你就可以通过tab键让焦点进入或者移出嵌入式部件。你甚至可以嵌入一个新的 QGraphicsView到你的场景中,从而提供复杂的嵌套的视图。
当改变一个嵌入式部件,图形视图可以确保部件转换时与分辨率无关,当放大时使字体和样式看起来干净利落。(注意:分辨率无关的效果取决于风格。)
六、性能
为了精确和快速的将坐标转换和特效应用到图元上,图形视图在编译的时候默认用户的硬件能够为浮点指令提供合理的性能。很多工作站和桌面电脑都配备了适当的硬件来加速这种类型的计算,但是一些嵌入式设备可能仅仅提供了处理数学运算的库,或者需要用软件来模拟浮点指令。
这样,在某些设备上,某些类型的特效可能要比预期的慢。有可能可以在其它方面进行优化来弥补性能上的损失,例如:用OpenGL来绘制场景。不过,如果优化本身是依赖于浮点计算硬件的话,可能都会带来性能上的损失。