图形视图,动画和状态机框架


  Qt提供了 图形视图框架动画框架状态机框架 来实现更加高级的图形和动画应用。使用这些框架可以快速设计出动态GUI应用程序和各种动画,游戏程序。

图形视图框架的结构

  通过Qt提供的2D绘图功能,我们已经可以绘制出各种图形,并且进行简单的控制。不过,如果要绘制成千上万相同或者不同的图形,并且对它们进行控制,比如拖动这些图形,检测它们的位置以及判断它们是否相互碰撞等,使用以前的方法就很难完成了。这时可以使用Qt提供的图形视图框架来进行设计。

  图形视图框架提供了一个基于图形项的模型视图编程方法,主要由场景视图图形项三部分组成,这三部分分别由QGraphicsSceneQGraphicsViewQGraphicsItem这三个类来表示。多个视图可以查看一个场景,场景中包含各种各样几何形状的图形项。图形试图框架在Qt 4.2中被引入,用来代替以前的QCanvas类组。

  图形视图框架可以管理数量庞大的自定义2D图形项,并且可以与它们进行交互。使用视图部件可以使这些图形项可视化,视图还支持缩放和旋转。

  框架中包含了一个事件传播构架,提供了和场景中的图形项进行精确的双精度的双精度交互的能力,图形项可以处理键盘事件,鼠标的按下,移动,释放和双击事件,还可以跟踪鼠标的移动。

&ems; 图形视图框架使用一个BSP(Binary Space Partitioning)树来快速发现图形项,也正是因为如此,它可以实时显示一个巨大的场景,甚至包含上百万个图形项。

场景

QGraphicsScene提供了图形视图框架中的场景,场景拥有以下功能:

  • 提供用于管理大量图形项的高速接口
  • 传播事件到每一个图形项
  • 管理图形项的状态,比如选择和处理焦点
  • 提供无变换的渲染功能,主要用于打印

  场景是图形项QGraphicsItem对象的容器。

  可以调用QGraphicsScene::addItem()函数将图形项添加到场景中,然后调用任意一个图形项发现函数来检索添加的图形项。

  QGraphicsSeneca:items()函数及其它几个重载函数可以返回所有符合条件的图形项,这些图形项不是与指定的点,矩形,多边形或者矢量路径相交,就是包含在它们之中。

  QGraphicsScene::itemAt()函数返回指定点的最上层的图形项。所有的图形项发现函数返回的图形项都是使用递减顺序(例如,第一个返回的图形项在最上层,最后返回的图形项在最下层)。如果要从场景中删除一个图形项,则可以使用QGraphicsScene::RemoveItem()函数。

  新建空的Qt项目(Empty qmake Project),项目名称myscene,项目创建完成后向其中添加一个新的C++源文件,名称为main.cpp。

在myscene.pro文件中添加:

QT += widgets

然后在main.cpp文件中添加如下代码:

#include <QApplication>
#include <QGraphicsScene>
#include <QGraphicsRectItem>
#include <QDebug>

int main(int argc, char argv[]) {
	QApplication app(argc, argv);

	// 创建场景
	QGraphicsScene scene;
	QGraphicsRectItem *item = new QGraphicsRectItem(0, 0, 100, 100);	// 矩形图形项
	scene.addItem(item);	// 输出点(50, 50)处的图形项(即刚才创建的矩形图形项)
	qDebug() << scene.itemAt(50, 50, QTransform());
	return app.exec();
}

在这里插入图片描述

  QGraphicsScene的事件传播构架可以将场景事件传递给图形项,也可以管理图形项之间事件的传播。例如:如果场景在一个特定的点接收到了一个鼠标按下事件,那么场景就会将这个事件传递给该点的图形项。

  QGraphicsScene也用来管理图形项的状态,如图形项的选择和焦点等。可以通过向QGraphicsScene::setSelectionArea()函数传递一个任意的形状来选择场景中指定的图形项。

  如果要获取当前选取的所有图形项的列表,则可以使用QGraphicsScene::selectedItems()函数。另外可以调用QGraphicsScene::setFocusItem()或者QGraphicsScene::setFocus()函数来为一个图形项设置焦点,调用QGraphicsScene::focusItem()函数获取当前获得焦点的图形项。

  QGraphicsScene也可以使用QGraphicsScene::render()函数将场景中的一部分渲染到一个绘图设备上。

视图

  QGraphicsView提供了视图部件,它用来使场景中的内容可视化。可以连接多个视图到同一个场景来为相同的数据集提供多个视口。视图部件是一个可滚动的区域,它提供了一个滚动条来游览大的场景。

  可以使用setDragMode()函数以QGraphicsView::ScrollHandDrag为参数为使光标变为手掌形状,从而可以拖动场景。如果设置setDragMode()参数为QGraphicsView::RubberBandDrag,那么可以在视图上使用鼠标拖出橡皮筋来选择图形项。

  默认的QGraphicsView提供了一个QWidget作为视口部件,如果要使用OPenGL进行渲染,则可以调用QGraphicsView::setViewport()设置QOpenGLWidget作为视口。QGraphicsView会获取视口部件的拥有权。

在前面的程序main.cpp中先添加头文件,然后添加如下代码:

// 创建视图
QGraphicsView view(&scene);
view.setForegroundBrush(QColor(255, 255, 0, 100));
view.setBackgroundBrush(QPixmap("../myscene/miss.jpg"));
view.resize(400, 300);
view.show();	// 展示视图

