Qt知识回顾(十)——图形视图、动画和状态机框架

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

图形视图框架的结构

  图形视图框架提供了一个基于图形项的模型视图编辑方法,主要由场景、视图和图形项三部分组成,这三部分分别由QGraphicsScene、QGraphicsView和QGraphicsItem这三个类来表示。多个视图可以查看一个场景,场景中包含各种各样的图形项。
  图形视图框架可以管理数量庞大的自定义2D图形项,并且可以和他们进行交互。使用视图部件可以使这些图形项可视化,视图还支持缩放和旋转。框架中包含了一个事件传播构架,提供了和场景中的图形项进行精确的双精度交互的能力,图形项可以处理键盘事件,鼠标的按下、移动、释放和双击事件,还可以跟踪鼠标的移动。图形视图框架使用一个BSP(Binary Space Partitioning)树来快速发现图形项,也正是因如此,它可以实时显示一个巨大的场景,甚至包含上百万个图形项。

场景

  QGraphicsScene提供了图形视图框架中的场景,场景拥有以下几个功能:1>提供用于管理大量图形项的高速接口;2>传播事件到每一个图形项;3>管理图形项的状态,比如选择和处理焦点;4>提供无变换的渲染功能,主要用于打印。
  场景使图形项QGraphicsItem对象的容器。可以调用QGraphicsScene::addItem()函数将图形项添加到场景中,然后调用任意一个图形项发现函数来检索添加的图形项。QGraphicsScene::items()函数及其他重载函数可以返回符合条件的所有图形项,这些图形项不是与指定的点、矩形、多边形或者矢量路径相交,就是包含在它们之中。QGraphicsScene::itemAt()函数返回指定点的最上层的图形项。所有的图形项发现函数返回的图形项都是使用递减顺序。如果要从场景中删除一个图形项,则可以使用QGriphicsScene::RevemoveItem()函数。
  QGriphicsScene的事件传播构架可以将场景事件传递给图形项,也可以管理图形项之间事件的传播。QGrophicsScene也用来管理图形项的状态,如图形项的选择和焦点等。可以通过向QGraphicsScene::setSelectionArea()函数传递一个任意的形状来选择场景中指定的图形项。如果要获取当前选取的所有图形项的列表,则可以使用QGraphicsScene::selectedItems()函数。另外可以调用QGraphicsScene::setFocusItem()或者QGraphicsScene::setFocus()函数来为一个图形项设置焦点,调用QGraphicsScene::focusItem()函数获取当前获得焦点的图形项。
  QGriphicsScene也可以使用QGraphicsScene::render()函数将场景中的一部分渲染到一个绘图设备上。

视图

  QGraphicsView提供了视图部件,它用来使场景中的内容可视化。可以连接多个视图到同一个场景为相同的数据集提供多个视口。视图部件是一个可滚动的区域,它提供了一个滚动条来浏览大的场景。可以使用setDragMode()函数以QGraphicsView::ScrollHandDrag为参数来使光标变为手掌形状,从而可以拖动场景。如果设置setDragMode()的参数为QGraphicsView::RubberBandDrap,那么可以在视图上使用鼠标拖出橡皮筋框来选择图形项。默认的QGraphicsView提供了一个QWidget作为视口部件,如果要使用OpenGL进行渲染,则可以调用QGriphicsView::setViewport()设置QOpenGLWidget作为视口。QGraphicsView会获取视口部件的拥有权。
  视图从键盘或者鼠标接收输入事件,然后会在发送这些事件到可视化的场景之前将它们转换为场景事件(将坐标转换为合适的场景坐标)。另外,使用视图的变换矩阵函数QGraphicsView::transform()时,可以通过视图来变换场景的坐标系统,这样便可以实现比如缩放和旋转等高级的导航功能。

