第7章 Graphics View框架
Qt的Graphics View框架使用MVC模式,适合对大量2D图元的管理。在Graphics View框架中,场景(scene)存储了图形数据,它通过视图(view)以多种形式表现,每个图元(item)可以单独进行控制。窗口部件也可以作为图元嵌入到Graphics View框架。
7.1 Graphics View概述
Qt4.2开始引入Graphics View框架用来取代Qt 3中的Canvas模块,并在很多地方做了改进。Graphics View框架实现了模型—视图结构的图形管理,能对大量的图元进行管理,支持碰撞检测、坐标变换和图元组等多种方便的功能。
Graphics View中增强的表现系统可以利用Qt4绘图系统的反锯齿、OpenGL工具来改善绘图性能。Graphics View支持事件传播体系结构,可以使图元在场景中得到提高了一倍的精确交互能力。图元能够处理键盘事件,鼠标按下、移动、释放和双击事件,也能跟踪鼠标的移动。在Graphics View框架中,通过BSP(二元空间划分树,Binary Space Partitioning)来提供快速的图元查找,这样就能实时地显示大场景,甚至上百万个图元。
7.1.1 Graphics View体系结构
Graphics View框架提供基于图元的模型—视图编程,类似于Qt InterView(第16章详细讲解)的模型—视图结构,只是这里的数据是图形。Graphics View框架中包括三个主要的类:QGraphicsScene、QGraphicsView和QGraphItem,分别是场景、视图和图元。一个场景可以通过多个视图表现,一个场景包括多个几何图形。
1.场景
QGraphicsScene类实现Graphics View中的场景。场景类完成如下功能:
● 提供管理大量图元的快速接口;
● 传播事件给场景中的每个图元;
● 管理图元状态,如选择和焦点处理;
● 提供无变换的绘制功能,如打印。
场景是QGraphicsItem对象的容器。通过函数QGraphicsScene::addItem()可以加入一个图元到场景中。图元可以通过多个函数进行检索。QGraphicsScene::items()和一些重载函数可以返回和点、矩形、多边形或向量路径相交的所有图元。QGraphicsScene::itemAt()返回指定点的最顶层图元。
QGraphicsScene的事件传播体系结构将场景事件发送给图元,同时也管理图元之间的事件传播。如果场景接收到了在某一点的鼠标单击事件,场景会把事件传给在这一点的图元。
QGraphicsScene负责管理一些图元的状态,如图元选择和焦点。可以通过QGraphicsScene::setSelectionArea()函数选择图元,选择区域可以是任意的形状,使用QPainterPath表示。要得到当前选择的图元列表可以使用函数QGraphicsScene::selectedItems()。QGraphicsScene还管理图元的键盘输入焦点状态。可以通过QGraphicsScene::setFocusItem()函数或QGraphicsItem::setFocus()函数来设置图元的焦点。获得当前具有焦点的图元使用函数QGraphicsScene::focusItem()。
如果需要将场景内容绘制到特定的绘图设备,可以使用QGraphicsScene::render()函数在绘图设备上绘制场景。
2.视图
QGraphicsView是视图窗口部件,它使场景的内容可视化。可以连接几个视图到一个场景,也可以为相同的数据集提供几种不同的视口。QGraphicsView是可滚动的窗口部件,可以提供滚动条来浏览大的场景。如果需要使用OpenGL,可以使用QGraphicsView::setViewport()将视口设置为QGLWidget。
视图接收键盘和鼠标的输入事件,并把它翻译为场景事件(将坐标转换为场景的坐标)。使用变换矩阵函数QGraphicsView::matrix()可以变换场景的坐标,通过这种方法可以实现场景缩放和旋转。QGraphicsView提供QGraphicsView::mapToScene()和QGraphicsView::mapFromScene()来和场景的坐标进行转换。
3.图元
QGraphicsItem是图元基类。QGraphics View框架提供了几种标准的图元,如矩形(QGraphicsRectIem)、椭圆(QGraphicsEllipseItem)和文本图元(QGraphicsTextItem)等。用户可以继承QGraphicsItem实现符合自己需要的图元。
QGraphicsItem具有下列功能:
● 处理鼠标按下、移动、释放、双击、悬停、滚轮和右键菜单事件;
● 处理键盘输入事件;
● 处理拖放事件;
● 分组;
● 碰撞检测。
图元有自己的坐标系统,也提供场景和图元、图元和图元之间的坐标变换函数。图元也可以通过QGraphicsItem::matrix()来进行自身的变换。图元可以包含子图元。
7.1.2 Graphics View坐标系统
Graphics View坐标基于笛卡儿坐标系,一个图元的场景坐标具有X坐标和Y坐标。当使用没有变换的视图观察场景时,场景中的一个单元对应屏幕上的一个像素。
在Graphics View中有三个有效的坐标系统:图元坐标、场景坐标和视图坐标。Graphics View提供了三个坐标系统之间的转换函数。在绘制图形时,Graphics View的场景坐标对应QPainter的逻辑坐标,视图坐标和设备坐标相同。
1.图元坐标
图元使用自己的本地坐标。这个坐标系统通常以图元中心为原点,这也是所有变换的原点。图元坐标方向是X轴正方向向右,Y轴正方向向下。创建图元后,只需要注意图元坐标就可以了,QGraphicsScene和QGraphicsView会完成所有的变换。
2.场景坐标
场景坐标是所有图元的基础坐标系统。场景坐标系统描述了顶层的图元,每个图元都有场景坐标和相应的包容框。场景坐标的原点在场景中心,坐标原点是X轴正方向向右,Y轴正方向向下。
3.视图坐标
视图坐标是窗口部件的坐标。视图坐标的单位是像素。QGraphicsView视口的左上角是(0, 0),X轴正方向向右,Y轴正方向向下。所有的鼠标事件最开始都是使用视图坐标。
4.坐标映射
在Graphics View框架中,经常需要将多种坐标变换,从场景到图元,从图元到图元,从视图到场景。Graphics View框架提供了如表7-1所示的多种变换函数。
表7-1 Graphics View框架坐标变换函数
7.1.3 深入Graphics View
Graphics View框架提供一些常见操作可直接使用,也可以进行改造以符合用户的需求。
1.缩放和旋转
QGraphicsView通过QGraphicsView::setMatrix()支持与QPainter一样的几何变换。当进行视图变换时,QGraphicsView保持视图的中心。通过应用变换,可以很容易地实现缩放和旋转。
下面的例子说明如何通过缩放和旋转槽来实现对视图的缩放和旋转。
class View : public QGraphicsView { Q_OBJECT ... public slots: void zoomIn() { scale(1.5, 1.5); } void zoomOut() { scale(1 / 1.5, 1 / 1.5); } void rotateLeft() { rotate(-90); } void rotateRight() { rotate(90); } ... };
将槽和具有autoRepeat属性的QToolButton进行连接,就可以实现连续的缩放操作。
2.光标和工具提示
和QWidget一样,QGraphicsItem支持图元特定的光标(应用QGraphicsItem:: setCursor())和工具提示(应用QGraphicsItem::setToolTip())。在鼠标进入图元区域时激活相应的光标和工具提示。
3.动画
Graphics View支持几种不同级别的动画。可以将动画路径通过QgraphicsItem Animation和图元关联。这可以使时间线性控制的图元在所有平台上速度一致。QGrapihicsItemAnimation允许创建图元的路径,包括位置、旋转、缩放、扭曲、平移等操作的路径,即在不同的时侯进行不同的变换。动画通常用QTimeLine来控制,也可以用QSlider来控制。
也可以创建从QObject和QGraphicsItem继承的图元,此类图元可以设置自己的定时器,通过QObject::timerEvent()来控制动画。
下面的代码模拟了太阳升起、落下的过程。
#include <QtGui> #include <cmath> using namespace std; const qreal PI = 3.14159265; int main(int argc, char* argv[]) { QApplication app(argc, argv); QGraphicsEllipseItem *sun = new QGraphicsEllipseItem(0, 0, 20, 20); sun->setBrush(Qt::red); sun->setPen(QPen(Qt::red)); QTimeLine *timeline = new QTimeLine(10000); timeline->setCurveShape(QTimeLine::LinearCurve); QGraphicsItemAnimation *animation = new QGraphicsItemAnimation; animation->setItem(sun); animation->setTimeLine(timeline); qreal x, y; qreal angle = PI; for (int i = 0; i <= 180; ++i) { x = 200.0 * cos(angle); y = 200.0 * sin(angle); animation->setPosAt(i/180.0, QPointF(x, y)); angle += PI/180.0; } QGraphicsScene *scene = new QGraphicsScene(); scene->addItem(sun); QGraphicsView *view = new QGraphicsView(scene); view->resize(640,480); view->show(); timeline->start(); return app.exec(); }
程序中使用了QTimeLine对象控制动画。QTimeLine的值变化曲线设为QTimeLine::LinearCurve,即QTimeLine的值是线性变化的。然后分180 步设置了每步的椭圆位置,并开始进行10秒钟的动画。程序模拟了太阳沿半圆形的轨迹运动的过程。
4.OpenGL绘制
要使用OpenGL绘制,可以调用QGraphicsView::setViewport()来设置QGLWidget作为QGraphicsView的视口。如果需要在OpenGL中打开反锯齿,可以通过调用QGLFormat::sampleBuffers()来使用OpenGL的采样缓冲区(sample buffer)。代码如下:
QGraphicsView view(&scene); view.setViewport(new QGLWidget(QGLFormat(QGL::SampleBuffers)));
使用OpenGL绘图后,可减轻CPU负担,大幅提高应用程序绘制效率。
5.图元组
使用图元组可以将图元组合在一起,对图元组的变换对所有子图元都有效。QGraphicsItem可以处理所有子图元的事件(使用GraphicsItem::setHandlesChildEvents()),即允许组合图元处理所有子图元的事件。
6.窗口部件与布局
Qt4.4开始支持具有位置和布局能力的图元项:QGraphicsWidget。QGraphicsWidget类似于QWidget,但不从QPaintDevice继承,它是QGraphicsItem的子类。QGraphics具有事件、信号与槽、尺寸策略等功能,可以通过QGraphicsLinearLayout或QGraphicsGridLayout布局管理器进行管理。
除了QGraphicsWidget,Graphics View还能嵌入任何QWidget,包括QMainWindow都可以嵌入。用户可调用QGraphicsScene::addWidget()或创建QGraphicsProxyWidget嵌入窗口部件。
7.实例
下面通过一个例子来说明如何使用Graphics View来绘制和管理图形。玩过“简氏舰队指挥官”的读者应该见过各种标号表示的舰艇、飞机等的作战标图。这里建立一个图形系统,能够显示类似于舰队指挥官中各种水面、水下、空中目标,分别使用不同的图形表示。每种目标具有“敌”、“我”、“不明”的属性,分别使用红、青、黄三种颜色表示。每个目标定义为一个QGraphicsItem,具体定义如下:
#ifndef TARGET_H #define TARGET_H #include <QGraphicsItem> #include <QObject> class Target : public QGraphicsItem { public: Target(); QRectF boundingRect() const; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); public: qreal course; qreal speed; short type; // 空中、水面、水下 short attribute; // 敌方、我方、不明 QColor color; protected: void contextMenuEvent(QGraphicsSceneContextMenuEvent *event); }; #endif
这里定义了每个图元的航向、航速、类型、属性及显示的颜色。
由于QGraphicsItem是抽象基类,所以至少要实现两个纯虚函数boundingRect()和paint(),具体实现如下:
#include "target.h" #include <QGraphicsScene> #include <QPainter> #include <QStyleOption> #include <math.h> static const double Pi = 3.14159265358979323846264338327950288419717; Target::Target() : speed(qrand()%10+1), type(qrand()%3), attribute(qrand()%3) { course = (qrand() % 360); switch(attribute) { case 0: // 敌方 color.setRgb(255, 0, 0); break; case 1: // 我方 color.setRgb(0, 255, 255); break; default: // 不明 color.setRgb(255, 255, 0); break; } }
在图元的构造函数中初始化了随机的航向、航速、类型属性,并根据敌方、我方、不明属性设置了图元的颜色。
boundingRect()函数返回图元的包容框。
QRectF Target::boundingRect() const { qreal adjust = 0.5; return QRectF(-20- adjust, -22- adjust, 40 + adjust, 83 + adjust); }
paint()函数绘制图元自身,根据不同类型的目标绘制不同的图形,代码如下:
void Target::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) { QPen pen(color); pen.setWidth(2); painter->setPen(pen); switch(type) { case 0: //水面目标 painter->drawEllipse(-15, -15, 30, 30); break; case 1: // 水下目标 painter->drawArc(-15, -15, 30, 30, 180*16, 180*16); break; default: // 空中目标 painter->drawLine(-15, 0, 0, -15); painter->drawLine(0, -15, 15, 0); break; } painter->drawLine(0, 0, int(speed*5*cos(course)), int(speed*5*sin(course))); }
在图元上可以弹出右键菜单,处理函数如下。
void Target::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) { QMenu menu; menu.setWindowOpacity(0.8); QAction *removeAction = menu.addAction(QObject::tr("武器发射")); QAction *markAction = menu.addAction(QObject::tr("电子干扰")); QAction *selectedAction = menu.exec(event->screenPos()); }
所有图形在一个QGraphicsView的视图类上显示,定义为radarView如下:
#ifndef RADAR_H #define RADAR_H #include <QGraphicsView> class radarView : public QGraphicsView { Q_OBJECT public: radarView( QGraphicsScene * scene, QWidget * parent = 0 ); QAction *actStrike; public slots: void timerEvent(QTimerEvent *event); }; #endif
视图类的实现如下:
#include <math.h> #include <QAction> #include <QMenu> #include <QContextMenuEvent> #include "radar.h" #include "target.h" radarView::radarView( QGraphicsScene * scene, QWidget * parent) : QGraphicsView(scene, parent) { startTimer(1000); actStrike = new QAction(tr("发射武器"),this); }
为简化模型,这里假设所有的图元都是匀速直线运动,所以要定时计算图元下一次的位置。
void radarView::timerEvent(QTimerEvent *) { QList<QGraphicsItem *> itemList = items(); QGraphicsItem *item; foreach(item, itemList) { Target* target = (Target*) item; target->setPos(target->mapToParent(target->speed * cos(target->course), target->speed * sin(target->course))); } }
最后实现主程序如下。
#include "target.h" #include <QtGui> #include <QTextCodec> #include <math.h> #include "radar.h" static const int TargetCount = 200; int main(int argc, char **argv) { QApplication app(argc, argv); QTextCodec::setCodecForTr(QTextCodec::codecForLocale()); qsrand(QTime(0,0,0).secsTo(QTime::currentTime())); QGraphicsScene scene; scene.setSceneRect(-400, -300, 800, 600); scene.setItemIndexMethod(QGraphicsScene::NoIndex); for (int i = 0; i < TargetCount; ++i) { Target *target = new Target; target->setPos(qrand() % 800-400, qrand() % 600-300); target->setVisible(true); scene.addItem(target); } radarView view(&scene); view.setRenderHint(QPainter::Antialiasing); view.setViewportUpdateMode(QGraphicsView::FullViewportUpdate); view.setBackgroundBrush(QColor(0,0,0)); view.setCacheMode(QGraphicsView::CacheBackground); view.setDragMode(QGraphicsView::ScrollHandDrag); view.setWindowTitle(QObject::tr("海战模拟")); view.resize(800, 600); view.show(); return app.exec(); }
在主程序中创建了场景、视图和大批量的图元。视图的setBackgroundBrush()函数将背景设为黑色。在图元很多时,将View的更新模式设为FullViewUpdate比缺省的MinimalViewportUpdate效率更高。
运行效果如图7-1所示。
图7-1 Graphics View框架示例