如何集成QML与C++?

如何集成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看起来象这样:

  1. #ifndef APPMANAGER_H 
  2. #define APPMANAGER_H 
  3.  
  4. #include <QObject> 
  5.  
  6. class AppManager : public QObject 
  7. { 
  8. Q_OBJECT 
  9. Q_PROPERTY(bool isNightMode READ isNightMode WRITE setIsNightMode NOTIFY isNightModeChanged) 
  10.  
  11. public: 
  12. explicit AppManager(QObject *parent = nullptr); 
  13.  
  14. bool isNightMode() const; 
  15. void setIsNightMode(bool isNightMode); 
  16.  
  17. signals: 
  18. void isNightModeChanged(); 
  19.  
  20. private: 
  21. bool m_isNightMode = false; 
  22. }; 
  23.  
  24. #endif // APPMANAGER_H 

main.cpp文件(或其它可以访问Qml 引擎的地方)实例化类对象,并使用引擎顶级上下文的方法:QQmlContext::setContextProperty(const QString &name, QObject *value) 来将其暴露。需要为方法传入即将暴露给Qml的可访问的对象名,以及指向该对象的指针。main.cpp大概是这个样子:

  1. #include <QGuiApplication> 
  2. #include <QQmlApplicationEngine> 
  3. #include <QQmlContext> 
  4. #include <AppManager.h> 
  5.  
  6. int main(int argc, char *argv[]) 
  7. { 
  8. QGuiApplication app(argc, argv); 
  9.  
  10. QQmlApplicationEngine engine; 
  11.  
  12. // exposing C++ object to Qml 
  13. AppManager *appManager = new AppManager(&app); 
  14. engine.rootContext()->setContextProperty("appManager", appManager); 
  15.  
  16. const QUrl url(u"qrc:/testapp/main.qml"_qs); 
  17. QObject::connect(&engine, &QQmlApplicationEngine::objectCreated, 
  18. &app, [url](QObject *obj, const QUrl &objUrl) { 
  19. if (!obj && url == objUrl) 
  20. QCoreApplication::exit(-1); 
  21. }, Qt::QueuedConnection); 
  22. engine.load(url); 
  23.  
  24. return app.exec(); 
  25. } 

大概就这样。你所需要做的就是调用setContextProperty() 方法。这样,就可以从Qml中访问C++ 对象了。 本例中,还会写一个简单的Qml 代码来根据appManager 的isNightMode属性值来更改应用的主题。应用会有一个按钮,以允许用户来更改属性值。

  1. import QtQuick 
  2. import QtQuick.Controls 
  3.  
  4. Window { 
  5. id: root 
  6.  
  7. readonly property color darkColor: "#218165" 
  8. readonly property color lightColor: "#EBEBEB" 
  9.  
  10. width: 280 
  11. height: 150 
  12. visible: true 
  13. title: qsTr("Expose C++ object test") 
  14.  
  15. color: root.lightColor 
  16.  
  17. Column { 
  18. anchors.centerIn: parent 
  19. spacing: 20 
  20.  
  21. Text { 
  22. id: resultText 
  23. color: root.darkColor 
  24. } 
  25.  
  26. Button { 
  27. anchors.horizontalCenter: parent.horizontalCenter 
  28.  
  29. text: qsTr("Start operation") 
  30. palette.buttonText: root.darkColor 
  31.  
  32. onClicked: { 
  33. appManager.performOperation() 
  34. } 
  35. } 
  36. } 
  37.  
  38. Connections { 
  39. target: appManager 
  40.  
  41. function onOperationFinished(result) { 
  42. resultText.text = "Operation result: " + result 
  43. } 
  44. } 
  45. } 

如你所见,C++ 对象的属性既可读又可写。因为宏Q_PROPERTY里的isNightModeChanged() 信号,文本内容及其颜色会自动调整。当程序运行时,效果如下:

如何调用C++ 对象的方法及处理其信号?

