图形视图,动画和状态机框架

在绘图中,如果需要处理从几个到几万的项时,而且要求用户能够单击,拖动和选取项,就需要使用到视图体系,Qt的视图体系包括一个由QGraphicsScene充当的场景和一些由QGraphicsItem的子类充当的场景中的项。

QGraphicsScene是一个图形项的集合,也是图形项的容器。一共有3层:背景层,前景层和项层,背景层和前景层通常由QBrush指定。如果使用了QGraphicsScene对象设置了场景背景或者前景,那么对所有关联了该场景的视图都有效,而QGraphicsView对象设置的场景的背景或前景只对它本身对应的视图有效。任何应用于项的变换都会自动应用于它的子对象中。通过向QGraphicsScene::setSelectionArea()函数传递一个任意的形状来选择场景中指定的图形项。QGraphicsScene::itemAt()函数返回指定点的最上层的图形,要获取当前所有项的列表,可以使用QGraphicsScene::selectedItems(),图形项都包含一个Z值来设置层叠顺序,越大越上。一个场景分为3层:图形项层,前景层和背景层,绘制时从里到外。只有视口坐标是以左上角为原点的,场景坐标和图形项坐标都是以自己的中心为坐标原点。场景的背景图片位置的变化也是场景位置的变化,默认的,如果场景中没有添加任何图形项,那么场景的中心,默认是原点会和视图的中心重合,如果添加了图形项,那么视图就会以图形项的中心为中心来显示场景,因此图形项的大小或者位置变化了,那么视口的位置也会变化。场景还有一个很重要的属性就是场景矩形,它是场景的边界矩形,场景矩形定义了场景的范围,它只会增加不会减小,默认就是包含所有图形项的最大边界矩形,如果设置了场景矩形,就可以指定视图显示的场景区域,比如讲场景的原点显示在视图的左上角,还可以使用centerOn()函数来设置场景中的一个点或者一个图形项作为视图的显示中心

QGraphicsView是一个窗口部件用来显示场景,提供滚动条等功能,默认使用二维图形引擎,调用setViewport改为使用QOPenGLWidget作为视口,可以附加多个视图到同一个场景,从而针对同一数据集提供几个视口(viewport)。视图体系提供了两种分组项的方法,使一个项成为另一个项的子项,和使用QGraphicsItemGroup。它有3个坐标,视口坐标是QGraphicsView的坐标,场景坐标是逻辑坐标。图形项使用自己的本地坐标系统,坐标通常是以他们的中心为原点,一个图形项的边界矩形和图形形状都是在图形项坐标系统中的,图形项的位置是指图形项的原点在其父图形项或场景中的位置。创建自定义图形项时,只需考虑图形项坐标即可,不过相对于场景来说,子图形项会随着父图形项进行转换和偏移。

图形视图框架为场景、每个图元提供了拖放支持。当视图接收到一个拖拽动作,它将拖放事件转换为一个QGraphicsSceneDragDropEvent,然后将其转发给场景。场景则会接管该事件的调度,并将其发送给鼠标下面第一个接受放下动作的图元。可以使用setDragMode()函数以QGraphicsView::ScrollHandDrag为参数来使光标变为手掌形状,从而可以拖动场景,如果设置为QGraphicsView::RubberBnadDrag,那么可以在视图上使用鼠标拖出橡皮筋框来选择图形项。所有的鼠标事件和拖放事件最初都是使用视图坐标接受的。要拖拽一个图元,只需要创建一个QDrag对象,将指针传给开始拖拽的部件。图元可以同时被多个视图观察,但是只有一个视图可以进行拖拽。

设置了场景矩形,就可以指定视图显示的场景区域了,如果要让场景和视图的原点显示在视图左上角,可以

scene.setSceneRect(0,0,400,300);

常见的图形视图类如下:

描述
QAbstractGraphicsShapeItem 所有路径图元的共同基类
QGraphicsAnchor 表示一个QGraphicsAnchorLayout中两个图元之间的anchor
QGraphicsAnchorLayout 布局可以anchor部件到图形视图中
QGraphicsEffect 所有图形特效的基类
QGraphicsEllipseItem 可以添加到QGraphicsScene的椭圆图元
QGraphicsGridLayout 图形视图中管理部件的网格布局
QGraphicsItem QGraphicsScene中所有图元的基类
QGraphicsItemGroup 一个将图元组当做单个图元来看待的容器
QGraphicsLayout 图形视图中所有布局类的基类
QGraphicsLayoutItem 可以被继承,允许布局类管理的自定义图元
QGraphicsLineItem 可以添加到QGraphicsScene的直线图元
QGraphicsLinearLayout 图形视图中管理部件的水平或垂直布局
QGraphicsObject 所有需要信号、槽、属性的图元的基类
QGraphicsPathItem 可以添加到QGraphicsScene的路径图元
QGraphicsPixmapItem 可以添加到QGraphicsScene的图形图元
QGraphicsPolygonItem 可以添加到QGraphicsScene的多边形图元
QGraphicsProxyWidget 代理,用于将一个QWidget对象嵌入到QGraphicsScene中
QGraphicsRectItem 可以添加到QGraphicsScene的矩形图元
QGraphicsScene 管理大量2D图元的管理器
QGraphicsSceneContextMenuEvent 图形视图框架中的上下文菜单事件
QGraphicsSceneDragDropEvent 图形视图框架中的拖放事件
QGraphicsSceneEvent 所有图形视图相关事件的基类
QGraphicsSceneHelpEvent Tooltip请求时的事件
QGraphicsSceneHoverEvent 图形视图框架中的悬停事件
QGraphicsSceneMouseEvent 图形视图框架中的鼠标事件
QGraphicsSceneMoveEvent 图形视图框架中的部件移动事件
QGraphicsSceneResizeEvent 图形视图框架中的部件大小改变事件
QGraphicsSceneWheelEvent 图形视图框架中的鼠标滚轮事件
QGraphicsSimpleTextItem 可以添加到QGraphicsScene的简单文本图元
QGraphicsSvgItem 可以用来呈现SVG文件内容的QGraphicsItem
QGraphicsTextItem 可以添加到QGraphicsScene的文本图元,用于显示格式化文本
QGraphicsTransform 创建QGraphicsItems高级矩阵变换的抽象基类
QGraphicsView 显示一个QGraphicsScene内容的部件
QGraphicsWidget QGraphicsScene中所有部件图元的基类
QStyleOptionGraphicsItem 用于描述绘制一个QGraphicsItem所需的参数

对于文本项,大致都

