QT—图形视图

x33g5p2x  于2022-07-22 转载在 其他  
字(25.5k)|赞(0)|评价(0)|浏览(586)

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

图形视图框架的结构

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

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

图形视图框架可以管理数量庞大的自定义2D图形项,并且可以与它们进行交互

使用视图部件可以使这些图形项可视化,视图还支持缩放和旋转
框架中包含了一个事件传播构架,提供了和场景中的图形项进行精确的双精度交互的能力,图形项可以处理键盘事件,鼠标的按下、移动,释放和双击事件,还可以跟踪鼠标的移动。
图形视图框架使用一个 BSP树(Binary Space Partitioning)快速发现图形项,也正是因为如此,它可以实时显示一个巨大的场景,甚至包含上百万个图形项。

本节的内容可以在帮助中通过Graphics View Framework关键字查看。

场景

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

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

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

可以调用QGraphicsScene::addItem()函数将图形项添加到场景中

然后调用任意一个图形项发现函数来检索添加的图形项。
QGraphicsScene::items()函数及其他几个重载函数可以返回符合条件的所有图形项,这些图形项不是与指定的点,矩形、多边形或者矢量路径相交,就是包含在它们之中。

QGraphicsScene::itemAt()函数返回指定点的最上层的图形项

所有的图形项发现函数返回的图形项都是使用递减顺序(例如,第一个返回的图形项在最上层,最后返回的图形项在最下层)。

如果要从场景中删除一个图形项,则可以使用QGraphicsScene::RemoveItem()函数

下面先来看一个简单的例子:
这次新建一个空项目,名称为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();
}

这里先创建了一个场景,然后创建了一个矩形图形项,并且将该图形项添加到了场景中。然后使用itemAt()函数返回指定坐标处最顶层的图形项,这里返回的就是刚才添加的矩形图形项。

现在可以运行程序,不过因为还没有设置视图,所以不会出现任何图形界面。这时可以在应用程序输出栏中看到输出项目的信息,要关闭运行的程序,则可以按下应用程序输出栏上的红色按钮,然后强行关闭应用程序。


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会获取视口部件的拥有权(ownership)。

在前面的程序中先添加头文件#include < QGraphicsView>,然后在主函数中“return app.exec();”一行代码前继续添加如下代码:

// 为场景创建视图
    QGraphicsView view(&scene);
   // 设置场景的前景色
    view.setForegroundBrush(QColor(255, 255, 0, 100));
    // 设置场景的背景图片
    view.setBackgroundBrush(QPixmap("../myScene/background.png"));

    view.resize(400, 300);
    view.show();

这里新建了视图部件,并指定了要可视化的场景。然后为该视图设置了场景前景色和背景图片。

一个场景分为3层:

  • 图形项层(ItemLayer)
  • 前景层(ForegroundLayer)
  • 背景层(BackgroundLayer)。

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

这里的前景色设置为半透明的黄色,当然也可以设置为其他的填充。
这里要提示一下,其实使用好前景色可以实现很多特殊的效果,比如使用半透明的黑色便可以实现夜幕降临的效果。
代码中使用了QGraphicsView类中的函数设置了场景中的背景和前景,其实也可以使用QGraphicsScene中的同名函数来实现,不过它们的效果并不完全一样。

使用QGraphicsScene对象设置了场景背景或者前景,那么对所有关联了该场景的视图都有效,而QGraphicsView对象设置的场景的背景或者前景只对它本身对应的视图有效。

可以在这里的代码后面再添加如下代码:
(使用QGraphicsScene对象设置了场景背景或者前景)

scene.setForegroundBrush(QColor(255, 255, 0, 100));
 scene.setBackgroundBrush(QPixmap("../myscene/background.png"));

这时再运行程序可以发现,两个视图的背景和前景都一样了。

当然,使用视图对象来设置场景背景的好处是可以在多个视图中使用不同的背景和前景来实现特定的效果。

最后设置了视图的大小,并调用show()函数来显示视图

现在场景中的内容就可以在图形界面中显示出来了,运行程序,效果如图所示。

可以看到矩形图形项和背景图片都是在视图中间部分绘制,这个问题会在坐标系统部分详细讲解。

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