为了能够在C++ 对象上运行方法,需要告知元对象 meta-object 系统,这个方法是存在的。这可以通过把方法声明在public slots下或用宏Q_INVOKABLE来标记方法。信号signal需要被放在类的 signals 下。
据此,我们假定从Qml中代理C++ 的某些耗时或数据操作密集 的方法。那么,会添加performOperation方法和operationFinished(const QString &operationResult)信号。方法会停5秒钟来模拟耗时操作,然后发出带有随机值的信号。
类现在长这样:

  1. #ifndef APPMANAGER_H 
  2. #define APPMANAGER_H 
  3.  
  4. #include <QObject> 
  5.  
  6. class AppManager : public QObject 
  7. { 
  8. Q_OBJECT 
  9. Q_PROPERTY(bool isNightMode READ isNightMode WRITE setIsNightMode NOTIFY isNightModeChanged) 
  10.  
  11. public: 
  12. explicit AppManager(QObject *parent = nullptr); 
  13.  
  14. bool isNightMode() const; 
  15. void setIsNightMode(bool isNightMode); 
  16.  
  17. public slots: 
  18. void performOperation(); 
  19.  
  20. signals: 
  21. void isNightModeChanged(); 
  22. void operationFinished(const QString &operationResult); 
  23.  
  24. private: 
  25. bool m_isNightMode = false; 
  26. }; 
  27.  
  28. #endif // APPMANAGER_H 

向元对象meta-object系统告知performOperation方法的存在,是很重要的。否则,从Qml中调用此方法时,会得到如下提示:

TypeError: Property ‚performOperation’ of object AppManager(0x6000027f8ca0) is not a function

这是方法本身可能的样子:

  1. void AppManager::performOperation() 
  2. { 
  3. QTimer *timer = new QTimer(this); 
  4. timer->setSingleShot(true); 
  5.  
  6. connect(timer, &QTimer::timeout, this, [this]() { 
  7. const int result = QRandomGenerator::global()->generate(); 
  8. const QString &operationResult = result % 2 == 0 
  9. ? "success" 
  10. : "error"; 
  11.  
  12. emit operationFinished(operationResult); 
  13. }); 
  14.  
  15. timer->start(5000); 
  16. } 

这时,就可以从Qml中访问C++ 类的方法和信号了。一起来写下Qml代码。

  1. import QtQuick 
  2. import QtQuick.Controls 
  3.  
  4. Window { 
  5. id: root 
  6.  
  7. readonly property color darkColor: "#218165" 
  8. readonly property color lightColor: "#EBEBEB" 
  9.  
  10. width: 280 
  11. height: 150 
  12. visible: true 
  13. title: qsTr("Expose C++ object test") 
  14.  
  15. color: root.lightColor 
  16.  
  17. Column { 
  18. anchors.centerIn: parent 
  19. spacing: 20 
  20.  
  21. Text { 
  22. id: resultText 
  23. color: root.darkColor 
  24. } 
  25.  
  26. Button { 
  27. anchors.horizontalCenter: parent.horizontalCenter 
  28.  
  29. text: qsTr("Start operation") 
  30. palette.buttonText: root.darkColor 
  31.  
  32. onClicked: { 
  33. appManager.performOperation() 
  34. } 
  35. } 
  36. } 
  37.  
  38. Connections { 
  39. target: appManager 
  40.  
  41. function onOperationFinished(result) { 
  42. resultText.text = "Operation result: " + result 
  43. } 
  44. } 
  45. } 

当程序启动时,运行效果如下:

记住,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++ 类。带宏的类长得象这样:

  1. #ifndef APPMANAGER_H 
  2. #define APPMANAGER_H 
  3.  
  4. #include <QObject> 
  5. #include <qqml.h> 
  6.  
  7. class AppManager : public QObject 
  8. { 
  9. Q_OBJECT 
  10. Q_PROPERTY(bool isNightMode READ isNightMode WRITE setIsNightMode NOTIFY isNightModeChanged) 
  11. QML_ELEMENT 
  12.  
  13. public: 
  14. explicit AppManager(QObject *parent = nullptr); 
  15.  
  16. .... 
  17. }; 
  18.  
  19. #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文件中会被用到,它看上去是这样:

  1. qt_add_qml_module(testapp 
  2. URI testapp 
  3. VERSION 1.0 
  4. QML_FILES main.qml 
  5. ) 

