基于Qt的状态转换机制的研究
摘 要: 研究了跨平台类库Qt的状态转换机制。介绍了QState类和QStateMachine类,对状态机中的历史状态、复合状态、平行状态做了详解,并给出了关键代码。在软件开发中,应用Qt状态转换机制能够简化编程过程。经过实际的软件研发,结果表明,在开发交互式应用程序和动画应用程序时应用Qt状态转换机制更能提高编程效率。
关键词: Qt; QState; 状态机; 状态转换机制; 编程过程简化
中图分类号: TN919?34; TP391.9 文献标识码: A 文章编号: 1004?373X(2013)08?0121?04
0 引 言
Qt中状态机(QStateMachine)的设计来源于有限状态机的概念,主要负责程序运行过程中不同状态间切换的管理工作。有限状态机(FSM)又称为有限状态自动机或简称状态机,是表示有限个状态以及这些状态之间的转移和动作等行为的数学模型。状态存储关于过去的信息,就是说它反映从系统开始时刻到当前时刻的输入变化。转移指示状态变更,并且用满足转移发生的条件来描述它。动作是在给定时刻要进行的活动的描述。有多种类型的动作,如在进入状态时发生的进入动作;在退出状态时发生的退出动作;在特定转移时发生时的转移动作;还有输入动作,它依赖于当前状态和输入条件。
1 简 介
1.1 Qt类库
Qt是一个图形库,它实现了对X Window的封装,就如同Windows平台上MFC对Windows的封装。在Linux平台上,还有其他性质相同的图形开发工具,比较有名的有Motif, Gtk+,Openwin等。与后几种不同的是Qt还是跨平台的,它也支持Windows系列的操作系统[1]。Qt给应用程序开发者提供建立艺术级图形用户界面所需的所有功能。它完全面向对象,很容易扩展,并且允许真正的组件编程[2]。
Qt组件库有开发速度快,代码复用效率高,易于学习掌握的特点。尤其他强大的可移植性符合软件工程中软件复用技术的要求,而且其可以与Motif/Xt组件库进行混合编程,与OpenGL集成,且与C++及C都兼容[3]。
Qt于1996年进入商业领域,已成为全世界范围内数千种成功的应用程序的基础,还是Linux桌面环境KDE的基础。
Qt提取了窗口和操作系统的底层基础构造函数,为软件开发工程师提供了一致的逻辑界面,Qt API在所有支持的平台上都是相同的。事实上,这是通过对不同平台(Linux, Windows,and Mac)的专有API进行了封装,如文件处理、网络(操作,协议),进程处理、线程、数据库访问等而完成的。所以Qt应用的开发和部署与平台无关,同一套源代码,通过Qt编译可以在所有支持的平台上进行本地化运行[4]。
1.2 Qt信号和槽的机制
Qt提供了信号和槽机制来完成界面操作的响应,这一机制是完成任意2个Qt对象之间的通信机制。信号是一个特定的标识;槽是一个函数,与一般的函数不同,槽函数既能够和信号关联,也能够像普通函数一样直接调用[5]。每个Qt对象都包含若干个预定义的信号和若干个预定义的槽,当某一个特定事件发生时,一个信号被发射,与信号相关联的槽则会响应信号并完成相应的处理。当一个类被继承时,该类的信号和槽也同时被继承,也可以根据需要自定义信号和槽。
信号和槽机制是类型安全的[6],需要关联的信号和槽的签名必须是等同的,即信号的参数类型和参数个数同接收该信号的槽的参数类型和参数个数相同。不过,一个槽的参数个数是可以少于信号的参数个数的,但缺少的参数必须是信号参数的最后一个或几个参数。如果信号和槽的签名不符,编译器就会报错。
信号和槽机制是松散耦合的,减弱了Qt对象的耦合度。激发信号的Qt对象无需知道是哪个对象的哪个槽需要接收它发出的信号,它只需做的是在适当的时间发送适当的信号就可以了,而不需要知道也不关心它的信号有没有被接收到,更不需要知道是哪个对象的哪个槽接收到了信号;同样的,对象的槽也不知道是哪些信号关联到了自己,而一旦关联信号和槽,Qt就保证了适合的槽得到了调用。即使关联的对象在运行时被删除,应用程序也不会出现崩溃。
信号和槽机制增强了对象间通信的灵活性,然而这也损失了一些性能。同回调函数相比较,信号和槽机制有些慢。通常,通过传递一个信号来调用槽函数将比直接调用非虚函数慢10倍。
原因主要有:需要定位接收信号的对象;安全地遍历所有的关联(例如,一个信号关联到多个槽的情况);编组/解组传递的参数;多线程的时候,信号可能需要排队等待。
然而,与创建堆对象的new操作以及删除堆对象的delete操作相比较,信号和槽的代价只是它们很少的一部分。信号和槽机制导致的这点性能损耗,对实时应用程序是可以忽略的;同信号和槽提供的灵活性和简便性相比,这点性能的损失也是值得的[7]。
2 状态转换机制简介
Qt的状态机由若干状态、输入和转换函数组成。设计状态机时要描述清楚状态机的要素,即如何进行状态转移,每个状态的输出是什么,状态转移的条件等。且要求状态机不能进入死循环和非预知的状态[8]。
2.1 QState类
QState类为QStateMachine类提供状态。一个QState对象可以有子状态,也可以有转向其他状态的转换。状态的ChildMode属性用来设置子状态之间的关系。属性值有ExclusiveStates和ParallelStates,若为ExclusiveStates,表示子状态之间是互斥的,必须调用setInitialState()函数设置初始状态,当转换的目标是父状态时,状态机还需知道要转换到哪个子状态中。若值为ParallelStates,各个子状态之间是平行的关系,当父状态进入到一个状态,所有的子状态都进入到这一状态,当进入到一个最终子状态时,状态发出finished()信号[9]。
一个状态机的状态图如图1所示,s1,s2和s3分别表示状态机的3个状态,黑点指示的状态s1为初始状态,当接收到button.clicked信号,即用户点击按钮时,状态发生转换,并在这3个状态中循环切换。
2.2 QStateMachine类
QStateMachine类建立在状态图概念的基础上,提供了一个分层的有限状态机。状态机体系的整体继承关系主要分为3部分,分别是:
(1)负责存储状态的QAbstractState接口;
(2)负责对信号进行处理的QAbstractTransition接口;
(3)为状态机类提供信号事件的QEvent接口。
状态机管理一组继承自QAbstractState类的状态和继承自QAbstractTransition类的转换,这些状态和转换确定一个状态图。状态图建立后,状态机即可执行它。
为状态机添加状态使用addState()函数,移除状态使用removeState()函数。重载onEntry()和onExit()函数可以使状态机在进入或退出某状态时进行指定的操作。
2.3 转换(Transition)
由一个状态到另一个状态变更的这一动作称为转换,每个状态都包含一个转换的集合。转换定义了如何对事件(Events)进行响应,Qt的信号和事件都可以触发转换。
转换本身也可以包含动作,在转换的过程中,状态机能够执行一些动作(Action)。例如,当进入某个状态时,状态机执行onentry 动作;当退出某个状态时,执行onexit动作。假设当状态机通过一个名为T的转换从状态S1转换到S2时,它先执行S1的 onexit动作,然后执行T本身所包含的动作,最后执行S2 的onentry动作。
addTransition()函数用来添加转换,添加的转换有3种情况。可以转换到另一个转换;可以是一个对象等待到一个信号后转换到目标状态;也可以无条件转到目标状态。removeTransition()函数用来移除一个转换。assignProperty()函数用来设置状态的属性。addDefaultAnimation()函数可以添加转换时的动态效果。postEvent() 函数可以为状态机设置优先级。
在任何时刻,状态机总处于一个状态中,这个状态被称为主动态。当某一事件发生时,状态机将检查主动态包含的所有转换,如果发现有一个和该事件匹配,状态机将从当前的主动态转移到该转换指定的目标态,即完成状态的转换;若找不到和该事件匹配的转换,则状态不发生变化。
3 状态转换机的多种状态
3.1 历史状态(History States)
历史状态是一个虚拟状态,同时也是一个子状态,它的父状态是最后一次退出时的那个父状态。历史状态用来记录当前状态,当状态机发现运行中有历史状态出现时,则当退出父状态时它会自动地记录当前子状态。一个转向历史状态的转换实际上是一个转向状态机之前存储的子状态的转换,状态机自动地转向该子状态。在中断机制中应用历史状态可以使状态转换过程变得非常简单。图2所示的状态机通过添加历史状态,使状态机在中断事件完成后仍然返回到发生中断之前所处的那个子状态。
关键代码如下:
QHistoryState *s1h = new QHistoryState(s1);
QState *s3 = new QState();
s1?>addTransition(interruptButton, SIGNAL(clicked()), s3);
s3?>addTransition(s1h);
首先添加一个以s1状态为父状态的QHistoryState类的子状态s1h和一个顶层状态s3,再添加一个中断按钮,然后添加由s1向s3的转换,点击中断按钮则发生转换,最后添加一个由s3向s1h的转换。在 s1状态的任何子状态均可发生中断,发生中断后则将当前子状态记录在s1h历史状态中,并进入到s3状态,然后从s3状态返回到s1h状态,即中断前所处的那个子状态。
3.2 复合状态(Compound States)
一个状态可以嵌套其他的状态,这样的状态称为复合状态。嵌套其他状态的状态称为父状态(parent state),而被嵌套的则称为子状态(child state)。子状态又可以嵌套其他的状态,直到任意深度。不包含任何子状态的状态,称为原子状态(atomic state)。
当一个子状态处于主动态(acitve)时,它的父状态也必须处于主动态。这样一来,在任何一点我们都将拥有一个包含原子状态和它所有祖先的主动态的集合。
由于复合状态的存在,转换(transition)不再是从一个原子状态转到另一个原子状态,而是从一个主动态集合转到另一个主动态集合的转换。如果状态转换的目标是一个原子状态,那么状态机将不仅进入到该原子状态,而且还将进入到它所有的处于活动状态的祖先状态中。如果转换的目标是一个复合状态,那么复合状态的子状态必须也被激活,由于转换并没有指定哪一个子状态,这时需要激活该复合状态的初始子状态。如果该状态依然是复合状态,则递归下去,直到原子状态为止。
复合状态会影响到转换的选择。当事件发生时,状态机从最深层嵌套的原子状态开始查找,如果未找到匹配的转换,则查找其父状态的转换,依次递归。如果状态机中所有转换均不匹配,事件被丢弃。
应用复合状态建立一个状态组,则可以通过为父状态应用转换使整个组的所有子状态都共享该转换,简化了为每个子状态都应用转换这一过程。
最终状态(Final State)和历史状态(History State)可以作为一个复合状态的子状态。
3.3 平行状态(Parallel States)
在状态机体系中,平行状态采用交叉存取方法,所有的平行操作以单一的、原子的事件处理方式执行,因此没有事件可以中断平行状态的操作。不过,事件仍然可以被顺序执行,因为状态机本身是单线程的。假设有两个转换,都是退出同一个平行状态组,使它们发生的条件同时为真。这时,最后被处理的事件不会生效,因为第一个事件已经使状态机从平行状态组退出了。
如果在状态机中所有状态都是互斥的,那么这些状态将拥有多种组合和转换。特别的,使用平行状态时,当增加一个状态后,组合状态和转换的数量将会以线性方式增加,避免了指数级的剧增方式。而且增加或删除状态也不会影响到其他状态。
建立平行状态的代码如下:
QState *s1 = new QState (QState::ParallelStates);
QState *s11 = new QState(s1);
QState *s12 = new QState(s1);
ParallelStates属性表明建立的s1是一个平行状态组,s1的子状态s11和s12是平行关系。 当进入到一个平行状态组时,也就同时进入了它所有的子状态。单个子状态转换正常,且可以应用一个退出父状态的转换。若退出父状态的转换发生,则同时退出父状态和它所有的子状态。
平行状态与复合状态差别很大,当复合状态处于主动态时,有且只有一个子状态处于主动态;而当平行状态处于主动态时,所有的子状态都必须处于主动态。
3.4 错误状态(Error States)
如果遇到错误,状态机不会因为进入错误状态而停止运行,而会寻找错误状态,一旦找到,即进入这个状态,并可通过error()函数得到错误类型,Error枚举类型给出可能的错误类型。如果处在正确的状态申请错误状态类型,状态机则停止执行,并将错误信息输出至控制台[10]。
4 状态转换机的启动与终止
使用start()函数启动状态机。状态机启动前,必须设置初始状态,即启动状态机时状态机进入的那个状态。状态机进入到初始状态后,即发射started()信号。
状态机是事件驱动的,它保持着自己的事件循环,事件通过postEvent()函数传给状态机,并异步执行,如果没有一个运行中的事件循环它将不再向前推进。不必像转换函数一样直接把事件传给状态机,比如QEventTransition类和它的子类会处理。但是对由事件触发的自定义的转换来说,postEvent()是非常有用的。
状态机不断地处理事件和转换,直到进入到顶层的最终状态,此时状态机发射finished()信号。也可以使用stop()函数终止状态机,此时状态机发射stopped()信号。
5 结 语
Qt作为一个通用的跨平台C++类库,其程序代码无需修改或做较少改动便能够在多种不同的操作系统下顺利编译并生成相对应的软件,为跨平台软件的开发提供了极佳的工作平台。
应用Qt状态机事先设定若干状态和状态的转换方式,使用户在进行了特定的操作后即转换到指定的状态,并执行指定的动作,从而实现按流程操作的功能。交互式应用程序中,用户的一些鼠标操作需按步骤进行,研发表明,使用Qt状态机能够使编程更为简便和高效。在开发动画应用程序时,应用Qt状态机也能够发挥很大的作用。
参考文献
[1] 王爱文.Linux平台下基于Qt的电子海图的研究与实现[D].哈尔滨:哈尔滨工程大学,2004.
[2] 邓飞.基于Qt的地震资料采集质量监控及评价系统的开发与研究[D].成都:成都理工大学,2004.
[3] 许德新,谈振藩,高延滨.基于Qt组件库应用程序的生成及其跨平台实现[J].东北农业大学学报,2006,37(3):373?376.
[4] 杨少鹏.SXD/Linux通信编码仿真平台的设计与实现[D].成都:西南交通大学,2005.
[5] 蔡志明.精通Qt4编程[M].2版.北京:电子工业出版社,2011.
[6] 李艳民.基于Qt跨平台的人机交互界面的研究和应用[D].重庆:重庆大学,2007.
[7] 郑阿奇.Qt4开发实践[M].北京:电子工业出版社,2011.
[8] 聂旭中.状态机设计研究[J].洛阳师范学院学报,2009(3):62?65.
[9] 齐亮.C++ GUI Qt 3编程[M].北京:北京航空航天大学出版社,2006.