图形项

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

QGraphicsItem 主要支持如下功能:

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

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

下面来自定义图形项。在前面的程序中添加新文件,模板选择C++类,类名为MyItem,基类设置
为QGraphicsltem。

添加完成后,将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);
};

#endif // MYITEM_H

再到myitem.cpp文件中添加头文件#include<QPainter>,然后定义添加的两个函数:

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

MyItem::MyItem()
{

}

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);
}

要实现自定义的图形项,那么首先要创建一个QGraphiesItem的子类,然后重新实
现它的两个纯虚公共函数: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文件中先添加头文件# include“myitem.h",然后将以前的图形项的创建代码:

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

更改为:

// 创建矩形图形项
    MyItem *item = new MyItem;

这时运行程序可以看到,自定义的红色小方块出现在了视图的正中间,背景图片的位置也有所变化,这些问题都会在后面的坐标系统中讲到。(使用的图形项是自定义的MyItem)

如果只想添加简单的图形项,那么也可以直接使用图形视图框架提供的标准图形项,它们的效果如图11 -2所示。

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

坐标系统

图形视图框架基于笛卡尔坐标系统,一个图形项在场景中的位置和几何形状由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,可以使用QGraphiesItem::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()函数。

不仅可以在视图、场景和图形项之间使用坐标映射,还可以在子图形项、父图形项或者图形项、图形项之间进行坐标映射。
图形视图框架提供的所有映射函数如表11-1所列,所有的映射函数都可以映射点、矩形、多边形和路径。

例子

下面通过例子来进一步学习图形视图框架的坐标系统
在前面的程序中添加新文件,模板选择C++类,类名为MyView,基类设置为QGraphicsView。完成后将myview. h文件更改如下:

#ifndef MYVIEW_H
#define MYVIEW_H

#include <QGraphicsView>

class MyView : public QGraphicsView
{
    Q_OBJECT
public:
    explicit MyView(QWidget *parent = 0);
protected:
    void mousePressEvent(QMouseEvent *event);
};

#endif // MYVIEW_H

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

#include "myview.h"
#include <QMouseEvent>
#include <QGraphicsItem>
#include <QDebug>

修改构造函数如下:

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

添加鼠标按下事件处理函数mousePressEvent:

void MyView::mousePressEvent(QMouseEvent *event)
{
    // 分别获取鼠标点击处在视图、场景和图形项中的坐标,并输出
    QPoint viewPos = event->pos();
    qDebug() << "viewPos: " << viewPos;
    QPointF scenePos = mapToScene(viewPos);
    qDebug() << "scenePos: " << scenePos;
    QGraphicsItem *item = scene()->itemAt(scenePos, QTransform());
    if (item) {
        QPointF itemPos = item->mapFromScene(scenePos);
        qDebug() << "itemPos: " << itemPos;
    }
}

这里先使用鼠标事件对象event获取了鼠标单击位置在视图中的坐标,然后使用映射函数mapToScene将这个坐标转换为了场景中的坐标.
并使用scene()函数获取视图当前的场景的指针item ,然后使用QGraphicsScene: :itemAt()函数获取了场景中该坐标处的图形项。
如果这里有图形项( if (item)),使用函数mapFromScene输出该点在图形项坐标系统中的坐标itemPos

下面到main. cpp文件中,先添加头文件井include“myview. h" ,然后更改主函数
的内容为:

#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;
    scene.setSceneRect(0, 0, 400, 300);
    MyItem *item = new MyItem;
    scene.addItem(item);
    item->setPos(10, 10);
    //item->setZValue(1);
    QGraphicsRectItem *rectItem = scene.addRect(QRect(0, 0, 100, 100),
                                            QPen(Qt::blue), QBrush(Qt::green));
    item->setParentItem(rectItem);
    rectItem->setRotation(45);
    rectItem->setPos(20, 20);
    MyView view;
    view.setScene(&scene);
    view.setForegroundBrush(QColor(255, 255, 0, 100));
    view.setBackgroundBrush(QPixmap("../myscene/background.png"));
    view.resize(400, 300);
    view.show();
    return app.exec();
}

这里先向场景中添加了一个Myltem图形项

MyItem *item = new MyItem;