图形项

  QGraphicsItem时场景中图形项的基类。图形试图框架为典型的形状提供了标准的图形项,比如矩形(QGraphicsRectItem)、椭圆(QGraphicsEllipseItem)和文本项(QGraphicsTextItem)。不过,只有编写自定义的图形项时才能发挥QGriphicsItem的强大功能。QGraphicsItem主要支持如下功能:
  >鼠标按下、移动、释放、双击、悬停、滚轮和右键菜单键
  >键盘输入焦点和键盘事件
  >拖放事件
  >分组,使用QGraphicsItemGroup通过parent-child关系实现
  >碰撞检测
  除此之外,图形项还可以存储自定义的数据,可以使用setData()进行数据存储,然后使用data()获取其中的数据。

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

坐标系统

  图形视图框架基于笛卡尔坐标系统,一个图形项在场景中的位置和几何形状由x和y坐标来表示。当使用一个没有变换的视图来观察场景时,场景中的一个单元代表屏幕上的一个像素。图形视图框架中有三个有效的坐标系统:图形项坐标、场景坐标和视图坐标。为了方便应用,图形视图框架中提供了一些便捷函数来完成3个坐标系统之间的映射。进行绘图时,场景坐标对应QPainter的逻辑坐标,视图坐标对应于设备坐标。

1.图形项坐标
  图形项使用自己的本地坐标系统,坐标通常是以它们的中心为原点(0,0),而这也是所有变换的中心。当要创建一个自定义图形项时,只需要考虑图形项的坐标系统,QGraphicsScene和QGraphicsView会完成其他所有的变换。而且,一个图形项的边界矩形和图形形状都是在图形项坐标系统中的。
  图形项的位置是指图形项的原点在其父图形项或者憧憬中的位置。如果一个图形项在另一个图形项之中,那么它被称为子图形项,而包含它的图形项称为它的父图形项。所以没有父图形项的图形项都会在场景的坐标系统中,它们被称为顶级图形项。可以使用setPos()函数来指定图形项的位置,如果没有指定,则默认出现在父图形项或者场景的原点处。
  子图形项的位置和坐标是相对于父图形项的,虽然父图形项的坐标变换会隐含地变换子图形项,但是子图形项的坐标不会受到父图形项的影响。但是相对于场景,子图形项会跟随父图形项的变换。
  所有的图形项都会使用确定的顺序来进行绘制,这个顺序也决定了单击场景时那个图形项会优先获得鼠标输入。一个子图形项会堆叠在父图形项的上面,而兄弟图形项会以插入顺序进行堆叠。默认的,父图形项会被最先进行绘制,然后按照顺序对其上的子图形项进行绘制。所有的图形项都包含一个Z值来设置他们的层叠顺序,一个图形项的Z值默认为0,可以使用QGraphicsItem::setZValue()来改变一个图形项的Z值,从而使他堆叠在其兄弟图形项的上面或者下面。

2.场景坐标
  场景项坐标是所有图形项的基础坐标系统。场景坐标系统描述了每一个顶层图形项的位置,也用于处理所有从视图传到场景上的事件。场景坐标的原点在场景的中心,x和y坐标分别向右和向下增大。每一个场景中的图形项除了拥有一个图形项的本地坐标和边界矩形外,还都拥有一个场景坐标(QGraphicsItem::scenePos())和一个场景中的边界矩形(QGraphicsItem::sceneBoundingRect())。场景坐标用来描述图形项在场景坐标系统的位置,而图形项的场景边界矩形用于QGraphicsScene判断场景中的哪些区域进行了更改。

3.视图坐标
  视图坐标就是部件的坐标。视图坐标的每一个单位对应一个像素,原点(0,0)总是在QGraphicsView部件的左上角,而右下角时(宽,高)、所有的鼠标事件和拖放事件最初都是使用视图坐标接收的。

