Qt IVI 生成器教程

逐步演示如何基于 QML 应用程序生成中间件 API。

本教程演示了您如何使用自己自动生成的中间件 API 扩展 QML 应用程序。我们使用现有的 QML 工具集群应用程序,并按以下步骤进行

  1. 集成不带后端的基本接口
  2. 扩展接口并添加注释
  3. 添加模拟后端和相应的模拟注释;使用 QML 插件
  4. 添加自定义模拟行为
  5. 添加模拟服务器并从 Qt Remote Objects 后端使用它
  6. 开发一个连接到 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_OBJECTQ_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。

定义只读属性速度

之前,我们以以下方式在我们QFace文件中定义了速度属性

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文件添加更多属性:rpmfueltemperature

module Example.IVI.InstrumentCluster 1.0

interface InstrumentCluster {
    readonly int speed;
    readonly int rpm;
    readonly real fuel;
    readonly real temperature;
}

您可能已经注意到,我们对fueltemperature属性使用了不同的类型。我们在这里使用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

类似地,QtIviqtivi文件夹中查找其后端插件。为了保证我们的模拟后端最终落入这样的文件夹中,我们添加以下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文件以及如何更改它。但首先,让我们看看如何以更动态的方式更改默认值。

QIviSimulationEngine 允许我们覆盖应加载到引擎中的JSON文件,当我们设置环境变量 QTIVI_SIMULATION_DATA_OVERRIDE 时。由于可以有多个由不同后端运行的引擎,我们需要定义我们指的是哪个引擎。在自动生成的代码中,模块名始终用作引擎指定符。对于本章,我们已经准备了一个第二JSON文件,该文件是源目录的一部分。按如下设置环境变量,将 systemType 修改为 mph 而不是 km/h

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.simulationDatasimulationData 是表示为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 服务器,并如下所示

示例项目 @ code.qt.io

©2020 Qt 公司版权所有。此处包含的文档贡献的版权属于其各自的所有者。提供的文档根据自由软件基金会发布的 GNU 自由文档许可证版本 1.3 的条款许可。Qt 及其相关标志是芬兰的 Qt 公司及其它国家和地区的商标。所有其他商标均为其各自所有者的财产。