然后又使用QGraphicsScene::addRect()函数添加了矩形图形项,并分别设置了它们在场景中的位置。

QGraphicsRectItem *rectItem = scene.addRect(QRect(0, 0, 100, 100),
                                            QPen(Qt::blue), QBrush(Qt::green));

然后使用自定义的视图类MyView创建了视图。

MyView view;
    view.setScene(&scene);

因为item在rectltem之前添加到场景中,所以rectItem会在Item之上进行绘制。
前面的:

MyItem *item = new MyItem;
    scene.addItem(item);
    item->setPos(10, 10);

运行程序

在视图上单击,然后查看应用程序输出栏中的输出数据:
分别单击视图的左上角、背景图片的交点处,小正方形的左上角以及大正方形的左,上角。
左上角:

背景图片的交点处:

大矩形的左上角:

小矩形的左上角: 跟大矩形一样,左上角是原点

通过数据可以发现:
视图的左上角是视图的原点,背景图片的交点处是场景的原点,而两个正方形的左上角分别是它们图形项坐标的原点。

如果想将item(小的)移动到rectItem(大的)之上,那么可以在创建item的代码之后添加如下一行代码:

item->setZValue(1);

前面讲过:

图形项都包含一个Z值来设置它们的层叠顺序
一个图形项的Z值默认为0
可以使用QGraphiesItem::setZValue()来改变一个图形项的Z值
从而使它堆叠到其兄弟图形项的上面(使用较大的Z值时)或者下面(使用较小的Z值时)。

这样就可以让item显示在rectltem之上了。
运行如下:

其实还可以将item作为rectItem的子图形项,这样item就会在rectItem的坐标系统上进行绘制,也就是不用使用setZValue( )兩数,item也是默认的显示在rectltem上的。

先注释掉添加的setZValue()的代码,然后在创建rectltem的代码的后面添加如下代码:

item->setParentItem(rectItem);
    rectItem->setRotation(45);
    rectItem->setPos(20, 20);

这里将rectltem设置为item的父项,然后将rectltem进行了旋转45°。
可以看到,rectItem会在自己的坐标系统中进行旋转,并且是以原点为中心进行旋转的。

虽然item也进行了旋转,但是它在rectItem中的相对位置却没有改变。

场景背景图片会随着图形项的不同而改变位置?(场景矩形)

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

其实场景背景图片位置的变化也就是场景位置的变化.

默认的:

  • 如果场景中没有添加任何图形项,那么场景的中心(默认的是原点)会和视图的中心重合
  • 如果添加了图形项,那么视图就会以图形项的中心为中心来显示场景。

就像前面看到的。因为图形项的大小或者位置变化了,所以视口的位置也就变化了,这样看起来好像是背景图片的位置发生了变化。

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

另外,场景矩形也用于QGraphiesScene管理图形项索引
可以使用QGraphisScene::SetSceneRect()来设置场景矩形
例如上面的:

//场景矩形
QGraphicsScene scene;
    scene.setSceneRect(0, 0, 400, 300);
    //设置了scene的范围(一个大小范围)(0, 0, 400, 300)(场景矩形)

如果没有设置
那么sceneRet()会返回一个包含了自从场景创建以来添加的所有图形项的最大边界矩形
(这个矩形会随着图形项的添加或者移动而不断增长,但是永远不会缩小)

所以操作一个较大的场景时。总应该设置一个场景矩形 。
设置了场景矩形,就可以指定视图显示的场景区域了。

比如将场景的原点显示在视图的左上角,那么可以在创建场景的代码下面添加如下一行代码:

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

设置到了0,0,大小高400,宽300

如果当场景很大时,还可以使用QGraphicsView类中的centerOn()函数来设置场景中的一个点或者一个图形项作为视图的显示中心。

事件处理和传播

图形视图框架中的事件都是先由视图进行接收,然后传递给场景,再由场景传递给相应的图形项

而对于键盘事件,它会传递给获得焦点的图形项,可以使用QGraphicsScene类的setFocusItem()函数或者图形项自身调用setFocus()函数设置焦点图形项
默认的,如果场景没有获得焦点,那么所有的键盘事件都会被丢弃。