坐标映射
  当处理场景中的图形项时,将坐标或者任意的形状从场景映射到图形项、或者从一个图形项映射到另一个图形项、或者从视图映射到场景,这些坐标变换都是很常用的。例如,在QGraphicsView的视口上单击了鼠标,则可调用QGraphicsView::mapToScene()以及QGraphicsScene::itemAt()来获取光标下的图形项;如果要获取一个图形项在视口中的位置,那么可以先在图形项上调用QGraphicsItem::mapToScene(),然后在视图上调用QGraphicsView::mapFromScene();如果要获取在视图的一个椭圆形包含的图形项,则可以先传递一个QPainterPath对象作为参数给mapToScene()函数,然后传递映射后的路径给QGraphicsScene::items()函数。
  不仅可以在视图、场景和图形项之间使用坐标映射,还可以在子图形项、父图形项或者图形项、图形项之间进行坐标映射。图形视图框架提供的所有映射函数如下表,所有的映射函数都可以映射点、矩形、多边形和路径。

映射函数描述
QGraphicsView::mapToScene()从视图坐标系统映射到场景坐标系统
QGraphicsView::mapFromScene()从场景坐标系统映射到视图坐标系统
QGraphicsItem::mapToScene()从图形项的坐标系统映射到场景的坐标系统
QGraphicsItem::mapFromScene()从场景的坐标系统映射到图形项的坐标系统
QGraphicsItem::mapToParent()从本图形项的坐标系统映射到其父图形项的坐标系统
QGraphicsItem::mapFromParent()从父图形项的坐标系统映射到本图形项的坐标系统
QGraphicsItem::mapToItem()从本图形项的坐标系统映射到另一个图形项的坐标系统
QGraphicsItem::mapFromItem()从另一个图形项的坐标系统映射到本图形项的坐标系统

事件处理和传播

  图形视图框架中的事件都是先由视图进行接收,然后传递给场景,再由场景传递给相应的图形项。而对于键盘事件,它会传递给获得焦点的图形项,可以使用QGraphicsScene类的setFocusItem()函数或者图形项自身调用setFocus()函数来设置焦点图形项。默认的,如果场景没有获得焦点,那么所有的键盘事件都会被丢弃。如果调用了场景的setFocus()函数或者场景中的一个图形项获得了焦点,那么场景也会自动获得焦点。如果场景丢失了焦点(比如调用了clearFocus()函数),然而它的一个图形项又获得了焦点,那么场景就会保存这个图形项的焦点信息;当场景重新获得焦点后,就会确保最后一个焦点项目重新获得焦点。
  对于鼠标悬停效果,QGraphicsScene会调度悬停事件。如果一个图形项可以接收悬停事件,那么当鼠标进入它的区域之中时,他就会收到一个GraphicsSceneHoverEnter事件。如果鼠标继续在图形项的区域之中进行移动,那么QGraphicsScene就会向该图形项发送GraphicsSceneHoverMove事件。当鼠标离开图形项的区域时,他将会收到一个GraphicsSceneHoverLeave事件。图形项默认是无法接收悬停事件的,可以使用QGraphicItem类的setAcceptEvent()函数使图形项可以接收悬停事件。
  所有的鼠标事件都会传递给当前鼠标抓取的图形项,一个图形项如果可以接收鼠标事件(默认可以)而且鼠标可以在它的上面被按下,那么它就会成为场景的鼠标抓取的图形项。

图形视图框架的其他特性

图形效果

  图形效果是Qt4.6添加的一个新的特色功能,QGraphicsEffect类是所有图形效果的基类。使用图形效果来改变元素的外观是通过在源对象(如一个图形项)和目标设备(如视图的视口)之间挂接了渲染管道和一些操作来实现的。图形效果可以实施在任何一个图形项或者非顶层窗口的任何窗口部件上,只须先创建一个图形效果对象,然后调用setGraphicsEffect()函数来使用这个图形效果即可。如果想停止使用该效果,可以调用setEnabled(false)。Qt提供了4种标准的效果。

