状态机的数据模型,决定状态机可以配置什么样的数据,可以使用哪些与数据有关的表达式和元素。
数据模型提供了状态机存储、读取和修改内部数据集的能力。特定的数据模型定义了一组表达式,表达式用于引用到数据模型的位置、计算值或给数据模型赋值,也可产生布尔条件。另外,数据集也包含了一组系统变量,这些系统变量由处理器自动持有。
<datamodel>
<data id="mydata"/>
datamodel>
标准附录描述了二种数据模型:
空数据模型
脚本数据模型
Qt定义了 一种数据模型:
空数据模型只支持in(id)
条件判断,确定是否处理某种状态,其他操作都是无效的,包括系统变量也是无法访问的.
ecmascript数据模型是在状态里按ecmascript的方式定义数据元素,并使用ecmascript脚本实现scritp\cond\expr等功能.
这种数据模型是直接使用自定义的一个c++类,并在状态机初始化时使用外部的对象。在状态机中脚本或表达式里就可以直接写c++代码来直接访问c++对象。c++数据模型只能使用预编译的模式,不能动态加载,这里因为只有对scxml文件预编译才能把scxml文件中的c++代码转换为实际源文件中的生成c++代码。
看一下mediaplayer-qml-cppdatamodel示例。
在状态机中声明c++数据类型,指定类名和声明文件
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" binding="early" name="MediaPlayerStateMachine" datamodel="cplusplus:TheDataModel:thedatamodel.h" qt:editorversion="4.8.2" xmlns:qt="http://www.qt.io/2015/02/scxml-ext" initial="stopped">
其中datamodel="cplusplus:TheDataModel:thedatamodel.h"
是数据类型的定义部分。
首先必须自定义一个类,这个类从QScxmlCppDataModel
继承,并加上Q_OBJECT和一个特殊的宏Q_SCXML_DATAMODEL
,并实现自己的其他自定义部分。
文件thedatamodel.h
:
#ifndef THEDATAMODEL_H
#define THEDATAMODEL_H
#include "qscxmlcppdatamodel.h"
class TheDataModel: public QScxmlCppDataModel
{
Q_OBJECT
Q_SCXML_DATAMODEL
private:
bool isValidMedia() const;
QVariantMap eventData() const;
QString media;
};
#endif // THEDATAMODEL_H
Q_SCXML_DATAMODEL
声明了几个公开函数:
#define Q_SCXML_DATAMODEL \
public: \
QString evaluateToString(QScxmlExecutableContent::EvaluatorId id, bool *ok) Q_DECL_OVERRIDE Q_DECL_FINAL; \
bool evaluateToBool(QScxmlExecutableContent::EvaluatorId id, bool *ok) Q_DECL_OVERRIDE Q_DECL_FINAL; \
QVariant evaluateToVariant(QScxmlExecutableContent::EvaluatorId id, bool *ok) Q_DECL_OVERRIDE Q_DECL_FINAL; \
void evaluateToVoid(QScxmlExecutableContent::EvaluatorId id, bool *ok) Q_DECL_OVERRIDE Q_DECL_FINAL; \
private:
这几个公开函数由scxmlc编译去实现,它是通过为scxml文件中script、cond、 expr等中定义的c++代码生成相应的逻辑代码,这些函数在编译生成的状态机的cpp文件中。这些函数会由状态机在执行到对应的位置时自动调用。
脚本script中的c++代码,注意引号被转义:
<onentry>
<script>media = eventData().value(QStringLiteral("media")).toString();script>
<send event="playbackStarted">
<param name="media" expr="media"/>
send>
onentry>
上述脚本将会转换为一段c++代码
void TheDataModel::evaluateToVoid(QScxmlExecutableContent::EvaluatorId id, bool *ok)
{
*ok = true;
switch (id) {
case 1:
[this]()->void{ media = eventData().value(QStringLiteral("media")).toString(); }();
return;
default: break;
}
Q_UNREACHABLE();
*ok = false;
}
注意,脚本中的代码被放进了一个lamada表达式中,由于没有返回值,因此被放到了evaluateToVoid函数中,这段脚本的EvaluatorId被scxmlc编译成了1。
条件cond中的c++代码,引号同样被转义:
<transition type="internal" event="tap" cond="!isValidMedia() || media == eventData().value(QStringLiteral("media"))" target="stopped"/>
<transition type="internal" event="tap" cond="isValidMedia() && media != eventData().value(QStringLiteral("media"))" target="playing"/>
这里因为是条件表达式,求值方法为evaluateToBool,这些表达式代码放在了这个函数里,并且表达式代码同样被放进了lamada表达式。这两个表达式的求值器id被编译成了4和5。
bool TheDataModel::evaluateToBool(QScxmlExecutableContent::EvaluatorId id, bool *ok)
{
*ok = true;
switch (id) {
case 4:
return [this]()->bool{ return !isValidMedia() || media == eventData().value(QStringLiteral("media")); }();
case 5:
return [this]()->bool{ return isValidMedia() && media != eventData().value(QStringLiteral("media")); }();
case 0:
return [this]()->bool{ return isValidMedia(); }();
default: break;
}
Q_UNREACHABLE();
*ok = false;
return false;
}
脚本代码或条件表达式为什么要放进lamada表达式呢?布尔表达式,应该是可以不需要的。大概是为了实现方便,统一这样处理,因为script元素里的脚本代码可能是一条或多条代码,并不一定是个表达式,所以就封装进一个lamada表达式,实际上也不一定要这样封装,用一个大括号也行吧?哪位知道一定要这么做的理由?
而非条件表达式被转换为函数evaluateToVariant
scxml中的表达式:
<onentry>
<script>media = eventData().value(QStringLiteral("media")).toString();script>
<send event="playbackStarted">
<param name="media" expr="media"/>
send>
onentry>
<onexit>
<send event="playbackStopped">
<param name="media" expr="media"/>
send>
onexit>
scxmlc生成的c++代码:
QVariant TheDataModel::evaluateToVariant(QScxmlExecutableContent::EvaluatorId id, bool *ok)
{
*ok = true;
switch (id) {
case 2:
return [this]()->QVariant{ return media; }();
case 3:
return [this]()->QVariant{ return media; }();
default: break;
}
Q_UNREACHABLE();
*ok = false;
return QVariant();
}
scxml中出现两次expr="media"
,scxmlc为它们各自生成了一个id, 2和3。所有的代码块,包括script、cond和expr中定义的都被统一生成了一个唯一的EvaluatorId。
c++数据模型是scxml的新特性,取代了先前的registerObject方案。由于这种数据模型是通过编译生成c++代码,因此具有最高的效率,因为可以随意使用类成员变量及函数,因此具备最完备与外部系统的交互能力。通过QScxmlStateMachine
的setDataModel
方法将外部创建的数据对象置入到状态机中,只能置入一次,一旦置入就不可再修改。c++数据模型先天编译型,因此无法用在动态加载的情况,也就是运行时加载scxml文件的情况。