在这里插入图片描述

  这里新建了视图部件,并指定了要可视化的场景。然后为该视图设置了场景前景色和背景图片。一个场景分为3层:图形项层(ItemLayer),前景层(ForegroundLayer)和背景层(BackgorundLayer)。

  场景的绘制总是从背景层开始,然后是图形项层,最后是前景层。前景层和背景层都可以使用QBrush进行填充,比如使用渐变和贴图等。

  上面代码中使用QGraphicsView对象来设置场景的背景和前景色,但也可以使用QGraphicsScene对象来设置场景的背景和前景,如果使用QGraphicsScene对象,那么那么它的设置将对场景中的所有视图生效,而QGraphicsView对象设置的背景和前景只对它本身对应的视图有效。

现在再向前面的程序中添加代码:

// 创建视图2
QGraphicsView view2(&scene);
view2.resize(400, 300);
view2.show();

  现在区别出来了,view视图的背景和前景由于使用QGraphicsScene对象设置过,所以存在颜色,而view2视图没有设置过背景和前景,它的画面就是默认的白色。

再看看使用QGraphicsScene对象scene来设置背景和前景会发生什么?

// 使用QGraphicsScene对象scene来设置背景和前景
scene.setForegroundBrush(QColor(255, 255, 0, 100));
scene.setBackgroundBrush(QPixmap("../myscene/miss.jpg"));

在这里插入图片描述

  视图从键盘或者鼠标接收输人事件,然后会在发送这些事件到可视化的场景之前将它们转换为场景事件(将坐标转换为合适的场景坐标)。另外,使用视图的变换矩阵函数QGraphicsView::transform()时,可以通过视图来变换场景的坐标系统,这样便可以实现比如缩放和旋转等高级的导航功能。

图形项

  QGraphicsItem是场景中图形项的基类。图形视图框架为典型的形状提供了标准的图形项,比如矩形(QGraphicsRectItem),椭圆(QGraphicsEllipseItem)和文本项(QGraphicsTextItem)。不过,只有编写自定义图形项时才会发挥出QGraphicsItem的强大功能。

QGraphicsItem主要支持以下功能:

  • 鼠标按下,移动,释放,双击悬停,滚轮和右键菜单事件
  • 键盘输入焦点和键盘事件
  • 拖放事件
  • 分组,使用QGraphicsItemGroup通过parent-child关系来实现
  • 碰撞检测

  除此之外,图形项还可以存储自定义的数据,可以使用setData()进行数据存储,然后使用data()获取其中的数据。

下面我们来自定义一个图形项(继承自QGraphicsItem类)。

  还是在myscene项目中,在前面的程序中添加新文件,模板选择C++类,类名MyItem,基类选择QGraphicsItem。添加完成后,将myitem.h修改如下:

#ifndef MYITEM_H
#define MYITEM_H

#include <QGraphicsItem>
#include <QPainter>

class MyItem : public QGraphicsItem
{
public:
    MyItem();
    QRectF boundingRect() const;
    void paint(QPainter * painter, const QStyleOptionGraphicsItem * option, QWidget *widget);
};

#endif // MYITEM_H

再到myitem.cpp中添加头文件,然后定义刚才在myitem.h里添加的两个函数:

QRectF MyItem::boundingRect() const
{
	qreal penWidth = 1;
	// 矩形定界框
	return QRectF(0 - penWidth / 2, 0 - penWidth / 2, 20 + penWidth, 20 + penWidth);
}

void MyItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *)
{
	painter->setBrush(Qt::red);
	painter->drawRect(0, 0, 20, 20);	// 使用画笔画一个矩形
}

  要实现自定义的图形项,那么首先要创建一个QGraphicsItem子类,然后重新实现它的两个纯虚公共函数::boundingRect()和paint(),前者用来返回要绘制图形项的矩形区域,后者用来执行实际的绘图操作。

  boundingRect()函数将图形项的外部边界定义为一个矩形,所有的绘图操作都必须限制在图形项的边界矩形中。QGraphicsView要使用这个边界矩形来剔除那些不可见的图形项还要使用它来确定绘制交叉项目时那些区域需要进行重新构建。另外,QGraphicsItem的碰撞检测机制也需要用到这个边界矩形。

  如果图形绘制了一个轮廓,那么在边界矩形中包含一般画笔的宽度是很重要的,尽管对于抗锯齿绘图并不需要这些补偿。

对于绘制函数paint(),它的原型如下:

void QGraphicsItem::paint(QPainter * painter, const QStyleOptionGraphicsItem * option, QWidget *widget = 0)

  这个函数一般会被QGraphicsView调用,用来本地坐标中绘制图形项中的内容。其中,painter参数用来进行一般的绘图操作;option参数为图形项提供了一个样式选项;widget参数是可选的,如果提供了该参数,那么它会指向那个要在其上绘图的部件,否则默认为0,表示使用缓冲绘图。

  painter的画笔宽度默认为0,它的画笔被初始化为绘图设备调色板的QPalette::Text画刷,而painter的画刷被初始化为QPalette::Window。

  一定要保证所有绘图都在boundingRect()的边界之中。特别是当QPainter使用了指定的QPen来渲染图形的边界轮廓,绘制的图形的边界线的一半会在外面,一半会在里面(例如使用了宽度为两个单位的画笔,就必须在boundingRect()里绘制一个单位的边界线)。这也是在boundingRect()中要包含半个画笔宽度的原因。QGraphicsItem不支持使用宽度非零的装饰笔。

定义好自定义图形项后,下面就可以在main.cpp中使用了。

先在main.cpp文件中添加头文件"myitem.h",然后将以前的图形项的创建代码:

QGraphicsRectItem *item = new QGraphicsRectItem(0, 0, 100, 100);

