C++和QML之间的分布关系
特点/比较维度 | QML | C++ Qt |
语言和语法 | 基于JavaScript和JSON的声明性语言 | 基于C++的面向对象编程库 |
适用领域 | 丰富的图形用户界面(GUI) | 复杂业务逻辑和底层功能 |
性能 | 相对较低,因为基于解释型语言 | 较高,因为基于编译型语言 |
开发效率 | 较高,语法简洁且易于学习 | 较低,适用于复杂功能开发 |
社区和支持 | 较新,可能资源和支持相对较少 | 庞大的开发者社区和丰富的资源 |
集成 | 可与C++ Qt共同使用 | 可与QML共同使用 |
在同一可执行文件中,C++和QML共享同一个进程,但是它们运行在不同的线程上。
在Qt中,C++和QML都是在同一个进程中运行的,因为QML只是一种声明式的UI语言,它通过Qt Quick框架解析和渲染,最终也是由C++代码实现的。因此,当我们编译并运行一个Qt应用程序时,只会生成一个可执行文件,在该可执行文件运行时,C++和QML都是在同一个进程中运行的。
当一个Qt应用程序启动时,其主线程会负责初始化QML引擎、创建C++对象、加载QML文件等操作。在此过程中,QML引擎会解析QML文件,将其转换为C++对象,并通过C++与QML之间的绑定实现交互。因此,虽然C++和QML的代码是分离的,但它们是在同一个进程中运行的,可以通过信号和槽、属性绑定、函数调用等方式进行通信。
在Qt中,GUI线程是Qt的主线程,负责处理GUI事件和更新UI界面。因此,QML中的所有UI操作都必须在GUI线程中执行,否则会引发线程安全问题。而C++代码可以在任何线程中执行,但是需要注意线程安全问题。
当我们在C++中调用QML中的方法或属性时,Qt会自动将该调用转发到GUI线程中执行。同时,当我们在QML中调用C++中的方法或属性时,Qt也会自动将该调用转发到C++所在的线程中执行。这种线程切换是由Qt自动完成的,我们不需要手动干预。
总之,虽然C++和QML运行在不同的线程中,但是它们之间的交互是由Qt自动完成的,我们只需要遵守Qt的线程安全规则即可。
C++和QML之间的通讯方式
属性绑定(Property Binding)
信号与槽:可以通过信号与槽:进行双向通讯(Signals and Slots)
直接调用(Direct Call){基于_INVOKABLE}
上下文属性(Context Property){基于Q_PROPERTY}
Qt Remote Objects
标准IPC通讯(Inter-Process Communication)
几种通讯方式的对比
方式 | 优点 | 缺点 | 适用场景 |
Property绑定 | 简单易用、代码量少、实时响应自动更新属性值,适合实现简单的交互。 | 只能在QML中读取和写入C++对象的属性,无法直接调用C++对象的函数。 | 适用于只需要传递属性值的场景 |
Signal/Slot | 可以传递复杂类型的参数, 可以在QML中直接调用C++对象的函数,同时也可以实现C++对象向QML发送消息。 |
需要编写一些额外的代码来处理信号和槽,代码量相对较多,不够自动化 | 数据量小,通讯频繁的场景, 实时交互的场景,如UI元素状态变化的反馈等 |
Q_INVOKABLE | 可以实现双向通信,可以直接调用C++函数 | 只能读取和写入属性,无法调用C++对象的函数 | 需要频繁调用C++函数的场景, 快速调用C++函数的场景,如计算等 |
Q_PROPERTY | 支持属性自动同步,线程安全 | 无法直接在QML中修改复杂数据类型,不支持多线程异步调用 | 属性数据量较小,需要频繁同步的场景 |
Context属性 | 可以实现双向通信,可以像JavaScript对象一样使用C++对象 | 需要手动编写C++类并注册,不够自动化 | 适用于需要在QML中使用C++对象的场景,如复杂业务逻辑的处理等 |
Qt Remote Objects | 支持远程通讯,支持多线程异步调用 | 实现较为复杂,需要对网络编程有一定了解 | 分布式应用场景 |
Qt Remote Objects (QtRO) 和标准 IPC (Inter-Process Communication) 之间的对比
优点:
- QtRO:
高度可扩展性和灵活性,支持跨进程、跨计算机的通信。
通过元对象系统进行数据序列化和远程方法调用,可以在不同的平台和编程语言之间进行通信。
支持 Qt Quick 的集成和信号-槽机制,简化了开发人员的工作。
提供了方便的工具和库,使得开发和调试更加容易。- 标准IPC:
比较通用,可以使用多种不同的通信协议和方式,例如管道、套接字、共享内存等。
相对简单,可以用于小规模应用或快速原型开发。
比较成熟,有大量的第三方库和工具可供使用。
缺点:
- QtRO:
学习曲线较陡峭,需要掌握元对象系统和远程方法调用等概念。
不支持非 Qt 平台和语言,可能存在一定的局限性。
在跨网络较远的计算机之间通信时,可能存在较高的延迟和网络负载。- 标准IPC:
通信协议和方式的选择可能存在困难,需要根据具体需求进行选择和调整。
不支持 Qt Quick 的集成和信号-槽机制,需要手动实现通信和数据传递。
缺乏一些高级功能和工具,例如自动序列化、远程对象代理等。
Q_INVOKABLE和Q_PROPERTY的区别
Q_INVOKABLE
宏用于声明一个成员函数可以从外部调用,即可以通过Qt元对象系统调用,类似于C++中的public成员函数,但是可以通过元对象系统跨线程和跨进程调用。Q_INVOKABLE与QMetaObject::invokeMethod均由元对象系统唤起。
Q_INVOKABLE是个空宏,目的在于让moc识别。 使用Q_INVOKABLE来修饰成员函数,目的在于被修饰的成员函数能够被元对象系统所唤起。
例如,你可以使用Q_INVOKABLE宏声明一个槽函数,然后使用connect函数将该槽函数与信号连接起来。Q_PROPERTY
宏用于声明一个类的属性,并提供读写函数和通知函数(可选)。属性是一个类的状态或特性,可以被设置和读取。通过Q_PROPERTY,可以使用元对象系统来访问这些属性,并且可以将它们与Qt框架中的其他类进行交互。例如,你可以使用Q_PROPERTY宏声明一个类的颜色属性,并实现一个读取和设置该属性的函数。
这两个宏通常用于不同的情况,但是可以同时使用。如果你希望从外部调用一个类的函数并且需要将该类的状态作为属性访问,则可以使用Q_INVOKABLE和Q_PROPERTY宏来实现这两个功能。
因此,
Q_PROPERTY
和Q_INVOKABLE
的作用不同,前者是用于定义C++类的属性,后者是用于将C++类的函数暴露给QML。
需要注意的是,Q_PROPERTY
也可以使用READ、WRITE、NOTIFY等参数来指定属性的读写方式和通知机制,而Q_INVOKABLE
没有这些参数。
总的来说,Q_PROPERTY
和Q_INVOKABLE
是Qt框架中用于定义C++类成员的宏,它们的用途不同,分别用于定义属性和函数,并且都可以在QML中使用。
属性绑定
可以在 C++ 代码中使用
Q_PROPERTY
宏定义一个属性,并且通过QObject::setProperty()
方法设置属性的值,而在 QML 中,您可以使用 Binding 来绑定 QML 中的属性和 C++ 中的属性。这样,当 C++ 中的属性发生变化时,QML 中的属性也会相应地发生变化;反过来,当 QML 中的属性发生变化时,C++ 中的属性也会相应地发生变化。
需要注意的是,实现双向绑定,需要在 C++ 代码中使用
QQmlProperty
实例对象来设置和获取 QML 中的属性,而不是使用QObject::setProperty()
和QObject::property()
方法。
因为
QObject::setProperty()
和QObject::property()
方法只能设置和获取 C++ 对象自身的属性,而无法访问到 QML 中的属性。
信号和槽
通过定义 C++ 对象的信号和槽,可以在 QML 中监听并处理这些信号。这种方式实现了双向通信,即 C++ 对象可以向 QML 发送信号,而 QML 也可以向 C++ 对象发送信号。但需要注意的是,信号和槽的参数类型必须在 QML 中可识别,否则会报错。
直接调用
在 QML 中通过 C++ 对象的方法名直接调用 C++ 对象的方法是基于
_INVOKABLE
。在 C++ 中,使用_Q_INVOKABLE宏声明的成员函数可以在 QML 中使用方法名直接调用。
这些成员函数必须是公共的,并且它们必须符合一定的函数签名规则,才能在 QML 中使用。这种方法比较直接,但需要注意的是,只有在 C++ 对象已经被实例化并在 QML 中注册后才能进行调用。
Qt Remote Objects
Qt Remote Objects是Qt提供的一种远程通信机制,它基于Qt的信号槽机制和RPC(Remote Procedure Call)技术实现了跨进程、跨网络的通信。
Qt Remote Objects的通信方式可以分为两种:
信号槽机制:与本地信号槽机制类似,Qt Remote Objects也支持跨进程、跨网络的信号槽连接,可以实现异步通信和事件驱动。
远程调用:Qt Remote Objects还支持远程过程调用(RPC),可以实现跨进程、跨网络的同步调用和返回结果。
在使用Qt Remote Objects时,需要先定义一个远程对象(Remote Object),并将其注册到Qt远程对象服务器(Remote Object Host)上。远程对象可以包含信号和槽,也可以包含远程方法(Remote Method),通过远程方法可以实现跨进程、跨网络的函数调用。
Qt Remote Objects支持多种通信协议,如纯TCP、SSL/TLS、WebSocket等。可以根据需要选择不同的协议进行通信。
总之,Qt Remote Objects是一种强大的跨进程、跨网络通信机制,可以方便地实现分布式应用程序的开发。
上下文属性
在QML和C++之间通讯时,利用上下文属性(Context Property)是基于
Q_PROPERTY
,通过将 C++ 对象注册为 QML 引擎的上下文属性,可以在 QML 中访问该对象的属性和方法。这种方法实现了双向通信,但需要注意的是,上下文属性只能在 QML 引擎的主线程中使用,否则会导致线程错误。
代码示例:
- 属性绑定:使用Qt的QML C++绑定功能来将C++代码与QML界面进行交互
首先,您需要在C++代码中定义一个QObject派生类,然后将其注册到QML引擎中。
接下来可以在QML中使用该类的实例,并调用其公共槽和属性。
具体步骤如下:
- 在C++代码中定义一个QObject派生类,例如:
class MyObject : public QObject { Q_OBJECT public: Q_INVOKABLE void myMethod(QString param); Q_PROPERTY(QString myProperty READ myProperty WRITE setMyProperty NOTIFY myPropertyChanged) QString myProperty() const; void setMyProperty(const QString& value); signals: void mySignal(QString message); void myPropertyChanged(); private: QString m_myProperty; };
- 在main函数中注册该类到QML引擎中,例如:
QQmlApplicationEngine engine; qmlRegisterType<MyObject>("com.mycompany", 1, 0, "MyObject"); engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
- 在QML中使用该类的实例,并调用其公共槽和属性,例如:
import com.mycompany 1.0 MyObject { id: myObject myProperty: "Hello, World!" onMyPropertyChanged: console.log("myProperty changed to", myProperty) Component.onCompleted: myMethod("Hello from QML!") }
- Q_INVOKABLE :使用Qt的QML中的JavaScript API来调用C++的Qt Quick模块提供的接口
- 在C++中定义一个QObject子类,该子类包含一个或多个Q_INVOKABLE方法,这些方法将成为QML中可调用的接口。
- 在QML文件中导入C++模块并创建一个C++对象。
- 使用JavaScript API中的call()方法来调用C++对象的方法。
C++代码:
class MyObject : public QObject { Q_OBJECT public: Q_INVOKABLE void myMethod(QString arg); }; void MyObject::myMethod(QString arg) { qDebug() << "Called with argument:" << arg; }
QML代码:
import MyModule 1.0 MyObject { id: myObject } Button { text: "Call C++ method" onClicked: { myObject.myMethod("Hello, world!"); } }
在这个示例中,我们定义了一个名为
MyObject的C++
类,并将其导入到QML中。我们还在QML中创建了一个名为myObject的MyObject
对象,并在按钮的点击事件中调用了myMethod()
方法。
当按钮被点击时,myMethod()
方法将被调用,并将字符串“Hello, world!”
作为参数传递给它。该方法将打印出“Called with argument: Hello, world!”
的消息。
- 信号和槽机制 :在 QML 中,可以使用 Qt 的信号和槽机制来实现与 C++ 的交互
在 C++ 中定义一个槽函数:
class MyObject : public QObject { Q_OBJECT public slots: void mySlot() { qDebug() << "My slot is called"; } };
在 QML 中定义一个信号:
import QtQuick 2.0 Rectangle { signal mySignal() }
在 QML 中连接信号和槽:
import QtQuick 2.0 Rectangle { signal mySignal() Connections { target: myObject // myObject 是在 C++ 中创建的对象 onMySignal: myObject.mySlot() } MouseArea { anchors.fill: parent onClicked: mySignal() } }
当点击
MouseArea
时,会触发mySignal()
信号,然后在Connections
中将该信号连接到myObject
对象的 mySlot() 槽函数中。
- 上下文属性(Context Property):基于Q_PROPERTY进行上下文属性在QML和C++之间通讯.
我们可以在C++中定义一个QObject子类,并使用Q_PROPERTY宏定义一个属性:
class MyObject : public QObject { Q_OBJECT Q_PROPERTY(int value READ value WRITE setValue NOTIFY valueChanged) public: MyObject(QObject *parent = nullptr) : QObject(parent), m_value(0) {} int value() const { return m_value; } void setValue(int value) { m_value = value; emit valueChanged(); } signals: void valueChanged(); private: int m_value; };
然后,我们可以将MyObject类注册为QML类型,并将其实例化并绑定到QML中的一个上下文属性上:
qmlRegisterType<MyObject>("MyLib", 1, 0, "MyObject"); MyObject myObject; QQmlApplicationEngine engine; engine.rootContext()->setContextProperty("myObject", &myObject); engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
在QML中,我们就可以访问myObject的属性和方法
import MyLib 1.0 Text { text: "My object value: " + myObject.value MouseArea { anchors.fill: parent onClicked: myObject.setValue(myObject.value + 1) } }
- Qt Remote Objects(QRO):需要分为服务端和客户端两部分
服务端:
#include <QObject> #include <QtRemoteObjects> #include <QCoreApplication> class MyObject : public QObject { Q_OBJECT Q_PROPERTY(int value READ value WRITE setValue NOTIFY valueChanged) public: MyObject(QObject *parent = nullptr) : QObject(parent), m_value(0) { m_replica.setData(m_value); m_replica.setParent(this); connect(this, &MyObject::valueChanged, [this](){ m_replica.changeData(m_value); }); } int value() const { return m_value; } void setValue(int value) { m_value = value; emit valueChanged(); } signals: void valueChanged(); private: int m_value; QRemoteObjectReplica<MyObject> m_replica; }; int main(int argc, char **argv) { QCoreApplication app(argc, argv); // 注册MyObject为远程对象 QRemoteObjectHost host; host.enableRemoting(new MyObject(&app), "MyObject"); return app.exec(); }
服务端:
import QtQuick 2.15 import QtQuick.Controls 2.15 import QtRemoteObjects 1.0 ApplicationWindow { width: 400 height: 400 visible: true title: "My Object Viewer" RemoteObject { id: myObject node: "tcp://localhost:5555" typeName: "MyObject" } Column { anchors.centerIn: parent spacing: 20 Text { text: "Value: " + myObject.value } Button { text: "Increase" onClicked: myObject.setValue(myObject.value + 1) } } }
结语
在我们的编程学习之旅中,理解是我们迈向更高层次的重要一步。然而,掌握新技能、新理念,始终需要时间和坚持。从心理学的角度看,学习往往伴随着不断的试错和调整,这就像是我们的大脑在逐渐优化其解决问题的“算法”。
这就是为什么当我们遇到错误,我们应该将其视为学习和进步的机会,而不仅仅是困扰。通过理解和解决这些问题,我们不仅可以修复当前的代码,更可以提升我们的编程能力,防止在未来的项目中犯相同的错误。
我鼓励大家积极参与进来,不断提升自己的编程技术。无论你是初学者还是有经验的开发者,我希望我的博客能对你的学习之路有所帮助。如果你觉得这篇文章有用,不妨点击收藏,或者留下你的评论分享你的见解和经验,也欢迎你对我博客的内容提出建议和问题。每一次的点赞、评论、分享和关注都是对我的最大支持,也是对我持续分享和创作的动力。