【QML与C++混合编程】在 QML 中使用 C++ 类和对象(一)
Qt Quick 技术的引入,使得你能够快速构建 UI ,具有动画、各种绚丽效果的 UI 都不在话下。但它不是万能的,也有很多局限性,原来 Qt 的一些技术,比如低阶的网络编程如 QTcpSocket ,多线程等等,在 QML 中要么不可用,要么用起来不方便,所以呢,很多时候我们是会基于这样的原则来混合使用 QML 和 C++: QML 构建界面, C++ 实现非界面的业务逻辑和复杂运算。
我们知道, QML 其实是对 JavaScript 的扩展,融合了 Qt Object 系统,它是一种新的解释型的语言, QML 引擎虽然由 Qt C++ 实现,但 QML 对象的运行环境,说到底和 C++ 对象的上下文环境是不同的,是平行的两个世界。如果你想在 QML 中访问 C++ 对象,那么必然要找到一种途径来在两个运行环境之间建立沟通桥梁。
Qt 提供了两种在 QML 环境中使用 C++ 对象的方式:
(1)在 C++ 中实现一个类,注册到 QML 环境中, QML 环境中使用该类型创建对象。
(2)在 C++ 中构造一个对象,将这个对象设置为 QML 的上下文属性,在 QML 环境中直接使用该属性。
一、前言
在 QML 中使用 C++ 类和对象,大概需要分四步:
(1)实现 C++ 类;
(2)注册 QML 类型;
(3)在 QML 中导入类型;
(4)在 QML 创建由 C++ 导出的类型的实例并使用。
下面就分别说明这 4 个步骤。
首先我们需要创建一个 Qt Quick App ,新建两个文件, CppObject.h 和 CppObject.cpp 。QmlCallCpp 只是一个示例项目,我在 C++ 中实现一个 CppObject 类,它可以被注册为一个 QML 类型供 QML 像内建类型一样使用,它的实例也可以导出为 QML 上下文属性在 QML 中访问。
二、实现 C++ 类
前提条件
要想将一个类或对象导出到 QML 中,下列前提条件必须满足:
- 从 QObject 或 QObject 的派生类继承。
- 使用 Q_OBJECT 宏。
看起来好像和使用信号与槽的前提条件一样……没错,的确是一样的。这两个条件是为了让一个类能够进入 Qt 强大的元对象系统(meta-object system)中,只有使用元对象系统,一个类的某些方法或属性才可能通过字符串形式的名字来调用,才具有了在 QML 中访问的基础条件。
2.1 信号,槽
只要是信号或者槽,都可以直接在 QML 中访问,你可以把 C++ 对象的信号连接到 QML 中定义的方法上,也可以把 QML 对象的信号连接到 C++ 对象的槽上,还可以直接调用 C++ 对象的槽或信号。所以,这是最简单好用的一种途径。
下面是 CppObject 类的声明:
#ifndef CPPOBJECT_H
#define CPPOBJECT_H
#include <QObject>
// 需要派生自QObject
// 使用qmlRegisterType注册到QML中
class CppObject : public QObject
{
Q_OBJECT
public:
explicit CppObject(QObject *parent = nullptr);
signals:
// 信号:可以直接在QML中访问信号
void cppSignalA();//一个无参信号
void cppSignalB(const QString &str,int value); // 一个带参数信号
void nameChanged(const QString name);
void yearChanged(int year);
public slots:
// 槽函数:可以直接在QML中访问public槽函数
void cppSlotA();//一个无参槽函数
void cppSlotB(const QString &str,int value); // 一个带参数槽函数
private:
// 类的属性
QString myName;
int myYear;
};
#endif // CPPOBJECT_H
mian.qml 中的部分相关代码如下:
import QtQuick 2.9
import QtQuick.Window 2.9
// 引入我们注册的模块
import MyCppObject 1.0
Window {
id: root
visible: true
width: 500
height: 300
title: qsTr("QML调用Cpp对象")
color: "green"
// 作为一个QML对象
CppObject{
id: cpp_obj
//也可以像原生QML对象一样操作,增加属性之类的
property int counts: 0
onYearChanged: {
counts++
console.log('qml onYearChanged', counts)
}
onCountsChanged: {
console.log('qml onCountsChanged', counts)
}
}
// 关联信号与信号处理函数的方式同QML中的类型
Component.onCompleted: {
// 1. Cpp对象的信号关联到Qml的槽函数
// cpp_obj.onCppSignalA.connect(function() {console.log('qml signalA process')})
cpp_obj.onCppSignalA.connect(()=>console.log('qml signalA process')) // js的lambda
cpp_obj.onCppSignalB.connect(processB)
// 2. Qml对象的信号关联到Cpp的槽函数
root.onQmlSignalA.connect(cpp_obj.cppSlotA)
root.onQmlSignalB.connect(cpp_obj.cppSlotB)
}
}
我们定义了一些信号和槽函数,都可以在 QML 中直接使用。 另外,Cpp 对象的信号关联到了 Qml 的槽函数,同时 Qml 对象的信号关联到了 Cpp 的槽函数。
2.2 使用 Q_INVOKABLE 修饰函数
在定义一个类的成员函数时使用 Q_INVOKABLE 宏来修饰,就可以让该方法被元对象系统调用。这个宏必须放在返回类型前面。
我给 CppObject 添加了两个使用 Q_INVOKABLE 宏修饰的方法,现在 CppObject 类的声明变成了这个样子:
#ifndef CPPOBJECT_H
#define CPPOBJECT_H
#include <QObject>
// 需要派生自QObject
// 使用qmlRegisterType注册到QML中
class CppObject : public QObject
{
Q_OBJECT
public:
explicit CppObject(QObject *parent = nullptr);
// 函数:通过Q_INVOKABLE宏标记的public函数可以在QML中访问
Q_INVOKABLE void testFun(); // 功能为打印信息
// ...
private:
// 类的属性
QString myName;
int myYear;
};
#endif // CPPOBJECT_H
一旦你使用 Q_INVOKABLE 将某个方法注册到元对象系统中,在 QML 中就可以用${Object}.${method} 来
访问了:
// 调用Q_INVOKABLE宏标记的函数
cpp_obj.testFun()
2.3 Q_PROPERTY
Q_PROPERTY 宏用来定义可通过元对象系统访问的属性,通过它定义的属性,可以在 QML 中访问、修改,也可以在属性变化时发射特定的信号。
下面是 Q_PROPERTY 宏的原型:
Q_PROPERTY(type name
(READ getFunction [WRITE setFunction] |
MEMBER memberName [(READ getFunction | WRITE setFunction)])
[RESET resetFunction]
[NOTIFY notifySignal]
[REVISION int]
[DESIGNABLE bool]
[SCRIPTABLE bool]
[STORED bool]
[USER bool]
[CONSTANT]
[FINAL])
是不是很复杂?你可以为一个属性命名,可以设定的选项数超过10个……我是觉得有点儿头疼。不过,不是所有的选项都必须设定,看一个最简短的属性声明:
Q_PROPERTY(int x READ getX)
上面的声明定义了一个类型为 int 名为 x 的属性,通过方法 getX() 来访问。
选项说明
其实我们在实际使用中,很少能够用全 Q_PROPERTY 的所有选项,这里介绍些常用的选项:
-
type 是属性的类型,可以是 int、QString、QObject、QColor 等等,name 就是属性的名字。
-
READ 标记,如果你没有为属性指定 MEMBER 标记,则 READ 标记必不可少。声明一个读取属性的函数,该函数一般没有参数,返回定义的属性。
-
WRITE 标记,可选配置。声明一个设定属性的函数。它指定的函数,只能有一个与属性类型匹配的参数,必须返回 void 。
-
NOTIFY 标记,可选配置。给属性关联一个信号(该信号必须是已经在类中声明过的),当属性的值发生变化时就会触发该信号。信号的参数,一般就是你定义的属性。
代码
#ifndef CPPOBJECT_H
#define CPPOBJECT_H
#include <QObject>
// 需要派生自QObject
// 使用qmlRegisterType注册到QML中
class CppObject : public QObject
{
Q_OBJECT
// 属性:使用Q_PROPERTY注册属性,使之可以在QML中访问
Q_PROPERTY(QString name READ getName WRITE setName NOTIFY nameChanged)
Q_PROPERTY(int year READ getYear WRITE setYear NOTIFY yearChanged)
public:
explicit CppObject(QObject *parent = nullptr);
// 给类属性添加访问方法--myName
void setName(const QString &name);
QString getName() const;
// 给类属性添加访问方法--myYear
void setYear(int year);
int getYear() const;
// ...
private:
// 类的属性
QString myName;
int myYear;
};
#endif // CPPOBJECT_H
2.4 C++ 类的完整代码
在头文件中,定义了信号和 public 槽函数,以及 Q_INVOKABLE 宏标记的 public 函数,还通过 Q_PROPERTY 注册了两个属性,这些方法和属性之后都可以在 QML 中进行访问。CppObject.h 的内容如下:
#ifndef CPPOBJECT_H
#define CPPOBJECT_H
#include <QObject>
// 需要派生自QObject
// 使用qmlRegisterType注册到QML中
class CppObject : public QObject
{
Q_OBJECT
// 属性:使用Q_PROPERTY注册属性,使之可以在QML中访问
Q_PROPERTY(QString name READ getName WRITE setName NOTIFY nameChanged)
Q_PROPERTY(int year READ getYear WRITE setYear NOTIFY yearChanged)
public:
explicit CppObject(QObject *parent = nullptr);
// 给类属性添加访问方法--myName
void setName(const QString &name);
QString getName() const;
// 给类属性添加访问方法--myYear
void setYear(int year);
int getYear() const;
// 函数:通过Q_INVOKABLE宏标记的public函数可以在QML中访问
Q_INVOKABLE void testFun(); // 功能为打印信息
signals:
// 信号:可以直接在QML中访问信号
void cppSignalA();//一个无参信号
void cppSignalB(const QString &str,int value); // 一个带参数信号
void nameChanged(const QString name);
void yearChanged(int year);
public slots:
// 槽函数:可以直接在QML中访问public槽函数
void cppSlotA();//一个无参槽函数
void cppSlotB(const QString &str,int value); // 一个带参数槽函数
private:
// 类的属性
QString myName;
int myYear;
};
#endif // CPPOBJECT_H
为了测试方便,我给每个函数都加了一个打印语句,稍后会在 QML 中调用这些函数。CppObject.cpp 的内容如下:
#include "CppObject.h"
#include <QDebug>
CppObject::CppObject(QObject *parent)
: QObject(parent),
myName("none"),
myYear(0)
{
}
void CppObject::setName(const QString &name)
{
qDebug() << "CppObject::setName"<<name;
if(myName != name){
qDebug() << "emit nameChanged";
myName = name;
emit nameChanged(name);
}
}
QString CppObject::getName() const
{
qDebug() << "CppObject::getName";
return myName;
}
void CppObject::setYear(int year)
{
qDebug() << "CppObject::setYear" << year;
if(year != myYear){
qDebug() << "emit yearChanged";
myYear = year;
emit yearChanged(myYear);
}
}
int CppObject::getYear() const
{
qDebug() << "CppObject::getYear";
return myYear;
}
void CppObject::testFun()
{
// 测试用,调用该函数后打印信息
qDebug() << "CppObject::testFun";
}
void CppObject::cppSlotA()
{
qDebug() << "CppObject::cppSlotA";
}
void CppObject::cppSlotB(const QString &str, int value)
{
qDebug() << "CppObject::cppSlotB" << str << value;
}
二、注册一个 QML 中可用的类型
CppObject 已经就绪了,现在看看怎样将其注册为 QML 可以使用的类型。有以下两种方式:
1、C++定义方式(主要使用setContextProperty()函数)
- a)、比如我们有一个功能单一的 Configure 类,我们需要把它暴露给 QML,在使用之前必须要先创建类对象 m_configuration,就是说类实例化一次,QML 中可以直接使用这个类,注意功能单一的类只适合该方式;
- b)、比如我们的业务比较复杂,我们有很多类,若要供 QML 调用,我们就要写一个总的被调用类 Complex(包含所有的业务类),然后实例化一次这个 Complex,然后 QML 中直接使用实例化后的对象;
两种业务方式的使用方式如下:
// C++方式:也可以注册为qml全局对象
engine.rootContext()->setContextProperty("cppObj", new CppObject(qApp));
cppObj 便可直接在 qml 中使用,cppObj 自然也是一个全局变量。
2、QML定义方式(主要使用qmlRegisterType()函数)
该方式都是使用在业务复杂情况下,还是上面的例子,我们有一堆业务类,这个时候我们使用注册的方式,用在 QML 中定义的方式去定义各个实例,也就不用再需要一个总类:
// QML方式:qmlRegisterType注册C++类型至QML
// 参数:qmlRegisterType<C++类型名> (import时模块名 主版本号 次版本号 QML中的类型名)
qmlRegisterType<CppObject>("MyCppObject", 1, 0, "CppObject");
我们可以在 QML 中直接使用 CppObject 去定义实例:
// 引入我们注册的模块
import MyCppObject 1.0
// 作为一个QML对象
CppObject{
id: cpp_obj
}
qmlRegisterSingletonType()
用来注册一个单例类型, qmlRegisterType()
注册一个非单例的类型,这里只说常规的类型注册,其它请参考 Qt 帮助文档。
3、二者比较
与 C++ 方式相比,QML 方式具有如下优势:
- 变量名前面可以加 $(
全局变量可用),
从而方便区分全局变量和局部变量,这个在 C++ 定义属性的时候是不允许的; - 如果某个全局变量(一般是 QML 对象)构造很慢,可以通过 QML 中的 Loader 来很方便异步构造,从而加速程序启动。
下面是 CppObject 示例的 main.cpp 文件:
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include "CppObject.h"
int main(int argc, char *argv[])
{
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication app(argc, argv);
// QML方式:qmlRegisterType注册C++类型至QML
// 参数:qmlRegisterType<C++类型名> (import时模块名 主版本号 次版本号 QML中的类型名)
qmlRegisterType<CppObject>("MyCppObject", 1, 0, "CppObject");
QQmlApplicationEngine engine;
// C++方式:也可以注册为qml全局对象
//engine.rootContext()->setContextProperty("cppObj", new CppObject(qApp));
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
if (engine.rootObjects().isEmpty())
return -1;
return app.exec();
}
上面的代码将 CppObject 类注册为 QML 类 cppObj,主版本为 1 ,次版本为 0 ,而我起的类名称则是 CppObject。注册动作一定要放在 QML 上下文创建之前,否则的话,会无效。
三、在 QML 中导入 C++ 注册的类型
一旦你在 C++ 中注册好了 QML 类型,就可以在 QML 文档中引入你注册的类,然后使用注册的类型。要引入类,使用 import 语句。比如要使用我们注册的 ColorMaker 类,可以在 QML 文档中加入下面的 import 语句:
import MyCppObject 1.0
四、在 QML 中创建 C++ 导入类型的实例
引入 C++ 类后,你就可以在 QML 中创建 C++ 导入类型的对象了,与 QML 内建类型的使用完全一样。这里看下完整的 main.qml 文档:
import QtQuick 2.9
import QtQuick.Window 2.9
// 引入我们注册的模块
import MyCppObject 1.0
Window {
id: root
visible: true
width: 500
height: 300
title: qsTr("QML调用Cpp对象")
color: "green"
signal qmlSignalA
signal qmlSignalB(string str, int value)
//定义的函数可以作为槽函数
function processB(str, value){
console.log('qml function processB', str, value)
}
// 鼠标点击区域
MouseArea{
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: {
if(mouse.button === Qt.LeftButton){
console.log('----qml 点击左键:Cpp发射信号')
// 1.修改属性会触发set函数,获取值会触发get函数
cpp_obj.name = "gongjianbo"
cpp_obj.year = 1992
// 2.调用Q_INVOKABLE宏标记的函数
cpp_obj.testFun()
// 3.发射C++信号
cpp_obj.cppSignalA()
cpp_obj.cppSignalB("chenglong", 1995)
}
else{
console.log('----qml 点击右键:QML发射信号')
root.qmlSignalA()
root.qmlSignalB('gongjianbo', 1992)
}
}
}
// 作为一个QML对象
CppObject{
id: cpp_obj
//也可以像原生QML对象一样操作,增加属性之类的
property int counts: 0
onYearChanged: {
counts++
console.log('qml onYearChanged', counts)
}
onCountsChanged: {
console.log('qml onCountsChanged', counts)
}
}
// 关联信号与信号处理函数的方式同QML中的类型
Component.onCompleted: {
// 1. Cpp对象的信号关联到Qml的槽函数
// cpp_obj.onCppSignalA.connect(function() {console.log('qml signalA process')})
cpp_obj.onCppSignalA.connect(()=>console.log('qml signalA process')) // js的lambda
cpp_obj.onCppSignalB.connect(processB)
// 2. Qml对象的信号关联到Cpp的槽函数
root.onQmlSignalA.connect(cpp_obj.cppSlotA)
root.onQmlSignalB.connect(cpp_obj.cppSlotB)
}
}
这个示例很简单,点击鼠标左键调用 CppObject 的 testFun 函数来发送信号,QML 处理;点击鼠标右键 QML 发送信号,CppObject 处理,效果图如下:
五、代码下载
GitHub 下载链接:https://github.com/confidentFeng/QML_Demo/tree/master/QmlCallCpp
参考:
《Qt Quick核心编程》第11章
QML与C++集成<二>——<使用C++属性及注册QML类型>