改为:

MyItem *item = new MyItem;

在这里插入图片描述

将myitem.cpp中painter()函数用于绘制的代码

painter->drawRect(0, 0, 20, 20);

改为:

QRadialGradient radialGradient(QPointF(200, 190), 50, QPointF(275, 200));
radialGradient.setColorAt(0, QColor(255, 255, 100, 150));
radialGradient.setColorAt(1, QColor(0, 0, 0, 50));
painter->setBrush(radialGradient);
painter->drawEllipse(QPointF(10, 10), 40, 30);

查看效果:
在这里插入图片描述

图形视图框架的坐标和事件处理

坐标系统

  图形视图框架基于笛卡尔坐标系,一个图形项在场景中的位置和几何形状由x坐标和y坐标来表示。当使用一个没有变换的视图来观察场景时,场景中的一个单元代表屏幕上的一个像素。

  图形视图框架中有3个有效的坐标系统:图形项坐标场景坐标视图坐标。为了方便应用,图形视图框架中提供了一些便捷函数来完成3个坐标系统之间的映射。进行绘图时,场景坐标对应QPainter的逻辑坐标,视图坐标对应设备坐标(视口-窗口映射)。

1. 图形项坐标

  图形项使用自己的本地坐标系统,坐标通常是以它们的中心为原点(0, 0),而这也是所有变换的中心。当要创建一个自定义图形项时,只需要考虑图形项的坐标系统,QGraphicsScene和QGraphicsView会完成其他所有的转换。而且,一个图形项的边界矩形和图形形状都是在图形项坐标系统中的。

  图形项的位置是指图形项的原点在其父图形项或者场景中的位置。如果一个图形项在另一个图形项之中,那么它被称为子图形项,而包含它的图形项称为它的父图形项。所有没有父图形项的图形项都会在场景的坐标系统中,它们被称为顶层图形项。可以使用setPos()函数来指定图形项的位置,如果没有指定,则默认出现在父图形项或者场景的原点处。

  子图形项的位置和坐标是相对于父图形项的,虽然父图形项的坐标变换会隐含地变换子图形项,但是,子图形项的坐标不会受到父图形项变换的影响。例如,在没有坐标变换,子图形项在父图形项的(10, 0)点,那么子图形项中的(0, 10)点就对应了父图形项的(10, 10)点。现在即使父图形项进行了旋转或缩放,子图形项的(0, 10)点也仍然对应着父图形项的(10, 10)点。

  但是相对于场景,子图形项会跟随父图形项的变换,例如,父图形项放大为(2x, 2x),那么子图形项在场景中的位置就会变为(20, 0),它的(10, 0)点会对应着场景中的(40, 0)。

  所有图形项都会使用确定的顺序来进行绘制,这个顺序也决定了单击场景时哪个图形项会先获得鼠标输入。一个子图形项会堆叠在父图形项的上面,而兄弟图形项会以插入顺序进行堆叠(也就是添加到场景或者父图形项中的顺序)。

  默认的,父图形项会被最先进行绘制,然后按照顺序对其上的字=子图形项进行绘制,所有的图形项都包含一个Z值来设置它们的层叠顺序,一个图形项的Z值默认为0,可以使用QGraphicsItem::setZvalue()来改变一个图形项的Z值,从而使它堆叠到其兄弟图形项的上面(使用较大的Z值)或者下面(使用较小的Z值时)。

2. 场景坐标

  场景坐标是所有图形项的基本坐标系统。场景坐标系统描述了每一个顶层图形项的位置,也用于处理所有从视图中传到场景上的事件。场景坐标的原点在场景的中心,x坐标和y坐标分别向右和向下增大。

  每一个场景中的图形项除了拥有一个图形项的本地坐标和边界矩形外,还都拥有一个场景坐标(QGraphicsItem::scenePos())和一个场景中的边界矩形(QGraphicsItem::sceneBoundingRect())。场景坐标用来描述图形项在场景坐标系统中的位置,而图形项的场景边界矩形拥有QGraphicsScene判断场景中的哪些区域进行了更改。

3. 视图坐标

  视图的坐标就是部件的坐标。视图坐标的每一个单位对应着一个像素,原点(0, 0)总在QGraphicsView视口的左上角,而右下角(宽, 高)。所有的鼠标事件和拖放事件最初都是使用视图坐标接收的

4. 坐标映射

  当处理场景中的图形项时,将坐标或者一个任意的形状从场景映射到图形项、或者从一个图形项映射到另一个图形项、或者从视图映射到场景,这些坐标变换都是很常用的。例如,在QGraphicsView的视口上单击了鼠标,则可调用QGraphicsView:mapToScene()以及QGraphicsScene::itemAt()来获取光标下的图形项;如果要获取一个图形项在视口中的位置,那么可以先在图形项上调用QGraphicsItem:mapToScene(),然后在视图上调用QGraphicsView::mapFromScene();如果要获取在视图的一个椭圆形中包含的图形项,则可以先传递一个 QPainterPath对象作为参数给mapToScene()函数,然后传递映射后的路径给QGraphicsScene::items()函数不仅可以在视图、场景和图形项之间使用坐标映射,还可以在子图形项、父图形项或者图形项、图形项之间进行坐标映射。图形视图框架提供的所有映射函数如下表所列,所有的映射函数都可以映射点矩形、多边形和路径。

在这里插入图片描述

  还是在前面的myscene项目中,往项目中添加新文件,模板选择C++类,类名为MyView,基类设置为QGraphicsView,完成后将myview.h更改如下:

#ifndef MYVIEW_H
#define MYVIEW_H

