如何集成QML与C++?
本文是关于如何向Qml暴露C++ 对象和注册C++ 类 这一系列教程的第一篇文章。这一系列的教程名字就叫“ 如何集成C++ 和Qml ”。在Qt软件开发中,使用Qt 6这一新版本来恰当和轻松地实现这一关键机制,还不够清晰。特别是有不少朋友正从qmake转为CMake。因此,我们认为这恰好是个好机会,来说清楚Qml和C++ 集成的各种方式的细节。
接下来的博文会涵盖诸如模型和插件,但现在我们先聚焦于基础一些的,来说清楚如何从qml访问C++ 对象和如何向qml注册C++ 类。
为什么要集成Qml和C++ ?
相比包括C++ 在内的其它语言,Qml无疑是非常漂亮的。大量当前的应用中的效果特性,只用Qml就可以实现。对于HTTP网络交互可以使用JavaScript的XmlHttpRequest,并且有象列表模型ListModel这样的Qml项来保存数据。这可能会吸引人们使用Qml,尤其是对于新的Qt开发人员。但是,仅用Qml写过几次应用后,会面临维护问题。
那么,简单来说,集成Qml和C++ 有啥好处?
- 在Qml的UI代码和在C++ 中的应用程序逻辑代码有清晰的分界。这意味着较好的可维护性。
- 可以访问大量以前仅以C++ API才能访问的Qt模块和特性。
- 通过C++ 与Android、Objective-C 或 C可以访问操作系统平台特性。
- 可以使用经Qt包装的纯C或C++ 库来实现特定特性。
- 有更高的性能表现。特别是在使用C++ 和多线程实现密集操作时。无论如何,必须承认Qml是经过优化的。
如何暴露C++ 对象给Qml?
首先要知道如何从Qml来访问C++ 对象。什么时候需要这么做?比如,当你的实现逻辑在C++ 类中,但你想要在Qml中访问该类的实例的属性、方法和信号。这在Qt 程序开发场景中非常流行。
在Qml中访问C++ 类的属性
现在开始学习如何在Qml中访问C++ 类的属性。首先需要从QObject
来继承一个新的类,并使用Q_OBJECT
宏。本文会创建一个AppManager
类,打算以它作为app的事件中心。这个类会保留整个app是否处于暗黑模式的信息。为此,要用宏Q_PROPERTY (type name READ getFunction WRITE setFunction NOTIFY notifySignal) 来添加属性。
这个宏的标准用法,要求传入属性的类型、名称、获取值的方法名、设置值的方法名以及值变更信号的名字。因此,类中的属性声明大概长这个样子:
Q_PROPERTY(bool isNightMode READ isNightMode WRITE setIsNightMode NOTIFY isNightModeChanged)
现在要做的就是在类定义中添加实际的方法和信号。好在是,无须自己来做这些事。把光标放在属性上,并点击 Alt + Enter
会调出重构菜单。可以看到自动生成成员属性的菜单选项。
注意
可以在Qt Creator 快捷键找到更多有用的快捷键
在完善语法格式后的类AppManager
看起来象这样:
- #ifndef APPMANAGER_H
- #define APPMANAGER_H
- #include <QObject>
- class AppManager : public QObject
- {
- Q_OBJECT
- Q_PROPERTY(bool isNightMode READ isNightMode WRITE setIsNightMode NOTIFY isNightModeChanged)
- public:
- explicit AppManager(QObject *parent = nullptr);
- bool isNightMode() const;
- void setIsNightMode(bool isNightMode);
- signals:
- void isNightModeChanged();
- private:
- bool m_isNightMode = false;
- };
- #endif // APPMANAGER_H
在main.cpp
文件(或其它可以访问Qml 引擎的地方)实例化类对象,并使用引擎顶级上下文的方法:QQmlContext::setContextProperty(const QString &name, QObject *value) 来将其暴露。需要为方法传入即将暴露给Qml的可访问的对象名,以及指向该对象的指针。main.cpp
大概是这个样子:
- #include <QGuiApplication>
- #include <QQmlApplicationEngine>
- #include <QQmlContext>
- #include <AppManager.h>
- int main(int argc, char *argv[])
- {
- QGuiApplication app(argc, argv);
- QQmlApplicationEngine engine;
- // exposing C++ object to Qml
- AppManager *appManager = new AppManager(&app);
- engine.rootContext()->setContextProperty("appManager", appManager);
- const QUrl url(u"qrc:/testapp/main.qml"_qs);
- QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
- &app, [url](QObject *obj, const QUrl &objUrl) {
- if (!obj && url == objUrl)
- QCoreApplication::exit(-1);
- }, Qt::QueuedConnection);
- engine.load(url);
- return app.exec();
- }
大概就这样。你所需要做的就是调用setContextProperty() 方法。这样,就可以从Qml中访问C++ 对象了。 本例中,还会写一个简单的Qml 代码来根据appManager 的isNightMode属性值来更改应用的主题。应用会有一个按钮,以允许用户来更改属性值。
- import QtQuick
- import QtQuick.Controls
- Window {
- id: root
- readonly property color darkColor: "#218165"
- readonly property color lightColor: "#EBEBEB"
- width: 280
- height: 150
- visible: true
- title: qsTr("Expose C++ object test")
- color: root.lightColor
- Column {
- anchors.centerIn: parent
- spacing: 20
- Text {
- id: resultText
- color: root.darkColor
- }
- Button {
- anchors.horizontalCenter: parent.horizontalCenter
- text: qsTr("Start operation")
- palette.buttonText: root.darkColor
- onClicked: {
- appManager.performOperation()
- }
- }
- }
- Connections {
- target: appManager
- function onOperationFinished(result) {
- resultText.text = "Operation result: " + result
- }
- }
- }
如你所见,C++ 对象的属性既可读又可写。因为宏Q_PROPERTY里的isNightModeChanged() 信号,文本内容及其颜色会自动调整。当程序运行时,效果如下:
如何调用C++ 对象的方法及处理其信号?
为了能够在C++ 对象上运行方法,需要告知元对象 meta-object 系统,这个方法是存在的。这可以通过把方法声明在public slots下或用宏Q_INVOKABLE来标记方法。信号signal需要被放在类的 signals 下。
据此,我们假定从Qml中代理C++ 的某些耗时或数据操作密集 的方法。那么,会添加performOperation
方法和operationFinished(const QString &operationResult)
信号。方法会停5秒钟来模拟耗时操作,然后发出带有随机值的信号。
类现在长这样:
- #ifndef APPMANAGER_H
- #define APPMANAGER_H
- #include <QObject>
- class AppManager : public QObject
- {
- Q_OBJECT
- Q_PROPERTY(bool isNightMode READ isNightMode WRITE setIsNightMode NOTIFY isNightModeChanged)
- public:
- explicit AppManager(QObject *parent = nullptr);
- bool isNightMode() const;
- void setIsNightMode(bool isNightMode);
- public slots:
- void performOperation();
- signals:
- void isNightModeChanged();
- void operationFinished(const QString &operationResult);
- private:
- bool m_isNightMode = false;
- };
- #endif // APPMANAGER_H
向元对象meta-object系统告知performOperation
方法的存在,是很重要的。否则,从Qml中调用此方法时,会得到如下提示:
TypeError: Property ‚performOperation’ of object AppManager(0x6000027f8ca0) is not a function
这是方法本身可能的样子:
- void AppManager::performOperation()
- {
- QTimer *timer = new QTimer(this);
- timer->setSingleShot(true);
- connect(timer, &QTimer::timeout, this, [this]() {
- const int result = QRandomGenerator::global()->generate();
- const QString &operationResult = result % 2 == 0
- ? "success"
- : "error";
- emit operationFinished(operationResult);
- });
- timer->start(5000);
- }
这时,就可以从Qml中访问C++ 类的方法和信号了。一起来写下Qml代码。
- import QtQuick
- import QtQuick.Controls
- Window {
- id: root
- readonly property color darkColor: "#218165"
- readonly property color lightColor: "#EBEBEB"
- width: 280
- height: 150
- visible: true
- title: qsTr("Expose C++ object test")
- color: root.lightColor
- Column {
- anchors.centerIn: parent
- spacing: 20
- Text {
- id: resultText
- color: root.darkColor
- }
- Button {
- anchors.horizontalCenter: parent.horizontalCenter
- text: qsTr("Start operation")
- palette.buttonText: root.darkColor
- onClicked: {
- appManager.performOperation()
- }
- }
- }
- Connections {
- target: appManager
- function onOperationFinished(result) {
- resultText.text = "Operation result: " + result
- }
- }
- }
当程序启动时,运行效果如下:
记住,C++ 槽不需要不需要象例子中的用法那样使用void。它实际上可以有返回值,但如果预期其执行会比较耗时,通过使用信号来通知其结果是比较好的方式。否则,可能会阻塞GUI界面线程。
如何向Qml注册C++ 类?
C++ 类可以注册为Qml类型,这样就可以在Qml代码中象使用其它Qml数据类型那样使用C++ 类了。如果不这样做,Qt Creator不会显示关于类属性和方法的任何提示,这样编程体验会更加困难。
其中有两种基本方法为C++ 注册类。可以注册一个可实例化的C++ 类或一个非实例化的C++ 类。有何不同呢?可实例化的类可以象其它Qml类型那样创建和使用。结果是,可以不用先在C++ 中实例化一个类对象,再使用context将其暴露给Qml。
如何向Qml注册可实例化的C++ 类?
为了能象其它Qml数据类型那样使用,需要将宏QML_ELEMENT或宏QML_NAMED_ELEMENT 放在类定义中。可以引入<qqml.h>以获得这些宏。这些宏是从Qt 5.15引入的,使得可以更直接地注册C++ 类。带宏的类长得象这样:
- #ifndef APPMANAGER_H
- #define APPMANAGER_H
- #include <QObject>
- #include <qqml.h>
- class AppManager : public QObject
- {
- Q_OBJECT
- Q_PROPERTY(bool isNightMode READ isNightMode WRITE setIsNightMode NOTIFY isNightModeChanged)
- QML_ELEMENT
- public:
- explicit AppManager(QObject *parent = nullptr);
- ....
- };
- #endif // APPMANAGER_H
QML_ELEMENT
最初是从 Qt 5.15 为qmake引入的,这在使用CMake时常遇到问题。幸好,在Qt 6.2中引入了QML_ELEMENT
与CMake配合使用的解决方案。CMake的方法 qt_add_qml_method()就是解决方案。当使用Qt Creator’s New Project向导来创建新的工程时, 在默认的CMakeLists.txt
文件中会被用到,它看上去是这样:
- qt_add_qml_module(testapp
- URI testapp
- VERSION 1.0
- QML_FILES main.qml
- )
为了向Qml注册C++ 类,需要通过这个CMake方法,指定添加到Qml模块的C++ 源文件列表。向下面的例子这样设置SOURCES
参数。
- qt_add_qml_module(testapp
- URI testapp
- VERSION 1.0
- QML_FILES main.qml
- SOURCES AppManager.h AppManager.cpp
- )
在Qml文件中导入模块后,就可以象实例化其它Qml类型那样实例化AppManager类了。
- import QtQuick
- import QtQuick.Controls
- import testapp // own module
- Window {
- id: root
- readonly property color darkColor: "#218165"
- readonly property color lightColor: "#EBEBEB"
- width: 280
- height: 150
- visible: true
- title: qsTr("Expose C++ object test")
- color: appManager.isNightMode ? root.darkColor : root.lightColor
- Column {
- anchors.centerIn: parent
- spacing: 20
- Text {
- color: appManager.isNightMode ? root.lightColor : root.darkColor
- text: qsTr("Is night mode on? - ") + appManager.isNightMode
- }
- Button {
- anchors.horizontalCenter: parent.horizontalCenter
- text: qsTr("Change mode")
- palette.buttonText: appManager.isNightMode ? root.lightColor : root.darkColor
- // change isNightMode on clicked
- onClicked: {
- appManager.isNightMode = !appManager.isNightMode
- }
- }
- }
- AppManager {
- id: appManager
- }
- }
如何向Qml注册不可实例化的C++ 类?
有些场景下,不想让C++ 类实例化为Qml类型。然而,你仍想可以从Qml中能识别它。本文中将仅介绍一种方法:QML_UNCREATABLE(reason) 宏。它用于这种场景:当C++ 类有枚举或绑定属性,想从Qml中访问这些枚举或绑定属性,而不是类本身时。
这个宏应该紧挨着QML_ELEMENT宏来使用,如下:
- #ifndef APPMANAGER_H
- #define APPMANAGER_H
- #include <QObject>
- #include <qqml.h>
- class AppManager : public QObject
- {
- Q_OBJECT
- Q_PROPERTY(bool isNightMode READ isNightMode WRITE setIsNightMode NOTIFY isNightModeChanged)
- QML_ELEMENT
- QML_UNCREATABLE("AppManager is for C++ instantiation only")
- public:
- explicit AppManager(QObject *parent = nullptr);
- ....
- };
- #endif // APPMANAGER_H
如果试图在Qml中实例化AppManager,将会显示错误原因。
关于注册C++ 类的其它方面,如注册单例或枚举,将在“如何集成C++ 和Qml”这一系列的下篇文章中涉及。现在,你可以考虑看下Qt官方关于这个话题的参考
总结
本文学习了如何暴露C++ 对象,以及将C++ 类注册为Qml类型。理解如何恰当的实现非常关键。几乎所有真正的Qt项目都会用到它。因此,如果你已经学习了这些知识,最好把它用在实际中。如果想获得更多关于Qt Qml开发的经验,可以读下关于如何编写整洁的Qml代码。
在“如何集成C++ 和Qml”这一系列的下篇文章中,将会涉及将C++ 注册为单例。请继续关注和等待有关此主题的未来博客文章!