Qt界面框架生成器教程
逐步演示如何基于QML应用生成中间件API。
本教程演示了如何通过扩展自动生成的中间件API来扩展QML应用。我们使用一个现有的QML仪表盘应用,并按照以下步骤进行:
在我们开始实际的中间件集成之前,让我们看看现有的仪表盘QML代码和它支持的所有功能
images
– 本文件夹包含QML代码中使用的所有图像。Cluster.qml
– 将所有其他QML组件组合在一起的主要QML文件。Dial.qml
– 用于显示速度或每分钟转速(RPM)等值的基础组件,使用指针显示。Fuel.qml
– 用于显示实际燃油水平的组件。Label.qml
– 一个小型辅助组件,用于设置用于显示文本的所有常用设置。LeftDial.qml
– 使用指针组件和文本以及当前速度的英里每小时(mph)或千米每小时(km/h)显示当前速度。RightDial.qml
– 显示当前RPM并提供显示警告指示器的方式。Top.qml
– 显示当前日期和当前温度的顶部栏。
接下来,我们使用我们的中间件API来添加以下功能的支持:
- 在左侧拨号器中显示当前速度。
- 在右侧拨号器中显示当前RPM。
- 在不同的计量单位之间切换。
- 在顶部栏中显示当前温度。
- 在右侧拨号器上显示不同的警告。
- 指示仪表盘是否连接并显示实际数据。
最终目标是连接所有这些功能,以模拟这种实时驾驶体验
第1章:使用界面框架生成器的基本中间件API
在本章中,我们将中间件API集成到现有的仪表盘QML代码中。我们不是像大多数基本QML示例那样手动编写所有这些部分,我们将使用界面框架生成器来自动生成所需的部件。
接口定义语言
要能自动生成中间件API,界面框架生成器需要一些有关要生成的内容的输入。这个输入是以接口定义语言(IDL),QFace的形式提供的,非常简单地描述了API。
让我们从定义一个非常简单的接口开始,这个接口提供给我们一个速度属性
ch1-basics/instrument-cluster.qface:
module Example.If.InstrumentClusterModule 1.0 interface InstrumentCluster { int speed; }
首先,我们需要定义我们想要描述哪个模块。该模块充当命名空间,因为IDL文件可以包含多个接口。
ch1-basics/instrument-cluster.qface:
module Example.If.InstrumentClusterModule 1.0
模块最重要的部分是其接口定义。
ch1-basics/instrument-cluster.qface:
interface InstrumentCluster { int speed; }
在这种情况下,我们定义了一个名为InstrumentCluster
的接口,它包含一个属性。每个属性定义必须至少包含一个类型和一个名称。大部分基本类型都是内建的,可以在QFace IDL语法中找到。
自动生成
现在我们的第一个IDL文件已经准备好了,是时候使用接口框架生成工具从它中自动生成一个API了。使用qmake,这个自动生成过程被集成到构建系统中,并在编译时完成。类似于moc。使用CMake,生成是在配置时发生的。
以下代码段展示如何根据我们的IDL文件构建一个C++库
CMake:
ch1-basics/frontend/CMakeLists.txt:
find_package(Qt6 REQUIRED COMPONENTS Core InterfaceFramework Qml Quick) qt_add_library(libIc_ch1) set_target_properties(libIc_ch1 PROPERTIES OUTPUT_NAME "InstrumentCluster") set_target_properties(libIc_ch1 PROPERTIES RUNTIME_OUTPUT_DIRECTORY ../) # Interface Framework Generator: qt_ifcodegen_extend_target(libIc_ch1 IDL_FILES ../instrument-cluster.qface TEMPLATE frontend ) set(import_path "${CMAKE_CURRENT_BINARY_DIR}/frontend/qml") if (NOT ${import_path} IN_LIST QML_IMPORT_PATH) list (APPEND QML_IMPORT_PATH "${import_path}") set(QML_IMPORT_PATH ${QML_IMPORT_PATH} CACHE INTERNAL "" FORCE) endif()
首先使用find_package获取所有所需的库到CMake构建系统中。使用qt_add_library定义一个新的库,并且使用CMake的target_properties设置输出名和输出目录。由于我们将来需要链接到这个库,因此将其放置在上一个目录中更容易。
通过调用qt_ifcodegen_extend_target函数,调用autogenerator
并使用之前定义的库扩展生成的文件。使用IDL_FILES参数指定输入文件。有关更多信息,请参阅构建系统集成。
qmake:
ch1-basics/frontend/frontend.pro:
TARGET = $$qtLibraryTarget(QtIfInstrumentCluster) TEMPLATE = lib DESTDIR = .. QT += interfaceframework qml quick CONFIG += ifcodegen IFCODEGEN_SOURCES = ../instrument-cluster.qface
大多数的.pro
文件是一个标准的用于定义C++库的设置,使用"lib" TEMPLATE
并在TARGET
变量中定义所需的文件名。我们使用的qtLibraryTarget
函数有助于正确附加"debug"后缀,为提供调试信息的库。将来我们需要链接这个文件,因此我们将DESTDIR
设置为上一个目录以简化这一过程。
注意:Windows会在同一目录下自动搜索库。
激活接口框架生成器集成需要将CONFIG
变量设置为指定ifcodegen
选项。这确保在构建过程中调用接口框架生成器,使用在IFCODEGEN_SOURCES
中指定的QFace文件。有关更多信息,请参阅构建系统集成。
哪些文件会自动生成
接口框架生成器基于生成模板工作。这些模板定义了从QFace文件生成什么内容。使用qmake,需要使用IFCODEGEN_TEMPLATE
变量来定义模板。如果没有定义,它将默认为"frontend"模板。在CMake中,需要使用qt_ifcodegen_extend_target及其关联函数的TEMPLATE
参数来指定模板。有关这些模板的更多详细信息,请参阅使用生成器。
简而言之,"frontend"模板为QFace文件中的每个接口生成
- 一个来自QIfAbstractFeature的C++类
- 一个模块类,它有助于将所有接口注册到QML中,并存储全局类型和函数。
要自行检查C++代码,您可以查看库构建文件夹中的这些文件。
目前对我们来说最重要的自动生成文件是我们的定义接口的C++类。它看起来是这样的
ch1-basics/frontend/frontend/instrumentcluster.h:
/**************************************************************************** ** Generated from 'instrument-cluster.qface': module Example.If.InstrumentClusterModule ** ** Created by: The Qt Interface Framework Generator (6.7.0) ** ** WARNING! All changes made in this file will be lost! *****************************************************************************/ #ifndef INSTRUMENTCLUSTERMODULE_INSTRUMENTCLUSTER_H_ #define INSTRUMENTCLUSTERMODULE_INSTRUMENTCLUSTER_H_ #include "instrumentclustermodule.h" #include <QtInterfaceFramework/QIfAbstractFeature> #include <QtInterfaceFramework/QIfPendingReply> #include <QtInterfaceFramework/QIfPagingModel> class InstrumentClusterPrivate; class InstrumentClusterBackendInterface; class Q_EXAMPLE_IF_INSTRUMENTCLUSTERMODULE_EXPORT InstrumentCluster : public QIfAbstractFeature { Q_OBJECT QML_NAMED_ELEMENT(InstrumentCluster) Q_PROPERTY(int speed READ speed WRITE setSpeed NOTIFY speedChanged FINAL) public: explicit InstrumentCluster(QObject *parent = nullptr); ~InstrumentCluster() override; 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(QIfServiceObject *service) Q_DECL_OVERRIDE; void clearServiceObject() Q_DECL_OVERRIDE; private: Q_PRIVATE_SLOT(d_func(), void onSpeedChanged(int speed)) Q_DECLARE_PRIVATE(InstrumentCluster) }; #endif // INSTRUMENTCLUSTERMODULE_INSTRUMENTCLUSTER_H_
正如你所见,自动生成的C++类实现了一个我们在QFace文件中之前定义的speed(速度)属性。通过使用Q_OBJECT
和Q_PROPERTY
宏,这个类现在可以直接在您的QML代码中使用。
将前端库与QML代码集成
为了这个集成,我们使用由QML代码自动生成的前端库。为了简单起见,我们遵循标准的Qt示例模式,使用一个小的C++主函数,该函数将我们的自动生成的类型注册到QML,并将仪表集群QML代码加载到QQmlApplicationEngine中
ch1-basics/instrument-cluster/main.cpp:
#include "instrumentclustermodule.h" using namespace Qt::StringLiterals; int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); QQmlApplicationEngine engine; InstrumentClusterModule::registerQmlTypes(); engine.load(QUrl(u"qrc:///Cluster.qml"_s)); return app.exec(); }
现在我们只需要实际集成InstrumentCluster QML元素,并将speed属性连接到leftDial。这通过首先使用instrumentCluster ID实例化元素来完成。
ch1-basics/instrument-cluster/Cluster.qml:
import QtQuick import QtQuick.Window import Example.If.InstrumentClusterModule Window { id: root width: 1920 height: 720 title: qsTr("QtIF Instrument Cluster Chapter 1") visible: true color: "#0c0c0c" InstrumentCluster { id: instrumentCluster }
最后,我们可以创建一个绑定,将LeftDial Item的value属性绑定到我们的InstrumentCluster API的speed属性。
LeftDial { id: leftDial anchors.left: parent.left anchors.leftMargin: 0.1 * width value: instrumentCluster.speed }
第二章:扩展接口并添加注解
在本章中,我们通过枚举和定义自己的结构来通过API添加更多属性,扩展我们的中间件API。
将速度定义为只读属性
之前,我们在QFace文件中以以下方式定义了speed属性
ch1-basics/instrument-cluster.qface:
module Example.If.InstrumentClusterModule 1.0 interface InstrumentCluster { int speed; }
该属性被定义为可读和可写,因为我们没有使用任何额外的指定符。然而,对于我们的仪表集群示例来说,不需要可写speed属性,因为它不是用来加速汽车,而只是用来可视化当前状态。
要将属性定义为只读,请使用readonly关键字。
ch2-enums-structs/instrument-cluster.qface:
module Example.If.InstrumentClusterModule 1.0 interface InstrumentCluster { readonly int speed; }
当我们再次构建我们的应用时,构建系统会识别这个更改并运行接口框架生成器来生成C++代码的更新版本。接口框架生成器完成后,打开构建文件夹中的instrumentcluster.h,你会注意到生成的speed属性已更改——它不再有设置器,现在是只读的。
ch2-enums-structs/frontend/frontend/instrumentcluster.h:
class Q_EXAMPLE_IF_INSTRUMENTCLUSTERMODULE_EXPORT InstrumentCluster : public QIfAbstractFeature { Q_OBJECT QML_NAMED_ELEMENT(InstrumentCluster) Q_PROPERTY(int speed READ speed NOTIFY speedChanged FINAL) ... };
扩展接口
为了实现提供完整仪表集群模拟的目标,我们需要将更多属性添加到我们的QFace文件中:rpm
、fuel
和 temperature
ch2-enums-structs/instrument-cluster.qface:
module Example.If.InstrumentClusterModule 1.0 interface InstrumentCluster { readonly int speed; readonly int rpm; readonly real fuel; readonly real temperature; }
你可能已经注意到,我们对fuel和temperature属性使用了不同的类型。在这里我们使用real,因为我们想以浮点数显示温度,显示当前油量时作为一个介于0和1之间的值。
定义一个新的枚举类型
一个有用的功能是能够在公制和英制系统之间切换,因此我们需要定义一个属性来表示我们当前使用的系统。使用布尔属性可能会工作,但不会提供良好的API,因此在QFace文件中定义一个新的枚举类型,并将其用作新system属性的类型。
ch2-enums-structs/instrument-cluster.qface:
module Example.If.InstrumentClusterModule 1.0 interface InstrumentCluster { readonly int speed; readonly int rpm; readonly real fuel; readonly real temperature; readonly SystemType systemType; } enum SystemType { Imperial, Metric }
在自动生成的代码中,这导致枚举成为模块类的一部分,使得相同的枚举可以被同一模块中的多个类使用
ch2-enums-structs/frontend/frontend/instrumentclustermodule.h:
/**************************************************************************** ** Generated from 'instrument-cluster.qface': module Example.If.InstrumentClusterModule ** ** Created by: The Qt Interface Framework Generator (6.7.0) ** ** WARNING! All changes made in this file will be lost! *****************************************************************************/ #ifndef INSTRUMENTCLUSTERMODULE_H_ #define INSTRUMENTCLUSTERMODULE_H_ #include "instrumentclustermoduleglobal.h" #include <QtCore/QObject> class Q_EXAMPLE_IF_INSTRUMENTCLUSTERMODULE_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.If.InstrumentClusterModule"), int majorVersion = 1, int minorVersion = 0); }; Q_EXAMPLE_IF_INSTRUMENTCLUSTERMODULE_EXPORT QDataStream &operator<<(QDataStream &out, InstrumentClusterModule::SystemType var); Q_EXAMPLE_IF_INSTRUMENTCLUSTERMODULE_EXPORT QDataStream &operator>>(QDataStream &in, InstrumentClusterModule::SystemType &var); #endif // INSTRUMENTCLUSTERMODULE_H_
添加一个新的结构
为了在仪表盘的右侧旋钮上显示警告,我们希望使用一个存储颜色、图标和文本的结构的警告;而不是使用三个独立的属性。类似于定义接口,我们可以在我们的QFace文件中使用struct
关键字
ch2-enums-structs/instrument-cluster.qface:
struct Warning { string color string text string icon }
将这个新结构用作属性的类型,其工作方式与使用枚举时相同。QFace文件现在应该看起来像这样
ch2-enums-structs/instrument-cluster.qface:
module Example.If.InstrumentClusterModule 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
属性来转换值。
ch2-enums-structs/instrument-cluster/Cluster.qml:
LeftDial { id: leftDial anchors.left: parent.left anchors.leftMargin: 0.1 * width value: instrumentCluster.speed metricSystem: instrumentCluster.systemType === InstrumentClusterModule.Metric }
这些枚举是模块类的一部分,也被导出到QML为InstrumentClusterModule
。要触发rightDial
项中的警告,我们使用三个绑定来连接结构中的三个成员变量
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 }
第三章:使用QML插件添加仿真后端和注释
在前两章中,我们使用QFace文件编写了一个中间件API,并使用接口框架生成器自动生成一个以库形式存在的C++ API。现在,在这一章中,我们进一步扩展这一功能,通过引入仿真后端和使用注释来为我们的仿真定义默认值。
前端和后端的分离
QtInterfaceFramework和接口框架生成器都允许你编写将前端从后端分离的代码。这允许你将API与其实际实现分开。Qt已经在许多领域使用这一概念,最显著的是在使用各种Qt平台(如Linux的XCB和macOS的Cocoa)时在底层窗口系统技术中的应用。
我们的中间件API也采用了这种分离,前端将API作为库提供;后端提供此API的实现。这个实现基于QtInterfaceFramework的Dynamic Backend System
,这使我们能够在运行时在这些后端之间切换。
添加仿真后端
对于我们的仪表盘,我们希望添加这样的后端来提供实际值。现在,我们只希望有一些模拟行为,因为我们不能轻松地将其连接到真实车辆。这就是为什么这些后端被称为“仿真后端”。要添加这种类型的后端,再次使用接口框架生成器为我们做繁重的工作并生成一个。这项工作是以与我们使用“前端”模板生成库类似的方式进行。但现在,我们使用“backend_simulator”模板
CMake:
ch3-simulation-backend/backend_simulator/CMakeLists.txt
find_package(Qt6 REQUIRED COMPONENTS Core Gui InterfaceFramework) qt_add_plugin(ic_ch3_simulation PLUGIN_TYPE interfaceframework) set_target_properties(ic_ch3_simulation PROPERTIES LIBRARY_OUTPUT_DIRECTORY ../interfaceframework) # Interface Framework Generator: qt_ifcodegen_extend_target(ic_ch3_simulation IDL_FILES ../instrument-cluster.qface TEMPLATE backend_simulator )
与前端的库类似,首先使用find_package导入所使用的组件。因为我们想构建一个在运行时加载的插件(动态库)而不是链接到它,所以我们使用qt_add_plugin函数。这里的一个重要方面是库名以"_simulation"结尾,这是一种告诉QtInterfaceFramework这是一个模拟后端的方式。当可用的“生产”后端时,它会 prefers 与“模拟”后端。有关更多信息,请参阅动态后端系统。
与之前一样,通过使用qt_ifcodegen_extend_target函数调用接口框架生成器,这次设置TEMPLATE
为"backend_simulator"。
qmake:
ch3-simulation-backend/backend_simulator/backend_simulator.pro:
TEMPLATE = lib TARGET = $$qtLibraryTarget(instrumentcluster_simulation) QT += core interfaceframework CONFIG += ifcodegen plugin IFCODEGEN_TEMPLATE = backend_simulator IFCODEGEN_SOURCES = ../instrument-cluster.qface PLUGIN_TYPE = interfaceframework # 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"结尾,这是一种告诉QtInterfaceFramework这是一个模拟后端的方式。当可用的“生产”后端时,它 prefered 与“模拟”后端。有关更多信息,请参阅动态后端系统。
启用接口框架生成器的方法与我们 earlier 相同:使用相同的IFCODEGEN_SOURCES
变量,但将IFCODEGEN_TEMPLATE
定义为"backend_simulator",以使用正确的生成模板。此外,我们需要将'plugin'添加到CONFIG
变量中,使这个库成为Qt插件,可以轻松在运行时加载。
链接设置和定位插件
现在尝试直接编译项目文件将导致编译和链接错误。这是因为:为了进行前端和后端的分离,我们需要后端实现一个定义的接口类,这个类为前端所知。这个接口称为“后端接口”,它是前端库的一部分自动生成的。因为这个类提供了信号和槽并使用QObject作为其基类,所以当你从它继承时需要链接到前端库。因为这对于后端插件是必需的,所以我们需要添加以下行
CMake:
ch3-simulation-backend/backend_simulator/CMakeLists.txt:
target_link_libraries(ic_ch3_simulation PUBLIC libIc_ch3 ) set(import_path "${CMAKE_CURRENT_BINARY_DIR}/backend_simulator/qml") if (NOT ${import_path} IN_LIST QML_IMPORT_PATH) list (APPEND QML_IMPORT_PATH "${import_path}") set(QML_IMPORT_PATH ${QML_IMPORT_PATH} CACHE INTERNAL "" FORCE) endif()
通过将前端库libIc_ch3定义为目标链接库,相应的更新了包含路径。
qmake:
ch3-simulation-backend/backend_simulator/backend_simulator.pro:
LIBS += -L$$OUT_PWD/../ -l$$qtLibraryTarget(QtIfInstrumentCluster) INCLUDEPATH += $$OUT_PWD/../frontend
现在项目应该可以顺利构建,并在您的构建文件夹中创建插件;或者如果您没有使用阴影构建,则是插件文件夹。再次启动仪表盘时,您应该看到以下消息
There is no production backend implementing "Example.If.InstrumentCluster.InstrumentCluster" . There is no simulation backend implementing "Example.If.InstrumentCluster.InstrumentCluster" . No suitable ServiceObject found.
此消息表示QtInterfaceFramework仍然无法找到我们刚刚创建的模拟插件。在此处,您需要了解一些关于Qt插件系统的更多信息,特别是它如何查找插件。
Qt在其多个目录中搜索插件,第一个是插件文件夹plugins
,这是Qt安装的一部分。在插件文件夹中,每个插件类型都有自己的子文件夹,例如用于与底层平台API和窗口系统通信的平台插件platforms
。
类似地,QtInterfaceFramework在其interfaceframework文件夹中查找后端插件。
为了确保我们的模拟后端最终落在这个文件夹中,我们在我们的构建系统文件中添加以下更改
CMake:
ch3-simulation-backend/backend_simulator/CMakeLists.txt:
set_target_properties(ic_ch3_simulation PROPERTIES LIBRARY_OUTPUT_DIRECTORY ../interfaceframework)
qmake:
ch3-simulation-backend/backend_simulator/backend_simulator.pro:
DESTDIR = ../interfaceframework
您可能想知道在上级目录中创建一个interfaceframework
文件夹如何解决插件查找的问题,因为这个文件夹并不属于系统的插件文件夹。但Qt支持在多个文件夹中搜索此类插件,其中一个文件夹是可执行文件所在路径。
或者,我们可以使用QCoreApplication::addLibraryPath()函数或使用环境变量QT_PLUGIN_PATH
来添加额外的插件路径。更多信息,请参考如何创建Qt插件。
现在一切准备就绪,但由于我们的插件链接到前端库,我们需要确保动态链接器可以找到库。这可以通过将环境变量LD_LIBRARY_PATH
设置为我们库文件夹实现。但这会导致这样的问题,即每个用户都需要设置这个变量才能使用我们的应用程序。
CMake:
使用CMake,自动将前端库的位置添加到二进制文件的RUNPATH中,无需进一步操作。
qmake:
在qmake中,我们可以使用相对的RPATH替代LD_LIBRARY_PATH
,并注释我们的插件,提供链接器可能找到所需库的信息,这些库相对于插件的位置。
ch3-simulation-backend/backend_simulator/backend_simulator.pro:
INCLUDEPATH += $$OUT_PWD/../frontend QMAKE_RPATHDIR += $$QMAKE_REL_RPATH_BASE/../
在QML插件中导出QML类型
在第一章中,我们扩展了我们的main.cpp
以注册我们自动生成的中间件API的所有类型。尽管这样操作是可行的,但在大型项目中通常使用QML插件并能够使用qml
可执行文件进行开发。尽管执行这一操作的代码并不复杂,但接口框架生成器也支持这种操作并使操作更加简单。
从第一章中我们知道,模块名称用于QML导入URI。这对于QML插件来说非常重要,因为QmlEngine期望插件位于一个特定文件夹中,该文件夹遵循模块名称,其中模块名称的每个部分都是一个子文件夹。我们的构建系统文件以生成QML插件如下所示
CMake:
ch3-simulation-backend/imports/CMakeLists.txt:
qt_ifcodegen_import_variables(CLUSTER IDL_FILES ../instrument-cluster.qface TEMPLATE qmlplugin ) qt_add_qml_module(ic_ch3_imports OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/${CLUSTER_URI_PATH}" URI ${CLUSTER_URI} VERSION ${CLUSTER_VERSION} CLASS_NAME InstrumentClusterModuleQmlPlugin RESOURCE_PREFIX "/" PLUGIN_TARGET ic_ch3_imports NO_PLUGIN_OPTIONAL NO_GENERATE_PLUGIN_SOURCE IMPORTS QtInterfaceFramework SOURCES ${CLUSTER_SOURCES} ) target_link_libraries(ic_ch3_imports PUBLIC libIc_ch3 )
与之前所有的生成器调用不同,我们不是扩展一个之前定义的目标,而是将生成的代码导入CMake,并将其传递给qt_add_qml_module函数。函数qt_ifcodegen_import_variables将调用生成器并将以CLUSTER作为前缀的变量导出到当前CMake作用域。这些变量引用了自动生成代码,但同时也公开了其他信息,如QML导入URI。在下一个调用中,使用这些变量定义了具有正确URI和版本的QML模块(如我们的IDL文件中所指定)。通过使用OUTPUT_DIRECTORY变量,我们可以确保生成正确的文件夹结构,并可以直接从构建文件夹中导入QML插件。
注意:除了生成QML插件外,新引入的QML类型注册也可以使用,该机制在6.3版本中引入。为了使用这种新机制,前端CMakeLists.txt必须像这样扩展
qt_ifcodegen_extend_target(libIc_ch3 IDL_FILES ../instrument-cluster.qface PREFIX CLUSTER TEMPLATE frontend ) qt_add_qml_module(libIc_ch3 OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/../imports/${CLUSTER_URI_PATH}" URI ${CLUSTER_URI} VERSION ${CLUSTER_VERSION} RESOURCE_PREFIX "/" IMPORTS QtInterfaceFramework/auto )
有关更多信息,请参阅QML类型注册。
qmake:
ch3-simulation-backend/imports/imports.pro:
TEMPLATE = lib CONFIG += plugin QT += interfaceframework LIBS += -L$$OUT_PWD/../ -l$$qtLibraryTarget(QtIfInstrumentCluster) INCLUDEPATH += $$OUT_PWD/../frontend IFCODEGEN_TEMPLATE = qmlplugin IFCODEGEN_SOURCES = ../instrument-cluster.qface load(ifcodegen) 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 }
所有直到 IFCODEGEN_SOURCES
的行都应该很熟悉。我们使用 CONFIG
来构建插件,然后定义链接器设置以链接到我们的前端库。然后,我们使用 IFCODEGEN_TEMPLATE
来定义 "qmlplugin" 作为生成模板。这次我们不是将 ifcodegen
添加到 CONFIG
中,而是使用 qmake 的 load() 函数 来明确加载功能。这使我们能够使用 "qmlplugin" 生成模板中的 URI
变量。这个 URI 可以用来通过将所有点替换为斜杠来定义 DESTDIR
。
除了文件夹结构外,QmlEngine 还需要一个 qmldir
文件,该文件指示哪些文件是插件的一部分以及在哪个 URI 下。有关更多信息,请参阅 模块定义 qmldir 文件。此 qmldir
文件和用于提供有关代码完成信息的 plugins.qmltypes
文件都由接口框架生成器自动生成;但它们需要放在库旁边。为此,我们向类似于 INSTALL
目标的范围内添加文件,但将其添加到 COPIES
变量中。这样,确保在构建插件时复制文件。
现在插件已准备好使用,但我们的仪表盘集群应用程序不知道在哪里查找它,仍在使用旧的硬编码注册。因此,我们现在可以从 instrument-cluster
构建系统文件中删除链接步骤,并相应地更改我们的主文件
ch3-simulation-backend/instrument-cluster/main.cpp:
#include <QGuiApplication> #include <QQmlApplicationEngine> using namespace Qt::StringLiterals; int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); QQmlApplicationEngine engine; engine.addImportPath(app.applicationDirPath() + "/imports"); engine.load(QUrl(u"qrc:///Cluster.qml"_s)); return app.exec(); }
变化之处在于我们现在添加了一个额外的导入路径,使用 addImportPath
函数,它指向二进制文件位置的 "imports" 文件夹。
第四章:添加自定义模拟
到目前为止,我们已经创建了一个中间件 API,并将其集成到我们的仪表盘集群 QML 代码中,扩展了它并使用一个 QML 插件,还生成了模拟后端。在后台,为了支持我们,已经发生了很多事情;但在 UI 方面,至今没有太大的变化。本章将介绍通过定义合理的默认值和开始模拟真实的车程来激活我们的模拟后端。
定义默认值
我们首先使用 QFace 文件中的注解来定义属性的默认值。注解是一种特殊的注释类型,它可以为接口、方法、属性等添加额外数据。对于此用例,我们使用 config_simulator
注解。有关更多信息,请参阅 注解。所有支持注解的参考资料可以在 此处 找到。
目前,在我们的仪表盘集群中,温度默认为 0。让我们将其更改为春天的温度,15 摄氏度,以下 YAML 片段需要添加到 qface 文件中属性定义之上。
ch4-simulation-behavior/instrument-cluster.qface:
readonly int speed; readonly int rpm; readonly real fuel; @config_simulator: { default: 15 } readonly real temperature;
为了使这个温度变化反映在我们的仪表盘集群中,重新编译插件。让我们看看这样实际上是如何工作的:当启动接口框架生成器时,config_simulator 注解被转换成 JSON 文件,现在它是 "simulation backend" 构建文件夹的一部分。这个 JSON 文件看起来像这样
ch4-simulation-behavior/backend_simulator/backend_simulator/instrumentclustermodule_simulation_data.json:
{ "InstrumentCluster": { "temperature": { "default": 15 } } }
但是这个JSON文件是如何与实际的模拟后端代码相关联的呢?自动生成的模拟后端代码使用了QIfSimulationEngine,它读取JSON文件并将数据提供给QML模拟文件。同时也会自动生成一个默认的QML文件,并通过QIfSimulationEngine加载。这个默认的QML文件定义了模拟后端应执行的行为。
在下一节中,我们将探讨QML文件以及如何对其进行更改。但在那之前,让我们看看如何更动态地更改默认值。
当设置环境变量QTIF_SIMULATION_DATA_OVERRIDE
时,QIfSimulationEngine允许我们覆盖要加载到引擎中的JSON文件。由于可能存在多个由不同后端运行的不同引擎,因此我们需要定义我们指的是哪个引擎。在自动生成的代码中,模块名称总是用作引擎指定器。对于本章,我们已经准备了一个位于源目录中的第二个JSON文件。按照以下方式设置环境变量,可以将systemType
从mph更改为km/h
QTIF_SIMULATION_DATA_OVERRIDE=example.if.instrumentclustermodule=<path-to-file>/kmh.json
定义QML行为
在我们定义自定义行为之前,让我们看看为我们自动生成的代码。有两个QML文件:第一个是instrumentcluster_simulation.qml
,相当简单。它定义了一个入口实例化第二个文件,即InstrumentClusterSimulation.qml
文件。这种分割是为了存在多个接口定义属于同一个模块的情况。
注意:一个QML引擎只能有一个入口点。当QIfSimulationEngine具有相同的限制,并且如果您有一个具有多个接口的模块,并且您希望有多个模拟文件——每个接口一个。这就是为什么第一个QML文件只实例化了所有它支持的接口的QML文件。在我们的例子中,只有一个接口。
InstrumentClusterSimulation.qml文件非常有趣
ch4-simulation-behavior/backend_simulator/backend_simulator/InstrumentClusterSimulation.qml:
/**************************************************************************** ** Generated from 'instrument-cluster.qface': module Example.If.InstrumentClusterModule ** ** Created by: The Qt Interface Framework Generator (6.7.0) ** ** WARNING! All changes made in this file will be lost! *****************************************************************************/ import QtQuick import Example.If.InstrumentClusterModule.simulation InstrumentClusterBackend { id: backend property QtObject d: QtObject { id: d property var settings: IfSimulator.findData(IfSimulator.simulationData, "InstrumentCluster") property bool defaultInitialized: false property LoggingCategory qLcInstrumentCluster: LoggingCategory { name: "example.if.instrumentclustermodule.simulation.instrumentclusterbackend" } } function initialize() { console.log(d.qLcInstrumentCluster, "INITIALIZE") if (!d.defaultInitialized) { IfSimulator.initializeDefault(d.settings, backend) d.defaultInitialized = true } Base.initialize() } function setSpeed(speed) { if ("speed" in d.settings && !IfSimulator.checkSettings(d.settings["speed"], speed)) { console.error(d.qLcInstrumentCluster, "SIMULATION changing speed is not possible: provided: " + speed + " constraint: " + IfSimulator.constraint(d.settings["speed"])); return; } console.log(d.qLcInstrumentCluster, "SIMULATION speed changed to: " + speed); backend.speed = speed } function setRpm(rpm) { if ("rpm" in d.settings && !IfSimulator.checkSettings(d.settings["rpm"], rpm)) { console.error(d.qLcInstrumentCluster, "SIMULATION changing rpm is not possible: provided: " + rpm + " constraint: " + IfSimulator.constraint(d.settings["rpm"])); return; } console.log(d.qLcInstrumentCluster, "SIMULATION rpm changed to: " + rpm); backend.rpm = rpm } function setFuel(fuel) { if ("fuel" in d.settings && !IfSimulator.checkSettings(d.settings["fuel"], fuel)) { console.error(d.qLcInstrumentCluster, "SIMULATION changing fuel is not possible: provided: " + fuel + " constraint: " + IfSimulator.constraint(d.settings["fuel"])); return; } console.log(d.qLcInstrumentCluster, "SIMULATION fuel changed to: " + fuel); backend.fuel = fuel } function setTemperature(temperature) { if ("temperature" in d.settings && !IfSimulator.checkSettings(d.settings["temperature"], temperature)) { console.error(d.qLcInstrumentCluster, "SIMULATION changing temperature is not possible: provided: " + temperature + " constraint: " + IfSimulator.constraint(d.settings["temperature"])); return; } console.log(d.qLcInstrumentCluster, "SIMULATION temperature changed to: " + temperature); backend.temperature = temperature } function setSystemType(systemType) { if ("systemType" in d.settings && !IfSimulator.checkSettings(d.settings["systemType"], systemType)) { console.error(d.qLcInstrumentCluster, "SIMULATION changing systemType is not possible: provided: " + systemType + " constraint: " + IfSimulator.constraint(d.settings["systemType"])); return; } console.log(d.qLcInstrumentCluster, "SIMULATION systemType changed to: " + systemType); backend.systemType = systemType } function setCurrentWarning(currentWarning) { if ("currentWarning" in d.settings && !IfSimulator.checkSettings(d.settings["currentWarning"], currentWarning)) { console.error(d.qLcInstrumentCluster, "SIMULATION changing currentWarning is not possible: provided: " + currentWarning + " constraint: " + IfSimulator.constraint(d.settings["currentWarning"])); return; } console.log(d.qLcInstrumentCluster, "SIMULATION currentWarning changed to: " + currentWarning); backend.currentWarning = currentWarning } }
首先,有一个settings
属性,它使用从IfSimulator.findData方法返回的值进行初始化,该方法采用IfSimulator.simulationData和字符串作为输入。simulationData
是以JavaScript对象表示的JSON文件。
《code translate="no">findData方法帮助我们从对这个接口感兴趣的数据中提取数据InstrumentCluster
。接下来的属性帮助接口了解是否设置了默认值。LoggingCategory
用于识别来自这个模拟文件的计算输出。
之后,实际行为通过实例化一个InstrumentClusterBackend
项并将其扩展为更多功能来定义。InstrumentClusterBackend
是向我们的 InstrumentCluster
QML前端类的接口。但是,除了前端外,这些属性也是可写的,以便可以将它们更改以提供有用的模拟。
每次前端实例连接到后端时,都会调用initialize()
函数。对于QML模拟也是如此:当initialize()
C++函数将其转发给QML实例时。这也适用于所有其他函数,例如属性的设置器和获取器或方法。有关更多详细信息,请参阅QIfSimulationEngine。
在QML的initialize()
函数中,我们调用IfSimulator.initializeDefault()
,从simulationData
对象中读取默认值并初始化所有属性。这是仅一次执行的,因为我们不希望在下次前端实例连接到后端时将属性重置为默认值。最后,调用基本实现以确保向前端发送initializationDone
信号。
类似地,为每个属性定义了一个setter函数;它们使用IfSimulator.checkSettings()
从simulationData
读取特定约束设置,并检查这些约束对新值是否有效。如果这些约束无效,则使用IfSimulator.constraint()
向用户提供一个有意义的错误消息。
定义我们自己的QML模拟
如上所述,InstrumentClusterBackend
项目提供了我们QFace文件的所有属性。这可以通过更改属性为我们想要的值来模拟行为。这种方法的最简单形式就是值赋值,但这将会非常静态,并非我们想要 达到的。相反,我们使用QML动画对象来在时间上改变值。
ch4-simulation-behavior/backend_simulator/simulation.qml:
NumberAnimation { target: backend property: "speed" from: 0 to: 80 duration: 4000 }
上述代码示例将速度属性在4000秒内改为80,并模拟一辆加速的汽车。通过将其他属性的应用扩展到时间和并行动画,我们可以创建一个完整的模拟。
ch4-simulation-behavior/backend_simulator/simulation.qml:
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 = InstrumentClusterModule.warning("red","LOW FUEL", "images/fuelsymbol_orange.png") } } NumberAnimation { target: backend property: "speed" to: 0 duration: 5000 } ScriptAction { script: { backend.currentWarning = InstrumentClusterModule.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
属性提供一个良好的模拟,我们使用一个绑定,该绑定根据当前速度执行一些计算。完整的模拟文件看起来像这样
ch4-simulation-behavior/backend_simulator/simulation.qml:
import QtQuick import Example.If.InstrumentClusterModule.simulation QtObject { property var settings : IfSimulator.findData(IfSimulator.simulationData, "InstrumentCluster") property bool defaultInitialized: false property LoggingCategory qLcInstrumentCluster: LoggingCategory { name: "example.if.instrumentclustermodule.simulation.instrumentclusterbackend" } property var backend : InstrumentClusterBackend { function initialize() { console.log(qLcInstrumentCluster, "INITIALIZE") if (!defaultInitialized) { IfSimulator.initializeDefault(settings, backend) defaultInitialized = true } Base.initialize() } property int gearSpeed: 260 / 6 property int currentGear: speed / gearSpeed rpm : currentGear >= 1 ? 3000 + (speed % gearSpeed) / gearSpeed * 2000 : (speed % gearSpeed) / gearSpeed * 5000 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 = InstrumentClusterModule.warning("red","LOW FUEL", "images/fuelsymbol_orange.png") } } NumberAnimation { target: backend property: "speed" to: 0 duration: 5000 } ScriptAction { script: { backend.currentWarning = InstrumentClusterModule.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 } } } }
下一步是将我们的新模拟文件通知接口框架生成器和QIfSimulationEngine
。类似于QML文件,这里最好的方法是将模拟文件放入一个资源文件中。在我们的例子中,我们添加了一个名为simulation.qrc
的新文件,该文件包含了我们的simulation.qml
,使用/
前缀。
在我们的QFace文件中,现在需要将此位置以注释的形式添加
ch4-simulation-behavior/instrument-cluster.qface;
@config_simulator: { simulationFile: "qrc:/simulation.qml" } module Example.If.InstrumentClusterModule 1.0 ...
现在,重建模拟后端将模拟文件嵌入到插件中,并将其传递给QIfSimulationEngine
,加载时启动模拟。
第5章:添加结合QtRemoteObjects的模拟服务器
在本章中,我们将仪表盘扩展以使用进程间通信(IPC)机制,并使用两个进程。目前,模拟作为插件加载,使其成为同一服务的一部分。虽然这对小型示例应用来说已经足够好了,但这不是现代多进程架构中常用的方法,在这些架构中,多个进程需要能够访问相同的值并对其变化做出反应。我们可以编写第二个应用程序来使用相同的Middlewar eAPI。然而,我们只需要启动仪表盘两次并检查动画是否同步即可实现相同的效果。目前,它们并没有同步。
添加QtRemoteObjects集成
本例的IPC为
这是通过以下构建系统文件完成的:
CMake:
ch5-ipc/backend_qtro/CMakeLists.txt:
qt_add_plugin(ic_chapter5_qtro PLUGIN_TYPE interfaceframework) set_target_properties(ic_chapter5_qtro PROPERTIES LIBRARY_OUTPUT_DIRECTORY ../interfaceframework) # Interface Framework Generator: qt_ifcodegen_extend_target(ic_chapter5_qtro IDL_FILES ../instrument-cluster.qface TEMPLATE backend_qtro ) target_link_libraries(ic_chapter5_qtro PUBLIC libIc_chapter5 )
qmake:
ch5-ipc/backend_qtro/backend_qtro.pro
TEMPLATE = lib TARGET = $$qtLibraryTarget(instrumentcluster_qtro) DESTDIR = ../interfaceframework QT += core interfaceframework CONFIG += ifcodegen plugin LIBS += -L$$OUT_PWD/../ -l$$qtLibraryTarget(QtIfInstrumentCluster) INCLUDEPATH += $$OUT_PWD/../frontend QMAKE_RPATHDIR += $$QMAKE_REL_RPATH_BASE/../ IFCODEGEN_TEMPLATE = backend_qtro IFCODEGEN_SOURCES = ../instrument-cluster.qface PLUGIN_TYPE = interfaceframework
这些文件几乎与我们在仿真后端中使用的文件相同。目前我们突出显示哪些有所改变。
插件名称不以"_simulation"结尾,以表示这是一个“生产”后端。模板已更改为"backend_qtro",以生成一个使用Qt Remote Objects Replicas连接到Qt Remote Objects Source的后端来提供值。除了基于QtRemoteObject的后端之外,我们还需要一个基于QtRemoteObject的服务器。这部分也可以使用类似的方式通过接口框架生成器自动生成。
CMake:
ch5-ipc/simulation_server/CMakeLists.txt:
qt_add_executable(chapter5-ipc-server) set_target_properties(chapter5-ipc-server PROPERTIES RUNTIME_OUTPUT_DIRECTORY ../) # Interface Framework Generator: qt_ifcodegen_extend_target(chapter5-ipc-server IDL_FILES ../instrument-cluster.qface TEMPLATE server_qtro_simulator ) set_target_properties(chapter5-ipc-server PROPERTIES WIN32_EXECUTABLE TRUE MACOSX_BUNDLE FALSE ) target_link_libraries(chapter5-ipc-server PUBLIC libIc_chapter5 )
qmake:
ch5-ipc/simulation_server/simulation_server.pro:
TARGET = chapter5-ipc-server DESTDIR = .. QT = core interfaceframework QT -= gui CONFIG -= app_bundle CONFIG += ifcodegen LIBS += -L$$OUT_PWD/../ -l$$qtLibraryTarget(QtIfInstrumentCluster) INCLUDEPATH += $$OUT_PWD/../frontend IFCODEGEN_TEMPLATE = server_qtro_simulator IFCODEGEN_SOURCES = ../instrument-cluster.qface QML_IMPORT_PATH = $$OUT_PWD/qml
因为我们想生成服务器二进制文件,所以qmake TEMPLATE
需要设置为"app",而不是"lib"。在CMake中,我们使用qt_add_executable来替代。与插件类似,服务器也需要链接到我们的库,以使其能够访问定义的枚举、结构和其它类型。我们用来生成仿真服务器用的模板名为"server_qtro_simulator"。
重用现有仿真行为
现在,如果你首先启动服务器然后启动仪表盘,你将不会看到我们上一章中的仿真了。原因在于仿真代码是我们仿真后端的一部分,但后来我们添加了基于
因为使用了"server_qtro_simulator"生成模板,这就容易修复了,因为生成的服务器代码同样使用了QIfSimulationEngine,并支持使用与我们的仿真后端相同的仿真文件。我们只需要像之前一样扩展项目文件,并且也能够使用相同的资源文件。
CMake:
ch5-ipc/simulation_server/CMakeLists.txt:
# Resources: set(simulation_resource_files "../backend_simulator/simulation.qml" ) qt_add_resources(chapter5-ipc-server "simulation" PREFIX "/" BASE "../backend_simulator" FILES ${simulation_resource_files} )
qmake:
ch5-ipc/simulation_server/simulation_server.pro:
RESOURCES += ../backend_simulator/simulation.qrc
同样地,我们可以使用上一章节中定义的其他仿真数据JSON文件,通过使用相同的环境变量。我们只需要将其传递到服务器而不是我们的仪表盘应用程序。
让我们进行最终测试:启动两个仪表盘实例应该能够现在同步显示动画。
第6章:使用D-Bus开发生产后端
之前,我们通过使用
我们已经准备了一个工作状态为D-Bus服务器,它提供有限的仿真服务。
首先,让我们看看服务器的代码,看看在那里做了什么;然后编写连接到它的后端。
D-Bus服务器
如上所述,我们本章中使用D-Bus,已经有一个描述D-Bus接口的XML文件,类似于我们的QFace文件。
ch6-own-backend/demo_server/instrumentcluster.xml
<!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.If.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.If.InstrumentCluster"的接口。为了模拟一些值,我们将其保持简单,并使用计时器事件以每100毫秒改变一次速度值。然后,我们从0开始,一旦达到最大值250,就将rpm
值增加到5000。对于所有其他属性,我们提供硬编码的值。
ch6-own-backend/demo_server/instrumentcluster.cpp:
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后端
让我们从我们后端的构建系统文件开始。它与之前的文件非常相似,但它不使用接口框架生成器。相反,它使用DBUS_INTERFACES
让qmake自动生成一些客户端代码,该代码可以通过D-Bus发送和接收消息。在CMake的情况下,使用qt_add_dbus_interface函数完成相同的操作。
现在,我们需要为我们的插件定义一个入口点。此插件类需要从QIfServiceInterface派生并实现两个函数
QStringList interfaces()
– 返回该插件支持的所有接口的列表。QIfFeatureInterface *interfaceInstance(const QString &interface)
– 返回请求的接口的实例。
此外,我们还需要提供我们将要支持的一组接口作为插件元数据,以JSON文件的形式,如下所示
ch6-own-backend/backend_dbus/instrumentcluster_dbus.json:
{ "interfaces" : [ "Example.If.InstrumentClusterModule.InstrumentCluster" ] }
我们需要这个列表,因为它是回应QtInterfaceFramework在实例化和加载仅应用程序代码需要的插件之前,了解后端支持哪些接口的机会。
我们的插件代码如下所示
ch6-own-backend/backend_dbus/instrumentclusterplugin.cpp:
#include "instrumentclusterplugin.h" InstrumentClusterPlugin::InstrumentClusterPlugin(QObject *parent) : QObject(parent) , m_backend(new InstrumentClusterBackend) { } QStringList InstrumentClusterPlugin::interfaces() const { return QStringList(InstrumentClusterModule_InstrumentCluster_iid); } QIfFeatureInterface *InstrumentClusterPlugin::interfaceInstance(const QString &interface) const { if (interface == QStringLiteral(InstrumentClusterModule_InstrumentCluster_iid)) return m_backend; return nullptr; } #include "moc_instrumentclusterplugin.cpp"
在interfaces()
中,我们使用在自动生成的库中定义的instrumentclusterbackendinterface.h
中的IID。在interfaceInstance()
中,我们检查正确的字符串并返回我们实现的仪器集后端的实例。
此后端在instrumentclusterbackend.h
中定义,并从InstrumentClusterBackendInterface
派生。在我们的InstrumentClusterBackend
类中,我们需要实现来自InstrumentClusterBackendInterface及其派生类的所有纯虚函数。
对于我们的示例,这并不复杂,因为我们只需要实现initialize()函数。如果我们的XML文件会使用可写属性或方法,则还需要实现这些。我们不需要实现属性获取器,因为QtInterfaceFramework在初始化阶段使用更改信号以获取有关当前状态的信息。尽管生成的D-Bus接口类将提供获取器从我们的服务器检索属性,但不建议在开发后端时使用这些获取器。这些获取器是通过同步调用实现的,这意味着它们将阻塞事件循环,直到接收到客户端的答案。由于这可能导致性能问题,我们建议使用异步调用。
在我们的后端中,我们为每个实现的属性定义一个此类形式的fetch函数
ch6-own-backend/backend_dbus/instrumentclusterbackend.cpp:
void InstrumentClusterBackend::fetchSpeed() { m_fetchList.append(u"speed"_s); auto reply = m_client->asyncCall(u"speed"_s); 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(u"speed"_s); this->onSpeedChanged(reply.value()); watcher->deleteLater(); this->checkInitDone(); } }); }
首先,我们将属性添加到一个列表中,以便知道哪些属性已经成功获取。然后,我们使用 asyncCall()
函数来调用 speed
属性的 getter,并使用一个 QDBusPendingCallWatcher
来等待结果。一旦结果准备就绪,lambda 会再次将属性从我们的 fetchList
中移除,使用 onSpeedChanged()
函数来存储值,并通知前端。由于我们不再需要监视器,我们将在下一次事件循环运行中使用 deleteLater()
来删除它,并调用 checkInitDone()
函数。
checkInitDone()
函数定义如下:
ch6-own-backend/backend_dbus/instrumentclusterbackend.cpp:
void InstrumentClusterBackend::checkInitDone() { if (m_fetchList.isEmpty()) { qInfo() << "All properties initialized"; emit initializationDone(); } }
它确保在从服务器获取所有属性并且初始化完成后,将 initializationDone()
信号发送到前端。
除了从服务器检索当前状态,我们还需要在每次属性更改时通知我们的前端。这是通过在服务器更改其属性之一时发出相应的更改信号来完成的。为了处理此问题,我们为每个属性定义一个槽。这个槽将属性保存到我们的类中,并发出更改信号。
ch6-own-backend/backend_dbus/instrumentclusterbackend.cpp:
void InstrumentClusterBackend::onSpeedChanged(int speed) { m_speed = speed; emit speedChanged(speed); }
在初始化阶段,相同的槽也用于保存和发射值。
您可能会 wonder 为什么保存值是必需的,如果我们只需发出信号。这是因为后端插件直接由 InstrumentCluster
类的每个实例使用,每个实例都调用 initialize()
函数来检索当前状态。而不是再次获取所有属性,第二次 initialize()
调用仅发出已保存的值;而槽保持它们的更新。
现在,当我们启动仪表群集时,我们的后端应该连接到我们的 D-Bus 服务器,并看起来像这样:
第七章:编写一个带有 D-Bus 的后端模板
现在我们已经有了自己的生产后端准备好了,是一个好时机将其移动到一个 ifcodegen 模板中。对于这样一个简单的例子,保持手写后端完全可以,但是一旦您定义了多个模块和接口,维护样板代码就很多。
从零开始创建一个模板
开始新模板的最佳方式是将现有代码复制到新文件夹中,作为我们的模板。首先,我们创建一个 templates
文件夹,在该文件夹内创建一个名为 backend_dbus
的文件夹。这遵循了 ifcodegen 的模板命名约定,但您也可以使用任何其他名称。
下一步是重命名模板文件夹中的文件,使其更具通用性,并通过添加额外的 tpl
后缀来标识它们为模板。
模板文件夹现在应该看起来像这样:
- backend.cpp.tpl
- backend.h.tpl
- plugin.cpp.tpl
- plugin.h.tpl
- plugin.json.tpl
将所有文件转换为模板
让我们回顾一下模板中的所有文件,并使用特殊的 Jinja 模板语法 来扩展它们以满足我们的需求。
plugin.h.tpl
我们通常想做的第一件事是添加一个自动生成的特殊注释。这将将其标记为自助生成,不应手动编辑。
以下行包含一个预定义的注释文件(ifcodegen 的一部分):
ch7-own-template/templates/backend_dbus/plugin.h.tpl:
{% include "common/generated_comment.cpp.tpl" %}
注意: 包含文件中 .cpp
后缀表示评论旨在用于 C++ 文件。
接下来,我们想用模块名称派生出来的东西替换硬编码的 #ifdef
名称。首先,这是通过定义一些辅助变量来完成的。第一个变量命名为 class
,它使用一个由模块名称和静态文本 DBusPlugin
组成的格式字符串。
假设我们模块的名称是 Example.If.InstrumentClusterModule
,得到的字符串将是 InstrumentClusterModuleDBusPlugin
。
第二个变量也使用格式字符串,但是使用之前定义的 class
变量后面跟着一个管道符号。管道表示应将变量提供给函数,将变量作为输入进行处理以创建新输出。在 Jinja 中,这称为 filter。该过滤器名为 upper
,将使给定的模块名称全部转换为大写。
下一步是使用新定义的变量来替换硬编码的定义。为了使用变量并将其内容打印到自动生成的代码中,必须使用双大括号。所有不使用 Jinja 语法 的文本将按原样打印。考虑到这一点,我们可以保留包含语句不变,模板文件应如下所示
ch7-own-template/templates/backend_dbus/plugin.h.tpl:
#ifndef {{oncedefine}} #define {{oncedefine}} ... #endif // {{oncedefine}}
以同样的方式,我们继续将所有出现的硬编码的类名替换为 {class}
,并将硬编码的插件元数据 JSON 替换为 {module.module_name|lower}
。
类声明现在将如下所示
ch7-own-template/templates/backend_dbus/plugin.h.tpl:
class {{class}} : public QObject, public QIfServiceInterface { Q_OBJECT Q_PLUGIN_METADATA(IID QIfServiceInterface_iid FILE "{{module.module_name|lower}}.json") Q_INTERFACES(QIfServiceInterface) public: explicit {{class}}(QObject *parent = nullptr); QStringList interfaces() const override; QIfFeatureInterface* interfaceInstance(const QString &interface) const override; private: QVector<QIfFeatureInterface *> m_interfaces; };
你可能已经注意到,我们现在不是使用硬编码指向 InstrumentClusterBackend
的指针,而是使用 QVector<QIfFeatureInterface
*>。这是必要的,因为一个模块可以有多个接口,插件需要为所有这些接口存储实例。这可以通过使用向量而不是硬编码的值来轻松实现。
plugin.cpp.tpl
以类似的方式,可以使用 Jinja 语法 扩展 plugin.cpp.tpl
。它首先包含生成的注释并定义 class
变量。除了包括自动生成的插件头文件外,我们还需要包括所有后端类的头文件。如前所述,一个模块可以有多个接口。为了为模块中每个接口生成包含语句,使用 Jinja 循环
ch7-own-template/templates/backend_dbus/plugin.cpp.tpl:
{% for interface in module.interfaces %} #include "{{interface|lower}}dbusbackend.h" {% endfor %}
之后,我们继续定义插件构造函数,并使用循环来填充包含后端类实例的 m_instances
向量。
此外,为了插件能够正确工作,需要实现 interfaces()
和 interfaceInstance()
方法。对于返回其提供的接口列表的 interfaces()
方法,也需要使用循环。在循环内,使用 Jinja 的 if 语句来决定是否需要在循环的开始和结束打印额外的内容。
完整的插件定义现在如下所示
ch7-own-template/templates/backend_dbus/plugin.cpp.tpl:
{{class}}::{{class}}(QObject *parent) : QObject(parent) { {% for interface in module.interfaces %} m_interfaces << new {{interface}}DBusBackend(this); {% endfor %} } QStringList {{class}}::interfaces() const { QStringList list; {% for iface in module.interfaces %} {% if loop.first %} list{% endif %} << {{module.module_name|upperfirst}}_{{iface}}_iid{% if loop.last %};{% endif %} {% endfor %} return list; } QIfFeatureInterface *{{class}}::interfaceInstance(const QString &interface) const { int index = interfaces().indexOf(interface); return index < 0 ? nullptr : m_interfaces.at(index); }
backend.h.tpl 和 backend.cpp.tpl
后端类文件遵循与插件相同的模式。所有抓取方法都使用类似于这样的循环生成
ch7-own-template/templates/backend_dbus/backend.h.tpl:
{% for property in interface.properties %} void fetch{{property|upperfirst}}(); {% endfor %}
以类似的方式生成更改值的槽。但由于这些槽作为参数接受属性,我们需要生成这部分内容。这是通过使用名为 parameter_type 的过滤器来完成的。
ch7-own-template/templates/backend_dbus/backend.h.tpl:
{% for property in interface.properties %} void on{{property|upperfirst}}Changed({{property|parameter_type}}); {% endfor %}
最后,还需要使用for循环来生成所有成员变量。要将属性属性的QFace IDL类型转换为正确的C++类型,我们使用return_type过滤器。
在源文件中,我们使用qDBusRegisterMetaType注册所有的枚举和结构体,并在initialize()
函数中将硬编码的propertyChanged
方法调用替换为Jinja for循环。
ch7-own-template/templates/backend_dbus/backend.cpp.tpl:
{{class}}::{{class}}(QObject *parent) : {{interface}}BackendInterface(parent) , m_client(nullptr) { {% for struct in module.structs %} qDBusRegisterMetaType<{{struct}}>(); {% endfor %} {% for enum in module.enums %} qDBusRegisterMetaType<{{module.module_name}}::{{enum|flag_type}}>(); {% endfor %} } void {{class}}::initialize() { if (!m_client) setupConnection(); if (m_fetchList.isEmpty()) { {% for property in interface.properties %} emit {{property}}Changed(m_{{property}}); {% endfor %} emit initializationDone(); } }
其余代码相应地移植,如下所示
ch7-own-template/templates/backend_dbus/backend.cpp.tpl:
void {{class}}::setupConnection() { qInfo() << "Connecting to the Server"; m_client = new {{interface.tags.config_dbus.className}}(u"{{interface.tags.config_dbus.interfaceName}}"_s, u"/"_s, QDBusConnection::sessionBus()); {% for property in interface.properties %} connect(m_client, &{{interface.tags.config_dbus.className}}::{{property}}Changed, this, &{{class}}::on{{property|upperfirst}}Changed); {% endfor %} {% for property in interface.properties %} void fetch{{property|upperfirst}}(); {% endfor %} } {% for property in interface.properties %} void {{class}}::fetch{{property|upperfirst}}() { m_fetchList.append(u"{{property}}"_s); auto reply = m_client->asyncCall(u"{{property}}"_s); auto watcher = new QDBusPendingCallWatcher(reply, this); connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) { QDBusPendingReply<{{property|return_type}}> reply = *watcher; if (reply.isError()) { qCritical() << reply.error(); } else { m_fetchList.removeAll(u"{{property}}"_s); this->on{{property|upperfirst}}Changed(reply.value()); watcher->deleteLater(); this->checkInitDone(); } }); } {% endfor %} void {{class}}::checkInitDone() { if (m_fetchList.isEmpty()) { qInfo() << "All properties initialized"; emit initializationDone(); } } {% for property in interface.properties %} void {{class}}::on{{property|upperfirst}}Changed({{property|parameter_type}}) { m_{{property}} = {{property}}; emit {{property}}Changed({{property}}); } {% endfor %}
plugin.json
为了正确加载插件,我们还需要生成plugin.json
文件,具体操作如下
ch7-own-template/templates/backend_dbus/plugin.json.tpl:
{ "interfaces" : [ {% for interface in module.interfaces %} "{{interface.qualified_name}}"{% if not loop.last %},{%endif%} {% endfor%} ] }
添加新的注解
如果你自己完成了移植所有文件的练习,你可能已经注意到了后端代码中并非所有内容都可以从QFace模块或接口名称中推导出来。
因为我们仍然想使用由qdbusxml2cpp生成的DBus接口,类名和dbus接口名由instrumentcluster.xml
文件给出,一旦我们在IDL文件中添加了额外的接口,我们还需要为该接口添加DBus XML。
这意味着,我们需要为每个接口在IDL文件中提供附加信息。这可以通过在接口中添加一个新的注解来实现
ch7-own-template/instrument-cluster.qface:
@config_dbus: { xml: "../demo_server/instrumentcluster.xml", interfaceName: "Example.If.InstrumentCluster", className: "ExampleIfInstrumentClusterInterface" } interface InstrumentCluster { ...
现在,由于这些信息是idl文件的一部分,我们也可以在模板中这样访问它
ch7-own-template/templates/backend_dbus/backend.cpp.tpl:
m_client = new {{interface.tags.config_dbus.className}}(u"{{interface.tags.config_dbus.interfaceName}}"_s, u"/"_s, QDBusConnection::sessionBus());
完成模板
为了完成模板,我们需要创建更多文件。
构建系统模板文件
目前,ifcodegen支持QMake和CMake作为构建系统。对于每一种,我们需要提供额外的文件,让构建系统知道如何生成和编译我们的代码。
对于QMake,我们添加一个plugin.pri.tpl
ch7-own-template/templates/backend_dbus/plugin.pri.tpl:
{% include "common/generated_comment.qmake.tpl" %} HEADERS += \ {% for interface in module.interfaces %} $$PWD/{{interface|lower}}dbusbackend.h \ {% endfor %} $$PWD/{{module.module_name|lower}}dbusplugin.h SOURCES += \ {% for interface in module.interfaces %} $$PWD/{{interface|lower}}dbusbackend.cpp \ {% endfor %} $$PWD/{{module.module_name|lower}}dbusplugin.cpp {% for interface in module.interfaces %} {{interface}}.files = {{interface.tags.config_dbus.xml}} {{interface}}.header_flags += -i dbus_conversion.h DBUS_INTERFACES += {{interface}} {% endfor %}
在那里,我们将所有生成的C++文件分别添加到HEADERS
和SOURCES
变量中。然后,我们添加一些额外的代码来为我们的模块中的每个接口生成DBus接口。
你可能想知道,为什么实际的文件名与模板名不同?我们在查看CMake集成后会解释这一点
ch7-own-template/templates/backend_dbus/CMakeLists.txt.tpl:
{% include "common/generated_comment.cmake.tpl" %} qt6_set_ifcodegen_variable(${VAR_PREFIX}_SOURCES {% for interface in module.interfaces %} ${CMAKE_CURRENT_LIST_DIR}/{{interface|lower}}dbusbackend.cpp {% endfor %} ${CMAKE_CURRENT_LIST_DIR}/{{module.module_name|lower}}dbusplugin.cpp ) qt6_set_ifcodegen_variable(${VAR_PREFIX}_LIBRARIES Qt6::DBus Qt6::InterfaceFramework ) if (TARGET ${CURRENT_TARGET}) {% for interface in module.interfaces %} set_source_files_properties({{interface.tags.config_dbus.xml}} PROPERTIES INCLUDE dbus_conversion.h) qt_add_dbus_interface(${VAR_PREFIX}_SOURCES {{interface.tags.config_dbus.xml}} {{interface|lower}}_interface ) {% endfor %} target_sources(${CURRENT_TARGET} PRIVATE ${${VAR_PREFIX}_SOURCES} ) target_include_directories(${CURRENT_TARGET} PRIVATE $<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}> ) target_link_libraries(${CURRENT_TARGET} PRIVATE ${${VAR_PREFIX}_LIBRARIES} ) endif()
对于CMake,它的工作方式略有不同,因为ifcodegen提供了两种不同的集成机制。首先,使用qt6_set_ifcodegen_variable调用,将所有所需信息保存为ifcodegen变量。所有变量都必须以${VAR_PREFIX}_
前缀。当导入文件时,CMake设置变量VAR_PREFIX
,并使用qt_ifcodegen_import_variables导入变量。
集成CMake的第二种方式是qt_ifcodegen_extend_target。在这种情况下,设置变量${CURRENT_TARGET}
,并使用先前定义的变量调用所需的cmake函数,例如
ch7-own-template/templates/backend_dbus/CMakeLists.txt.tpl:
target_sources(${CURRENT_TARGET} PRIVATE ${${VAR_PREFIX}_SOURCES} )
创建一个生成YAML文件
为了使ifcodegen生成有用的内容,仍缺失一个重要信息。它不知道生成的文件应该如何命名,或者一个文件是否应该只生成一次或按模块生成。
所有这些内容都定义在 生成 YAML 文件中,该文件以模板命名,位于同一目录下
ch7-own-template/templates/backend_dbus.yaml:
backend_dbus: module: documents: - "{{module.module_name|lower}}dbusplugin.h": "plugin.h.tpl" - "{{module.module_name|lower}}dbusplugin.cpp": "plugin.cpp.tpl" - "{{module.module_name|lower}}.json": "plugin.json.tpl" - "{{srcBase|lower}}.pri": "plugin.pri.tpl" - '{{srcBase|lower}}.cmake': 'CMakeLists.txt.tpl' interface: documents: - '{{interface|lower}}dbusbackend.h': 'backend.h.tpl' - '{{interface|lower}}dbusbackend.cpp': 'backend.cpp.tpl'
首先,YAML 定义了对于 IDL 文件中的每个模块,应该生成一些文件。每个文档都由输出文件名(使用 Jinja 语法)组成,其后跟着输入模板文件名。
对于 IDL 文件中每个接口应该生成的所有文件,操作方式相同
ch7-own-template/templates/backend_dbus.yaml:
interface: documents: - '{{interface|lower}}dbusbackend.h': 'backend.h.tpl' - '{{interface|lower}}dbusbackend.cpp': 'backend.cpp.tpl'
在构建系统中使用新模板
为了使用新模板,您需要将其集成到构建系统中
CMake:
ch7-own-template/backend_dbus/CMakeLists.txt:
qt_ifcodegen_extend_target(ic_chapter7_dbus IDL_FILES ../instrument-cluster.qface TEMPLATE ../templates/backend_dbus )
QMake:
ch7-own-template/backend_dbus/backend_dbus.pro:
IFCODEGEN_TEMPLATE = $$PWD/../templates/backend_dbus IFCODEGEN_SOURCES = ../instrument-cluster.qface
您需要提供模板的完整路径,或者将模板文件夹添加到 模板搜索路径 中,而不是仅仅使用模板名称。
© 2024 Qt 公司有限公司。此处包含的文档贡献是各自所有者的版权。此处提供的文档是根据自由软件基金会发布的 GNU 自由文档许可证版本 1.3 的条款许可的。Qt 和相关标志是芬兰以及/或全球其他国家的 Qt 公司的商标。所有其他商标均为各自所有者的财产。