#include <QGraphicsView>

class MyView : public QGraphicsView
{
public:
    explicit MyView(QWidget *parent = 0);

protected:
    void mousePressEvent(QMouseEvent *event);
};

#endif // MYVIEW_H

然后到myview.cpp文件中,添加头文件:

#include <QMouseEvent>
#include <QGraphicsItem>
#include <QDebug>

之后将构造函数改为:

MyView::MyView(QWidget *parent)
    :QGraphicsView(parent)
{
}

然后添加鼠标按下事件处理函数的定义:

void MyView::mousePressEvent(QMouseEvent *event){
    QPoint viewPos = event->pos();
    qDebug() << "viewPos" << viewPos;
    QPointF scenePos = mapToScene(viewPos);	// 视图坐标转换为场景坐标
    qDebug() << "scenePos: " << scenePos;
    // 获取scenePos坐标处的图形项
    QGraphicsItem *item = scene()->itemAt(scenePos, QTransform());
    if(item){
        QPointF itemPos = item->mapFromScene(scenePos);
        qDebug() << "itemPos: " << itemPos;
    }
}

下面到main.cpp文件中,先添加头文件"myview.h",这样一来,main.cpp中的内容就变为:

#include <QApplication>
#include <QGraphicsScene>
#include <QGraphicsRectItem>
#include <QDebug>
#include <QGraphicsView>
#include "myitem.h"
#include "myview.h"

int main(int argc, char *argv[]){
    QApplication app(argc, argv);

    QGraphicsScene scene;
    MyItem *item = new MyItem;	// 使用前面创建的自定义视图类MyItem
    scene.addItem(item);
    item->setPos(10, 10);		// 设置图形项的坐标位置
    QGraphicsRectItem *rectItem = scene.addRect(QRect(0, 0, 100, 100), QPen(Qt::blue), QBrush(Qt::green));
    item->setParentItem(rectItem);	// 将item的父图形项设为rectItem
    rectItem->setRotation(45);	// 旋转45度
    rectItem->setPos(20, 20);
    MyView view;
    view.setScene(&scene);
    view.setForegroundBrush(QColor(255, 255, 0, 100));
    view.setBackgroundBrush(QPixmap("../myscene/miss.jpg"));
    view.resize(400, 300);
    view.show();

    return app.exec();
}

在这里插入图片描述

  上面代码中,由于item图形项在rectItem图形项之前定义,所以在场景中,rectItem会在Item之上绘制。如果想要item在rectItem上面,则可以使用setZValue()设置它的Z值:

item->setZValue(1);

上面代码中这两行代码:

item->setParentItem(rectItem);
rectItem->setRotation(45);

  将rectItem旋转了,可以看到,rectItem在自己的坐标系统中进行旋转,并且是以原点为中心进行旋转的。虽然item也进行了旋转,但是它在rectItem中的相对位置却没有改变。

下面再来看一下为什么场景背景图片会随着图形项的不同而改变位置?

  其实场景背景图片位置的变化也就是场景位置的变化,默认的,如果场景中没有添加任何图形项,那么场景的中心(默认的是原点)会和视图的中心重合。如果添加了图形项,那么视图就会以图形项的中心为中心来显示场景。就像前面看到的,因为图形项的大小或者位置变化了,所以视口的位置也就变化了,这样看起来好像是背景图片的位置发生了变化。

  其实,场景还有一个很重要的属性就是场景矩形,它是场景的边界矩形。场景矩形定义了场景的范围,主要用于QGraphicsView来判断视图默认的滚动区域,当视图小于场景矩形时,就会自动生成水平和垂直的滚动条来显示更大的区域。

  另外,场景矩形也用于QGraphicsScene来管理图形项索引。可以使用QGraphicsScene::setSceneRect()来设置场景矩形,如果没有设置,那么sceneRect()会返回一个包含了自从场景创建以来添加的所有图形项的最大边界矩形(这个矩形会随着图形项的添加或者移动而不断增长,但是永远不会缩小),所以操作一个较大的场景时,总应该设置一个场景矩形。

  设置了场景矩形,就可以指定视图显示的场景区域了。比如将场景的原点显示在视图的左上角,那么可以在创建场景的代码下面添加如下一行代码:

scene.setSceneRect(0, 0, 400, 300);

在这里插入图片描述

事件处理与传播

  图形视图框架中的事件都是先由视图进行接收,然后传递给场景,再由场景传递给
相应的图形项。而对于键盘事件,它会传递给获得焦点的图形项,可以使用QGraphicsCene类的setFocusItem()函数或者图形项自身调用setFocus()函数来设置焦点图形项。默认的,如果场景没有获得焦点,那么所有的键盘事件都会被丢弃。如果调用了场景的setFocus()函数或者场景中的一个图形项获得了焦点,那么场景也会自动获得焦点。如果场景丢失了焦点(比如调用了clearfocus()函数),然而它的一个图形项获得有焦点,那么场景就会保存这个图形项的焦点信息;当场景重新获得焦点后,就会确保最后一个焦点项目重新获得焦点。

  对于鼠标悬停效果,QGraphicsScene会调度悬停事件。如果一个图形项可以接收
