找回密码
 立即注册
收起左侧

第19篇 Qt5之2D绘图(九)图形视图框架(上)

13
回复
10416
查看
[复制链接]
累计签到:1564 天
连续签到:1 天
来源: 2017-5-17 23:25:02 显示全部楼层 |阅读模式
图形视图框架(上)

版权声明


该文章原创于Qt开源社区(www.qter.org),作者yafeilinux,转载请注明出处!



导语



在前面讲的基本绘图中,我们可以自己绘制各种图形,并且控制它们。但是,如果需要同时绘制很多个相同或不同的图形,并且要控制它们的移动、检测它们的碰撞和叠加;或者我们想让自己绘制的图形可以拖动位置、进行缩放和旋转等操作。实现这些功能,要是还使用以前的方法,那么会十分困难。解决这些问题,可以使用Qt提供的图形视图框架。
        图形视图可以对大量定制的2D图形项进行管理和相互作用。视图部件可以让所有图形项可视化,它还提供了缩放和旋转功能。我们在帮助中搜索Graphics View 关键字,内容如下图:



        这里一开始对这个框架进行了简单介绍,整个图形视图结构主要包含三部分:场景(Scene)、视图(View)和图形项(Item),它们分别对应 QGraphicsScene QGraphicsView QGraphicsItem三个类。其实图形视图框架是一组类的集合,在帮助中可以看到所有与它相关的类。下面我们就开始结合程序对整个框架进行介绍。



环境:Windows 7 + Qt 5.8.0(包含Qt Creator 4.2.1)



目录



一、基本应用
二、图形项(QGraphicsItem
(一)自定义图形项
(二)光标和提示
(三)拖放
(四)键盘与鼠标事件
(五)碰撞检测
(六)移动
(七)动画



正文



一、基本应用

        我们新建空的Qt项目(Empty qmake Project),项目名称为graphicsview01。然后在这个项目中添加新的C++ 源文件,命名为main.cpp,将main.cpp的内容更改如下。


#include <QtWidgets>

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

    // 场景
    QGraphicsScene *scene = new QGraphicsScene;  
    // 矩形项
    QGraphicsRectItem *item =
            new QGraphicsRectItem(100,100,50,50);  
    // 项添加到场景
    scene->addItem(item);  
    // 视图
    QGraphicsView *view = new QGraphicsView;  
    // 视图关联场景
    view->setScene(scene);  
    // 显示视图
    view->show();  

    return app.exec();
}


        这里我们建立了一个最简单的基于这个图形视图框架的程序。分别新建了一个场景,一个图形项和一个视图,并将图形项添加到场景中,将视图与场景关联,最后显示视图就可以了。基于这个框架的所有程序都是这样实现的。因为使用了Qt Widgets模块,所以需要在graphicsview01.pro文件中添加QT += widgets 一行代码。运行效果如下。



        就像我们看到的,场景是管理图形项的,所有的图形项必须添加到一个场景中,但是场景本身无法可视化,要想看到场景上的内容,必须使用视图。本篇后面的内容会详细介绍图形项,场景和视图放到下一篇进行介绍。


二、图形项(QGraphicsItem

QGraphicsItem类是所有图形项的基类。图形视图框架对一些典型的形状提供了一些标准的图形项。比如上面我们使用的矩形(QGraphicsRectItem)、椭圆(QGraphicsEllipseItem)、文本(QGraphicsTextItem)等多个图形项。但只有继承QGraphicsItem 类实现我们自定义的图形项时,才能显示出这个类的强大。QGraphicsItem支持以下功能:

  • 鼠标的按下、移动、释放和双击事件,也支持鼠标悬停、滚轮和右键菜单事件。
  • 键盘输入焦点和键盘事件
  • 拖放
  • 利用QGraphicsItemGroup进行分组
  • 碰撞检测


(一)自定义图形项

1.在前面的项目中添加新的C++类,类名设为 MyItem,基类设为QGraphicsItem

2.然后,将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


3.下面到myitem.cpp中先添加#include <QPainter>头文件,然后对两个函数进行定义:

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 *option,
                   QWidget *widget)
{
    Q_UNUSED(option);  //标明该参数没有使用
    Q_UNUSED(widget);
    painter->setBrush(Qt::red);
    painter->drawRect(0,0,20,20);
}