图形效果类介绍
QGraphicsBlurEffect该类提供了一个模糊效果,该效果一般用来减少源对象细节的显示。可以使用setBlurRadius()函数来修改细节等级,默认的模糊半径是5像素,还可以使用setBlurHints()来指定模糊怎样执行
QGraphicsColorizeEffect该类提供了一个染色效果,该效果用来为源对象进行染色,可以使用setColor()函数修改颜色,默认是浅蓝色QColor(0,0,192);还可以使用setStrength()来修改效果的强度,强度在0.0~1.0之间,默认为1.0
QGraphicsDropShadowEffect该类提供了一个阴影效果,该效果可以为源对象提供一个阴影。可以使用setColor()来修改阴影的颜色,默认是透明的黑灰色QColor(63,63,63,180);可以使用setOffset()来改变阴影的偏移值,默认为右下方8像素,还可以使用setBlurRadius()来改变阴影的模糊半径,其默认值为1
QGraphicsOpacityEffect该类提供了一个透明效果,该效果可以使源对象透明。可以使用setOpacity()函数来修改透明度,其值在0.0~1.0之间,0.0表示完全透明,1.0表示完全不透明。

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

动画
  图形视图框架支持几种级别的动画。1.通过动画框架来实现动画效果;2.创建一个继承自QObject和QGraphicsItem的自定义图形项,然后创建它自己的定时器来实现动画;3.使用QGraphicsScene::advance()来推进场景。

碰撞检测
  图形视图框架提供了图形项之间的碰撞检测,碰撞检测可以使用两种方法来实现:
  >重新实现QGraphicsItem::shape()函数来返回图形项准确的形状,然后使用默认的collidesWithItem()函数通过两个图形项形状之间的交集来判断是否发生碰撞,如果图形项的形状很复杂,那么进行这个操作是非常耗时的。如果没有重新实现shape()函数,那么它会默认调用boundingRect()函数返回一个简单的矩形。
  >重新实现collidesWithItem()函数来提供一个自定义的图形项碰撞算法。
  可以使用QGraphicsItem类中的collidesWithItem()函数来判断是否与指定的图形项进行了碰撞;使用collidesWithPath()来判断是否与指定的路径碰撞,使用collidingItems()来获取与该图形项碰撞的所有图形项的列表;也可以调用QGraphicsScene类的collidingItems()、这几个函数都有一个Qt::ItemSelectionMode参数来指定怎样进行图形项的选取,它一共四个值,如下表所示

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

图形项组
  QGraphicsItemGroup图形项组为图形项提供了一个容器,它可以将多个图形项组合在一起而将它本身以及所有的子图形项看作一个独立的图形项。与父图形项不同,图形项组中的所有图形项都是平等的。

打印和使用openGL进行渲染

打印
  图形视图框架提供渲染函数QGraphicsScene::render()和QGriphicsView::render()来完成打印功能。这个两个函数提供了相同的API,可以在绘图设备上绘制场景或者视图的全部或者部分内容。两者的不同之处就是一个在场景坐标上进行操作而另一个在视图坐标上。QGraphicsScene::render()经常用来打印没有变换的场景,比如几何数据和文档等;而QGraphicsView::render()函数适合用来实现屏幕快照,要在打印机上进行打印,可以使用如下代码:

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

屏幕快照的实现如下:

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

使用OpenGL进行渲染
  使用OpenGL进行渲染时,可以使用QGraphicsView::setViewport()将QOpenGLWidget作为QGraphicsView的视口。使用view.setViewport(new QOpenGLWidget())就可以使用OpenGL进行渲染了。

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

  从Qt4.4开始通过QGraphicsWidget类引入了支持几何和布局的图形项。图形部件QGraphicsWidget与QWidget很相似,但是与QWidget不同,它不是继承自QPaintDevice,而是QGraphicsItem。通过它可以实现一个拥有事件、信号和槽、大小提示和策略完整的部件,还可以使用QGraphicsLinearLayout和QGraphicsGridLayout来实现部件的布局。
  QGraphicsWidget继承自QGraphicsObject和QGraphicsLayoutItem,而QGraphicsObject继承自QObject和QGraphicsItem,所以QGraphicsWidget既拥有以前窗口部件的一些特性也拥有图形项的一些特性。图形视图框架提供了对任意的窗口部件嵌入场景的无缝支持,这时通过QGraphicsWidget的子类QGraphicsProxyWidget实现的。可以使用QGraphicsScene类的addWidget()函数将任何一个窗口部件嵌入到场景中,这也可以通过创建QGraphicsProxyWidget类的实例来实现。