悬停事件,那么当鼠标进入它的区域之中时,它就会收到一个QGraphicsSceneHoverEnter事件。如果鼠标继续在图形项的区域之中进行移动,那么QGraphicsScene就会向该图形项发送QGraphicsSceneHoverMove事件。当鼠标离开图形项的区域时,它将会收到一个QGraphicsSceneHoverLeave事件。图形项默认是无法接收悬停事件的,可以使用QGraphicsItem类的setAcceptHoverEvents()函数使图形项可以接收悬停事件。

  所有的鼠标事件都会传递到当前鼠标抓取的图形项,一个图形项如果可以接收鼠标事件(默认可以)而且鼠标在它的上面被按下,那么它就会成为场景的鼠标抓取的图形项。

  下面再来看一个例子,新建空的Qt项目,名称为myview。完成后先在myview.pro中添加"QT += widgets"一行代码。

  向之前那个例子中一样,向该项目中添加自定义图形项MyItem,也就是向项目中添加C++类文件myitem.h和myitem.cpp,将它们的内容改为:

/* myitem.h */
#ifndef MYITEM_H
#define MYITEM_H

#include <QGraphicsItem>

class MyItem : public QGraphicsItem
{
public:
    MyItem();
    QRectF boundingRect() const;
    void paint(QPainter * painter, const QStyleOptionGraphicsItem * option, QWidget *widget);
    void setColor(const QColor &color) { brushColor = color; }

private:
	QColor brushColor;
};

#endif // MYITEM_H

/* --------------------------------- */

/* myitem.cpp */
#include "myitem.h"
#include <QPainter>

MyItem::MyItem()
{
	brushColor = Qt::red;
}

QRectF MyItem::boundingRect() const
{
    qreal adjust = 0.5;
    return QRectF(-10 - adjust, -10 - adjust, 20 + adjust, 20 + adjust);
}

void MyItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *)
{
    if (hasFocus()) {	// 根据图形项是否获得焦点来使用不同的颜色绘制图形项的轮廓
    	painter->setPen(QPen(QColor(255, 255, 255, 200)));
    } else {
    	painter->setPen(QPen(QColor(100, 100, 100, 100)));
    }

    painter->setBrush(brushColor);
    painter->drawRect(-10, -10, 20, 20);
}

  完成了自定义图形项类的添加后,再向项目中添加C++类文件myview.h和myview.cpp,先在myview.h中添加键盘按下事件处理函数的声明:

#ifndef MYVIEW_H
#define MYVIEW_H
#include<QGraphicsView>

class MyView : public QGraphicsView
{
public:
    explicit MyView(QWidget * parent = 0);

protected:
	void keyPressEvent(QKeyEvent * event);
};

#endif // MYVIEW_H

然后到myview.cpp文件中添加头文件,并修改构造函数:

MyView::MyView(QWidget *parent) :
	QGraphicsView(parent)
{
}

然后添加keyPressEvent()函数的定义:

// 按键缩放视图功能
void MyView::keyPressEvent(QKeyEvent *event)
{
	switch(event->key())
	{
		case Qt::Key_Plus:	// 键盘上的"+"号
			scale(1.2, 1.2);
			break;
		case Qt::Key_Minus:
			scale(1 / 1.2, 1 / 1.2);
			break;
		case Qt::Key_Right:
			rotate(30);
			break;
	}
	QGraphicsView::keyPressEvent(event);
}

  这里使用不同的按键来实现视图的缩放和旋转等操作。注意,在视图的事件处理函数的最后一定要调用QGraphicsView类的keyPressEvent()函数,不然在场景或者图形项中就无法再接收该事件了。

最后就是写main.cpp文件了,并将其内容改为:

#include <QApplication>
#include "myitem.h"
#include "myview.h"
#include <QTime>

int main(int argc, char* argv[])
{
	QApplication app(argc, argv);
	qsrand(QTime(0, 0, 0).secsTo(QTime::currentTime()));
	QGraphicsScene scene;
	scene.setSceneRect(-200, -150, 400, 300);
	// 添加5个图形项
	for(int i = 0; i < 5; i++)
	{
		MyItem *item = new MyItem;
		// 为每个图形项设置随机颜色
		item->setColor(QColor(qrand() % 256, qrand() % 256, qrand() % 256));
		item->setPos(i * 50 - 90, -50);
		scene.addItem(item);
	}

	MyView view;
	view.setScene(&scene);
	view.setBackgroundBrush(QPixmap("../myview/love.jpg"));
	view.show();

	return app.exec();
}

在这里插入图片描述

由于定义了按键事件处理函数,可以使用键盘上的"+“和”-"号来放大和缩小视图。

  上面我们测试了键盘事件的效果,下面再来看一下其他事件的应用,先在myitem.h文件中添加一些事件处理函数的声明:

protected:
	void keyPressEvent(QKeyEvent *event);
	void mousePressEvent(QGraphicsSceneMouseEvent *event);
	void hoverEnterEvent(QGraphicsSceneHoverEvent *event);
	void contextMenuEvent(QGraphicsSceneContextMenuEvent *event);

然后到myitem.cpp文件中添加头文件:

#include <QCursor>
#include <QkeyEvent>
#include <QGraphicsSceneHoverEvent>
#include <QGraphicsSceneContextMenuEvent>
#include <QMenu>

再在MyItem构造函数中添加如下代码:

setFlag(QGraphicsItem::ItemIsFocusable);	// 使图形项可以获得焦点(以便于开启使用键盘控制图形项的功能)
setFlag(QGraphicsItem::ItemIsMovable);		// 使图形项可以拖动(以便于开启鼠标拖动图形项功能)
setAcceptHoverEvents(true);		// 图形项支持鼠标悬停事件

下面添加事件处理函数的定义:

// 鼠标按下事件处理函数,设置被点击的图形项获得焦点,并改变光标外观
void MyItem::mousePressEvent(QGraphicsSceneMouseEvent *)
{
	setFocus();	// 获得焦点
	setCursor(Qt::ClosedHandCursor);	// 设置鼠标样式为手柄
}