为了向Qml注册C++ 类,需要通过这个CMake方法,指定添加到Qml模块的C++ 源文件列表。向下面的例子这样设置SOURCES参数。

  1. qt_add_qml_module(testapp 
  2. URI testapp 
  3. VERSION 1.0 
  4. QML_FILES main.qml 
  5. SOURCES AppManager.h AppManager.cpp 
  6. ) 

在Qml文件中导入模块后,就可以象实例化其它Qml类型那样实例化AppManager类了。

  1. import QtQuick 
  2. import QtQuick.Controls 
  3. import testapp // own module 
  4.  
  5. Window { 
  6. id: root 
  7.  
  8. readonly property color darkColor: "#218165" 
  9. readonly property color lightColor: "#EBEBEB" 
  10.  
  11. width: 280 
  12. height: 150 
  13. visible: true 
  14. title: qsTr("Expose C++ object test") 
  15.  
  16. color: appManager.isNightMode ? root.darkColor : root.lightColor 
  17.  
  18. Column { 
  19. anchors.centerIn: parent 
  20. spacing: 20 
  21.  
  22. Text { 
  23. color: appManager.isNightMode ? root.lightColor : root.darkColor 
  24. text: qsTr("Is night mode on? - ") + appManager.isNightMode 
  25. } 
  26.  
  27. Button { 
  28. anchors.horizontalCenter: parent.horizontalCenter 
  29.  
  30. text: qsTr("Change mode") 
  31. palette.buttonText: appManager.isNightMode ? root.lightColor : root.darkColor 
  32.  
  33. // change isNightMode on clicked 
  34. onClicked: { 
  35. appManager.isNightMode = !appManager.isNightMode 
  36. } 
  37. } 
  38. } 
  39.  
  40. AppManager { 
  41. id: appManager 
  42. } 
  43. } 

如何向Qml注册不可实例化的C++ 类?

有些场景下,不想让C++ 类实例化为Qml类型。然而,你仍想可以从Qml中能识别它。本文中将仅介绍一种方法:QML_UNCREATABLE(reason) 宏。它用于这种场景:当C++ 类有枚举或绑定属性,想从Qml中访问这些枚举或绑定属性,而不是类本身时。
这个宏应该紧挨着QML_ELEMENT宏来使用,如下:

  1. #ifndef APPMANAGER_H 
  2. #define APPMANAGER_H 
  3.  
  4. #include <QObject> 
  5. #include <qqml.h> 
  6.  
  7. class AppManager : public QObject 
  8. { 
  9. Q_OBJECT 
  10. Q_PROPERTY(bool isNightMode READ isNightMode WRITE setIsNightMode NOTIFY isNightModeChanged) 
  11. QML_ELEMENT 
  12. QML_UNCREATABLE("AppManager is for C++ instantiation only") 
  13.  
  14. public: 
  15. explicit AppManager(QObject *parent = nullptr); 
  16.  
  17. .... 
  18. }; 
  19.  
  20. #endif // APPMANAGER_H 

如果试图在Qml中实例化AppManager,将会显示错误原因。
关于注册C++ 类的其它方面,如注册单例或枚举,将在“如何集成C++ 和Qml”这一系列的下篇文章中涉及。现在,你可以考虑看下Qt官方关于这个话题的参考

总结

本文学习了如何暴露C++ 对象,以及将C++ 类注册为Qml类型。理解如何恰当的实现非常关键。几乎所有真正的Qt项目都会用到它。因此,如果你已经学习了这些知识,最好把它用在实际中。如果想获得更多关于Qt Qml开发的经验,可以读下关于如何编写整洁的Qml代码
在“如何集成C++ 和Qml”这一系列的下篇文章中,将会涉及将C++ 注册为单例。请继续关注和等待有关此主题的未来博客文章!

参考:How to integrate Qml and C++ ?

posted @ 2022-04-23 03:00  sammy621  阅读(454)  评论(0编辑  收藏  举报