如果调用了场景的setFocus()函数或者场景中的一个图形项获得了焦点,那么场景也会自动获得焦点
如果场景丢失了焦点(比如调用了clearFocus()函数),然而它的一个图形项获得焦点,那么场景就会保存这个图形项的焦点信息;

当场景重新获得焦点后,就会确保最后一个焦点项目重新获得焦点。

  • 对于鼠标悬停效果
    QGraphicsScene调度悬停事件
    如果一个图形项可以接收悬停事件,那么当鼠标进入它的区域之中时,它就会收到GraphicsSceneHoverEnter事件。
    鼠标继续在图形项的区域之中进行移动,那么QGraphicsScene就会向该图形项发送GraphiesSeeneHoverMove 事件。
    当鼠标离开图形项的区域时,它将会收到一个GraphicsSceneHoverLeave事件。

图形项默认是无法接收悬停事件的,可以使用QGraphicsItem 类的setAcceptHoverEvents()函数使图形项可以接收悬停事件。

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

例子(放大和缩小视图,旋转视图)

下面来看一个例子。新建空的Qt项目,名称为myview。

完成后先在myview. pro中添加“QT十= widgets"一行代码,然后按照项目11-2 那样向本项目中添加一个Myltem自定义图形项。

在myitem.h文件中声明boundingRect()和paint()两个纯虚函数,再声明并定义一个公共的用于设置图形项填充色的函数和变量,完成后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 < 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);
}

paint里面根据图形项是否获得焦点hasFocus()使用不同的颜色绘制图形项的轮廓。这个会在后面使用到。还使用了变量作为画刷的颜色,这样就可以动态指定图形项的填充色了。

完成了自定义图形项类的添加后,再按照项目11-3那样添加一个MyView自定义视图类

添加完成后,先在myview.h中添加键盘按下事件处理函数的声明:

#ifndef MYVIEW_H
#define MYVIEW_H

#include <QGraphicsView>

class MyView : public QGraphicsView
{
    Q_OBJECT
public:
    explicit MyView(QWidget *parent = 0);
protected:
    void keyPressEvent(QKeyEvent *event);

};

#endif // MYVIEW_H

然后到myview.cpp文件中添加头文件#include< QKeyEvent>,并修改构造函数如下:

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()函数

QGraphicsView::keyPressEvent(event);

不然在场景或者图形项中就无法再接收到该事件了。

最后添加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);
    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/background.png"));
    view.show();
    return app.exec();
}
这里在场景中添加了5个图形项,分别为它们设置了随机颜色。

运行程序,可以使用键盘上的“+”和“-”键来放大和缩小视图,也可以使用向右方向键"->”来旋转视图。

按下-每次缩小1/2,+相反扩大1/2

右方向键"->”来旋转视图
每次 rotate(30);顺时针选择30度

例子2(添加事件,一些按键函数)

下面再来看一下其他事件的应用。先在myitem.h文件中添加一些事件处理函数
的声明:

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

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

#include "myitem.h"
#include <QPainter>
#include <QCursor>
#include <QKeyEvent>
#include <QGraphicsSceneHoverEvent>
#include <QGraphicsSceneContextMenuEvent>
#include <QMenu>

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

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

    setFlag(QGraphicsItem::ItemIsFocusable);
    setFlag(QGraphicsItem::ItemIsMovable);
    setAcceptHoverEvents(true);

}

这里的setFlag()函数可以开启图形项的一些特殊功能:

  • 比如要想使用键盘控制图形项,则必须使图形项可以获得焦点,所以要先设置ItemIsFocusable标志;
  • 如果想使用鼠标来拖动图形项进行移动,那么就必须先设置ItemIsMovable标志

这些标志也可以在创建图形项时进行设置,更多的标志可以参见QGraphiesItem类的帮助文档。