// 键盘按下事件处理函数,判断是否是向下方向键,如果是,则向下移动图形项
void MyItem::keyPressEvent(QKeyEvent * event)
{
	if(event->key() == Qt::Key_Down)
		moveBy(0, 10);	// 相对当前的位置进行移动
}

// 悬停事件处理函数,设置光标外观和显示
void MyItem::hoverEnterEvent(QGraphicsSceneHoverEvent *)
{
	setCursor(Qt::OpenHandCursor);
	setToolTip("I am item");	// 提示框
}

// 右键菜单事件处理函数,为图形项添加一个右键菜单
void MyItem::contextMenuEvent(QGraphicsSceneContextMenuEvent *event)
{
	QMenu menu;
	QAction *moveAction = menu.addAction("move back");	// 添加一个右键菜单项——moveAction
	QAction *selectedAction = menu.exec(event->screenPos());
	if(selectedAction == moveAction)	// 选中的右键菜单项为moveAction
		setPos(0, 0);	// 图形项移动到场景原点
}

在这里插入图片描述

图形视图框架的其他特性

  图形效果是Qt 4.6添加的一个新的特色功能,QGraphicsEffect类是所有图形效果的基类,使用图形效果来改变元素的外观是通过在源对象(如一个图形项)和目标设备(视图的视口)之间挂接了渲染管道和一些操作来实现的。

  图形效果可以实施在任何一个图形项或者非顶层窗口的任何窗口部件上,只须先创建一个图形效果对象,然后调用setGraphicsEffect()函数来使用这个图形效果即可。如果想要停止使用该效果,可以调用setEnabled(false)。

Qt提供了4种标准的效果,如下表所示:

在这里插入图片描述

当然,也可以自定义效果,这需要创建QGraphicsEffect的子类。

  还是在上一个例子myview项目中,在前面程序的基础上进行修改,首先在myitem.cpp文件中添加头文件,然后更改keyPressEvent()函数如下:

void MyItem::keyPressEvent(QKeyEvent *event)
{
	switch(event->key())
	{
		case Qt::Key_1: {	// 图形项模糊效果
			QGraphicsBlurEffect *blurEffect = new QGraphicsBlurEffect;
			blurEffect->setBlurHints(QGraphicsBlurEffect::QualityHint);
			blurEffect->setBlurRadius(8);
			setGraphicsEffect(blurEffect);
			break;
		}
		case Qt::Key_2: {	// 染色效果
			QGraphicsColorizeEffect *colorizeEffect = new QGraphicsColorizeEffect;
			colorizeEffect->setColor(Qt::white);
			colorizeEffect->setStrength(0.6);
			setGraphicsEffect(colorizeEffect);
			break;
		}
		case Qt::Key_3: {	// 阴影效果
			QGraphicsDropShadowEffect *dropShadowEffect = new QGraphicsDropShadowEffect;
			dropShadowEffect->setColor(QColor(63, 63, 63, 100));
			dropShadowEffect->setBlurRadius(2);
			dropShadowEffect->setOffset(10);
			setGraphicsEffect(dropShadowEffect);
			break;
		}
		case Qt::Key_4: {	// 透明效果
			QGraphicsOpacityEffect *opacityEffect = new QGraphicsOpacityEffect;
			opacityEffect->setOpacity(0.4);
			setGraphicsEffect(opacityEffect);
			break;
		}
		case Qt::Key_5: {	// 取消图形项项的这些效果
			graphicsEffect()->setEnabled(false);
			break;
		}
	}
}

在这里插入图片描述

动画,碰撞检测和图形项组

1. 动画

  图形视图框架支持几种级别的动画。以前可以使用QGraphicsItemAnimation类容易地实现图形项的动画效果,不过该类现在已经过时,现在主要是通过动画框架来实现动画效果。

  另外的方法是创建一个继承自QObject和QGraphicsItem的自定义图形项,然后创建它自己的定时器来实现这个动画。

  第三种方法就是下面要讲解的使用QGraphicsScene::advance()来推进场景。

  还是之前的myview项目,继续在前面程序的基础上进行修改。首先myitem.h文件中的public部分添加函数声明:

void advance(int space);

然后到myitem.cpp文件中进行该函数的定义:

void MyItem::advance(int phase)
{
	// 在第一个阶段不进行处理
	if(!phase)
		return;
	// 图形项不同方向随机移动
	int value = qrand() % 100;
	if(value < 25) {
		setRotation(45);
		moveBy(qrand() % 10, qrand() % 10);
	} else if(value < 50) {
		setRotation(-45);
		moveBy(-qrand() % 10, -qrand() % 10);
	} else if(value < 75) {
		setRotation(30);
		moveBy(-qrand() % 10, qrand() % 10);
	} else {
		setRotation(-30);
		moveBy(qrand() % 10, -qrand() % 10);
	}
}

  调用场景的advance()函数就会自动调用场景中所有图形项的advance()函数,而且图形项的advance()函数会被分为两个阶段调用两次。

  第一次phase为0,告知所有的图形项场景将要改变;第二次phase为1,在这时才进行具体的操作,这里就是让图形项在不同的方向上移动一个随机的数值。

下面到main.cpp文件,先添加头文件,然后添加代码:

QTimer timer;
QObject::connect(&timer, &QTimer::timeout, &scene, &QGraphicsScene::advance);
timer.start(300);

这里创建了一个定时器,当定时器溢出时会调用场景的advance()函数。
在这里插入图片描述

