• 如何集成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++ ?

  • 相关阅读:
    反射(8)程序集反射 Type 类
    反射(5)CLR 运行时探测程序集引用的步骤
    反射(1)程序集基础知识
    csc.exe(C# 编译器)
    证书(1)数字签名基础知识
    反射(7)动态程序集加载Load方法
    SignTool.exe(签名工具)
    反射(3)程序集加载 Assembly类
    关于卡巴斯基安全免疫区随笔
    文本提取工具 TextHelper
  • 原文地址:https://www.cnblogs.com/sammy621/p/16181342.html
Copyright © 2020-2023  润新知