为了使图形项支持悬停事件,需要调用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");
    QAction *selectedAction = menu.exec(event->screenPos());
    if (selectedAction == moveAction) {
        setPos(0, 0);
    }
}
  • 鼠标按下事件处理函数中,为鼠标单击的图形项设置了焦点setFocus();,则当按下键盘时该图形项就会接收到按键事件。

  • 如果按下了键盘的向下方向键,那么获得焦点的图形项就会向下移动,这里使用了moveBy(qreal dx, qreal dy)函数;
    它用来进行相对移动,就是相对于当前位置在水平方向移动dx,在垂直方向移动dy。在进行项目移动时,经常使用到该函数。

  • 然后是右键菜单事件,在一个图形项上右击鼠标,则弹出一个菜单,如果选中该菜单,那么图形项会移动到场景原点

现在运行程序,效果如图所示,将鼠标光标移动到一个图形项上,则可以看到光标外观改变了,而且出现了工具提示。

可以使用鼠标拖动图形项,在一个图形项上右击鼠标,则还可以看到弹出的右键菜单,也可以选中一个图形项.
然后使用键盘来移动它。(这里只设置了一个向下键事件,所以按下键像下面移动)

图形视图框架还可以处理一些其他的事件﹐比如拖放事件,这个可以参考前面的相关知识来学习,也可以参考一下 Drag and Drop Robot Example示例程序;还有一个Diagram Scene Example示例程序,是一个使用图形视图框架设计的绘图程序,也可以参考一下。

图形视图框架的其他特性(效果)

图形效果

图形效果(graphics effect)是Qt 4.6添加的一个新的特色功能,QGraphicsEffect类是所有图形效果的基类

使用图形效果来改变元素的外观是通过在源对象(如一个图形项)和目标设备(如视图的视口)之间挂接了渲染管道和一些操作来实现的
图形效果可以实施在任何一个图形项或者非顶层窗口的任何窗口部件上,只须先创建一个图形效果对象,然后调用setGraphicsEffect()函数来使用这个图形效果即可。
如果想停止使用该效果,可以调用setEnabled(false)

Qt提供了4种标准的效果,如表11-2所列。也可以自定义效果,这需要创建QGraphicsEffect的子类,可以查看该类的帮助文档来了解更多的相关内容

例子 按键判断实现效果

继续在前面程序的基础上进行更改,先在myitem.cpp文件中添加头文件
#include <QGraphicsEffect>,然后更改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;
    }

}

这里分别使用不同的按键来实现不同的图形效果,现在运行程序,然后分别选中一个图形项设置为不同的图形效果,数字键5graphicsEffect()->setEnabled(false);可以取消图形项的图形效果。

这里注意要选中目标(图形项,非顶层窗口部件)

  • 按键1:QGraphicsBlurEffect 模糊效果

  • 按键2:QGraphicsColorizeEffect 染色
//白色,强度0.6
  colorizeEffect->setColor(Qt::white);
        colorizeEffect->setStrength(0.6);

  • 按键3:QGraphicsDropShadowEffect 阴影效果
dropShadowEffect->setColor(QColor(63, 63, 63, 100));
        dropShadowEffect->setBlurRadius(2);
        dropShadowEffect->setOffset(10);
       
  /*  使用setColor()来修改阴影的颜色,默认是透明的黑灰色QColor(63,63,63,180);
可以使用setOffset()来改变阴影的偏移值﹐默认为右下方8像素;
还可以使用setBlurRadius()来改变阴影的模糊半径,其默认值为1 */

  • 按键4:QGraphicsOpacityEffect 透明效果
//透明度0.4
 opacityEffect->setOpacity(0.4);

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

1.动画

图形视图框架支持几种级别的动画。

以前可以使用QGraphicsItemAnimation类很容易地实现图形项的动画效果,不过该类现在已经过时,所以不再讲解。

现在1. 主要是通过动画框架来实现动画效果。另外的方法是2. 创建一个继承自QObjectQGraphicsItem的自定义图形项,然后创建它自己的定时器来实现动画,这个这里也不再讲解

第三种方法是使用QGraphicsScene::advance()来推进场景

下面来看一下它的应用。

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

void advance(int phase);

然后到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文件中,先添加头文件"include< QTimer>",然后在主函数的最后return语句前添加如下代码:

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

这里创建了一个定时器,当定时器溢出时会调用场景的advance()函数。

现在可以运行程序查看效果。

下面来完成他们碰撞咋办?

