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

第20篇 2D绘图(十)图形视图框架(下)

21
回复
48139
查看
[复制链接]
累计签到:1564 天
连续签到:1 天
来源: 2013-5-4 15:43:02 显示全部楼层 |阅读模式
图形视图框架(下)
版权声明

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




导语



环境:Windows Xp + Qt 4.8.4+QtCreator 2.6.2




目录

三、场景(QGraphicsScene
(一)场景层
(二)索引算法
(三)边界矩形
(四)图形项查找
(五)事件处理和传播
(六)打印
四、视图(QGraphicsView
(一)缩放与旋转
(二)场景边框与场景对齐方式
(三)拖动模式
(四)事件传递
(五)背景缓存
(六)OpenGL渲染
(七)图形项查找与图形项组
(八)打印




正文



三、场景(QGraphicsScene
QGraphicsScene提供了图形视图框架的场景,它有以下功能:
  • 提供了一个管理大量图形项的快速接口
  • 向每个图形项传播事件
  • 管理图形项的状态,比如选择和焦点处理
  • 提供无转换的渲染功能,主要用于打印
我们新建空的Qt项目,项目名称为graphicsView03,完成后添加main.cpp文件,更改其内容如下:

#include <QtGui>

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

   QGraphicsScene scene;
   scene.addText("Hello, world!");
   QGraphicsView view(&scene);
   view.show();

   return app.exec();
}


运行程序,效果如下:

这里使用addText()函数添加了一个文本图形项。执行这条语句就相当于执行了下面两条语句:

QGraphicsTextItem*item = new QGraphicsTextItem("Hello, world!");
scene.addItem(item);

如果要删除一个图形项我们可以调用removeItem()函数,如:scene.removeItem(item);



(一)场景层
一个场景分为三个层:图形项层(ItemLayer)、前景层(ForegroundLayer)和背景层(BackgroundLayer)。场景的绘制总是从背景层开始,然后是图形项层,最后是前景层。看下面的例子:
我们在上面的程序中添加代码:

scene.setForegroundBrush(QColor(255,255,255,100));
scene.setBackgroundBrush(Qt::green);

运行程序,效果如下:

对于前景层,我们一般不进行设置,或者像上面这样设置为半透明的白色。对于背景层,这里设置为了绿色,当然,我们也可以将一张图片设置为背景。

scene.setBackgroundBrush(QPixmap("../graphicsView03/yafeilinux.jpg"));

运行程序,我们可以看到,图片默认是平铺的。

如果想进一步控制前景和背景层,我们可以重新实现drawForeground()函数和drawBackground()函数。



(二)索引算法
索引算法,是指在场景中进行图形项查找的算法。QGraphicsScene中提供了两种选择,它们在一个枚举变量QGraphicsScene::ItemIndexMethod中,分别是:
  • QGraphicsSecne::BspTreeIndex :应用Binary Space Partition tree,适合于大量的静态图形项。这个是默认值。
  • QGraphicsScene::NoIndex :不用索引,搜索场景中所有的图形项,适合于经常进行图形项的添加、移动和删除等操作的情况。
我们可以使用setItemIndexMethod()函数进行索引算法的更改。



(三)边界矩形
图形项可以放到场景的任何位置,场景的大小默认是没有限制的。而场景的边界矩形仅用于场景内部进行索引的维护。因为如果没有边界矩形,场景就要搜索所有的图形项,然后确定出其边界,这是十分费时的。所以如果要操作一个较大的场景,我们应该给出它的边界矩形。设置边界矩形,可以使用setSceneRect()函数。



(四)图形项查找
场景最大的优势之一就是可以快速的锁定图形项的位置,即使有上百万个图形项,items()函数也能在数毫秒的时间内锁定一个图形项的位置。items()函数有几个重载函数来方便的进行图形项的查找。但是有时在场景的一个点可能重叠着几个图形项,这时我们可以使用itemAt()函数返回最上面的一个图形项。对于这些函数的使用,我们到后面讲视图时再举例讲解。



(五)事件处理和传播
场景可以传播来自视图的事件,将事件传播给该点最顶层的图形项。但是就像我们在讲图形项时所说的那样,如果一个图形项要接收键盘事件,那么它必须获得焦点。而且,如果我们在场景中重写了事件处理函数,那么在该函数的最后,必须调用场景默认的事件处理函数,只有这样,图形项才能接收到该事件。这一点我们也到后面讲视图时再细讲。



(六)打印
该部分内容也放到后面和视图一起讲。




四、视图(QGraphicsView
QGraphicsView 提供了视图窗口部件,它使场景的内容可视化。你可以给一个场景关联多个视图,从而给一个数据集提供多个视口。视图部件是一个滚动区域,就是说,它可以提供一个滚动条来显示大型的场景。如果要使用OpenGL,你可以使用QGraphicsView::setViewport()函数来添加QGLWidget 。


(一)缩放与旋转
我们新建空的Qt项目,项目名称为graphicsView04,然后添加main.cpp文件,再新添一个C++ 类,类名为MyView,基类为QGraphicsView,类型信息选择“继承自QWidget”

然后在myview.h中添加头文件:#include <QtGui>

然后声明事件槽函数:

protected:
   void wheelEvent(QWheelEvent *event);
voidmousePressEvent(QMouseEvent *event);


我们到myview.cpp文件中进行函数的定义:


MyView::MyView(QWidget *parent) :
   QGraphicsView(parent)
{
   resize(400,400);
    setBackgroundBrush(QPixmap("../graphicsView04/01.jpg"));//其实就是设置场景的背景
   QGraphicsScene *scene = new QGraphicsScene(this);
   scene->setSceneRect(0,0,200,200);
   QGraphicsRectItem *item = new QGraphicsRectItem(0,0,20,20);
   item->setBrush(Qt::red);
   scene->addItem(item);
   setScene(scene);
}


void MyView::wheelEvent(QWheelEvent*event)  //滚轮事件
{
   if(event->delta() > 0)  //如果鼠标滚轮远离使用者,则delta()返回正值
       scale(0.5,0.5);  //视图缩放
   else scale(2,2);
}


void MyView::mousePressEvent(QMouseEvent*event)
{
   rotate(90);  //视图旋转顺时针90
}


这里我们定义了鼠标的滚轮事件和按下事件,在滚轮事件中,利用delta()函数返回值的正负来判断滚轮的移动方向,然后我们让视图进行缩放。

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


#include "myview.h"

int main(int argc,char *argv[])
{
   QApplication app(argc,argv);
   MyView *view = new MyView;
   view->show();
   return app.exec();
}


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

上面四幅图分别是:正常,旋转90度后,缩小后,放大后的效果。可以看到实现视图的变换是十分简单的。



(二)场景边框与场景对齐方式
我们在上面讲场景时就提到了场景边框(SceneRect),这里再说说它在视图中的作用。我们前面说过,视图是可以提供滚动条的,但是,这只是在视图窗口小于场景时才自动出现的。如果我们不定义场景边框,那么当场景中的图形项移动到视图可视窗口以外的地方时,视图就会自动出现滚动条,但是即使是图形项再次回到可视区域,滚动条也不会消失。为了解决这个问题,我们可以为场景设置边框,这样,当图形项移动到场景边框以外时,视图是不会提供额外的滚动区域的。
       而当整个场景都可视时,也就是说视图没有滚动条时,我们可以通过setAlignment()函数来设置场景在视图中的对齐方式,如左对齐Qt::AlignLeft ,向上对齐Qt::AlignTop ,中心对齐Qt::AlignCenter。更多的对齐方式,可以查看帮助中Qt::Alignment 关键字。默认的对齐方式是Qt::AlignCenter 。而且几种对齐方式可以通过“按位或”操作一起使用。我们在上面的程序中的myitem.cpp文件中的构造函数最后添加一行代码:


setAlignment(Qt::AlignLeft | Qt::AlignTop);


运行效果如下图所示。



(三)拖动模式
QGraphicView中提供了三种拖动模式,分别是:
  • QGraphicsView::NoDrag :忽略鼠标事件,不可以拖动。
  • QGraphicsView::ScrollHandDrag :光标变为手型,可以拖动场景进行移动。
  • QGraphicsView::RubberBandDrag :使用橡皮筋效果,进行区域选择,可以选中一个区域内的所有图形项。
我们可以利用setDragMode()函数进行相应设置。
下面我们更改上面的程序。在myview.cpp中的构造函数中的最后添加一行代码:


setDragMode(QGraphicsView::ScrollHandDrag);//手型拖动


并将场景外框放大一点:


scene->setSceneRect(0,0,800,800);


这时运行程序,虽然出现了小手,但是并不能拖动场景。为什么呢?我们在mousePressEvent()函数中添加一行代码:


QGraphicsView::mousePressEvent(event);


这时再运行程序,发现已经成功了。效果如下:


我们在事件函数的最后添加了一行:QGraphicsView::mousePressEvent(event);这样程序才能执行默认的事件。这也是我们下面要说的事件传播的内容的一部分。



(四)事件传递
在上面我们看到必须在事件函数的最后将event参数传递出去,才能执行默认的事件操作。其实不止上面那一种情况,在图形视图框架中,鼠标键盘等事件是从视图进入的,视图将它们传递给场景,场景再将事件传递给该点的图形项,如果该点有多个图形项,那么就传给最上面的图形项。所以要想使这个事件能一直传播下去,我们就需要在重新实现事件处理函数时,在其最后将event参数传给默认的事件处理函数。比如我们重写了场景的键盘按下事件处理函数,那么我们就在该函数的最后写上QGraphicsScene::keyPressEvent(event);一行代码。



(五)背景缓存
如果场景的背景需要大量耗时的渲染,可以利用CacheBackground来缓存背景,当下次需要渲染背景时,可以快速进行渲染。它的原理就是,把整个视口先绘制到一个pixmap上。但是这个只适合较小的视口,也就是说,如果视图窗口很大,而且有滚动条,那么就不再适合缓存背景。我们可以使用setCacheMode(QGraphicsView::CacheBackground);来设置背景缓存。默认设置是没有缓存QGraphicsView::CacheNone



(六)OpenGL渲染
QGraphicsView默认使用一个QWidget作为视口部件,如果我们要使用OpenGL进行渲染,可以使用setViewport()函数来添加一个QGLWidget对象。看下面的例子。
我们先在项目文件graphicsView04.pro中加入

QT += opengl

说明要使用OpenGL模块,然后在myview.cpp文件中添加头文件:

#include <QtOpenGL>

最后在构造函数中加入代码:

QGLWidget *widget =new QGLWidget(this);
setViewport(widget);

这样便使用OpenGL进行渲染了。关于OpenGL,我们在后面的3D绘图部分再讲。



(七)图形项查找与图形项组
在前面讲场景时,我们就涉及了图形项查找的内容,当时没有细讲,现在我们把它和图形项组放到一起来讲解。先看一个例子,然后再介绍。
myview.cpp中的构造函数里将以前那个item改名为item1,然后再加入一个item2和一个图形项组对象group。更改后构造函数的部分代码如下:

QGraphicsRectItem *item1 = newQGraphicsRectItem(0,0,20,20);
item1->setBrush(Qt::red);
item1->setPos(10,0);
scene->addItem(item1);

QGraphicsRectItem *item2 = newQGraphicsRectItem(0,0,20,20);
item2->setBrush(Qt::green);
item2->setPos(30,0);
scene->addItem(item2);

QGraphicsItemGroup *group = newQGraphicsItemGroup;  //新建图形项组
group->addToGroup(item1);
group->addToGroup(item2);
scene->addItem(group);

setScene(scene);
qDebug() << "itemAt(10,0) :" <<itemAt(10,0); //输出(10,0)点的图形项
qDebug() << "itemAt(30,0) :" <<itemAt(30,0);
qDebug() <<"#################################"; //分割线


然后我们到myview.h文件中protected下声明键盘按下事件槽函数:


void keyPressEvent(QKeyEvent *event);


再到myview.cpp中定义它,如下:


void MyView::keyPressEvent(QKeyEvent*event)
{
   qDebug() << items();  //输出场景中所有的图形项
   items().at(0)->setPos(100,0);
   items().at(1)->setPos(0,100);
   QGraphicsView::keyPressEvent(event); //执行默认的事件处理
}


这时运行程序,当按下键盘上任意键后,效果如下:

下面是输出框输出的信息:


可以看到,itemAt()函数可以输出场景上任意点的图形项。而items()函数可以输出场景上所有的图形项。这里应该说明,items()函数返回的图形项列表是按栈的降序排序的,也就是说,items().at(0)返回的是最后加入场景的图形项。从上面可以看出,最后加入的图形项是item2,其实,因为我们使用了group,而item1item2都在group里,所以我们只需将group加入场景中就可以了,前面把item1item2也加入场景是多余的。我们可以将scene->addItem(item1);scene->addItem(item2);两行代码删掉。那么这时加入场景的顺序就是,先加入group,因为item1先加入group,所以下面将item1加入场景,最后加入场景的是item2,这就是为什么items.at(0)会是item2的原因。
       下面再说图形项组,其实图形项组也是一个图形项,它有图形项所拥有的所有特性。其作用就是,将加入它的所有图形项作为一个整体,对这个图形项组进行操作,就相当于对齐中所有图形项进行操作。图形项组是加入它的所有图形项的父图形项,在上面的输出的parent信息中我们可以看到这一点。下面我们将程序中的代码更改如下:


void MyView::keyPressEvent(QKeyEvent*event)
{
   items().at(2)->setPos(100,100);
   QGraphicsView::keyPressEvent(event); //执行默认的事件处理
}


运行程序,按下键盘上任意键,效果如下:

可以看到,两个图形项是同时移动的。我们要从图形项组中移除一个图形项,可以使用removeFromGroup()函数,它可以将给定的itemgroup中删除,要注意这时item依然存在,它会回到group的父图形项中,如果group没有父图形项,那么item就会回到场景中。我们可以使用场景的removeItme()函数来删除group,这样也会将group中所有的图形项从场景中删除。还有一种办法是利用场景的destroyItemGroup()函数,它会删除group并销毁它,但是group中的所有图形项会回到group的父图形项中,如果它没有父图形项,那么所有图形项就会回到场景中。



(八)打印
图形视图框架提供了两个打印函数render(),一个是在QGraphicsScene中,一个是在QGraphicsView中,并且它们的函数原型是一模一样的。不过它们实现的效果稍有不同。看一面的例子。
我们更改鼠标按下事件槽函数的内容如下:


void MyView::mousePressEvent(QMouseEvent*event)
{
    rotate(90); //视图旋转顺时针90
   QPixmap pixmap(400,400);  //必须指定大小
   QPainter painter(&pixmap);
   render(&painter,QRectF(0,0,400,400),QRect(0,0,400,400));  //打印视图指定区域内容
    pixmap.save("../graphicsView04/save.png");
   QGraphicsView::mousePressEvent(event);
}

这里我们使用了视图的render()函数,其中的QRectF参数是指设备的区域,这里是指pixmap。而QRect参数是指视图上要打印的区域。我们利用QPixmap类的save()函数,将pixmap图片保存到我们项目源码目录中,文件名为“save.png”。下面是运行程序后,点击鼠标,生成的图片的效果:
  

我们每点击一次鼠标,就会旋转视图,那么生成的图片就是当前视口的截图。下面我们使用场景的打印函数,将上面的打印一行的代码改为:

scene()->render(&painter,QRectF(0,0,400,400),QRect(0,0,400,400));//打印场景内容

查看图片效果:

这时无论视图怎样变换,生成的图片总是一样的。而且它并没有打印场景背景的图片。就像我们看到的,视图的打印函数是依据视图的坐标系进行打印的,我们看到的就是打印出来后的效果,它可以看做是程序窗口的截屏。而场景的打印函数,是依据场景的坐标系的,无论视图怎么转换,只要场景坐标系没有变换,它打印出来的图片都是一样的。




结语

       图形视图框架是一个非常强大而且庞杂的系统,我们教程中也只是很笼统的介绍了一些最基本最常用的内容。如果大家想系统学习该部分知识,想学习如何使用该框架轻松搭建一个游戏,可以参考《Qt Creator快速入门》的第11章,以及《Qt 及Qt Quick开发实战精解》的第二章。




涉及到的源码:



上一篇:  第19篇 2D绘图(九)图形视图框架(上)

下一篇: 第21篇 数据库(一)Qt数据库应用简介

返回:系列教程目录    


本帖子中包含更多资源

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

x
回复

使用道具 举报

累计签到:5 天
连续签到:1 天
2013-12-8 19:53:37 显示全部楼层
在vs2010里面建立的Qt工程,怎么包含QtOpenGL模块呢,都已经建立好了工程了,我只在建立工程的时候看见可以选择模块,当时没选QtOpenGL模块,现在怎么添加进去呢
回复 支持 反对

使用道具 举报

累计签到:1564 天
连续签到:1 天
2013-12-9 13:39:37 显示全部楼层
lzyinformation 发表于 2013-12-8 19:53
在vs2010里面建立的Qt工程,怎么包含QtOpenGL模块呢,都已经建立好了工程了,我只在建立工程的时候看见可以 ...

先在.pro文件中添加 QT += opengl

然后使用时添加相应的头文件应该就可以了。
回复 支持 反对

使用道具 举报

累计签到:1564 天
连续签到:1 天
2013-12-9 15:29:11 显示全部楼层
lzyinformation 发表于 2013-12-8 19:53
在vs2010里面建立的Qt工程,怎么包含QtOpenGL模块呢,都已经建立好了工程了,我只在建立工程的时候看见可以 ...

http://blog.chinaunix.net/uid-693168-id-3254700.html

使用这种方法试试。
回复 支持 反对

使用道具 举报

尚未签到

2014-3-5 12:22:28 显示全部楼层
yafei老师你好,我用的QT5.1,建立空Qt项目,基类里面没有QGraphicsView,不知道如何继承啊?
回复 支持 反对

使用道具 举报

累计签到:1564 天
连续签到:1 天
2014-3-5 20:57:58 显示全部楼层
wangxiangjun88 发表于 2014-3-5 12:22
yafei老师你好,我用的QT5.1,建立空Qt项目,基类里面没有QGraphicsView,不知道如何继承啊? ...

手动打上去即可。
回复 支持 反对

使用道具 举报

尚未签到

2014-3-5 21:26:28 显示全部楼层
yafeilinux 发表于 2014-3-5 20:57
手动打上去即可。

好的,已经解决了,多谢
回复 支持 反对

使用道具 举报

累计签到:12 天
连续签到:1 天
2015-11-30 17:04:44 显示全部楼层
mark可一下,提醒自己记住:
Qt OpenGL 模块已被弃用,替代它的是Qt gui模块中的相应的OpenGL类。
QGLWidget类是Qt OpenGL遗留下的一部分,应该避免在新的代码里面使用。从5.4版本开始,推荐使用QOpenGLWidget和QOpenGL类。
回复 支持 反对

使用道具 举报

累计签到:10 天
连续签到:1 天
2016-3-2 19:36:10 显示全部楼层
您好我想问个问题,看到您这篇教程时我使用的是qtcreator3.2版本。然后我在创建C++类时发现没有地方能选择类型信息“继承自QWidget”。
于是我有下载了其他版本,发现高版本的都没有这个选项。而2.8版本我却找到了这个类型信息选择项。
那么如果之后使用高版本的qtcreator该怎么办。
回复 支持 反对

使用道具 举报

累计签到:1564 天
连续签到:1 天
2016-3-2 22:41:35 显示全部楼层
太阳的冷漠 发表于 2016-3-2 19:36
您好我想问个问题,看到您这篇教程时我使用的是qtcreator3.2版本。然后我在创建C++类时发现没有地方能选择 ...

应该有base class选项的,如果没有可以手动输入。
回复 支持 反对

使用道具 举报

累计签到:10 天
连续签到:1 天
2016-3-3 08:37:56 显示全部楼层
本帖最后由 太阳的冷漠 于 2016-3-3 08:39 编辑
yafeilinux 发表于 2016-3-2 22:41
应该有base class选项的,如果没有可以手动输入。

我想表达的是这个“选择基类”没有问题,而是高版本里没有类型信息“继承自……”这个选项,高版本里只有include……。而这其实与选择类型信息后做出的类声明时存在差别。如果使用高版本的话只能自己手动输入吗?

本帖子中包含更多资源

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

x
回复 支持 反对

使用道具 举报

累计签到:1564 天
连续签到:1 天
2016-3-3 10:42:36 显示全部楼层
太阳的冷漠 发表于 2016-3-3 08:37
我想表达的是这个“选择基类”没有问题,而是高版本里没有类型信息“继承自……”这个选项,高版本里只有i ...

嗯,手动就行。
回复 支持 反对

使用道具 举报

累计签到:16 天
连续签到:1 天
2016-3-11 13:45:23 显示全部楼层
太阳的冷漠 发表于 2016-3-3 08:37
我想表达的是这个“选择基类”没有问题,而是高版本里没有类型信息“继承自……”这个选项,高版本里只有i ...

写构造函数的时候加入就行了,比如这个教程中的 MyView 类

MyView::MyView(QWidget *parent) : QGraphicsView(parent)
{
    ....
}
回复 支持 反对

使用道具 举报

累计签到:3 天
连续签到:1 天
2016-7-11 16:24:22 显示全部楼层
scene.setBackgroundBrush(QPixmap("../graphicsView03/yafeilinux.jpg"));请问怎么设置图片为缩放模式啊
回复 支持 反对

使用道具 举报

累计签到:1 天
连续签到:1 天
2016-11-17 09:55:51 显示全部楼层
楼主,能给我解答一下这两个错误是什么意思吗

本帖子中包含更多资源

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

x
回复 支持 反对

使用道具 举报

累计签到:1 天
连续签到:1 天
2016-11-17 09:58:21 显示全部楼层
Qt小菜鸟 发表于 2016-11-17 09:55
楼主,能给我解答一下这两个错误是什么意思吗

这个可能清楚点,麻烦帮我看一下

本帖子中包含更多资源

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

x
回复 支持 反对

使用道具 举报

累计签到:1564 天
连续签到:1 天
2016-11-23 16:31:54 显示全部楼层
Qt小菜鸟 发表于 2016-11-17 09:58
这个可能清楚点,麻烦帮我看一下

你用的是什么版本?
回复 支持 反对

使用道具 举报

累计签到:1 天
连续签到:1 天
2017-1-18 14:39:28 显示全部楼层
我是一名新手,我想问一下,下面的程序为什么用注释掉的语句来替代的话,关闭执行程序后会报错?谢谢!
#include <QtGui>

int main(int argc, char* argv[])
{
    QApplication app(argc, argv);
    //QGraphicsTextItem text;
    //text.setPlainText("Hello World!");
    QGraphicsTextItem *text = new QGraphicsTextItem("Hello world!");
    QGraphicsScene scene;
    scene.addItem(text);  //scene.addItem(&Atext);
    QGraphicsView view(&scene);
    view.resize(200,200);
    view.show();
    return app.exec();
}
回复 支持 反对

使用道具 举报

累计签到:1564 天
连续签到:1 天
2017-1-19 10:30:40 显示全部楼层
HJUN 发表于 2017-1-18 14:39
我是一名新手,我想问一下,下面的程序为什么用注释掉的语句来替代的话,关闭执行程序后会报错?谢谢!
#in ...

QGraphicsTextItem text;换成指针形式试试。
回复 支持 反对

使用道具 举报

累计签到:30 天
连续签到:1 天
2017-1-31 20:41:02 显示全部楼层
请问我按照教程来的 为什么出现这么多错误 版本是Qt5 怎么解决啊

本帖子中包含更多资源

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

x
回复 支持 反对

使用道具 举报

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

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