一、图形视图框架结构
图形视图框架由场景QGraphicsScene、视图QGraphicsView、图形项QGraphicsItem组成,它提供了一套基于图形项模型视图编程方法。图形视图框架可以管理数量庞大的自定义2D图形项,比如要绘制上万个图形并对这些图形进行拖动、检测位置等操作的话使用图形视图框架就可以方便的管理它们。场景中可以包含各种形状的图形项,使用视图来使图形项可视化,多个视图可以在一个场景中查看。视图还支持缩放和旋转,使用视图的变换矩阵函数QGraphicsView::transfrom()可以变换场景的坐标系统,这样便可以实现缩放、旋转等功能。
场景是图形项的容器,场景提供了管理大量图形项的快速接口,并传播事件给图形项和管理图形项的状态(选择、焦点处理等)。为场景设置视图后才可以看到图形项,视图部件是一个可滚动的区域,默认QGraphicsView提供了一个QWidget作为视口部件,如果要使用OpenGL进行渲染可以调用setViewport()设置QGLWidget作为视口。场景可以分为图形项层、前景层、背景层,场景的 绘制总是从背景开始、最后是前景,前景和背景还可以使用渐变和贴图等来实现特殊效果,比如半透明的黑色可以实现夜幕降临的效果。场景中也有跟视图同名的设置前景、背景的方法,不过它对所有关联的视图都有效。
#include <QApplication>#include <QGraphicsScene>#include <QGraphicsView>#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); //添加矩形图形项到场景QList<QGraphicsItem *> l = scene.items(); //获得场景中所有的图形项(递减顺序,最上面的图形项在前面)l = scene.items(Qt::AscendingOrder); //获得场景中所有的图形项,增序QGraphicsItem* pItem = scene.itemAt(50, 50); //获得(50, 50)点处的图形项qDebug() <<pItem; QPainterPath path; path.addRect(0, 0, 200, 200); scene.setSelectionArea(path); //选择区域中所有的图形项scene.setFocusItem(pItem); //设置图形项焦点//scene.removeItem(pItem); //删除图形项QGraphicsView view(&scene); //为场景创建视图view.setDragMode(QGraphicsView::ScrollHandDrag); //设置光标变为手掌形状从而可以拖动场景view.setDragMode(QGraphicsView::RubberBandDrag); //设置可以使用鼠标拖出橡皮筋来选择图形项view.setForegroundBrush(QColor(255, 255, 255, 100)); //设置场景的前景色view.setBackgroundBrush(QPixmap("./background.png")); //设置场景的背景图片view.resize(500, 400); //设置视图大小view.show(); //显示视图returnapp.exec(); }
下面是上面代码的效果图,可以看到矩形图形项和背景图片默认都是在视图中间部分进行绘制的,而且背景图片被平铺开来显示:
我们再添加以下代码再添加一个视图的话可以看到出现了两个视图:
QGraphicsView view2(&scene); //为场景再关联一个视图view2.resize(400, 300); view2.show();
QGraphicsItem是图形项的基类,我们一般是继承它(并实现纯虚函数boundingRect和paint)来实现自定义的图形项,图形项支持鼠标、键盘、拖放、右键菜单、拖放事件,并支持分组(使用QGraphicsItemGroup通过parent-child关系来实现)和碰撞检测,而且可以使用setData(int key, const QVariant& value)/data()来存储数据和获取数据。以下为Qt中的几种图形项,其分别为QGraphicsRectItem、QGraphicsEllipseItem、QGraphicsPolygonItem、QGraphicsLineItem、QGraphicsEllipseItem、QGraphicsPathItem、QGraphicsSimpleTextItem、QGraphicsTextItem、QGraphicsPixmapItem:
下面是实现了自定义的图形项:
#include <QApplication>#include <QGraphicsScene>#include <QGraphicsView>#include <QGraphicsRectItem>#include <QDebug>class MyItem: publicQGraphicsItem {public: MyItem(){}//返回要绘制图形项的矩形区域,如果图形绘制了一个轮廓,那么在边界矩形中包含一半的画笔宽度是很重要的QRectF boundingRect()const override{//如果在图形项绘制了一个边框,那么矩形边界应该包含一半的画笔宽度double penWidth = 1.0;return QRectF(0 - penWidth / 2, 0 - penWidth / 2, 40 + penWidth, 40 +penWidth); }//绘图方法,要保证绘图在boundingRect()的边界之中void paint(QPainter* painter, const QStyleOptionGraphicsItem* option/*风格选项*/, QWidget* widget/*要进行绘图的部件,默认为0*/)override{ painter->setBrush(Qt::red); painter->drawRect(0, 0, 40, 40); } };int main(int argc, char**argv) { QApplication app(argc, argv); QGraphicsScene scene; //创建场景QGraphicsItem * item = new MyItem; //创建自定义图形项scene.addItem(item); //添加矩形图形项到场景QGraphicsView view(&scene); //为场景创建视图view.setBackgroundBrush(QPixmap("./background.png")); //设置场景的背景图片view.resize(500, 400); //设置视图大小view.show(); //显示视图returnapp.exec(); }
二、图形视图框架的坐标系统
一个图形项可以在另一个图形项之中,这就是子图形项,子图形项的坐标位置是相对于其父图形项的原点,而没有父图形项的图形项都是在场景中,其坐标位置是基于场景的原点。图形项包含一个Z值(默认为0)来设置它们的层叠顺序(通过setZValue())。场景坐标的原点在场景的中心,在场景中的图形项拥有一个场景坐标和场景中的边界矩形,场景边界矩形构成判断场景中那些区域被更改了的基础。视图的坐标就是部件的坐标,所有的鼠标事件最初都是使用视图坐标被接收的。
可以使用坐标映射方法来转换场景、视图、图形项坐标系,如想要在视图上单击了鼠标,便可以通过QGraphicsView::mapToScene()和QGraphicsScene::itemAt()来获得光标下的图形项,如果想要获取一个图形项在视图中的位置,可以在图形项上调用QGraphicsItem::mapToScene(),然后在视图上调用QGraphicsView::mapFromScene,如果想要获取视图中椭圆形区域中包含的图形项,可以先传递一个QPainterPath对象作为参数给mapToScene()方法,然后将映射后的路径传给QGraphicsScene::items()方法。还可以在子图形项和父图形项或者图形项和图形项之间进行坐标映射,所有的映射函数都可以映射点、矩形、多边形、路径,如下所示:
下面为使用映射方法的一个示例,其结果图是分别点击部件的左上角、红色图形项的左上角、和绿色图形项左上角后的输出:
class MyView: publicQGraphicsView {protected:void mousePressEvent(QMouseEvent* event)override{ qDebug() << "==================="; QPoint viewPos = event->pos(); qDebug() << "viewPos: " <<viewPos; QPointF scenePos =mapToScene(viewPos); qDebug() << "scenePos " <<scenePos; QGraphicsItem* item = scene()->itemAt(scenePos);if(item) { QPointF itemPos = item->mapFromScene(scenePos); qDebug() << "itemPos: " <<itemPos; } QGraphicsView::mousePressEvent(event); } };int main(int argc, char**argv) { QApplication app(argc, argv); QGraphicsScene scene; //创建场景QGraphicsItem * item = new MyItem; //创建自定义图形项scene.addItem(item); //添加图形项到场景item->setPos(0, 0); QGraphicsRectItem* rectItem = scene.addRect(QRect(0, 0, 100, 100), QPen(Qt::blue), QBrush(Qt::green));//创建矩形图形项并添加到场景rectItem->setPos(10, 10); MyView view; //为场景创建视图view.setScene(&scene); view.setBackgroundBrush(QPixmap("./background.png")); //设置场景的背景图片view.resize(400, 300); //设置视图大小view.show(); //显示视图returnapp.exec(); }
如果想让后添加的矩形图形项在自定义矩形项后面的话可以添加以下代码:
item->setZValue(1);
还可以将自定义的图形项放到矩形图形项里,并将矩形项旋转:
item->setParentItem(rectItem); rectItem->rotate(45);
在以上示例的效果可以看到场景的原点坐标并不是跟视口的原点一样,当场景中没有图形项的时候场景的原点在视图的中心,而场景中有图形项的时候场景原点也不跟视口原点一致。我们可以通过QGraphicsScene::setSceneRect()来设置场景矩形(当视图小于场景矩形时就会自动生成滚动条),比如如下的代码就实现了将场景的原点显示在视图的左上角:
#include <QApplication>#include <QGraphicsScene>#include <QGraphicsView>#include <QGraphicsRectItem>int main(int argc, char**argv) { QApplication app(argc, argv); QGraphicsScene scene; //创建场景scene.setSceneRect(0, 0, 500, 400); //设置场景矩形QGraphicsRectItem* item = new QGraphicsRectItem(0, 0, 100, 100); //创建矩形图形项scene.addItem(item); //添加矩形图形项到场景QGraphicsView view(&scene); //为场景创建视图view.setBackgroundBrush(QPixmap("./background.png")); //设置场景的背景图片view.resize(500, 400); //设置视图大小view.show(); //显示视图returnapp.exec(); }
还可以使用QGraphicsView::centerOn()方法来设置场景中的一个点或一个图形项作为视图的显示中心。
三、事件处理与传播
图形视图框架中的事件都是首先由视图进行接收,然后传递给场景,再由场景传递给相应的图形项。需要注意的是图形项默认是无法接收鼠标进入、移动、离开这些悬停事件的,可以调用setAcceptHoverEvents()方法来开启接收,图形项默认不能获得焦点和被选择,可以使用setFlag(QGraphicsItem::ItemIsFocusable)来设置图形项可以获得焦点,使用setFlag(QGraphicsItem::ItemIsSelectable)来设置图形项可以被选择,setFlag(QGraphicsItem::ItemIsMovable)可以设置点击图形项并拖动鼠标就能移动图形项。。下面的示例创建了5个背景颜色随机的图形项,并设置+、-、→、↓键来控制其缩放、旋转、移动,添加了一些鼠标事件处理方法:
#include <QApplication>#include <QGraphicsScene>#include <QGraphicsView>#include <QGraphicsRectItem>#include <QKeyEvent>#include <QGraphicsSceneContextMenuEvent>#include <QDebug>#include <QMenu>#include <QTime>class MyItem: publicQGraphicsItem {public: MyItem() { m_brushColor =Qt::red; setFlag(QGraphicsItem::ItemIsFocusable); //设置图形项可以获得焦点setFlag(QGraphicsItem::ItemIsMovable); //设置拖动鼠标移动图形项setAcceptHoverEvents(true); } QRectF boundingRect()const override{ qreal adjust = 0.5;return QRectF(-10 - adjust, -10 - adjust, 20 + adjust, 20 +adjust); }void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget)override{//根据图形项是否获得焦点来使用不同颜色的轮廓if(hasFocus()) painter->setPen(QPen(QColor(255, 255, 255, 200)));elsepainter->setPen(QPen(QColor(100, 100, 100, 100))); painter->setBrush(m_brushColor); painter->drawRect(-10, -10, 20, 20); }void mousePressEvent(QGraphicsSceneMouseEvent* event)override{ setFocus(); //设置焦点setCursor(Qt::ClosedHandCursor); //改变光标}void keyPressEvent(QKeyEvent* event)override{if(event->key() == Qt::Key_Down) //如果是↓键moveBy(0, 10); //向下移动10像素}void hoverEnterEvent(QGraphicsSceneHoverEvent* event)override //鼠标进入事件{ setCursor(Qt::OpenHandCursor); //改变光标setToolTip("I am item"); //设置鼠标悬停提示}void contextMenuEvent(QGraphicsSceneContextMenuEvent* event)override //右键菜单事件{ QMenu menu; QAction* moveAction = menu.addAction("move back"); //添加菜单项QAction* selectedAction = menu.exec(event->screenPos()); //显示菜单,获得用户选择的菜单项if(selectedAction ==moveAction) setPos(0, 0); //移动图形项到原点}void setColor(const QColor &color){m_brushColor =color;}private: QColor m_brushColor; };class MyView: publicQGraphicsView {protected:void keyPressEvent(QKeyEvent* event)override{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); //顺时针旋转30度break;default:break; }//最后要记得调用QGraphicsView::keyPressEvent()QGraphicsView::keyPressEvent(event); } };int main(int argc, char**argv) { QApplication app(argc, argv); QGraphicsScene scene; //创建场景scene.setSceneRect(-200, -150, 400, 300); //设置场景矩形qsrand(QTime(0,0,0).secsTo(QTime::currentTime()));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("./background.png")); //设置场景的背景图片view.show(); //显示视图returnapp.exec(); }
图形视图中的拖放事件可以参考第六章的内容或者Graphics View分类下的Drag and Drop Robot示例程序,在它下面还有一个使用图形视图框架设计的绘图示例程序Diagram Scene。
四、图形效果
图形效果可以设置在图形项或非顶层窗口的部件上,Qt提供了4中标准的图形效果,如下所示,也可以通过继承QGraphicsEffect来实现自定义效果,下面的代码对上面的示例程序中MyItem的键盘按键处理方法进行了修改,实现了图形项中通过按下数字键来使用Qt中标准图形效果:
void keyPressEvent(QKeyEvent* event)override{switch (event->key()) {caseQt::Key_1:{ QGraphicsBlurEffect* blurEffect = newQGraphicsBlurEffect; blurEffect->setBlurHints(QGraphicsBlurEffect::QualityHint); blurEffect->setBlurRadius(8); setGraphicsEffect(blurEffect);break; }caseQt::Key_2:{ QGraphicsColorizeEffect* colorizeEffect = newQGraphicsColorizeEffect; colorizeEffect->setColor(Qt::white); colorizeEffect->setStrength(0.6); setGraphicsEffect(colorizeEffect);break; }caseQt::Key_3:{ QGraphicsDropShadowEffect* dropShadowEffect = newQGraphicsDropShadowEffect; dropShadowEffect->setColor(QColor(63, 63, 63, 100)); dropShadowEffect->setBlurRadius(2); dropShadowEffect->setOffset(10); setGraphicsEffect(dropShadowEffect);break; }caseQt::Key_4:{ QGraphicsOpacityEffect* opacityEffect = newQGraphicsOpacityEffect; opacityEffect->setOpacity(0.4); setGraphicsEffect(opacityEffect);break; }caseQt::Key_5:{if(graphicsEffect()) graphicsEffect()->setEnabled(false); //关闭图形效果break; }default:break; } }
下面是依次对图形项输入按键前后效果示例:
五、动画、碰撞检测、图形项组
图形视图框架支持三种级别的动画:1、使用QGraphicsItemAnimation类实现图形项的动画效果,但该类现在已过时。2、创建继承自QObject和QGraphicsItem的自定义图形项,然后创建它的定时器来实现动画效果。3、使用QGraphicsScene::advance()推进场景来实现动画效果,比如下面的代码在上面的示例程序中添加的,main函数里的定时器定时调用场景的advance()方法,而场景中的advance()会调用所有图形项的advance()方法,而且图形项的advance()方法会分为两个阶段被调用两次,第一次参数为0,用来告知图形项场景将要改变,第二次调用的时候参数为1,此时进行具体的移位操作,在图形项的advance方法中做的是让图形项在不同的方向上移动一个随机的数值:
class MyItem: publicQGraphicsItem {public:void advance(intphase) {if(!phase)return;int value = qrand() % 100;if(value < 25) { rotate(45); moveBy(qrand() % 10, qrand() % 10); }else if(value < 50) { rotate(-45); moveBy(-qrand() % 10, -qrand() % 10); }else if(value < 75) { rotate(30); moveBy(-qrand() % 10, qrand() % 10); }else{ rotate(-30); moveBy(qrand() % 10, -qrand() % 10); } } ...... }int main(int argc, char**argv) { ...... QTimer timer; QObject::connect(&timer, SIGNAL(timeout()), &scene, SLOT(advance())); timer.start(300); ...... }
图形项之间的碰撞检测可以使用两种方法啦实现:1、重写图形项的shape()方法来返回其准确的形状,然后可以使用图形项的collidesWithItem()方法来判断两个图形项是否发生碰撞(其返回图形项的交集,如果图形项形状很复杂那么这个方法会很耗时)。默认的shape()方法会调用boundingRect()函数返回一个简单的矩形。2、重写collidesWithItem()方法来提供一个自定义的图形项碰撞算法。collidesWithPath()用来判断是否与指定的路径碰撞,调用图形项的collidingItems()或场景的collidingItems()来获取与图形项碰撞的所有图形项的列表,这几个方法都有一个Qt::ItemSelectionMode类型的参数用来指定怎样进行图形项的选取,如下所示。下面的代码在上面项目的基础上添加的,其重写了图形项的shape()方法,在其中返回了图形项对应的矩形,然后在paint()修改代码使图形项在与其它图形项碰撞时其轮廓变为白色:
class MyItem: publicQGraphicsItem {public: QPainterPath shape() { QPainterPath path; path.addRect(-10, -10, 20, 20);returnpath; }void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget)override{//根据图形项是否碰撞来使用不同颜色的轮廓if(!collidingItems().isEmpty()) painter->setPen(QPen(QColor(255, 255, 255, 200)));elsepainter->setPen(QPen(QColor(100, 100, 100, 100))); painter->setBrush(m_brushColor); painter->drawRect(-10, -10, 20, 20); } ...... }
QGraphicsItemGroup图形项组为图形项提供了一个容器,而且可以通过拖动其中一个图形项来使它们一起移动,添加图形项组到场景中后其中的图形项就自动加入到了场景中,比如下面的第一段代码是在前面程序基础上加的。场景对象也可以直接创建图形项组,并添加图形项到组,例如可以让QGraphicsView对象通过调用setDragMode(QGraphicsView::RubberBandDrag)函数来使鼠标可以在视图上拖动出橡皮筋来选择图形项(注意要使图形项可以被选择还需要调用setFlag(QGraphicsItem::ItemIsSelectable)来设置标志),而此时就可以将选中的图形项添加到图形项组来使其能整体移动,参见第二段代码:
int main(int argc, char**argv) { ........//创建两个图形项MyItem* item1 = newMyItem; item1->setColor(Qt::blue); MyItem* item2 = newMyItem; item2->setColor(Qt::green);//添加这两个图形项到图形项组QGraphicsItemGroup* group = newQGraphicsItemGroup; group->addToGroup(item1); group->addToGroup(item2); group->setFlag(QGraphicsItem::ItemIsMovable); //设置拖动鼠标就会移动图形项item2->setPos(30, 0); scene.addItem(group);//group->removeFromGroup(item1); //删除图形项,被删除的图形项会被移动到父图形项组或场景中。//scene.destroyItemGroup(group); //删除图形项组,被删除的图形项会被移动到父图形项组或场景中。returnapp.exec(); }
//将被选中的图形项添加到一个图形项组中QGraphicsItemGroup* group = scene.createItemGroup(scene.selectedItems());
六、打印和使用OpenGL
图形视图框架提供场景的render()方法和视图的render()方法这两个渲染函数来完成在绘图设备上绘制场景(使用场景坐标)和视图(使用视图坐标)的内容。QGraphicsScene::render()经常用来打印没有变换的场景,如文本文档和几何数据,QGraphicsView::render()适合用来实现屏幕快照,比如下面的例子就是在前面程序的基础上使用QGraphicsView::render()实现抓取视图快照并将其保存到文件的例子:
...... #include <QPainter>#include <QPixmap>int main(int argc, char**argv) { ...... QPixmap pixmap(400, 300); QPainter painter(&pixmap); painter.setRenderHint(QPainter::Antialiasing); view.render(&painter); painter.end(); pixmap.save("view.png");returnapp.exec(); }
可以使用QGraphicsView::setViewport()来将QGLWidget作为QGraphicsView的视口,这样就可以使用OpenGL进行渲染,代码示例如下。如果想OpenGL进行抗锯齿,可以使用采样缓冲区(sample buffer)。需要注意的是要使用OpenGL相关的类需要在.pro文件中添加"QT += opengl"。
#include <QGLWidget>int main(int argc, char**argv) { MyView view; //创建视图view.setViewport(newQGLWidget(QGLFormat(QGLFormat(QGL::SampleBuffers)))); }
七、图形部件、布局、内嵌部件
图形部件QGraphicsWidget继承自QGraphicsItem,但它与QWidget很相似(与QWidget不同,图形部件不继承自QPaintDevice),它拥有QWidget的一些特性,通过它可以实现一个拥有事件、信号和槽、大小提示和策略的完整部件,还可以使用QGraphicsLinearLayout和QGraphicsGridLayout来实现部件的布局。使用场景类的addWidget()方法可以很方便的将一个窗口部件嵌入到场景中,这也可以通过QGraphicsProxyWidget类(QGraphicsWidget的子类)的实例来实现。下面是使用图形部件的一个示例和效果:
#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 = newQTextEdit; 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 = newQGraphicsLinearLayout; layout->addItem(textEdit); layout->addItem(pushButton);//创建图形部件,设置其为一个顶层窗口,然后在其上应用上面的布局管理器QGraphicsWidget* form = newQGraphicsWidget; form->setWindowFlags(Qt::Window); form->setWindowTitle("Widget Item"); form->setLayout(layout);//将图形部件进行扭曲,然后添加到场景中form->shear(2, -0.5); scene.addItem(form); QGraphicsView view(&scene); view.show();returnapp.exec(); }
上面示例中的将图形部件扭曲的方法shear在其帮助文档中提示使用setTransform()方法替换:
form->setTransform(QTransform().shear(2, -0.5), true);
QGraphicsWebView是QGraphicsWidget的子类,使用它可以直接将网页内容集成到视图中。Qt中提供了一个Pad Navigator Example的示例程序,它在Graphics View分类中,还有一个Embedded Dialogs演示程序。Qt中还提供了一个40 000 Chips演示程序,它使用了图形视图框架来管理大量的图形项。