Qt IVI 生成器教程
逐步演示如何基于 QML 应用程序生成中间件 API。
本教程演示了您如何使用自己自动生成的中间件 API 扩展 QML 应用程序。我们使用现有的 QML 工具集群应用程序,并按以下步骤进行
- 集成不带后端的基本接口
- 扩展接口并添加注释
- 添加模拟后端和相应的模拟注释;使用 QML 插件
- 添加自定义模拟行为
- 添加模拟服务器并从 Qt Remote Objects 后端使用它
- 开发一个连接到 DBus 接口的生产后端
在我们开始实际的中间件集成之前,让我们先看看现有的仪器集群 QML 代码及其支持的所有功能
images
-- 此文件夹包含 QML 代码中使用的所有图像。Cluster.qml
-- 将其他所有 QML 组件组合在一起的主要 QML 文件。Dial.qml
-- 显示速度或每分钟转数 (RPM) 等值的基组件,使用指针。Fuel.qml
-- 显示实际油位的组件。Label.qml
-- 一个小型辅助组件,用于设置用于显示文本的所有常用设置。LeftDial.qml
-- 使用 Dial 组件和文本以及当前英里每小时 (mph) 或千米每小时 (km/h) 的当前度量值显示当前速度。RightDial.qml
-- 显示当前 RPM 并提供显示警告指示符的方式。Top.qml
-- 展示当前日期和当前温度的顶部栏。
接下来,我们使用我们的中间件 API 来添加以下功能的支持
- 在左侧仪表中显示当前速度。
- 在右侧仪表中显示当前 RPM。
- 在不同度量值之间切换。
- 在顶部栏中显示当前温度。
- 在右侧仪表上显示不同的警告。
- 指示仪器集群是否连接并显示真实数据。
最终目标是将这些功能连接起来,模拟类似于这样的实时驾驶体验
第1章:使用IVI生成器的基本中间件API
在本章中,我们将中间件API集成到现有的仪表盘QML代码中。我们不再手动编写所有这些部分,这在大多数基本的QML示例中是必须的,我们将使用IVI生成器来自动生成所需的部分。
接口定义语言
为了能够自动生成中间件API,IVI生成器需要对要生成的内容进行一些输入。这种输入是通过接口定义语言(IDL),QFace来提供的,它以一种非常简单的方式描述API。
让我们开始定义一个非常简单的接口,该接口提供速度属性
module Example.IVI.InstrumentCluster 1.0 interface InstrumentCluster { int speed; }
首先,我们需要定义我们要描述的模块。模块充当命名空间,因为IDL文件可以包含多个接口。
module Example.IVI.InstrumentCluster 1.0
模块最重要的部分是它的接口定义。
interface InstrumentCluster { int speed; }
在这种情况下,我们定义了一个名为InstrumentCluster
的接口,它由一个属性组成。每个属性定义必须至少包含一个类型和一个名称。大多数基本类型都是内置的,可以在QFace IDL语法中找到。
自动生成
现在我们的IDL文件的第一版已经准备好了,是时候使用IVI生成器工具从这个文件中自动生成API了。类似于moc,这个自动生成过程集成到qmake构建系统中,并在编译时完成。
在下述.pro
文件中,我们基于我们的IDL文件构建一个C++库
TARGET = $$qtLibraryTarget(QtIviInstrumentCluster) TEMPLATE = lib DESTDIR = .. QT += ivicore ivicore-private qml quick DEFINES += QT_BUILD_EXAMPLE_IVI_INSTRUMENTCLUSTER_LIB CONFIG += ivigenerator QFACE_SOURCES = ../instrument-cluster.qface
大多数的.pro
文件是定义C++库的标准配置,使用"lib" TEMPLATE
,并在TARGET
变量中定义所需的文件名。我们使用的qtLibraryTarget
函数可以帮助正确地追加文件名上的"CREATE"后缀,为提供调试信息的库。在未来,我们需要链接到这个文件,所以我们将DESTDIR
设置在上一个目录以简化这个。
注意:Windows会自动在同一个目录中搜索库。
激活IVI生成器集成需要指定CONFIG
变量来指定ivigenerator
选项。这确保在构建过程中调用IVI生成器,使用我们在QFACE_SOURCES
中指定的QFace文件。有关更多信息,请参阅qmake集成。
为了确保我们构建的库在Windows上可以工作,重要的事情是向DEFINES
变量中添加QT_BUILD_EXAMPLE_IVI_INSTRUMENTCLUSTER_LIB
。这样,在构建库时,所有符号都将导出,但在链接时则导入。有关更多信息,请参阅创建共享库。
自动生成哪些文件
IVI生成器基于生成模板操作。这些模板定义了从QFace文件生成什么内容。如果没有定义QFACE_FORMAT
,这会自动默认为"frontend"模板。有关这些模板的更多详细信息,请参阅使用生成器。
简而言之,"frontend"模板会生成
- 一个从QIviAbstractFeature派生的C++类,每个类都在QFace文件中的一个接口
- 一个模块类,该类有助于将所有接口注册到QML中,并存储全局类型和函数。
要自行检查C++代码,您可以在您的库构建文件夹中查看这些文件。
目前,对我们来说最重要的自动生成的文件是定义的接口生成的C++类。它看起来像这样
/**************************************************************************** ** Generated from 'Example.IVI.InstrumentCluster.qface' ** ** Created by: The QFace generator (QtAS 5.15.1) ** ** WARNING! All changes made in this file will be lost! *****************************************************************************/ #ifndef INSTRUMENTCLUSTER_INSTRUMENTCLUSTER_H_ #define INSTRUMENTCLUSTER_INSTRUMENTCLUSTER_H_ #include "instrumentclustermodule.h" #include <QtIviCore/QIviAbstractFeature> #include <QtIviCore/QIviPendingReply> #include <QtIviCore/QIviPagingModel> class InstrumentClusterPrivate; class InstrumentClusterBackendInterface; class Q_EXAMPLE_IVI_INSTRUMENTCLUSTER_EXPORT InstrumentCluster : public QIviAbstractFeature { Q_OBJECT Q_PROPERTY(int speed READ speed WRITE setSpeed NOTIFY speedChanged) public: explicit InstrumentCluster(QObject *parent = nullptr); ~InstrumentCluster(); static void registerQmlTypes(const QString& uri, int majorVersion=1, int minorVersion=0); int speed() const; public Q_SLOTS: void setSpeed(int speed); Q_SIGNALS: void speedChanged(int speed); protected: InstrumentClusterBackendInterface *instrumentclusterBackend() const; void connectToServiceObject(QIviServiceObject *service) Q_DECL_OVERRIDE; void clearServiceObject() Q_DECL_OVERRIDE; private: Q_PRIVATE_SLOT(d_func(), void onSpeedChanged(int speed)) Q_DECLARE_PRIVATE(InstrumentCluster) }; #endif // INSTRUMENTCLUSTER_INSTRUMENTCLUSTER_H_
如您所见,自动生成的C++类实现了一个speed
属性,这是我们之前在QFace文件中定义的。通过使用Q_OBJECT
和Q_PROPERTY
宏,该类现在可以直接在您的QML代码中使用。
将前端库与QML代码集成
对于此集成,我们使用从QML代码生成的自动生成的 batches 前端库。为了简单起见,我们遵循标准的Qt示例模式,使用一个小的C++主函数来注册我们的自动生成的类型到QML,并将仪表盘QML代码加载到QQmlApplicationEngine
#include "instrumentclustermodule.h" int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); QQmlApplicationEngine engine; InstrumentClusterModule::registerQmlTypes(); engine.load(QUrl(QStringLiteral("qrc:///Cluster.qml"))); return app.exec(); }
现在我们需要的只是将仪表盘QML元素的实际集成以及连接到speed
属性到leftDial
。这是通过首先用instrumentCluster
ID实例化元素来完成的。
import QtQuick 2.1 import QtQuick.Window 2.2 import Example.IVI.InstrumentCluster 1.0 Window { id: root width: 1920 height: 720 title: qsTr("QtIVI Instrument Cluster Chapter 1") visible: true color: "#0c0c0c" InstrumentCluster { id: instrumentCluster }
最后,我们可以创建一个绑定,将LeftDial
项目value
属性绑定到我们的仪表盘API的speed
属性。
LeftDial { id: leftDial anchors.left: parent.left anchors.leftMargin: 0.1 * width value: instrumentCluster.speed }
第2章:扩展接口并添加注释
在本章中,我们通过枚举和定义我们自己的结构来添加更多属性,以扩展我们的中间件API。
定义只读属性速度
module Example.IVI.InstrumentCluster 1.0 interface InstrumentCluster { int speed; }
由于我们没有使用任何额外的指定项,所以该属性被定义为可读和可写的。然而,在我们的仪表盘示例中没有必要有一个可写的speed
属性,因为它不被用于加速汽车,只是用来可视化当前状态。
要定义属性为只读,使用readonly
关键字。
module Example.IVI.InstrumentCluster 1.0 interface InstrumentCluster { readonly int speed; }
当我们再次构建我们的应用程序时,构建系统会识别这个更改并运行IVI生成器以生成更新的C++代码版本。在IVI生成器完成后,打开构建文件夹中的instrumentcluster.h
,您会注意到生成的speed
属性发生了变化--它不再有设置器,现在是只读的。
class Q_EXAMPLE_IVI_INSTRUMENTCLUSTER_EXPORT InstrumentCluster : public QIviAbstractFeature { Q_OBJECT Q_PROPERTY(int speed READ speed NOTIFY speedChanged) ... };
扩展接口
为了达到为仪表盘提供完整模拟的目标,我们还需要向我们的QFace文件添加更多属性:rpm
、fuel
和temperature
module Example.IVI.InstrumentCluster 1.0 interface InstrumentCluster { readonly int speed; readonly int rpm; readonly real fuel; readonly real temperature; }
您可能已经注意到,我们对fuel
和temperature
属性使用了不同的类型。我们在这里使用real
,因为我们想将温度显示为一个浮点数,并将当前的燃油水平显示为一个介于0到1之间的值。
定义一个新的枚举类型
一个很有用的特性是能够在公制和英制系统之间切换,因此我们需要定义一个表示我们当前使用的系统的属性。使用布尔属性是可行的,但它不提供良好的API,因此在QFace文件中定义一个新枚举类型,并将其用作新system
属性的类型
module Example.IVI.InstrumentCluster 1.0 interface InstrumentCluster { readonly int speed; readonly int rpm; readonly real fuel; readonly real temperature; readonly SystemType systemType; } enum SystemType { Imperial, Metric }
在自动生成的代码中,这会 result in an enum,它是模块类的一部分,使相同的枚举能够在同一模块的多个类中使用
/**************************************************************************** ** Generated from 'Example.IVI.InstrumentCluster.qface' ** ** Created by: The QFace generator (QtAS 5.15.1) ** ** WARNING! All changes made in this file will be lost! *****************************************************************************/ #ifndef INSTRUMENTCLUSTERMODULE_H_ #define INSTRUMENTCLUSTERMODULE_H_ #include "instrumentclusterglobal.h" #include <QObject> class Q_EXAMPLE_IVI_INSTRUMENTCLUSTER_EXPORT InstrumentClusterModule : public QObject { Q_OBJECT public: InstrumentClusterModule(QObject *parent=nullptr); enum SystemType { Imperial = 0, Metric = 1, }; Q_ENUM(SystemType) static SystemType toSystemType(quint32 v, bool *ok); static void registerTypes(); static void registerQmlTypes(const QString& uri = QStringLiteral("Example.IVI.InstrumentCluster"), int majorVersion = 1, int minorVersion = 0); }; Q_EXAMPLE_IVI_INSTRUMENTCLUSTER_EXPORT QDataStream &operator<<(QDataStream &out, InstrumentClusterModule::SystemType var); Q_EXAMPLE_IVI_INSTRUMENTCLUSTER_EXPORT QDataStream &operator>>(QDataStream &in, InstrumentClusterModule::SystemType &var); #endif // INSTRUMENTCLUSTERMODULE_H_
添加新结构
要在仪表盘右侧仪表上显示警告,我们希望使用一个存储颜色、图标和文本的警告结构,而不是使用三个独立的属性。类似于定义接口,我们可以在QFace文件中使用struct
关键字
struct Warning { string color string text string icon }
将这个新结构用作属性的类型,与使用枚举的方式相同。现在的QFace文件应该看起来像这样
module Example.IVI.InstrumentCluster 1.0 interface InstrumentCluster { readonly int speed; readonly int rpm; readonly real fuel; readonly real temperature; readonly SystemType systemType; readonly Warning currentWarning; } enum SystemType { Imperial, Metric } struct Warning { string color string text string icon }
集成新属性
与前一章类似,实际上集成新属性需要创建绑定。可以将rpm
属性直接连接到rightDial
项的value
属性;对顶部项的temperature
属性也执行相同的操作。要控制左侧仪表显示的单元,leftDial
项提供了一个名为metricSystem
的布尔属性。因为我们已在我们的QFace文件中使用了枚举,所以需要首先通过测试"Metric"值对systemType
属性进行值转换。
LeftDial { id: leftDial anchors.left: parent.left anchors.leftMargin: 0.1 * width value: instrumentCluster.speed metricSystem: instrumentCluster.systemType === InstrumentCluster.Metric }
这些枚举是模块类的一部分,也作为InstrumentClusterModule
导出到QML中。要触发rightDial
项中的警告,我们使用3个绑定连接到结构中的3个成员变量
RightDial { id: rightDial anchors.right: parent.right anchors.rightMargin: 0.1 * width value: instrumentCluster.rpm warningColor: instrumentCluster.currentWarning.color warningText: instrumentCluster.currentWarning.text warningIcon: instrumentCluster.currentWarning.icon fuelLevel: instrumentCluster.fuel }
第3章:使用QML插件添加仿真后端和注释
在前两章中,我们使用QFace文件编写了一个中间件API,并使用IVI Generator自动生成库形式的C++ API。现在,在本章中,我们进一步扩展了这一点,通过引入仿真后端和使用注释为我们的仿真定义默认值。
前端和后端的分离
QtIvi和IVI Generator都允许您编写将前端与后端分离的代码——将API与其实际实现分开。Qt已经在很多领域使用了这个概念,最显著的是在底层窗口系统技术上,例如Linux上的XCB和在macOS上的Cocoa。
我们的中间件API也进行了相同的分离,其中前端将API作为库提供;后端提供此API的实现。这个实现基于QtIvi的动态后端系统,这使我们能够在运行时切换这些后端。
添加仿真后端
对于我们仪表盘,我们希望能够添加这样的后端来提供实际值。目前,我们只想有一些模拟行为,因为我们无法轻易将其连接到真实车辆。这就是为什么这种后端被称为“仿真后端”。要添加此类后端,我们再次使用IVI Generator为我们做繁重的工作并生成一个。这项工作与使用“前端”模板生成库的方式类似。但现在,我们使用"backend_simulator"模板
TEMPLATE = lib TARGET = $$qtLibraryTarget(instrumentcluster_simulation) QT += core ivicore CONFIG += ivigenerator plugin QFACE_FORMAT = backend_simulator QFACE_SOURCES = ../instrument-cluster.qface PLUGIN_TYPE = qtivi # Additional import path used to resolve QML modules in Qt Creator's code model QML_IMPORT_PATH = $$OUT_PWD/../frontend/qml
就像前端库一样,项目文件构建了一个lib
,并使用qtLibraryTarget
定义库的名称,以支持Windows调试后缀。这里的一个重要方面是库的名称以"_simulation"结尾,这是一种告诉QtIvi这是仿真后端的方法。当有“生产”后端可用时,它将优先于“仿真”后端。有关更多信息,请参阅动态后端系统。
启用IVI生成器的方式与之前相同:通过使用相同的QFACE_SOURCE
变量,将QFACE_FORMAT
定义为"backend_simulator",以使用正确的生成模板。此外,我们还需要向CONFIG
变量中添加'plugin',使这个库成为Qt插件,便于在运行时轻松加载。
链接设置和查找插件
尝试直接构建项目文件,现在会导致编译和链接错误。这是因为:为了实现前端和后端的分离,我们需要后端实现一个定义好的接口类,该类为前端所熟知。这个接口被称为“后端接口”,它是前端库的一部分自动生成的。由于这个类提供了信号和槽,并使用QObject作为其基类,您需要当从它继承时链接到前端库。因为这对于后端插件是必需的,所以我们需要在下面添加以下行
LIBS += -L$$OUT_PWD/../ -l$$qtLibraryTarget(QtIviInstrumentCluster) INCLUDEPATH += $$OUT_PWD/../frontend
现在项目应该可以顺利构建,并在您的构建文件夹中创建插件;如果您不使用影子构建,则是插件文件夹。当您再次启动仪表盘时,您应该看到以下消息
There is no production backend implementing "Example.IVI.InstrumentCluster.InstrumentCluster" . There is no simulation backend implementing "Example.IVI.InstrumentCluster.InstrumentCluster" . No suitable ServiceObject found.
此消息表明QtIvi仍然无法找到我们刚刚创建的模拟插件。在此,您需要了解一些Qt插件系统,尤其是它如何查找插件。
Qt在多个目录中查找其插件,第一个是插件文件夹plugins
,这就是您Qt安装的一部分。在插件文件夹内,每个插件类型都有自己的子文件夹,比如用于与底层平台API和窗口系统通信的平台插件platforms
。
类似地,QtIvi在qtivi
文件夹中查找其后端插件。为了保证我们的模拟后端最终落入这样的文件夹中,我们添加以下DESTDIR
定义。
DESTDIR = ../qtivi
您可能会想,在上层目录中创建一个qtivi
文件夹是如何解决查找插件问题的,因为它不是系统插件文件夹的一部分。但Qt支持在多个文件夹中搜索这样的插件,其中一个文件夹就是可执行文件位置的路径。
或者,我们可以使用QCoreApplication::addLibraryPath()函数或使用环境变量QT_PLUGIN_PATH
来添加额外的插件路径。更多信息,请参见如何创建Qt插件。
现在一切就绪,但由于我们的插件链接到前端库,我们需要确保动态链接器可以找到该库。这可以通过将LD_LIBRARY_PATH
环境变量设置为我们库文件夹来实现。但这会造成一个问题,即每个用户都需要设置这个变量才能使用我们的应用程序。为了简化用户操作,我们更愿意使用一个相对RPATH并注明连接器可能会找到所需库的信息,相对于插件的位置。
INCLUDEPATH += $$OUT_PWD/../frontend QMAKE_RPATHDIR += $$QMAKE_REL_RPATH_BASE/../
在QML插件中导出QML类型
在第一章中,我们扩展了我们的main.cpp
以注册所有自动生成的中间件API的类型。虽然这样做可以正常工作,但在更大的项目中,通常会使用QML插件,并能够使用qmlscene进行开发。尽管完成此操作的代码并不复杂,但IVI生成器也支持这一点,并使其更加简单。
从第一章开始,我们就知道模块名用于QML导入URI。对于QML插件而言,这是非常重要的,因为QmlEngine期望插件在一个特定的文件夹中,该文件夹遵循模块名,每个模块名的部分都是一个子文件夹。我们的项目文件用于生成一个QML插件,看起来是这样的:
TEMPLATE = lib CONFIG += plugin QT += ivicore LIBS += -L$$OUT_PWD/../ -l$$qtLibraryTarget(QtIviInstrumentCluster) INCLUDEPATH += $$OUT_PWD/../frontend QFACE_FORMAT = qmlplugin QFACE_SOURCES = ../instrument-cluster.qface load(ivigenerator) DESTDIR = $$OUT_PWD/$$replace(URI, \\., /) QMAKE_RPATHDIR += $$QMAKE_REL_RPATH_BASE/../../../../ exists($$OUT_PWD/qmldir) { cpqmldir.files = $$OUT_PWD/qmldir \ $$OUT_PWD/plugins.qmltypes cpqmldir.path = $$DESTDIR cpqmldir.CONFIG = no_check_exist COPIES += cpqmldir }
直到QFACE_SOURCES
的所有行都应该很熟悉。我们使用CONFIG
来构建一个插件,然后定义链接器的设置以链接到我们的前端库。然后,我们使用QFACE_FORMAT
来定义“qmlplugin”作为生成模板。这次我们不是将ivigenerator
添加到CONFIG
中,而是使用qmake的load()函数来显式加载功能。这使得我们能够使用“qmlplugin”生成模板中的一部分URI
变量。这个URI可以用来自定义DESTDIR
,即用斜杠代替所有点。
除了文件夹结构之外,QmlEngine还需要一个qmldir
文件,该文件指明了哪些文件属于插件,以及哪些URI。更多详情请参见模块定义qmldir文件。这个qmldir
文件和一个提供关于代码补全信息的plugins.qmltypes
文件都是由IVI Generator自动生成的;但它们需要放置在库旁边。为了做到这一点,我们将文件添加到一个类似于INSTALL
目标的范围中,但将其添加到COPIES
变量中。这样,在插件构建时,确保文件被复制。
现在插件已经准备好使用了,但是我们的仪表盘应用程序不知道在哪里查找它,并仍在使用旧的硬编码注册。因此,我们现在可以从instrument-cluster.pro
文件中删除链接步骤,并相应地更改我们的主要文件。
#include <QGuiApplication> #include <QQmlApplicationEngine> int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); QQmlApplicationEngine engine; engine.addImportPath(app.applicationDirPath() + "/imports"); engine.load(QUrl(QStringLiteral("qrc:///Cluster.qml"))); return app.exec(); }
发生变化的是,我们现在添加了一个额外的导入路径,使用addImportPath
函数,该函数指向二进制文件所在位置的“导入”文件夹。
第四章:添加自定义模拟
到目前为止,我们创建了一个中间件API,并将其集成到我们的仪表盘QML代码中,使用QML插件扩展了它,并生成了模拟后端。在幕后,为了支持我们,已经做了很多事情;但在UI方面,直到现在变化不大。本章的内容是定义合理的默认值,开始模拟真实的车程,让我们的模拟后端栩栩如生。
定义默认值
我们首先在QFace文件中为我们属性的默认值定义,使用注释。注释是一种特殊的注释,用于向接口、方法、属性等添加额外的数据。对于此用例,我们使用config_simulator
注释。更多详细信息,请参见注释。
在我们当前的仪表盘应用程序中,温度默认为0。让我们将其更改为春天的温度,15摄氏度,如下面的YAML片段所示:
@config_simulator: { simulationFile: "qrc:/simulation.qml" }
再次编译插件,以便此温度更改反映在我们的仪表盘上。让我们看看这实际上是怎样工作的:当启动IVI Generator时,config_simulator注释被转换成了一个JSON文件,现在它是“模拟后端”构建文件夹的一部分。这个JSON文件看起来是这样的:
{ "interfaces" : [ "Example.IVI.InstrumentCluster.InstrumentCluster" ] }
但是,这个JSON文件与实际的模拟后端代码有什么关系?自动生成的模拟后端代码使用QIviSimulationEngine读取JSON文件,并将数据提供给一个QML模拟文件。一个默认的QML文件也是自动生成的,并从QIviSimulationEngine加载。这个默认的QML文件提供了模拟后端应该发生的行为。
在下一节中,我们将查看QML文件以及如何更改它。但首先,让我们看看如何以更动态的方式更改默认值。
QTIVI_SIMULATION_DATA_OVERRIDE=instrumentcluster=<path-to-file>/miles.json
定义QML行为
在我们定义自定义行为之前,让我们看看为我们自动生成了什么。有两个QML文件:第一个是 instrumentcluster_simulation.qml
,非常简单。它定义了一个入口点,用于实例化第二个文件,即 InstrumentClusterSimulation.qml
文件。这种划分是为了使同一模块中可以定义多个接口。
注意:一个QML引擎只能有一个入口点。虽然 QIviSimulationEngine 有同样的限制,如果您有一个具有多个接口的模块,并且想要有多个模拟文件(每个接口一个)——这是为什么第一个QML文件仅实例化了它所支持的所有接口的QML文件。在我们的例子中,只有一个接口。
InstrumentClusterSimulation.qml
文件非常有趣
import QtQuick 2.10 import Example.IVI.InstrumentCluster.simulation 1.0 QtObject { property var settings : IviSimulator.findData(IviSimulator.simulationData, "InstrumentCluster") property bool defaultInitialized: false property LoggingCategory qLcInstrumentCluster: LoggingCategory { name: "example.ivi.instrumentcluster.simulation.instrumentclusterbackend" } property var backend : InstrumentClusterBackend { function initialize() { console.log(qLcInstrumentCluster, "INITIALIZE") if (!defaultInitialized) { IviSimulator.initializeDefault(settings, backend) defaultInitialized = true } Base.initialize() } function setSpeed(speed) { if ("speed" in settings && !IviSimulator.checkSettings(settings["speed"], speed)) { console.error(qLcInstrumentCluster, "SIMULATION changing speed is not possible: provided: " + speed + " constraint: " + IviSimulator.constraint(settings["speed"])); return; } console.log(qLcInstrumentCluster, "SIMULATION speed changed to: " + speed); backend.speed = speed } function setRpm(rpm) { if ("rpm" in settings && !IviSimulator.checkSettings(settings["rpm"], rpm)) { console.error(qLcInstrumentCluster, "SIMULATION changing rpm is not possible: provided: " + rpm + " constraint: " + IviSimulator.constraint(settings["rpm"])); return; } console.log(qLcInstrumentCluster, "SIMULATION rpm changed to: " + rpm); backend.rpm = rpm } function setFuel(fuel) { if ("fuel" in settings && !IviSimulator.checkSettings(settings["fuel"], fuel)) { console.error(qLcInstrumentCluster, "SIMULATION changing fuel is not possible: provided: " + fuel + " constraint: " + IviSimulator.constraint(settings["fuel"])); return; } console.log(qLcInstrumentCluster, "SIMULATION fuel changed to: " + fuel); backend.fuel = fuel } function setTemperature(temperature) { if ("temperature" in settings && !IviSimulator.checkSettings(settings["temperature"], temperature)) { console.error(qLcInstrumentCluster, "SIMULATION changing temperature is not possible: provided: " + temperature + " constraint: " + IviSimulator.constraint(settings["temperature"])); return; } console.log(qLcInstrumentCluster, "SIMULATION temperature changed to: " + temperature); backend.temperature = temperature } function setSystemType(systemType) { if ("systemType" in settings && !IviSimulator.checkSettings(settings["systemType"], systemType)) { console.error(qLcInstrumentCluster, "SIMULATION changing systemType is not possible: provided: " + systemType + " constraint: " + IviSimulator.constraint(settings["systemType"])); return; } console.log(qLcInstrumentCluster, "SIMULATION systemType changed to: " + systemType); backend.systemType = systemType } function setCurrentWarning(currentWarning) { if ("currentWarning" in settings && !IviSimulator.checkSettings(settings["currentWarning"], currentWarning)) { console.error(qLcInstrumentCluster, "SIMULATION changing currentWarning is not possible: provided: " + currentWarning + " constraint: " + IviSimulator.constraint(settings["currentWarning"])); return; } console.log(qLcInstrumentCluster, "SIMULATION currentWarning changed to: " + currentWarning); backend.currentWarning = currentWarning } } }
首先,有一个 settings
属性,它是使用从 IviSimulator.findData 方法返回的值初始化的,该方法接受一个字符串作为输入 IviSimulator.simulationData。simulationData
是表示为JavaScript对象的JSON文件。
findData
方法帮助我们从对接口感兴趣的 InstrumentCluster
中提取数据。后续的属性帮助接口了解是否已设置默认值。LoggingCategory
用于标识此模拟文件的日志输出。
之后,通过实例化一个 InstrumentClusterBackend
对象并扩展更多功能来定义实际行为。InstrumentClusterBackend
是指向我们的 InstrumentCluster
QML前端类的接口。但是,除了前端之外,这些属性也是可写的,以便我们可以更改它们以提供有用的模拟。
每次前端实例连接到后端时,都会调用 initialize()
函数。这对于QML模拟也是适用的:因为 initialize()
C++ 函数将其转发到QML实例。这也适用于所有其他函数,如属性的设置器、获取器或方法。有关更多详细信息,请参阅 QIviSimulationEngine。
在QML的 initialize()
函数内部,我们调用 IviSimulator.initializeDefault()
,从 simulationData
对象中读取默认值并初始化所有属性。这只做 一次,因为当下一个前端实例连接到后端时,我们不希望将属性重置为默认值。最后,调用基本实现以确保向前端发送了 initializationDone
信号。
同样,为每个属性定义了一个设置器函数;它们使用 IviSimulator.checkSettings()
从 simulationData
中读取特定约束设置,并检查这些约束对新值是否有效。如果不有效,则使用 IviSimulator.constraint()
向用户提供一个有意义的错误消息。
定义我们自己的QML模拟
如上所述,InstrumentClusterBackend
项确实提供了我们QFace文件的所有属性。这可以用来通过改变属性值为我们所希望值来模拟行为。这个最简单的形式就是值赋值,但这将是非常静态的,并不是我们想要实现的效果。相反,我们使用QML动画对象来在一段时间内改变值
NumberAnimation { target: backend property: "speed" from: 0 to: 80 duration: 4000 }
上面的代码片段将速度属性在4000秒内改为80,并模拟了一辆加速的汽车。扩展到其他属性,并结合串联和并行动画,我们可以创建一个完整的模拟
property var animation: SequentialAnimation { loops: Animation.Infinite running: true ParallelAnimation { SequentialAnimation { NumberAnimation { target: backend property: "speed" from: 0 to: 80 duration: 4000 } NumberAnimation { target: backend property: "speed" to: 50 duration: 2000 } NumberAnimation { target: backend property: "speed" to: 200 duration: 7000 } ScriptAction { script: { backend.currentWarning = InstrumentCluster.warning("red","LOW FUEL", "images/fuelsymbol_orange.png") } } NumberAnimation { target: backend property: "speed" to: 0 duration: 5000 } ScriptAction { script: { backend.currentWarning = InstrumentCluster.warning() } } } NumberAnimation { target: backend property: "fuel" from: 1 to: 0.22 duration: 18000 } } NumberAnimation { target: backend property: "fuel" from: 0.22 to: 1 duration: 4000 } }
然后,为了提供漂亮的rpm
属性模拟,我们使用了一个绑定,根据当前的速率进行一些计算。完整的模拟文件如下所示
下一步是告诉IVI生成器以及QIviSimulationEngine关于我们新的模拟文件。与QML文件类似,这里最好的方法是将模拟文件放入资源文件中。在我们的例子中,我们添加了一个名为simulation.qrc
的新文件,其中使用/
前缀包含simulation.qml
。
在我们的QFace文件中,现在需要以注释的形式添加此位置
@config_simulator: { simulationFile: "qrc:/simulation.qml" } module Example.IVI.InstrumentCluster 1.0 ...
现在,重新构建模拟后端将模拟文件嵌入到插件中,并将其传递给QIviSimulationEngine,从而在加载时启动模拟
第五章:添加一个与QtRemoteObjects结合的仿真服务器
在本章中,我们将仪表盘扩展到使用进程间通信(IPC)机制,并使用两个进程。目前,模拟被作为插件加载,使它成为同一服务的一部分。虽然这对于一个小型的示例应用程序来说已经足够好了,但这并不是现代多进程架构中所采用的方法,其中多个进程需要能够访问同一个值并对其变化做出反应。我们可以编写第二个应用程序,使用相同的中间件API。然而,我们只需将仪表盘启动两次并检查动画是否同步,就能实现相同的效果。目前,它们并不同步。
添加QtRemoteObjects集成
该示例的IPC是QtRemoteObjects,因为IVI生成器已经原生支持它。要使用QtRemoteObjects,我们生成一个插件,这次是一个“生产”后端。与之前引入的模拟后端相比,自动选择自动优先级。
以下是一个项目文件.pro
TEMPLATE = lib TARGET = $$qtLibraryTarget(instrumentcluster_qtro) DESTDIR = ../qtivi QT += core ivicore CONFIG += ivigenerator plugin LIBS += -L$$OUT_PWD/../ -l$$qtLibraryTarget(QtIviInstrumentCluster) INCLUDEPATH += $$OUT_PWD/../frontend QMAKE_RPATHDIR += $$QMAKE_REL_RPATH_BASE/../ QFACE_FORMAT = backend_qtro QFACE_SOURCES = ../instrument-cluster.qface PLUGIN_TYPE = qtivi
此.pro
文件与我们之前用于模拟后端的那个几乎相同。现在我们指出了改变了哪些。
插件的名字不以"_simulation"结尾,以表示这是一个“生产”后端。《code>QFACE_FORMAT现在更改为"backend_qtro",以生成一个使用QtRemoteObjects复制品连接到一个QtRemoteObjects源,该源提供了值。除了基于QtRemoteObject的后端之外,我们还需要一个基于QtRemoteObject的服务器。这部分也可以使用IVI生成器以类似的方式自动生成。
TARGET = chapter5-ipc-server DESTDIR = .. QT = core ivicore QT -= gui CONFIG -= app_bundle CONFIG += ivigenerator LIBS += -L$$OUT_PWD/../ -l$$qtLibraryTarget(QtIviInstrumentCluster) INCLUDEPATH += $$OUT_PWD/../frontend QFACE_FORMAT = server_qtro_simulator QFACE_SOURCES = ../instrument-cluster.qface QML_IMPORT_PATH = $$OUT_PWD/qml
因为我们想要生成一个服务器可执行文件,所以需要将TEMPLATE
设置为"app"而不是"lib"。类似于插件,服务器也需要链接到我们的库,以便它可以访问定义的枚举、结构和其他类型。我们用来生成模拟服务器的模板称为"server_qtro_simulator"。
重用现有的仿真行为
现在,如果你先启动服务器,然后是仪表盘,你将不再看到我们上一章的模拟。原因在于,模拟代码是我们模拟后端的一部分,但这个后端已经不再使用,因为我们添加了一个基于 QtRemoteObjects 的 "生产" 后端。
由于我们使用了 "server_qtro_simulator" 生成模板,这可以很容易地修复,因为生成的服务器代码也使用了 QIviSimulationEngine 并支持使用与我们的模拟后端相同的模拟文件。我们只需要像以前一样扩展项目文件,并且也可以为这个项目使用相同的资源文件。
RESOURCES += ../backend_simulator/simulation.qrc
同样,我们也可以使用我们在上一章定义的其他模拟数据 JSON 文件,只需使用相同的环境变量。我们只需要将其传递给服务器,而不是我们的仪表盘应用程序。
让我们进行最终的测试:现在启动两个仪表盘实例应该可以同步显示动画
第六章:使用 D-Bus 开发生产后端
以前,我们通过使用 QtRemoteObjects 作为 IPC 并自动生成后端以及提供模拟的服务器来扩展我们的仪表盘代码。在本章中,我们希望通过使用 D-Bus 作为 IPC 手动编写我们自己的后端。
我们已经准备了一个可以工作的 D-Bus 服务器,它提供了有限的模拟。
首先,让我们看看服务器代码在这里做了什么;然后编写一个连接到它的后端。
D-Bus 服务器
如上所述,我们使用 D-Bus 来解释这一章节,并且我们已经有一个描述 D-Bus 接口的 XML 文件,类似于我们的 QFace 文件。
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd"> <node> <interface name="Example.IVI.InstrumentCluster"> <property name="speed" type="i" access="read"/> <property name="rpm" type="i" access="read"/> <property name="fuel" type="d" access="read"/> <property name="temperature" type="d" access="read"/> <property name="systemType" type="(i)" access="read"> <annotation name="org.qtproject.QtDBus.QtTypeName" value="InstrumentClusterModule::SystemType"/> </property> <property name="currentWarning" type="(sss)" access="read"> <annotation name="org.qtproject.QtDBus.QtTypeName" value="Warning"/> </property> <method name="speed" > <arg name="speed" type="i" direction="out"/> </method> <method name="rpm" > <arg name="rpm" type="i" direction="out"/> </method> <method name="fuel" > <arg name="fuel" type="d" direction="out"/> </method> <method name="temperature" > <arg name="temperature" type="d" direction="out"/> </method> <method name="systemType" > <annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="InstrumentClusterModule::SystemType"/> <arg name="systemType" type="(i)" direction="out"/> </method> <method name="currentWarning" > <annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="Warning"/> <arg name="currentWarning" type="(sss)" direction="out"/> </method> <signal name="speedChanged"> <arg name="speed" type="i" direction="out"/> </signal> <signal name="rpmChanged"> <arg name="rpm" type="i" direction="out"/> </signal> <signal name="fuelChanged"> <arg name="fuel" type="d" direction="out"/> </signal> <signal name="temperatureChanged"> <arg name="temperature" type="d" direction="out"/> </signal> <signal name="systemTypeChanged"> <annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="InstrumentClusterModule::SystemType"/> <arg name="systemType" type="(i)" direction="out"/> </signal> <signal name="currentWarningChanged"> <annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="Warning"/> <arg name="currentWarning" type="(sss)" direction="out"/> </signal> </interface> </node>
此 XML 文件用于让 qmake 生成一个基本类,服务器将使用实际功能扩展这个类。有关更多信息,请参阅 QtDBus。
我们的 D-Bus 服务器在会话总线上启动,在 /
路径上,并提供了名为 "Example.IVI.InstrumentCluster" 的接口。为了模拟一些值,我们让它保持简单,使用定时器事件每 100 毫秒更改速度值。然后从 0 开始,一旦达到最大值 250。同样,增加 rpm
值到 5000。对于所有其他属性,我们提供硬编码的值。
void InstrumentCluster::timerEvent(QTimerEvent *event) { Q_UNUSED(event); if (speed() >= 250) setSpeed(0); else setSpeed(speed() + 1); if (rpm() >= 5000) setRpm(0); else setRpm(rpm() + 100); }
编写我们自己的 D-Bus 后端
让我们从我们的后端的一个 .pro
文件开始。这与之前的 .pro
文件非常相似,但它不使用 IVI 生成器。相反,它使用 DBUS_INTERFACES
自动生成一些客户端代码,这些代码通过 D-Bus 发送和接收消息。
现在,我们需要为我们插件定义一个入口点。此插件类需要继承自 QIviServiceInterface 并实现两个函数
QStringList interfaces()
-- 该函数返回该插件支持的接口列表。QIviFeatureInterface *interfaceInstance(const QString &interface)
-- 该函数返回请求的接口实例。
此外,我们还需要提供我们支持的接口列表作为插件元数据,该列表以类似这样的 JSON 文件的形式提供
{ "interfaces" : [ "Example.IVI.InstrumentCluster.InstrumentCluster" ] }
我们需要这个列表,因为它给 QtIvi 一个机会在实例化它并加载应用程序代码需要的插件之前,知道后端支持哪些接口。
我们的插件代码如下
#include "instrumentclusterplugin.h" InstrumentClusterPlugin::InstrumentClusterPlugin(QObject *parent) : QObject(parent) , m_backend(new InstrumentClusterBackend) { } QStringList InstrumentClusterPlugin::interfaces() const { return QStringList(InstrumentCluster_InstrumentCluster_iid); } QIviFeatureInterface *InstrumentClusterPlugin::interfaceInstance(const QString &interface) const { if (interface == InstrumentCluster_InstrumentCluster_iid) return m_backend; return nullptr; }
在 interfaces()
函数中,我们使用定义在我们自动生成的库中的 IID,该 IID 在 instrumentclusterbackendinterface.h
中。在 insterfaceInstance()
函数中,我们检查正确的字符串,并返回我们所实现的仪表盘后端的实例。
该后端定义在 instrumentclusterbackend.h
中,并从 InstrumentClusterBackendInterface
继承。在我们 InstrumentClusterBackend
类中,我们需要实现来自 InstrumentClusterBackendInterface 及其派生类的所有纯虚函数。
对于我们的例子,这并不复杂,因为我们只需要实现 initialize() 函数。如果我们的 XML 文件使用可写属性或方法,那么我们还需要实现这些。我们不需要实现属性获取器,因为 QtIvi 在初始化阶段使用变化信号来获取当前状态信息。尽管生成的 D-Bus 接口类会提供获取器以从我们的服务器检索属性,但在开发后端时并不推荐使用这些获取器。这些获取器是通过使用同步调用实现的,这意味着它们将在收到客户端的响应之前阻塞事件循环。由于这可能会导致性能问题,我们建议使用 异步 调用。
在我们的后端中,为每个实现的属性定义一个如下所示的获取函数
void InstrumentClusterBackend::fetchSpeed() { m_fetchList.append("speed"); auto reply = m_client->asyncCall("speed"); auto watcher = new QDBusPendingCallWatcher(reply, this); connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) { QDBusPendingReply<int> reply = *watcher; if (reply.isError()) { qCritical() << reply.error(); } else { m_fetchList.removeAll("speed"); this->onSpeedChanged(reply.value()); watcher->deleteLater(); this->checkInitDone(); } }); }
首先,我们将属性添加到列表中,以知道哪些属性已成功获取。接下来,我们使用 asyncCall()
函数调用 speed
属性的获取器,并使用 QDBusPendingCallWatcher
等待结果。一旦结果准备好,lambda 从我们的 fetchList
中再次删除属性,使用 onSpeedChanged()
函数存储值,并通知前端。由于我们不再需要监视器,我们使用 deleteLater()
在下一个事件循环运行时将其删除,并调用 checkInitDone()
函数。
checkInitDone()
函数的定义如下
void InstrumentClusterBackend::checkInitDone() { if (m_fetchList.isEmpty()) { qInfo() << "All properties initialized"; emit initializationDone(); } }
它确保一旦从服务器获取了所有属性,前端就会收到 initializationDone() 信号,初始化完成。
除了从服务器获取当前状态之外,我们还需要在每次属性更改时通知我们的前端。这是通过在服务器更改其属性时发出相应的变更信号来完成的。为此,我们为每个属性定义一个槽。此槽将属性保存到我们的类中并发出变更信号
void InstrumentClusterBackend::onSpeedChanged(int speed) { m_speed = speed; emit speedChanged(speed); }
同一槽也在初始化阶段用于保存和发出值。
您可能会想知道为什么在我们可以直接发出信号的情况下,还需要保存值。这是因为后端插件被 InstrumentCluster
类的每个实例直接使用,每个实例都调用 initialize()
函数来检索当前状态。第二次 initialize()
调用只是发出之前已保存的值;而槽则保持它们的更新。
现在,当我们启动仪表盘时,我们的后端应该连接到我们的 D-Bus 服务器,并如下所示
©2020 Qt 公司版权所有。此处包含的文档贡献的版权属于其各自的所有者。提供的文档根据自由软件基金会发布的 GNU 自由文档许可证版本 1.3 的条款许可。Qt 及其相关标志是芬兰的 Qt 公司及其它国家和地区的商标。所有其他商标均为其各自所有者的财产。