4.下面到main.cpp中添加#include "myitem.h",然后将以前那个矩形项的定义语句改为:

MyItem *item = new MyItem;

运行程序,效果如下:


        可以看到,我们要继承QGraphicsItem类实现自定义的图形项,必须先实现两个纯虚函数boundingRect()paint(),前者用于定义Item的绘制范围,后者用于绘制图形项。其实boundingRect()还有很多用途,后面会涉及到。


(二)光标和提示

1.myitem.cpp 中先添加头文件#include <QCursor>,然后在构造函数中添加两行代码:

MyItem::MyItem()
{
    setToolTip("Click and drag me!");  //提示
    setCursor(Qt::OpenHandCursor);    //改变光标形状
}


然后运行程序,效果如下:



        当光标放到小方块上时,光标变为了手型,并且弹出了提示。更多的光标形状可以查看Qt::CursorShape,我们也可以使用图片自定义光标形状。


(三)拖放

下面写这样一个程序,有几个不同颜色的圆形和一个大矩形,我们可以拖动圆形到矩形上,从而改变矩形的颜色为该圆形的颜色。

1、面的程序进行改进,用来实现圆形图形项。myitem.h中添加一个私有变量和几个键盘事件处理函数的声明:


protected:
    void mousePressEvent(QGraphicsSceneMouseEvent *event);
    void mouseMoveEvent(QGraphicsSceneMouseEvent *event);
    void mouseReleaseEvent(QGraphicsSceneMouseEvent *event);

private:
    QColor color;


2.然后到myitem.cpp中,在构造函数中初始化颜色变量:


//初始化随机颜色
color = QColor(qrand() % 256, qrand() % 256, qrand() % 256);


paint()函数中将绘制矩形的代码更改如下:


painter->setBrush(color);
painter->drawEllipse(0, 0, 20, 20);


3.下面先添加头文件包含:

#include <QGraphicsSceneMouseEvent>
#include <QDrag>
#include <QMimeData>
#include <QApplication>
#include <QWidget>

        然后添加几个键盘事件处理函数的实现:

void MyItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
    if(event->button() != Qt::LeftButton)
    {
        //如果不是鼠标左键按下,则忽略该事件
        event->ignore();
        return;
    }
    //如果是鼠标左键按下,改变光标形状
    setCursor(Qt::ClosedHandCursor);
}

void MyItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
    if(QLineF(event->screenPos(),
            event->buttonDownScreenPos(Qt::LeftButton)).length()
            < QApplication::startDragDistance())
    {
      //如果按下的点到现在的点的距离小于程序默认的拖动距离,表明没有拖动,则返回
        return;
    }
    //为event所在窗口部件新建拖动对象
    QDrag *drag = new QDrag(event->widget());
    //新建QMimeData对象,它用来存储拖动的数据
    QMimeData *mime = new QMimeData;
     //关联
    drag->setMimeData(mime);
    //放入颜色数据
    mime->setColorData(color);

    //新建QPixmap对象,它用来重新绘制圆形,在拖动时显示
    QPixmap pix(21,21);
    pix.fill(Qt::white);
    QPainter painter(&pix);
    paint(&painter, 0, 0);
    drag->setPixmap(pix);

    //我们让指针指向圆形的(10,15)点
    drag->setHotSpot(QPoint(10, 15));
    //开始拖动
    drag->exec();
    //改变光标形状
    setCursor(Qt::OpenHandCursor);
}

void MyItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
    setCursor(Qt::OpenHandCursor);    //改变光标形状
}


        此时运行程序,效果如下:



4.下面我们新添一个类,它用来提供矩形图形项,并且可以接收拖拽来的数据。在myitem.h中,我们加入该类的声明:

class RectItem : public QGraphicsItem
{
public:
    RectItem();
    QRectF boundingRect() const;
    void paint(QPainter *painter, const QStyleOptionGraphicsItem *option,
               QWidget *widget);
protected:
     void dragEnterEvent(QGraphicsSceneDragDropEvent *event);
     void dragLeaveEvent(QGraphicsSceneDragDropEvent *event);
     void dropEvent(QGraphicsSceneDragDropEvent *event);  
private:
     QColor color;
     bool dragOver;  //标志是否有拖动进入
};


5.然后进入myitem.cpp进行相关函数的定义:


RectItem::RectItem()
{
    setAcceptDrops(true);  //设置接收拖放
    color = QColor(Qt::lightGray);
}

QRectF RectItem::boundingRect() const
{
     return QRectF(0, 0, 50, 50);
}

void RectItem::paint(QPainter *painter,
                     const QStyleOptionGraphicsItem *,
                     QWidget *)
{
    //如果其上有拖动,颜色变亮
    painter->setBrush(dragOver ? color.light(130) : color);  
    painter->drawRect(0,0,50,50);
}

void RectItem::dragEnterEvent(QGraphicsSceneDragDropEvent *event)
{
    if(event->mimeData()->hasColor()) //如果拖动的数据中有颜色数据,便接收
    {
        event->setAccepted(true);
        dragOver = true;
        update();
    }
    else event->setAccepted(false);
}

void RectItem::dragLeaveEvent(QGraphicsSceneDragDropEvent *event)
{
    Q_UNUSED(event);
    dragOver = false;
    update();
}

void RectItem::dropEvent(QGraphicsSceneDragDropEvent *event)
{
    dragOver = false;
    if (event->mimeData()->hasColor())
       //我们通过类型转换来获得颜色
        color = qvariant_cast<QColor>(event->mimeData()->colorData());
     update();
}

6.下面进入main.cpp文件,更改main()函数中的内容如下:

int main(int argc,char* argv[ ])
{
    QApplication app(argc,argv);
    //设置随机数初值
    qsrand(QTime(0,0,0).secsTo(QTime::currentTime()));
    QGraphicsScene *scene = new QGraphicsScene;
    for(int i=0; i<5; i++) //在不同位置新建5个圆形
    {
        MyItem *item = new MyItem;
        item->setPos(i*50+20, 100);
        scene->addItem(item);
    }
    RectItem *rect = new RectItem; //新建矩形
    rect->setPos(100,200);
    scene->addItem(rect);
    QGraphicsView *view = new QGraphicsView;
    view->setScene(scene);
    view->resize(400, 300); //设置视图大小
    view->show();

    return app.exec();
}
        这是运行程序,效果如下:





这时我们已经实现了想要的效果。可以看到,要想实现拖放,必须源图形项和目标图形项都进行相关设置。在源图形项的鼠标事件中新建并执行拖动,而在目标图形项中必须指定setAcceptDrops(true),这样才能接收拖放,然后需要实现拖放的几个事件处理函数。


(四)键盘与鼠标事件

1.新建项目graphicsview02,然后按照前面自定义图形项一节内容自定义红色小方块图形项(可以直接把那里的代码拷贝过来)。下面先来看键盘事件。

2.myitem.h文件中声明键盘按下事件处理函数:


protected:
    void keyPressEvent(QKeyEvent *event);

然后在myitem.cpp中进行定义:

void MyItem::keyPressEvent(QKeyEvent *event)
{
    moveBy(0, 10);  //相对现在的位置移动
}


        这时运行程序,发现无论怎样方块都不会移动。其实要想使图形项接收键盘事件,就必须使其可获得焦点。我们在构造函数里添加一行代码:


//图形项可获得焦点
setFlag(QGraphicsItem::ItemIsFocusable);