动画框架

  动画框架的目的是提供一种简单的方法来创建平滑的、具有动画效果的GUI界面。该框架是通过控制Qt的属性来实现动画的,它可以应用在窗口部件和其他QObject对象上,也可以应用在图形视图框架中。
  动画框架中主要的类及其关系如下图,其中,基类QAbstractAnimation和它的两个子类QVariantAnimation以及QAnimationGroup构成了动画框架的基础,这里的QVariantAnimation是所有动画类的祖先,它定义了一些所有动画类都共享的功能函数,比如动画的开始、停止和暂停等;它也可以接收时间变化的通知,通过继承这个类可以创建自定义的动画类。
在这里插入图片描述
  动画框架中提供了QPropertyAnimation类,继承自QVariantAnimation,用来执行Qt属性的动画。这个类使用缓和曲线来对属性进行插值。如果要对一个值使用动画,则可以创建继承自QObject的类,然后在类中将该值定义为一个属性。属性动画为现有的窗口部件以及其他QObject子类提供了非常灵活的动画控制。Qt现在支持的可以进行插值的QVariant类型有:Int、Uint、Double、Float、QLine、QLineF、QPoint、QPointF、QSize、QSizeF、QRect、QRectF和QColor等。如果要实现复杂的动画,可以通过动画组QAnimationGroup类实现,它的功能是作为其他动画类的容器,一个动画组还可以包含另外的动画组。

状态机框架

  状态机框架提供了一些类来创建和执行状态图,状态图为一个系统如何对外界激励进行反应提供了一个图形化模型,该模型是通过定义一些系统可能进入的状态以及系统怎样从一个状态切换到另一个状态来实现的。事件驱动的系统(比如Qt应用程序)的一个关键特性就是它的行为不仅仅依赖于最后一个或者当前的事件,而且也依赖于将要执行的事件。通过使用状态图,这些信息会非常容易地表达。
  状态机框架提供了一个API和一个执行模型来有效地将状态图地元素和语义嵌入到Qt应用程序中。该框架与Qt的元对象系统是紧密结合的,例如,状态间的切换可以由信号来触发。Qt的事件系统用来驱动状态机。状态机框架中的状态图是分层的,状态可以嵌套在其他状态中,状态机一个有效配置中的所有状态都拥有一个共同的祖先。

创建状态机

  假定状态机有一个QPushButton控制,包含3个状态:即s1、s2和s3,其中s1是初始状态。但单击按钮时,状态机切换到另一个状态。下图是该状态机的状态图。
在这里插入图片描述

在状态机中使用动画

  如果将状态机API和Qt中的动画API相关联,那么么就可以使分配到状态上的属性自动实现效果。在属性上添加动画,就意味着进入一个状态时分配的属性将无法立即生效,而是在进入时开始播放动画,然后以平滑的动画来达到属性分配的值。使用该状态的propertiesAssigned()信号,该信号会在属性被分配到最终的值时被发送,而无论使用了动画与否。
  如果一个状态在动画结束前退出了,那么状态机的行为会依赖于切换的目标状态。如果目标状态明确地为该属性分配了一个值,那么该属性就会使用目标状态设置地这个值。如果目标状态没有为该属性分配任何值,这样会有两种选择:默认的,该属性会被分配切换时离开地那个状态所定义的值;但是如果设置了全局恢复策略,那么恢复策略指定的值优先。

状态机框架的其他属性

