Qt 图形视图框架<二>——<QGraphicsView、QGraphicsScene>
接着上一章的部分,本章介绍图形视图框架中的视图和场景部分,三者关系本章就不再讲了,请参考上一章内容
一 场景(QGaphicsScene)
- 提供了一个管理大量图形项的快速接口
- 向每个图形项传播事件
- 管理图形项的状态,比如选择和焦点处理
- 提供无转换的渲染功能,主要用于打印
#include <QApplication>
#include <QtWidgets> 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);
1.1 场景层
一个场景分为三个层:图形项层(ItemLayer)、前景层(ForegroundLayer)和背景层(BackgroundLayer)。场景的绘制总是从背景层开始,然后是图形项层,最后是前景层。我们修改代码如下:
#include <QApplication> #include <QtWidgets> int main(int argc,char**argv) { QApplication app(argc,argv); QGraphicsScene scene; scene.addText("Hello, world!"); scene.setForegroundBrush(QColor(255, 255, 255, 100)); scene.setBackgroundBrush(Qt::green); QGraphicsView view(&scene); view.show(); return app.exec(); }
运行结果如下:
对于前景层,我们一般不进行设置,或者像上面这样设置为半透明的白色。对于背景层,这里设置为了绿色,当然,我们也可以将一张图片设置为背景,修改代码如下:
scene.setBackgroundBrush(QPixmap(":/20200212141848.png"));
运行结果如下:
图片默认是平铺的,如果想进一步控制前景和背景层,我们可以重新实现drawForeground()函数和drawBackground()函数。
1.2 索引算法
索引算法,是指在场景中进行图形项查找的算法。QGraphicsScene中提供了两种选择,它们在一个枚举类型QGraphicsScene::ItemIndexMethod中,分别是:
- QGraphicsSecne::BspTreeIndex :应用Binary Space Partition tree,适合于大量的静态图形项。这个是默认值。
- QGraphicsScene::NoIndex :不用索引,搜索场景中所有的图形项,适合于经常进行图形项的添加、移动和删除等操作的情况。
我们可以使用setItemIndexMethod()函数进行索引算法的更改。
1.3 边界矩形
图形项可以放到场景的任何位置,场景的大小默认是没有限制的。而场景的边界矩形仅用于场景内部进行索引的维护(能很快进行索引查找图元,不设置会通过itemsBoundingRect默认生成,但是不方便我们后续操作,一般我们设置了边界矩形后续索引可以只关注这个矩形区域);因为如果没有边界矩形,场景就要搜索所有的图形项,然后确定出其边界,这是十分费时的。所以如果要操作一个较大的场景,我们应该给出它的边界矩形。设置边界矩形,可以使用setSceneRect()函数。
注意:可以通过itemsBoundingRect()来得知现在场景的大小,itemsBoundingRect()是一个比较耗时的功能,它的运作通过收集场景中所有元素的位置信息找出其中最远坐标的位置,所以最好先设置好自己的场景大小不要使用默认大小。
1.4 图形项查找
场景最大的优势之一就是可以快速的锁定图形项的位置,即使有上百万个图形项,items()函数也能在数毫秒的时间内锁定一个图形项的位置。items()函数有几个重载函数来方便的进行图形项的查找。但是有时在场景的一个点可能重叠着几个图形项,这时我们可以使用itemAt()函数返回最上面的一个图形项。对于这些函数的使用,我们到后面讲视图时再举例讲解。
1.5 事件处理和传播
场景可以传播来自视图的事件,将事件传播给该点最顶层的图形项。但是就像我们在讲图形项时所说的那样,如果一个图形项要接收键盘事件,那么它必须获得焦点。而且,如果我们在场景中重写了事件处理函数,那么在该函数的最后,必须调用场景默认的事件处理函数,只有这样,图形项才能接收到该事件。这一点我们也到后面讲视图时再细讲。
二、视图(QGraphicsView)
QGraphicsView 提供了视图窗口部件,它使场景的内容可视化。可以给一个场景关联多个视图,从而给一个数据集提供多个视口。视图部件是一个滚动区域,也就是说,它可以提供一个滚动条来显示大型的场景。如果要使用OpenGL,可以使用QGraphicsView::setViewport()函数来添加QGLWidget 。
2.1 缩放与旋转
在前面项目基础上,我们添加处理类,代码如下:
#ifndef MYVIEW_H #define MYVIEW_H #include <QGraphicsView> #include <QWheelEvent > class MyView : public QGraphicsView { public: MyView(); protected: void wheelEvent(QWheelEvent *event)override;//鼠标滚轮事件 void mousePressEvent(QMouseEvent *event) override; }; #endif // MYVIEW_H
#include "MyView.h" #include <QGraphicsRectItem> MyView::MyView() { resize(400, 400); setBackgroundBrush(QPixmap(":/20200212141848.png")); QGraphicsScene *scene = new QGraphicsScene(this); scene->setSceneRect(0, 0, 100, 100); QGraphicsRectItem *item = new QGraphicsRectItem(0, 0, 20, 20); item->setBrush(Qt::red); scene->addItem(item); setScene(scene); } void MyView::wheelEvent(QWheelEvent *event) { //利用delta()函数返回值的正负来判断滚轮的移动方向,然后我们让视图进行缩放 if(event->delta() > 0) scale(0.5,0.5); //视图缩放 else scale(2,2); } void MyView::mousePressEvent(QMouseEvent *event) { rotate(90); //视图旋转顺时针90度 }
#include "MyView.h" #include <QApplication> #include <QtWidgets> int main(int argc,char**argv) { QApplication app(argc,argv); MyView *view = new MyView; view->show(); return app.exec(); }
运行效果如下:
2.2 场景边框与场景对齐方式
上面的窗口大小看的一脸懵逼,别急,在文章最后我们会讲解坐标系统,我们在上面讲场景时就提到了场景边框(SceneRect),这里再说说它在视图中的作用。我们前面说过,视图是可以提供滚动条的,但是,这只是在视图窗口小于场景时才自动出现的。如果我们不定义场景边框,那么当场景中的图形项移动到视图可视窗口以外的地方时,视图就会自动出现滚动条,但是即使是图形项再次回到可视区域,滚动条也不会消失。为了解决这个问题,我们可以为场景设置边框,这样,当图形项移动到场景边框以外时,视图是不会提供额外的滚动区域的。
而当整个场景都可视时,也就是说视图没有滚动条时,我们可以通过setAlignment()函数来设置场景在视图中的对齐方式,如左对齐Qt::AlignLeft ,向上对齐Qt::AlignTop ,中心对齐Qt::AlignCenter。更多的对齐方式,可以查看帮助中Qt::Alignment 关键字。默认的对齐方式是Qt::AlignCenter 。而且几种对齐方式可以通过“按位或”操作一起使用。我们在上面的程序中的myview.cpp文件中的构造函数最后添加一行代码:
setAlignment(Qt::AlignLeft | Qt::AlignTop);
运行效果如下:
2.3 拖动模式
QGraphicView中提供了三种拖动模式,分别是:
- QGraphicsView::NoDrag :忽略鼠标事件,不可以拖动。
- QGraphicsView::ScrollHandDrag :光标变为手型,可以拖动场景进行移动。
- QGraphicsView::RubberBandDrag :使用橡皮筋效果,进行区域选择,可以选中一个区域内的所有图形项。
我们可以利用setDragMode()函数进行相应设置。下面更改前面的程序,在myview.cpp中的构造函数中的最后添加代码:
构造函数修改添加代码:
setDragMode(QGraphicsView::ScrollHandDrag ); //手型拖动 scene->setSceneRect(0, 0, 800, 800);
mousePressEvent函数修改如下:
void MyView::mousePressEvent(QMouseEvent *event) { //rotate(90); //视图旋转顺时针90度 QGraphicsView::mousePressEvent(event); }
此时,由于我们设置的场景大于视图窗口(resize(400, 400);),所以我们可以点击鼠标进行拖动:
2.4 事件传递
event参数传递出去,才能执行默认的事件操作。其实不止上面那一种情况,在图形视图框架中,鼠标键盘等事件是从视图进入的,视图将它们传递给场景,场景再将事件传递给该点的图形项,如果该点有多个图形项,那么就传给最上面的图形项。所以要想使这个事件能一直传播下去,我们就需要在重新实现事件处理函数时,在其最后将event参数传给默认的事件处理函数。比如我们重写了场景的键盘按下事件处理函数,那么我们就在该函数的最后写上QGraphicsScene::keyPressEvent(event);一行代码。
QGraphicsView收到event,就会转换成QGraphicsScene的事件,QGraphicsScene对象再传递给对应的QGraphicsItem:
2.5 背景缓存
2.6 OpenGL渲染
QT += opengl
构造函数添加:
QGLWidget *widget =new QGLWidget(this); setViewport(widget);
这样便使用OpenGL进行渲染了。
2.7 图形项查找与图形项组
resize(400, 400); setBackgroundBrush(QPixmap("../graphicsview03/back.jpg")); QGraphicsScene *scene = new QGraphicsScene(this); scene->setSceneRect(0, 0, 200, 200); QGraphicsRectItem *item1 = new QGraphicsRectItem(0,0,20,20); item1->setBrush(Qt::red); item1->setPos(10,0); //scene->addItem(item1); QGraphicsRectItem *item2 = new QGraphicsRectItem(0,0,20,20); item2->setBrush(Qt::green); item2->setPos(40,0); //scene->addItem(item2); //新建图形项组 QGraphicsItemGroup *group = new QGraphicsItemGroup; group->addToGroup(item1); group->addToGroup(item2); scene->addItem(group); setScene(scene); setAlignment(Qt::AlignLeft | Qt::AlignTop); //手型拖动 setDragMode(QGraphicsView::ScrollHandDrag); scene->setSceneRect(0, 0, 800, 800); QGLWidget *widget =new QGLWidget(this); setViewport(widget); //输出(10, 0)点的图形项 qDebug() << "itemAt(10,0) : " <<itemAt(10, 0); qDebug() << "itemAt(40,0) : " <<itemAt(40, 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,而item1和item2都在group里,所以我们只需将group加入场景中就可以了,那么这时加入场景的顺序就是,先加入group,因为item1先加入group,所以下面将item1加入场景,最后加入场景的是item2,这就是为什么items.at(0)会是item2的原因,此时里面有3个图形项:
再说图形项组,其实图形项组也是一个图形项,它有图形项所拥有的所有特性。其作用就是,将加入它的所有图形项作为一个整体,对这个图形项组进行操作,就相当于对其中所有图形项进行操作。图形项组是加入它的所有图形项的父图形项,在上面的输出的parent信息中我们可以看到这一点。下面我们将程序中的代码更改如下:
void MyView::keyPressEvent(QKeyEvent *event) { items().at(2)->setPos(100,100); QGraphicsView::keyPressEvent(event);
items().at(2)操作的是group
可以看到,两个图形项是同时移动的。我们要从图形项组中移除一个图形项,可以使用removeFromGroup()函数,它可以将给定的item从group中删除,要注意这时item依然存在,它会回到group的父图形项中,如果group没有父图形项,那么item就会回到场景中。我们可以使用场景的removeItme()函数来删除group,这样也会将group中所有的图形项从场景中删除。还有一种办法是利用场景的destroyItemGroup()函数,它会删除group并销毁它,但是group中的所有图形项会回到group的父图形项中,如果它没有父图形项,那么所有图形项就会回到场景中。
2.8 打印
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("../graphicsview03/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)); //打印场景内容
运行程序,看图片效果:
这时无论视图怎样变换,生成的图片总是一样的。而且它并没有打印背景的图片。就像我们看到的,视图的打印函数是依据视图的坐标系进行打印的,我们看到的就是打印出来后的效果,它可以看做是程序窗口的截屏。而场景的打印函数,是依据场景的坐标系的,无论视图怎么转换,只要场景坐标系没有变换,它打印出来的图片都是一样的,在下一章,我们讲解视图中的坐标系。