(我们在新建图形项时指定也是可以的,如item->setFlag(QGraphicsItem::ItemIsFocusable);)
这时运行程序,然后用鼠标点击一下方块,再按下任意按键,方块就会向下移动。效果如下图所示。



3.再看鼠标事件。我们先在myitem.h文件中声明鼠标按下事件处理函数:

void mousePressEvent(QGraphicsSceneMouseEvent *event);


然后再myitem.cpp文件中对其进行定义:


void MyItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
    moveBy(10,0);
}


        此时运行程序,点击小方块,它便会向右移动。如果我们想让鼠标可以拖动小方块,那么我们可以重新实现mouseMoveEvent()函数,还有一种更简单的方法是,我们在构造函数中指明该图形项是可移动的:

setFlag(QGraphicsItem::ItemIsMovable);

        (当然我们也可以在新建图形项时指定它)运行程序。



(五)碰撞检测


        下面先看一个例子,再进行讲解。将上面程序中myitem.cpp文件中的paint()函数中的设置画刷的代码更改如下:


//如果与其他图形项碰撞则显示红色,否则显示绿色

painter->setBrush(!collidingItems().isEmpty()? Qt::red : Qt::green);

然后在main.cpp文件中在场景中添加一个直线图形项:

QGraphicsLineItem *line = new QGraphicsLineItem(0, 50, 300, 50);
scene->addItem(line);


这时运行程序,效果如下:



刚开始,方块是绿色的,当我们拖动它与直线相交时,它就变成了红色。

QGraphicsItem类中有三个碰撞检测函数,分别是collidesWithItem()、collidesWithPath()和collidingItems(),我们使用的是第三个。第一个是该图形项是否与指定的图形项碰撞,第二个是该图形项是否与指定的路径碰撞,第三个是返回所有与该图形项碰撞的图形项的列表。在帮助中我们可以查看它们的函数原型和介绍,这里要说明的是,这三个函数都有一个共同的参数Qt::ItemSelectionMode,它指明了怎样去检测碰撞。我们在帮助中进行查看,可以发现它是一个枚举类型,一共有四个值,分别是:

  • Qt::ContainsItemShape :只有图形项的shape被完全包含时;
  • Qt::IntersectsItemShape :当图形项的shape被完全包含时,或者图形项与其边界相交;
  • Qt::ContainsItemBoundingRect : 只有图形项的bounding rectangle被完全包含时;
  • Qt::IntersectsItemBoundingRect :图形项的bounding rectangle被完全包含时,或者图形项与其边界相交。

如果我们不设置该参数,那么他默认使用Qt::IntersectsItemShape 。这里所说的shape是指什么呢?在QGraphicsItem类中我们可以找到shape()函数,它返回的是一个QPainterPath对象,也就是说它能确定我们图形项的形状。但是默认的,它只是返回boundingRect()函数返回的矩形的形状。下面我们具体验证一下。main.cpp函数中添加两行代码:

qDebug() << item->shape();   //输出itemshape信息
qDebug() << item->boundingRect();  //输出itemboundingRect信息

这时运行程序,在下面的程序输出窗口会输出如下信息:


        我们发现,现在shapeboundingRect的大小是一样的。这时我们在到myitem.cpp中更改函数boundingRect()函数中的内容,将大小由20,改为50

return QRectF(0 - penWidth / 2, 0 - penWidth / 2,
               50 + penWidth, 50 + penWidth);

        这时再次运行程序,效果如下:

        小方块一出来便成为了红色,下面的输出信息也显示了,现在shape的大小也变成了50。怎样才能使小方块按照它本身的形状,而不是其boundingRect的大小来进行碰撞检测呢?我们需要重新实现shape()函数。
        myitem.h中,我们在public里进行函数声明:

QPainterPath shape() const;

        然后到myitem.cpp中进行其定义:

