精通Qt4编程
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

第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框架示例