QML 作为一种灵活高效的界面开发语言已经越来越得到业界的认可。QML 负责界面,C++ 负责逻辑,这也是 Qt 官方推荐的开发方式。那么 QML 与 C++ 的交互必然是需要我们掌握并且精通的。本 Chat 将详细介绍 QML 与 C++ 的几种交互方式,以及在项目中的实际应用方法。通过实际的例子来实现,体验并且应用这几种交互方式,为我们后续的产品开发提供便利。本 Chat 将分 4 大部分讲解:
- C++ 对象注册到 QML,QML 访问 C++ 对象;
- QML 暴露对象给 C++ 进行交互;
- C++ 创建 QML 对象并进行交互;
- C++ 对象与 QML 通过信号槽交互。
通过本 Chat 的阅读和交流,您将了解熟悉 QML 与 C++ 的交互方式,并且从中获得改造提升自身项目代码结构或者获得交互上的解惑顺利完成项目。
方式1:C++ 对象注册到元对象系统
QQmlApplicationEngine::rootContext()->setContextProperty()
方式2:C++ 对象注册到Qml系统
qmlRegisterType()
qmlRegisterSingletonType()
方式3:C++ 创建/获取Qml对象
QQmlComponent::create()
QQmlApplicationEngine::rootObjects()[0]->findChild<QObject*>()
Qml | 描述 | 关键词 |
1 | Q_INVOKABLE | |
2 | Q_PROPERTY | |
3 | public slots | |
4 | Connections |
C++ | 描述 | 关键词 |
1 | QMetaObject::invokeMethod | |
2 | QObject::setProperty() QQmlProperty | |
3 | function | |
4 | QObject::connect |
C++ 对象注册到元对象系统
首先我们新建一个工程。
然后我们新建一个 C++ 的类,然后就可以通过 C++的类和 main.qml 这界面进行交互了。
首先,右键工程,选择添加新文件...
选择 C++ Class
类名我们就随便写一个,QmlCpp。下面 Base class 基类我们选择 QObject,Include QObject 会被自动勾上。然后填写头文件名,源文件名。
这样就成功的添加了一个 C++ 的类进来。下面我们对当前的 C++ 类进行改造,增加两个函数, 一个函数用来保存整型,一个函数用来读取整型。
函数定义的前面多了一个 Q_INVOKABLE。官网是这么描述的: Q_INVOKABLE
从第一句可以看出来,这个宏是将函数申明为元对象系统可调用的函数。QtQuick 也在元对象系统内,所以也就可以访问这个函数了。接下来我们继续实现:
头文件里写好函数后,直接调用重构的功能,自动生成函数的实现结构。
然后在头文件里添加一个整型成员变量。
然后我们直接在函数中添加我们的实现代码:
C++ 类的代码就写的差不多了,接下来我们需要将这个 C++ 类注册到 Qml 中。打开 main.cpp:
在 Qml 引擎实例的下面,添加代码:
这时候我们会发现 Qml 引擎的上下文没法显示出方法,原来是没有 include,我们添加一下:
接着我们继续注册:
可以看出来, 这里需要填写注册的名称和对象指针。我们将 QmlCpp 的头文件引用进来,然后定义一个 QmlCpp 类的实例,调用设置整型的函数,并将这个 C++ 实例注册到 Qml 引擎上下文中标示为 “QmlCpp” 的名字, 这样 Qml 中就可以通过 QmlCpp 来访问这个 C++ 实例。
下面我们来改造 Qml 代码
Qml 中添加一个 button,按下事件添加代码 QmlCpp.getValue(); 通过这个代码来获取 C++ 实例中那个被保存的整型数据。代码修改完成, 运行一下看看效果:
可以看到达到了我们预期的效果,C++ 实例中的整型数据已经显示在了 Qml 中的按钮文本上。
下来我们再增加一个 button,添加调用 QmlCpp 设置整型的函数:
然后我们再运行一下看看效果:
可以看到 button1 已经起到了修改的作用。这就和我们理解的一致了,注册进元对象系统后, 不仅 C++ 可以调用,Qml 同样也可以调用。
C++ 对象注册到 Qml 系统
首先我们来看一个函数 qmlRegisterType :
描述的相当清楚了, 将 C++ 类型的对象注册到 Qml 系统中,并且还有版本号,方便版本管理。
我们试着将刚才的工程改改,用这种方式注册:
其中 “com.mycompany.qmlCpp” 这个名称是 Qml 中的组件名,后面是版本号,然后是用于 Qml 中的类名。
然后修改一下 Qml 文件:
导入刚才注册的组件,然后在 Qml 窗体空白区域增加一个对象,id 使用相同的 QmlCpp,这样下面按钮调用的代码我们就不用修改了。运行看看效果:
已经达到和方式1 一样的效果。可以看出来,这种方法多了一个重要的版本管理,而且 C++ 端注册的时候,C++ 类并没有实例化,而实例化是放到了 Qml 端来完成的。那么如果 C++ 类产生很多复杂的派生类,则在 Qml 端可以有一个很良好的调用环境。
接下来我们看看另外一种注册方式,注册单例:
这是单例注册的方式,首先创建一个单例函数
内容很简单,一个整型作为单例的属性值,这样方便我们 Qml 中直接访问
注释掉我们之前写的 QmlRegisterType,然后添加 QmlRegisterSingletonType 的注册方式,为了少改代码,我们组件名版本什么的都不变。最后一个参数是单例函数。
接下来是 Qml 的修改,注释掉 QmlCpp 的实例, 修改 button 和 button1 的按钮事件,一个获取值,一个累加值。
运行一下, 看看效果:
可以看出来,就是我们代码所希望的累加方式。
C++ 创建/获取Qml对象
首先新建一个名称为 QmlWindow.qml 的 Qml 文件
填写这些内容, 就是一个很简单的红色窗体
转到 main.cpp,增加一个头文件的引用 QQmlComponent,看名字也知道,这是 Qml 组件的头文件
注释掉上一章的 QmlRegisterSingletonType 单例注册,增加组件加载的相关代码
QQmlComponent qmlComponent(&engine); qmlComponent.loadUrl(QUrl(QStringLiteral("qrc:/QmlWindow.qml")));
QObject* qmlWindows = qmlComponent.create();
qmlWindows->setParent(engine.rootObjects()[0]);
engine.rootContext()->setContextProperty("qmlWindows", qmlWindows);
其中 engine.rootObjects()[0] 是指主窗体
接下来我们修改 Qml,运行一下看看效果
可以看出来,我们在 C++ 端创建的一个 Qml 窗体组件,已经成功的被 Qml 修改成了绿色~ 并且显示出来。
接下来,我们再对这个窗体做一个修改,在窗体内部增加一个 rectangle
并且将这个 rectangle 的名称(不是 ID)设置为 objectName: "rectangle"
转到 main.cpp,通过 findChild 查找子窗体的对象,就是刚刚我们加的"rectangle",然后也把它变成蓝色~
运行一下,看看效果:
Qml 调用 C++ 对象公共函数
这个专题我们前面其实已经提到了 Q_INVOKABLE,这个宏写在 C++ 公共函数定义的前面
Q_INVOKABLE void setValue(int nValue);
Q_INVOKABLE int getValue();
然后当 C++ 类被注册后,C++ 端和 Qml 端均可以调用这个接口了。
Qml 访问 C++ 对象属性
C++ 的属性如果需要被 Qml 访问,那么可以用 Q_PROPERTY 我们来看看实际怎么应用
Q_PROPERTY(QString value READ getValue WRITE setValue NOTIFY valueChanged)
C++ 类定义中增加一行代码,注意代码的位置。然后增加一个信号,这个信号后一章节会介绍,这节我们先关注属性的访问 这句代码的意思是定义一个名称为 value 的属性,读取属性的函数是 getValue,写入属性的函数是 setValue,当属性变化是触发的信号是 valueChanged。
转到 main.cpp,注释掉 QQmlComponent 相关代码,还原 engine.rootContext()->setContextProperty,重新用注册的方式
然后 Qml 端就可以用属性的方式对 C++ 对象进行访问了。
运行效果和预期一致
Qml 调用 C++ 对象槽函数
C++ 端首先弄一个槽函数
实现也写上
Qml 可以直接进行槽函数的调用, 因为槽函数本来就在元对象系统里,而 C++ 类我们已经注册进元对象系统里。
运行一下:
Qml 接收 C++ 对象信号
上上章我们在 C++ 端添加属性的时候随便添加了一个 valueChanged 的信号,然后我们看看这个信号 Qml 端如何接收。
首先我们修改一下 C++ 类的注册方式,改为注册 C++ 类到 Qml 系统,因为这样 Qml 端来进行 C++ 实例的创建
转到 main.cpp,注释掉 engine.rootContext()->setContextProperty 相关代码,打开 QmlRegisterType 代码:
C++ 属性的操作函数,我们加上触发信号:
Qml 端也恢复注册的代码:
然后根据 valueChanged 的信号名,前面添加上 "on"。里面写上信号触发后的执行代码,通过 value 可以直接给实例进行属性赋值。运行一下:
可以看到,我们点击 value 累加函数,属性值改变的通知也触发了 C++ 的信号,从而 Qml 端接收到了信号,改变了显示。
C++ 调用 Qml 对象槽函数
转到main.cpp,修改代码如下
使用 engine.rootContext()->setContextProperty 的方式注册 C++ 类,因为我们在 C++ 这边希望做实例化并且关联信号槽。打开 QQmlComponent 创建组件的代码,添加关键代码
QObject::connect(&qmlCpp, SIGNAL(valueChanged()), qmlWindows, SLOT(addData()));
这是信号槽的连接函数,可以看出来,C++ 实例和 Qml 的组件进行了信号与槽的关联。
修改 main.qml 代码:
修改 QmlWindow.qml 代码:
增加了一个 function,Qml 中的函数,运行一下
可以看到,属性值的改变触发了 valueChanged 信号,然后 Qml 端的 function 就被触发了修改了颜色
C++ 访问 Qml 对象属性
细心的读者应该发现了,前面的章节已经有这样的使用了
QObject* qmlRect = qmlWindows->findChild<QObject*>("rectangle");
qmlRect->setProperty("color", "blue");
这就是很典型的 C++ 端直接访问 Qml 对象的属性
C++ 调用 Qml 对象公共函数
C++ 直接调用 Qml 对象的公共函数,需要用到 QMetaObject::invokeMethod。
转到 main.cpp,注释掉 QObject::connect 信号槽的关联 添加代码:
QMetaObject::invokeMethod(qmlWindows, "addData");
意思是执行 qmlWindows 的 addData 函数,很简单吧:
运行一下:
可以看到 C++ 代码在启动执行就已经调用addData改变了颜色。
C++ 接收 Qml 对象信号
C++ 端添加一个信号槽:
实现就触发属性变化吧:
修改 QmlWindow.qml:
增加一个信号 signal valueChanged(); 增加一个 button 来触发这个信号。转到 main.cpp:
打开 QObject::connect代码,注释掉 QMetaObject::invokeMethod,然后添加关键代码:
QObject::connect(qmlWindows, SIGNAL(valueChanged()), &qmlCpp, SLOT(setColor()));
将 Qml 组件的 valueChanged 属性值改变关联到刚才添加的 C++ 槽函数上,运行看看效果:
可以看到,Qml 端的 valueChanged 信号触发了 C++ 端的 setColor 槽函数,然后 setColor 槽函数又触发了 Qml 端的 addData 公共函数(可以理解为槽函数),然后颜色值被成功修改。