QPainterPath MyItem::shape() const
{
    QPainterPath path;
    path.addRect(0,0,20,20);  //图形项的真实大小
    return path;
}

        这时我们再运行程序,效果如下:


        可以看到,现在shapeboundingRect的大小已经不同了。所以对于不是矩形的形状,我们都可以利用shape()函数来返回它的真实形状。


(六)移动

        对于图形项的移动,我们有很多办法实现,也可以在很多层面上对其进行控制,比如说在View上控制或者在Scene上控制。但是对于大量的不同类型的图形项,怎样能一起控制呢?在图形视图框架中提供了advance()槽函数,这个函数在QGraphicsSceneQGraphicsItem中都有,在图形项类中它的原型是advance(int phase)。它的实现流程是,我们利用QGraphicsScene类的对象调用QGraphicsSceneadvance()函数,这时就会执行两次该场景中所有图形项的advance(int phase)函数,第一次phase0,告诉所有图形项即将要移动;第二次phase的值为1,这时执行移动。下面我们看一个例子。
        我们在myitem.h中的protected中声明函数:


void advance(int phase);


        然后在myitem.cpp中对其进行定义:


void MyItem::advance(int phase)
{
    if(!phase) return;  //如果phase为0,则返回
    moveBy(0,10);
}


        在到main.cpp中添加以下代码:


QTimer timer;
QObject::connect(&timer, SIGNAL(timeout()), scene, SLOT(advance()));
timer.start(1000);         

        这时运行程序,小方块就会每秒下移一下。


(七)动画


其实实现图形项的动画效果,也可以在不同的层面进行。如果我们只想控制一两个图形项的动画,一般在场景或视图中实现。但是要是想让一个图形项类的多个对象都进行同样的动画,那么我们就可以在图形项类中进行实现。我们先看一个例子。
        myitem.cpp文件中添加头文件包含:

#include <QGraphicsItemAnimation>
#include <QTimeLine>

        然后在构造函数中添加代码:


//新建动画类对象
QGraphicsItemAnimation *anim = new QGraphicsItemAnimation;
//将该图形项加入动画类对象中
anim->setItem(this);
//新建长为1秒的时间线
QTimeLine *timeLine = new QTimeLine(1000);
//动画循环次数为0,表示无限循环
timeLine->setLoopCount(0);
//将时间线加入动画类对象中
anim->setTimeLine(timeLine);
//在动画时间的一半时图形项旋转180度
anim->setRotationAt(0.5,180);
//在动画执行完时图形项旋转360度
anim->setRotationAt(1,360);
//开始动画
timeLine->start();


        这时执行程序,效果如下:



        小方块会在一秒内旋转一圈。我们这里使用了QGraphicsItemAnimation动画类和QTimeLine时间线类。



结语



这一节先介绍了图形项的相关内容,而场景、视图等内容放到下一节来讲。


源码下载:


本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
回复

使用道具 举报

累计签到:56 天
连续签到:1 天
2017-6-25 14:56:51 显示全部楼层
老师:你好
我学习第19篇第一段程序,编译出错,说找不到头文件,如图:


之前的例子没这问题。之前是建的QT应用程序,

这次按要求建的空项目,如图:

是不是还要在哪里设path?
我与先前项目粗略对比了一下,没有找到差别,请老师指点

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
回复 支持 反对

使用道具 举报

累计签到:1564 天
连续签到:1 天
2017-6-28 16:59:36 显示全部楼层
cncqzxj 发表于 2017-6-25 14:56
老师:你好
我学习第19篇第一段程序,编译出错,说找不到头文件,如图:

你没在.pro文件中添加QT += widgets
回复 支持 反对

使用道具 举报

累计签到:6 天
连续签到:1 天
2017-7-4 10:40:56 显示全部楼层
在这个网址http://www.qter.org/portal.php?mod=view&aid=48上有个小漏洞,
//我们通过类型转换来获得颜色
        color = qvariant_cast(event->mimeData()->colorData());
应改为:
//我们通过类型转换来获得颜色
        color = qvariant_cast<QColor>(event->mimeData()->colorData());

回复 支持 反对