2. 碰撞检测

  图形视图框架提供了图形项之间的碰撞检测,碰撞检测可以使用两种方法来实现:

  • 重新实现QGraphicsItem::shape()函数来返回图形项准确的形状,然后使用默认的collidesWithItem()函数通过两个图形项形状之间的交集来判断是否发生碰撞。如果图形项的形状很复杂,那么进行这个操作是非常耗时的。如果没有重新实现shape()函数,那么它默认会调用boundingRect()函数返回一个简单的矩形。
  • 重新实现collidesWithItem()函数来提供一个自定义的图形项碰撞算法。

  可以使用QGraphicsItem类中的collidesWithPath()来判断是否与指定的图形项进行了碰撞;使用collidesWithPath()来判断是否与指定的路径碰撞;使用collidingItems()来获取与该图形项碰撞的所有图形项的列表;也可以调用QGraphicsScene类的collidingItems()。

  上面所说的几个函数都有一个Qt::ItemSelectionMode参数来指定怎样进行图形项的选取,它一共有4个值。如下表所示,其中Qt::IntersectsItemShape是默认值。
在这里插入图片描述

  还是在myview项目中,继续在前面的程序中添加代码。首先在myitem.h文件的public部分进行函数声明:

QPainterPath shape();

然后到myitem.cpp文件中定义该函数:

QPainterPath MyItem::shape()
{
	QPainterPath path;
	path.addRect(-10, -10, 20, 20);
	return path;
}

  这里只是简单地返回了图形项对应的矩形,然后将paint()函数以前用来判断是否获得焦点的if语句改为:

if(hasFocus() || !collidingItems().isEmpty())	// 如果获得焦点且与该图形项碰撞的列表不为空

这样就可以在图形与其他图形项碰撞时使其轮廓线变为白色。

在这里插入图片描述

3. 图形项组

  QGraphicsItemGroup图形项组为图形项提供了一个容器,它可以将多个图形项组合在一起而将它本身以及所有的子图形项看作一个独立的图形项。与父图形项不同,图形项组中的所有图形项都是平等的。例如,可以通过拖动其中任意一个来将它们一起进行移动。而如果只想将一个图形项存储在另一个图形项之中,那么可以使用setParentItem()来为设置父图形项。

下面仍然在先前的程序中添加代码,在main.cpp中添加:

// 创建两个图形项
MyItem *item1 = new MyItem;
item1->setColor(Qt::blue);
MyItem *item2 = new MyItem;
item2->setColor(Qt::green);

// 创建图形项组,将上面的两个图形项合并
QGraphicsItemGroup *group = new QGraphicsItemGroup;
group->addToGroup(item1);
group->addToGroup(item2);
group->setFlag(QGraphicsItem::ItemIsMovable);
item2->setPos(30, 0);
scene.addItem(group);	// 将图形项组添加进场景,两个图形项也就自然添加进场景了

在这里插入图片描述

动画框架

  动画框架的目的是提供一种简单的方法来创建平滑的,具有动画效果的GUI界面。该框架是通过控制Qt的属性来实现动画的,它可以应用在窗口部件和其他QObject对象上,也可以应用在图形视图框架中。动画框架在Qt 4.6中被引入。

  动画框架中主要的类及其关系如下图所示:
在这里插入图片描述

&ems; 其中,基类QAbstractAnimation和它的两个子类QVariantAnimation以及QAnimationGroup构成了动画框架的基础。这里的QAbstractAnimation是所有动画类的祖先,它定义了一些所有动画类都共享的功能函数,比如动画的开始,停止和暂停等;它也可以接收时间变化的通知,通过继承这个类可以创建自定义的动画类。

  动画框架中提供了QPropertyAnimation类,继承自QVariantAnimation,用来执行Qt属性的动画。这个类使用缓和曲线来对属性进行插值。如果要对一个值使用动画,则可以创建继承自QObject的类,然后在类中将该值定义为一个属性。

  属性动画为现有的窗口部件以及其他QObject子类提供了非常灵活的动画控制。Qt现在支持的可以进行插值的QVariant类型有:Int,Uint,Double,Float,QLine,QLineF,QPoint,QPointF,QSize,QSizeF,QRect,QRectF和QColor等。

  如果要实现复杂的动画,则可以通过动画组QAnimationGroup类实现,它的功能是作为其他动画类的容器,一个动画组还可以包含另外的动画组。

实现属性动画

  前面已经讲到QPropertyAnimation类可以对Qt属性进行插值,如果一个值要实
现动画效果,则就要使用这个类,而它的父类QVariantAnimation是一个抽象类,无法直接使用。之所以要使用Qt属性来进行动画的最主要原因是这样可以为已经存在的Qt API中的类提供灵活的动画设置。可以在QWidget类的帮助文档中查看它所有的属性,当然,并不是所有的属性都可以设置动画,必须是前面讲到的Qt支持的QVariant类型。

  新建空的Qt项目Empty qmake Project,名称为myanimation,完成后首先在项目文件myanimation.pro中添加"QT += widgets"一行代码并保存该文件,然后添加新文件main.cpp,并在其中添加如下代码:

#include <QApplication>
#include <QPushButton>
#include <QPropertyAnimation>
int main(int argc, char * argv[]) {
	QApplication app(argc, argv);
	QPushButton button("Animated Button");	// 创建一个按钮
	button.show();
	QPropertyAnimation animation(&button, "geometry");

	// 在10s内让按钮从(0, 0)移动到(250, 250)点,同时由宽120,高60变为宽200,高60
	animation.setDuration(10000);
	animation.setStartValue(QRect(0, 0, 120, 30));
	animation.setEndValue(QRect(250, 250, 200, 60));
	animation.start();

	return app.exec();
}