2. 碰撞检测

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

  • ➢ 重新实现QGraphicsItem::shape()函数来返回图形项准确的形状,然后使用默认的collidesWithItem( )函数通过两个图形项形状之间的交集来判断是否发生碰撞。如果没有重新实现shape()函数,那么它默认会调用boundingRect()函数返回一个简单的矩形。
  • ➢ 重新实现collidesWithltem()函数来提供一 个自定义的图形项碰撞算法。
  1. 使用QGraphicsltem类中的collidesWithItem()函数来判断是否与指定的图形项进行了碰撞;
  2. 使用collidesWithPath()来判断是否与指定的路径碰撞;
  3. 使用collidingItems()获取与该图形项碰撞的所有图形项的列表;
  4. 也可以调用QGraphicsScene类的collidingItems()

这几个函数都有一个Qt::ItemSelectionMode参数来指定怎样进行图形项的选取,它一共有4个值,如表11 - 3所列,其中,Qt::IntersectsItemShape是默认值。

常量描述
Qt::ContainsltemShape选取只有形状完全包含在选择区域之中的图形项
Qt::IntersectsItemShape选取形状完全包含在选择区域之中或者与区域的边界相交的图形项
Qt::ContainsItemBoundingRect选取只有边界矩形完全包含在选择区域之中的图形项
Qt::IntersectsItemBoundingRect选取边界矩形完全包含在选择区域之中或者与区域的边界相交的图形项

表11-3 图形项选取模式

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

QPainterPath shape();

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

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

Qt 之图形(QPainterPath)
创建了一个容器,然后添加个矩形返回。

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

if(hasFocus() || !collidingItems().isEmpty())

这样就可以在图形项与其他图形项碰撞时使其轮廓线变为白色了。
advance()函数和碰撞检测的使用可以参考CollidingMiceExample示例程序。

测试下:
碰撞会使其轮廓线变为白色了:

3. 图形项组

QGraphicsItemGroup图形项组为图形项提供了一个容器,它可以将多个图形项组合在一起,而将它本身以及所有的子图形项看作一个独立的图形项

与父图形项不同图形项组中的所有图形项都是平等的- - - 例如,可以通过拖动其中任意一个来将它们一起进行移动。

如果只想将一个图形项存储在另一个图形项之中,那么可以使用setParentItem()来为其设置父图形项

下面仍然在前面的程序中添加代码。在main()函数的return语句前添加如下代码: ’

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);

这里创建了两个图形项item1 \item2 和一个图形项组group ,然后将两个图形项加入到图形项组中。

这样只需要将图形项组添加到场景中(scene.addItem(group);),那么两个图形项也就自动添加到场景中了。

运行程序,可以通过鼠标拖动其中一个图形项来一起移动两个图形项。

除了手动创建图形项组,常用的方法还有使用场景对象直接创建图形项组,并将指定的图形项添加到其中,例如:

QGraphicsItemGroup *group = scene->createItemGroup(scene->selecteditems( ));

这个一般用来选取场景中的图形项,可以让QGraphicsView类的对象通过调用setDragMode(QGraphicsView::RubberBandDrag)函数来使鼠标可以在视图上拖出橡皮筋框来选择图形项。

注意,如果要使图形项可以被选择,还要使用setFlag()指定它们的ItemIsSelectable标志。

如果要从图形项组中删除一个图形项,则可以调用removeFromGroup()函数,还可以调用QGraphicsScene::destroyItemGroup()来销毁整个图形项组;
这两个函数都不会销毁图形项组中的图形项,而会将它们移动到父图形项组或者场景中。

打印和使用OpenGL进行渲染

1.打印

图形视图框架提供渲染函数QGraphicsScene::render()QGraphicsView::render()来完成打印功能

这个两个函数提供了相同的API,可以在绘图设备上绘制场景或者视图的全部或者部分内容。

两者的不同之处就是一个在场景坐标上进行操作而另一个在视图坐标上。

  • QGraphicsScene::render()经常用来打印没有变换的场景,比如几何数据和文本文档等;
  • QGraphicsView::render()函数适合用来实现屏幕快照。

要在打印机上进行打印,可以使用如下代码:

QPrinter printer;
if(QPrintDialog(&printer).exec()==QDialog::Accepted){
QPainter painter(&printer);
painter.setRenderHint(QPainter::Antialiasing);
scene.render(&painter);
}