1.为状态分组来共享切换
  假设要使一个退出按钮在任何时候都可以退出应用程序,那么可以创建一个QFinalState最终状态,然后让它作为切换的目标状态,并且将切换关联到退出按钮地单击信号上,这样虽然可以将最终状态和s1、s2以及s3状态分别进行切换,不过这样看起来很乱,而且如果以后再添加新的状态还要记得让它和最终状态进行切换。其实将s1、s2和s3分组从而达到相同的效果。这就是创建一个新的状态,然后将这3个状态作为新状态的子状态;相对于子状态而言,前者直接添加到状态机中地状态都可以看作顶层状态,新的状态机如下图所示:
在这里插入图片描述

2.使用历史状态来保存或者恢复当前状态
  假设要添加一个“中断机制”,当按下一个按钮后可以让状态机执行一些无关的工作,而完成后又可以恢复到以前的状态,这可以通过历史状态QHistoryState来完成。历史状态是一个伪状态,它代表了当父状态退出时所在的那个子状态。历史状态应创建为一个状态的子状态,这个状态就是要记录的父状态,例如,在s1的任何一个子状态进行了中断,那么要记录的状态就是这个发生中断的子状态,而历史状态应该创建为s1的子状态。当父状态退出时,则自动记录当前的子状态,切换到历史状态实际上就是切换到状态机先前保存的子状态。添加了中断机制的状态机如下所示:
在这里插入图片描述
3.使用并行状态来避免组合爆炸
  假定在一个单一的状态机包含了一个汽车的一组互斥的属性,例如,clean对dirty、moving对not moving,则可以使用4个互斥的状态和8个切换来表示所有可能出现的组合。如下图所示
在这里插入图片描述
  但是如果再添加第三个属性,总的状态数就会翻倍变为8,如果再添加第四个属性,那么状态总数就变为16。使用并行状态就可以使状态的综述线性增长而不是指数增长,而且向并行状态中添加或移除状态都不会影响其他的兄弟状态。如下图所示
在这里插入图片描述
4.检测符合状态的结束信号
  一个子状态可以是一个最终状态,进入了一个最终子状态时,其父状态就回发射QState::finished()信号。下图显示了一个复合状态s1在进入最终状态前进行了一些处理工作。如果想隐藏一个复合状态的内部细节,那么使用复合状态的最终状态是非常有效的。对于外界来说需要做的只是进入这个状态,然后等该状态完成工作后获得一个通知。
在这里插入图片描述
5.无目标切换
  一个切换也可以没有目标状态,一个没有目标状态的切换也可以像其他切换那样被触发。其不同之处在于,当一个没有目标的切换被触发时,他不会引起任何的状态变化,这样便可以让状态机在一个特定的状态时响应信号或者事件而不用离开这个状态。

6.事件、切换和守护
  QStateMachine在它自己的事件循环中运行。对于信号切换(QSignalTransition对象),当状态机截获相应的信号时,QStateMachine自动发送QStateMachine::SingnalEvent事件给自己;相似的,对于QObject事件切换(QEventTransition对象)QStateMachine将会发送QStateMachine::WrappedEvent事件。另外,还可以使用QStateMachine::postEvent()发送自定义的事件给状态机,当发送自定义事件时,还需要继承QAbstractTransition类来创建自定义的切换。

7.使用恢复策略自动恢复属性
  当状态分配的属性不再活动时,可能希望将其恢复到初值,通过设置全局的恢复策略可以使状态机进入一个状态而不用明确指定属性的值。

QStateMachine machine;
machine.setGlobalRestorePolicy(QStateMachine::RestoreProperties);

  当设置了恢复策略以后,状态机将自动恢复所有的属性。如果进入一个状态,且该状态没有为指定的属性设置值,那么状态机就会首先查找状态层次中该状态的祖先是否定义了该属性,如果是,那么属性将会恢复为最邻近的祖先所定义的的值;否则他将会恢复到初始值。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值