在这里插入图片描述

除了直接像上面那样设置动画的开启和结束的值外,还可以调用setKeyValueAt(qreal step, const QVariant &value)函数在动画中间为属性设置值。其中,step取值在0.0 ~ 1.0之间,将上面程序中的setStartValue()和setEndValue()改为:

animation.setKeyValueAt(0, QRect(0, 0, 120, 30));
animation.setKeyValueAt(0.8, QRect(250, 250, 200, 60));
animation.setKeyValueAt(1, QRect(0, 0, 120, 30));

在这里插入图片描述

  在动画可以使用pause()来暂停动画;使用resume()来恢复暂停状态;使用stop()来停止动画;可以使用setDirection()来设置动画的方向,这里可以设置为两个方向,默认是QAbstractAnimation::Forward,动画的当前时间随着时间而递减,即从结束位置到开始位置;还可以使用setLoopCount()函数来设置动画的重复次数,默认为1,表示执行一次,如果设置为0,那么动画不会执行,如果设置为1,那么在调用stop()函数停止动画之前,它会一直持续。

使用缓和曲线

  按钮部件的运动过程都是线性的,即匀速运动。除了在动画中添加更多的关键点;还可以使用缓和曲线,缓和曲线描述了怎么来控制0和1之间的插值速度的功能,这样就可以在不手动改变插值的情况下来控制动画的速度。

还是在之前的项目myanimation中,将前面程序中定义动画的中间部分改为:

animation.setDuration(2000);
animation.setStartValue(QRect(250, 0, 120, 30));
animation.setEndValue(QRect(250, 300, 120, 30));
animation.setEasingCurve(QEasingCurve::OutBounce);	// 弹跳缓和曲线

在这里插入图片描述

动画组

  在一个应用中经常会包含多个动画,例如,要同时移动多个图形项或者让它们一个接一个地串行移动。使用QAnimationGroup类可以实现复杂的动画,它的两个子类QSequentialAnimationGroup和QParallelAnimationGroup分别提供了串行动画组和并行动画组。

  在之前程序的main.cpp中添加头文件,再将主函数的中间部分内容更改如下:

QPushButton button("Animated Button");
button.show();
// 按钮部件的动画1
QPropertyAnimation *animation1 = new QPropertyAnimation(&button, "geometry");
animation1->setDuration(4000);
animation1->setStartValue(QRect(250, 0, 120, 30));
animation1->setEndValue(QRect(250, 300, 120, 30));
animation1->setEasingCurve(QEasingCurve::OutBounce);

// 按钮部件的动画2
QPropertyAnimation *animation2 = new QPropertyAnimation(&button, "geometry");
animation2->setDuration(2000);
animation1->setStartValue(QRect(250, 300, 120, 30));
animation1->setEndValue(QRect(250, 300, 200, 60));

// 串行动画组(将上面的两个动画串行成一个组)
QSequentialAnimationGroup group;
group.addAnimation(animation1);
group.addAnimation(animation2);
group.start();

在这里插入图片描述

先执行动画1,执行完成之后再执行的动画2,动画的执行顺序与加入动画组的顺序是一致的。

在图形视图框架中使用动画

  要对QGraphicsItem使用动画,也可以使用QPropertyAnimation类。但是,QGraphicsItem并不是继承自QObject,所以直接继承自QGraphicsItem的图形项并不能使用QPropertyAnimation类创建动画。

  Qt 4.6中提供了一个QGraphicsItem的子类QGraphicsObject,它继承自QObject和QGraphicsItem,这个类为所有需要使用信号。

  QGraphicsObject还提供了多个常用的属性,比如位置pos,透明度opacity,旋转rotation和缩放scale等。这些都可以直接用来设置动画。

  新建空的Qt项目,项目名称MyItemanimation。项目创建完成后先在项目文件中添加"QT += widgets"一行代码并保存该文件,然后添加新的C++类,类名MyItem,基类设置为QGraphicsObject。

完成后更改myitem.h文件内容如下:

#ifndef MYITEM_H
#define MYITEM_H
#include <QGraphicsObject>

class MyItem : public QGraphicsObject
{
public:
    MyItem(QGraphicsItem *parent = 0);
    QRectF boundingRect() const;
    void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget);
};

#endif // MYITEM_H

然后到myitem.cpp文件中,更改其内容如下:

#include "myitem.h"
#include <QPainter>

MyItem::MyItem(QGraphicsItem *parent):
	QGraphicsObject(parent)
{
}

QRectF MyItem::boundingRect() const
{
	return QRectF(-10 - 0.5, -10 - 0.5, 20 + 1, 20 + 1);
}

void MyItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *)
{
	painter->drawRect(-10, -10, 20, 20);
}

最后添加新的math.cpp文件,并更改其内容如下:

#include <QApplication>
#include <QGraphicsScene>
#include <QGraphicsView>
#include "myitem.h"

int main(int argc, char *argv[])
{
	QApplication app(argc, argv);
	QGraphicsScene scene;
	scene.setSceneRect(-200, -150, 400, 300);
	MyItem *item = new MyItem;
	scene.addItem(item);

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

	// 为图形项的rotation属性创建动画
	QPropertyAnimation *animation = new QPropertyAnimation(item, "rotation");
	animation->setDuration(2000);
	animation->setStartValue(0);
	animation->setEndValue(360);	// 指定DeleteWhenStopped删除策略(动画执行结束后自动删除该动画对象)
	animation->start(QAbstractAnimation::DeleteWhenStopped);
	return app.exec();
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值