QGraphicsSimpleTextItem *newText=new QGraphicsSimpleTextItem(QString("123");

QGraphicsScene *newScene=new QGraohicsScene();

newScene->addItem(QGraphicsTextItem);

QGraphicsView *newView=new QGraphicsView();

newView->setScene(newScene);

newView->show():

当编写自定义的图形项时,通常的做法就是继承 QGraphicsItem,然后重新实现它的boundingRect和Paint函数,QRectF boundingRect() ,将 item 的外边界作为矩形返回,还可以存储自定义的数据,通过setData(),如果图形绘制了一个轮廓,那么在边界矩形中包含一半画笔的宽度时很重要的,因为一定要保证所有绘图都在boundingRect()边界中,特别是使用了QPen来渲染边界轮廓时,绘制的图形的边界线的一半会在外面,一半会在里面,所以假如使用了宽度为2个单位的画笔,就必须在矩形中多保留一个单位的边界线。由 QGraphicsView 调用以确定什么区域需要重绘,也可以实现shape()来实现它的特定形状,并且由 contains() 和 collidesWithPath() 用于碰撞检测,shape() 默认实现调用 boundingRect() 返回一个简单的矩形形状。

图形之间的碰撞检测取决于他们的shape,可以重新实现他们的shape函数来返回准确的形状,然后使用默认的collidesWithItem函数通过两个图形项形状之间的交集来判断是否发生碰撞,如果没有重新实现就会默认的boundingRect函数返回一个简单的矩形。还可以使用collidesWithPath判断是否与指定路径碰撞,使用collidingItems获取与该图形项碰撞的所有图形项的列表,它们都有一个Qt::ItemSelectionMode参数来指定怎样进行图形项的选取。

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

图形视图在几个层面上提供了对动画的支持。QGraphicsWidget 继承自 QGraphicsObject 和 QGraphicsLayoutItem,具有QGraphicsItem的所有功能,保持了较小的资源占用,同时提供了两者的优势:来自QWidget的额外的功能,例如:样式、字体、调色板、布局、几何形状,来自QGraphicsItem的分辨率独立性和坐标转换的支持。而QGraphicsObject继承自QObject和QGraphicsItem,QGraphicsWidget是 QGraphicsScene 中所有 widget items 的基类。由于 QGraphicsWidget 类似于 QWidget,并有着相似的 API,更容易从 QWidget 到 QGraphicsWidget,而不是 QGraphicsItem。QGraphicsObject 类为需要信号/槽和属性的所有 items 提供一个基类,将 QGraphicsItem 的许多基本 setters 和 getters 映射到属性,并为其中的许多添加了通知信号。

图形视图框架提供了对任意的窗口部件嵌入场景的无缝支持,这是通过QGprahicsWidget的子类QGraphicsProxyWidget实现的,可以使用QGraphicsScene类的addWidget()函数将任何一个窗口部件嵌入到场景中,也可以使用QGraphicsProxyWidget类来实现,使用的大概逻辑:

 QGraphicsScene scene;
    // 创建部件,并关联它们的信号和槽
 QTextEdit *edit = new QTextEdit;
 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 = new QGraphicsLinearLayout;
  layout->addItem(textEdit);
  layout->addItem(pushButton);
    // 创建图形部件,设置其为一个顶层窗口,然后在其上应用布局
  QGraphicsWidget *form = new QGraphicsWidget;
  form->setWindowFlags(Qt::Window);
  form->setWindowTitle("Widget Item");
  form->setLayout(layout);
    // 将图形部件进行扭曲,然后添加到场景中
  form->setTransform(QTransform().shear(2, -0.5), true);
  scene.addItem(form);

添加图形项组

MyItem *item1=new MyItem;
MyIten *item2=new MyItem;
QGraphicsItemGroup *group=new QGraphicsItemGroup;
group->addToGroup(item1);
group->addToGroup(item2);
gourp->setFlag(QGrpahicsItem::ItemIsMovable);
scene.addItem(group);

QGraphicsItemGroup 的 boundingRect() 函数返回位于其中所有 items 的边界矩形。最普通的

QGraphicsItemGroup *group = scene->createItemGroup(scene->selecteditems());


// 销毁 group,并删除 group item

scene->destroyItemGroup(group);

QGraphicsItem 分组比较简单,但在分组之后 group 中的 QGraphicsItem 无法捕获自己的相关事件,处理方式有两种:

void QGraphicsItem::setHandlesChildEvents(bool enabled) //设置为真,那么都会由QGraphicsItemGroup处理,为false,就自己处理

要让 QGraphicsItemGroup 中的 item 处理自己的事件,还需要在构造 group 后,再手动调用:

QGraphicsItemGroup::setHandlesChildEvents(false);

要获取一个图形项在视口的位置可以先在图形项上调用QGraphicsItem::mapToScene(),然后再视图上调用QGraphicsView::mapFromScene(),,具体的映射函数

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

默认的,如果场景没有获得焦点,那么所有的键盘事件都会被丢弃。可以调用hasFocus()来检查是否获得焦点。如果一个图形项可以接受悬停事件,那么当鼠标进入它的区域中时,它就会收到一个GraphicsSceneHoverEnter事件,移动和离开会有GraphicsSceneHoverMove事件和GraphicsSceneHoverLeave事件,图形项默认是无法接受悬停事件的,可以使用setAcceptHoverEvents()函数接受悬停事件

setFlag()函数可以开启图形项的一些特殊功能

setFlag(QGraohicsItem::ItemIsFocusable);
setFlag(QGraphicsItem::ItemIsMovable);
setAcceptHoverEvent(true);

QGraphicsEffect类是所有图形效果的基类,只需先创建一个图形效果对象,然后调用setGraphicsEffect()函数来使用这个图形效果即可,也可以自定义效果,创建QGraphicsEffect的子类就可。标注图形效果由如下:

效果     介绍
QGraphicsBlurEffect 提供一个模糊效果,用来减少源对象细节的显示,可以通过setBluerRadius和setBlurHints设置
QGraphicsColorizeEffect 提供一个染色效果,为源对象染色,使用setColor改变颜色,setStrength修改效果强度
QGraphicsDropShadowEffect 提供一个阴影效果,为源对象提供一个阴影,使用setColor改变颜色,setOffset改变阴影偏移值
QGraphicsOpacityEffect 提供一个透明效果,可以是源对象透明,通过设置setOpacity来修改透明度 0-1之间0为完全透明
QGraphicsBlurEffect *blurEffect=new QGraphicsBlurEffect;
blurEffect->setBlurHints(QGraphicsBlurEffect::QualityHint);
blurEffect->setBlurRadius(8);
setGraphicsEffect(blurEffect);

QGraphicsColorizeEffect *colorizeEffect=new QGraphicsColorzeEffect;
colorizeEffect->setColor(Qt::white);
colorizeEffect->setStrength(0.6);

QGraphicsDropShadowEffect *dropShadowEffect=new QGraphicsDropShadowEffect;
dropShadowEffect->setColor(QColor(63,63,63,100));
dropShadowEffect->setBlurRadius(2);
dropShadowEffect->setOffset(10);

QGraphicsOpacityEffect *opacityEffect=new QGraphicsOpacityEffect;
opacityEffect->setOpacity(0.4);

QGraphicsScene::advance可以推进场景,它会自动调用场景中所有图形项的advance函数,然后可以重新实现advance来创建动画。

图形视图框架提供渲染函数QGrapahicsScenc::render()和QGraphicsView::render()来完成打印功能,两者的不同在于一个在场景坐标上进行操作另一个在视图坐标上。QGraphicsScene::render()经常用来打印没有变换的场景,比如几何数据和文档,而QGraphicsView::render()函数适合用来实现屏幕快照。用法:

QPrinter painter;
if(QPrinDialog(&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");

动画框架是通过控制Qt的属性来实现动画的,它可以应用在窗口部件和其他QObject对象上,动画框架主要类关系图如下:

 QPropertyAnimation类用来执行属性动画,它使用缓和曲线来对属性进行插值,如果要对一个值使用动画,则可以创建继承自QObject的类,然后再类中将该值定义为一个属性,并不是所有的属性都可以设置动画,只能是值类型为QVariant。可以使用setKeyValueAt(qreal step,const QVariant &value)函数在动画中间为属性设置值,step取值在0.0-1.0之间。还可以使用setDirection()函数设置动画的方向,这里可以设置为两个方向,默认是QAbstractAnimation::Forward,可以设置为QAbstractAnimation::Backward从结束到开始方向进行。

QPushButton button("Animated Button");
button.show();
QPropertyAnimation animation(&button,"geometry");
animation.setDuration(10000);

// animation.setStartValue(QRect(0,0,120,30));
// animation.setEndValue(QRect(250,250,200,60));

animation.setKeyValueAt(0,QRect(0,0,120,30));
animation.setKeyValueAt(0.8,QRect(250,250,200,60);
animation.setKeyValueAt(1,QRect(0,0,120,30));

animation.start();

缓和曲线描述了怎样来控制0和1之间的插值速度的功能,这样能在不改变插值的情况下来控制动画的速度

 animation.setEasingCurve(QEasingCurve::OutBounce);

QEasingCurve类提供了40多种缓和曲线,而且还可以自定义缓和曲线。 

如果要实现复杂的动画,则可以通过动画组QAnimationGroup类实现,它的功能是作为其他动画类的容器,它的两个子类QSequentialAnimationGroup和QParallelAnimationGroup分别提供了串行动画组和并行动画组。

 QPropertyAnimation *animation1 = new QPropertyAnimation(&button, "geometry");
    animation1->setDuration(2000);
    animation1->setStartValue(QRect(250, 0, 120, 30));
    animation1->setEndValue(QRect(250, 300, 120, 30));
    animation1->setEasingCurve(QEasingCurve::OutBounce);
    // 按钮部件的动画2
    QPropertyAnimation *animation2 = new QPropertyAnimation(&button, "geometry");
    animation2->setDuration(1000);
    animation2->setStartValue(QRect(250, 300, 120, 30));
    animation2->setEndValue(QRect(250, 300, 200, 60));

QSequentialAnimationGroup group;// 串行动画组  
// QParallelAnimationGroup 并行动画组
group.addAnimation(animation1);
group.addAnimation(animation2);
group.start();

要对QGraphicsItem使用动画,只能对QGraphicsObject使用,因为它继承自QObject和QGraphicsItem。

animation->start(QAbstractAnimation::DeleteWhenStopped); //使用后删除

状态机 框架提供了一些类来创建和执行状态图,状态图为一个系统如何对外界激励进行反应提供了一个图形化模型,该模型是通过定义一些系统可能进入的状态以及系统怎样从一个状态切换到另一个状态来实现的。状态机框架中的状态图是分层的,状态可以嵌套在其他状态中,状态机一个有效配置中的所有状态都拥有一个共同的祖先。

QStateMachine machine;
QState *s1=new QState(&machine);
QState *s2=new QState(&machine);
QState *s3=new QState(&machine);

s1->assignProperty(&button,"geometry",QRect(100,100,120,150));
s2->assignProperty(&button,"geometry",QRect(300,100,120,50));
s3->assignProperty(&button,"geometry",QRect(200,200,120,50));

QSignalTransition *transition1=s1->addTransition(&button,&QPushButton::clicked(),s2);
QSignalTransition *transition2=s2->addTransition(&button,&QPushButton::clicked(),s3);
QSignalTransition *transition3=s3->addTransition(&button,&QPushButton::clicked(),s1);

QPropertyAnimation *animation=new QPropertyAnimation(&button,"geometry");
transition1->addAnimation(animation);
trnasition2->addAnimation(animation);
trnasition3->addAnimation(animation);

machine.setInitialState(s1);
machine.start();

状态机是异步执行的,它会成为应用程序时间循环的一部分。当状态机进入一个状态时会发射QState::entered()信号,而退出一个状态时会发射QState::exited()信号,如果想让状态机完成一个状态后就停止,那么可以设置这个状态为QFinalState对象,将它加入状态图中,等到切换到该状态时,状态机就会发射finished()信号并停止。

QFinalState *s2=new QFinalState(&machine);
s1->addTransition(&quitButton,&QPushButton::clicked,s2);
QObject::connect(&machine,QStateMachine::finished,qApp,&QApplication::quit);

当使用状态机配合动画,那么意味着进入一个状态时分配的属性将无法立即生效,而是在进入时开始播放动画,然后以平滑的动画来达到属性分配的值。当使用动画时可以考虑propertiesAssigned()信号,该信号会在属性被分配到最终的值时被发射,而无论使用了动画与否。

如果一个状态在动画结束前退出了,那么状态机的行为会依赖于切换的目标状态。如果目标状态明确的未改属性分配了一个值,就使用这个值,如果没有那么默认的会被分配切换时离开的那个状态所定义的值,但是如果设置了全局恢复策略,那么恢复策略指定的值优先。

子状态默认会继承父状态的切换,但是也可以覆盖它,一个切换的目标状态可以是任意的状态。

如果需要状态去执行一些无关工作,而完成后又可以恢复到以前的状态,可以使用历史状态QHistoryState完成。历史状态时一个伪状态,它代表了当父状态退出时所在的那个子状态,所以历史状态应创建为一个状态的子状态。

QHistoryState *s1h=new QHistoryState(s1);
QState *s3=new QState(&machine);
s3->addTransition(s1h);
s1->addTransition(&interruptButton,&QPushButton::clicked,s3);

使用并行状态可以使状态的总数线性增长而不是指数增长,而且向并行状态中添加或者移除状态都不会影响其他的兄弟状态。

 

进入一个并行状态组,也就同时进入了它的所有子状态,在独立的子状态间的切换操作可以正常进行,任何一个子状态都可以通过切换退出父状态,从而退出所有的子状态。

QState *s1=new QState(QState::ParallelStates);
QState *s11=new QState(s1);
QState *clean=new QState(s11);
QState *dirty=new QState(s11);

QState *s12=new QState(s1);
QState *moving=new QState(s12);
QState *notMoving=new QState(s12);

一个子状态可以是一个最终状态,进入了一个最终子状态时,其父状态就会发射QState::finished()信号 ,如果想隐藏一个复合状态的内部细节,那么使用复合状态的最终状态时非常有效的

一个切换也可以没有目标状态,一个没有目标状态的切换也可以像其他切换那样被触发,但它不会引起任何的状态变化,这样可以让状态机在一个特定的状态时响应信号或者事件而不用离开这个状态,但是设置目标状态为自身,那么它会退出然后重新进入

状态机在自己的事件循环中运行,对于信号切换(QSignalTransition对象),当状态机截获相应的信号时,它自动发送SignalEvent事件给它自己,还可以使用QStateMachine::postEvent()发送自定义的事件给状态机,当发送自定义事件时,还需要继承QAbstractTransition类来创建自定义的切换。

通过设置全局的恢复策略可以使状态机进入一个状态而不用明确指定属性值。当进入一个状态,该状态没有为指定的属性设置值,那么状态机就会首先查找状态层次中该状态的祖先是否定义了该属性,如果是,那么属性会被恢复为最邻近的祖先所定义的值,否则,会恢复到恢复策略指定的值,像RestoreProperties就是恢复最开始的值。

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

猜你喜欢

转载自blog.csdn.net/weixin_38893389/article/details/81369539