下面来实现屏幕快照,在上面的代码基础上,main.cpp中添加
头文件如下:

#include <QTimer>
#include <QPainter>
#include <QPixmap>
#include <QOpenGLWidget>

main函数后面最后的return语句之前添加如下代码:

QPixmap pixmap(400, 300);
    QPainter painter(&pixmap);
    painter.setRenderHint(QPainter::Antialiasing);
    view.render(&painter);
    painter.end();
    pixmap.save("view.png");

就说创造一个pixmap图像400,300尺寸的,view视图打印下来,保存为view.png

2. 使用OpenGL进行渲染

使用OpenGL进行渲染时,可以使用QGraphicsView::set Viewport()将QOpenGLWidget作为QGraphicsView的视口。

继续在前面的程序中添加代码。到main.cpp文件中添加头文件# include < QOpenGLWidget> ,并在main()主函数中创建MyView对象的代码后添加如下一行代码:

view.setViewport(new QOpenGLWidget());

这样就可以使用 OpenGL进行渲染了

关于OpenGL的教程:入门OpenGL渲染

窗口部件、布局和内嵌部件

从Qt4.4开始通过QGraphicsWidget类引人了支持几何和布局的图形项。

图形部件QGraphicsWidget与QWidget 很相似,但是与QWidget 不同,它不是继承自QPaintDevice,而是QGraphicsItem

通过它可以实现一个拥有事件、信号和槽、大小提示和策略的完整的部件
还可以使用QGraphicsLinearLayout和QGraphicsGridLayout来实现部件的布局。

QGraphicsWidget继承自QGraphicsObject 和QGraphicsLayoutltem,而QGraphicsObject继承自QObject和QGraphicsItem,所以QGraphicsWidget既拥有以前窗口部件的一些特性也拥有图形项的一些特性

图形视图框架提供了对任意的窗口部件嵌入场景的无缝支持,这是通过QGraphicsWidget的子类QGraphicsProxyWidget实现的。

可以使用QGraphicsScene类的addWidget()函数将任何一个窗口部件嵌入到场景中,这也可以通过创建QGraphicsProxyWidget类的实例来实现。

下面新建一个空的项目来测试,新建空的Qt项目,名称为mywidgetitem,完成后先在项目文件中添加“QT十= widgets”一行代码,并保存该文件,然后添加新文件main.cpp,并在其中添加如下代码:

#include <QApplication>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QGraphicsWidget>
#include <QTextEdit>
#include <QPushButton>
#include <QGraphicsProxyWidget>
#include <QGraphicsLinearLayout>
#include <QObject>

int main(int argc, char* argv[ ])
{
    QApplication app(argc, argv);
    QGraphicsScene scene;
    // 创建部件,并关联它们的信号和槽
    QTextEdit *edit = new QTextEdit;
    QPushButton *button = new QPushButton("clear");
    QObject::connect(button, SIGNAL(clicked()), edit, SLOT(clear()));
    // 将部件添加到场景中
    QGraphicsWidget *textEdit = scene.addWidget(edit);
    QGraphicsWidget *pushButton = scene.addWidget(button);
    // 将部件添加到布局管理器中
    QGraphicsLinearLayout *layout = new QGraphicsLinearLayout;
    layout->addItem(textEdit);
    layout->addItem(pushButton);
    // 创建图形部件,设置其为一个顶层窗口,然后在其上应用布局
    QGraphicsWidget *form = new QGraphicsWidget;
    form->setWindowFlags(Qt::Window);
    form->setWindowTitle("Widget Item");
    form->setLayout(layout);
    // 将图形部件进行扭曲,然后添加到场景中
    form->setTransform(QTransform().shear(2, -0.5), true);
    scene.addItem(form);
    QGraphicsView view(&scene);
    view.show();
    return app.exec();
}

现在运行程序,效果如图所示。

可以看到,嵌入窗口部件结合了以前的窗口部件的功能和现在的图形项的功能,可以实现些特殊的效果。
Qt中提供了一个PadNavigator Example的示例程序及一个Embedded Dialogs演示程序,都是关于这部分内容的,可以作为参考。

相关文章