使用道具 举报

累计签到:1564 天
连续签到:1 天
2017-7-4 18:41:56 显示全部楼层
战斗者 发表于 2017-7-4 10:40
在这个网址http://www.qter.org/portal.php?mod=view&aid=48上有个小漏洞,
//我们通过类型转换来获得颜色 ...

恩,好的,谢谢指出,应该是粘贴的时候,有些字符自动被屏蔽了。
回复 支持 反对

使用道具 举报

累计签到:18 天
连续签到:1 天
2017-7-18 13:53:23 显示全部楼层
老师,您好
我在学习第19篇的自定义图形项时,按照您的代码进行编写,最后系统提示产生如下错误:
main.obj:-1: error: LNK2001: 无法解析的外部符号 "public: virtual class QRectF __cdecl MyItem::boundingRect(void)const " (?boundingRect@MyItem@@UEBA?AVQRectF@@XZ)

main.obj:-1: error: LNK2001: 无法解析的外部符号 "public: virtual void __cdecl MyItem::paint(class QPainter *,class QStyleOptionGraphicsItem const *,class QWidget *)" (?paint@MyItem@@UEAAXPEAVQPainter@@PEBVQStyleOptionGraphicsItem@@PEAVQWidget@@@Z)

还望老师能够解答

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
回复 支持 反对

使用道具 举报

累计签到:1564 天
连续签到:1 天
2017-7-22 11:07:54 显示全部楼层
kangyang94 发表于 2017-7-18 13:53
老师,您好
我在学习第19篇的自定义图形项时,按照您的代码进行编写,最后系统提示产生如下错误:
main.obj ...

下载源码测试了没?
回复 支持 反对

使用道具 举报

尚未签到

2017-8-1 16:34:08 显示全部楼层
大师,那怎么把旋转改为闪烁呢
回复 支持 反对

使用道具 举报

累计签到:1564 天
连续签到:1 天
2017-8-7 23:23:27 显示全部楼层
wangshichunbai 发表于 2017-8-1 16:34
大师,那怎么把旋转改为闪烁呢

定时器,绘制颜色白红交替。
回复 支持 反对

使用道具 举报

累计签到:8 天
连续签到:1 天
2017-9-6 20:14:54 显示全部楼层
kangyang94 发表于 2017-7-18 13:53
老师,您好
我在学习第19篇的自定义图形项时,按照您的代码进行编写,最后系统提示产生如下错误:
main.obj ...

是不是因为没有定义构造函数
回复 支持 反对

使用道具 举报

累计签到:12 天
连续签到:1 天
2017-10-26 17:34:06 显示全部楼层
第六移动,是QGraphicsScene类的对象调用QGraphicsItem的advance()函数吧
回复 支持 反对

使用道具 举报

累计签到:1564 天
连续签到:1 天
2017-11-5 19:42:52 显示全部楼层
大白菜 发表于 2017-10-26 17:34
第六移动,是QGraphicsScene类的对象调用QGraphicsItem的advance()函数吧

不是,调用场景的该函数会自动调用图形项的函数。
回复 支持 反对

使用道具 举报

累计签到:14 天
连续签到:1 天
2019-2-28 22:19:27 显示全部楼层
新手,只能说大体上懂了,思路了解了,可是有些细节还就是有些函数不太清楚作用,想问老师书上有没有对细节的解释,最近都在看网上教程还没来得及看书!
回复 支持 反对

使用道具 举报

累计签到:1564 天
连续签到:1 天
2019-3-1 12:26:22 显示全部楼层
staticnull 发表于 2019-2-28 22:19
新手,只能说大体上懂了,思路了解了,可是有些细节还就是有些函数不太清楚作用,想问老师书上有没有对细节 ...

书上讲的比这里详细,但函数的使用还得亲自使用尝试才行。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

公告
可以关注我们的微信公众号yafeilinux_friends获取最新动态,或者加入QQ会员群进行交流:190741849、186601429(已满) 我知道了