C++与QML交互

C++加载QML的方式

QQmlApplicationEngine加载

QQmlApplicationEngine是 Qt Quick 应用程序中用于加载和执行 QML 文件的类。它是 Qt 框架的一部分,专门用于处理 QML 语言编写的用户界面和应用程序逻辑。QtCreater在加载QML文件时默认就是采用的这种方式。

  • 下面是QtCreater默认的加载方式。
1
2
3
4
5
6
7
8
9
10
QQmlApplicationEngine engine; //创建 QQmlApplicationEngine 对象
const QUrl url(QStringLiteral("qrc:/qmltest/Main.qml")); // QUrl用来存储qml文件的资源路径

QObject::connect( //连接引擎和应用的信号与槽,如果qml文件加载失败,应用程序就作出对应的操作
&engine,
&QQmlApplicationEngine::objectCreationFailed,
&app,
[]() { QCoreApplication::exit(-1); },
Qt::QueuedConnection);
engine.load(url); //加载qml文件资源
  • 加载自定义QML窗口,新建一个控件MyWindow,生成MyWindow.qml文件。
1
2
3
4
5
6
7
8
9
//MyWindow.qml
import QtQuick
import QtQuick.Controls
Window {
width: 1000
height: 800
visible: true
title: "hello myWindow"
}
  • 在main.cpp加载这个窗口。
1
2
3
4
5
6
7
8
9
10
11
#include <QQmlApplicationEngine> //需要包含头文件

//使用构造函数加载控件
QQmlApplicationEngine engine("qrc:/qmltest/MyWindow.qml");

//使用load方法加载。
QQmlApplicationEngine engine;
engine.load("qrc:/qmltest/MyWindow.qml");

//void load(const QUrl &url)
//void load(const QString &filePath)

QQuickView加载

QQuickView是Qt Quick模块中的一个类,它提供了一个窗口,用于显示Qt Quick用户界面。

  • 新建一个控件MyText,生成MyText.qml文件。
1
2
3
4
5
6
7
8
9
10
//MyText.qml
import QtQuick

Text {
width:200
height:100
anchors.centerIn: parent
text:"mytest"
}

  • 根对象必须是QQuickItem的子类QQuickView只支持加载从QQuickItem派生的根对象。
  • 避免使用Window或ApplicationWindow作为根对象:当使用QQuickView时,应避免在QML文件中使用WindowApplicationWindow作为根对象。因为QQuickView本身已经是一个窗口,如果在QML文件中再使用WindowApplicationWindow,会导致运行时弹出两个窗口。
  • 在main.cpp加载这个控件。
1
2
3
4
5
6
7
8
9
10
11
12
#include <QQuickView> //需要包含头文件

//使用构造函数加载控件
QQuickView view(QUrl("qrc:/qmltest/MyText.qml"));
view.show();

//使用setSource方法加载。
QQuickView view;
view.setSource(QUrl("qrc:/qmltest/MyText.qml"));
view.show();

// void QQuickView::setSource(const QUrl &url)

QQmlComponent加载

QQmlComponent 是 Qt Quick 中用于动态加载 QML 文件的类。它允许你在运行时加载 QML 组件,这在创建插件或需要根据用户选择动态更改界面时非常有用。

1
2
3
4
5
6
7
#include <QQmlComponent> //需要包含头文件
#include <QQmlEngine>

QQmlEngine eng; //用于管理多个QQmlComponent对象
QQmlComponent com(&eng); //创建QQmlComponent对象,并绑定QQmlEngine
com.loadUrl(QUrl("qrc:/qmltest/MyWindow.qml")); //加载qml文件,“MyWindow.qml”文件同上。
com.create(); //创建组件

三种方式加载怎么选择?

  • QQmlApplicationEngine:适合加载完整的应用程序,以 Window 或为根对象的 QML 文件。
  • QQuickView:适合在 C++ 应用程序中嵌入 QML 界面,Item(及以 Item 为根的组件)作为根对象。
  • QQmlComponent: 适合动态加载和创建 QML 组件,允许在运行时加载 QML 组件。

C++获取QML对象并修改

QQmlApplicationEngine获取rootObject

  • 新建一个MyWindow.qml文件,我们要对根对象Window的title属性以及它的子对象Rectange的color属性进行获取和修改。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import QtQuick
import QtQuick.Controls
Window { //根对象Window
width: 300
height: 100
visible: true
title: "hello myWindow"
Rectangle //子对象Rectangle
{
objectName: "myRectangle"
width: 100
height: 100
color:"red"
}
}

窗口运行图片

  • 通过C++代码使Window的title属性修改为hi myWindow,Rectange的color修改为green,并在控制台打印改完后的属性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//获取qml文件里面的所有根对象,并保存到QList<QObject*>中。
QList<QObject*> rootObjects = engine.rootObjects();

//如果QList容器为空,表示没有根对象,直接返回报错。
if(rootObjects.isEmpty())
{
return -1;
}

//得到第一个根对象的地址,即Window,用QObject*保存。
QObject* rootObject = rootObjects.first();

//setProperty设置根对象的属性,第一个参数是属性名,第二个参数是属性值。
rootObject->setProperty("title","hi myWindow");

//property获取根对象的属性,参数是属性名,返回的是QVarian类型(QVarian是Qt的通用类型),使用内置的toString()方法转换成字符串。
qDebug()<<rootObject->property("title").toString();

//findChild可以获取子对象,参数是在qml文件里相应子对象objectName的值,返回的也是QObject* 类型。
QObject* childObject = rootObject->findChild<QObject*>("myRectangle");

//下面设置和获取子对象的属性和上述一致。
childObject->setProperty("color","green");
qDebug()<<childObject->property("color").toString();

修改后窗口运行图片

控制台打印修改后的信息(绿色对应的十六进制是:”#008000”)

QQmlComponent获取rootObject

QQmlComponent在运行时动态加载 QML 文件,并获取其根对象rootObject。

1
2
3
4
5
6
QQmlEngine eng;
QQmlComponent com(&eng);
com.loadUrl(QUrl("qrc:/qmltest/MyWindow.qml"));
//在创建组件时,会返回对象地址
QObject* rootObject = com.create();
//剩下的对根对象以及和子对象的操作和 QQmlApplicationEngine一致
rootObject对象管理优化

使用std::unique_ptr智能指针,C++11以上

1
2
3
4
5
6
7
8
9
QQmlEngine eng;
QQmlComponent com(&eng);
com.loadUrl(QUrl("qrc:/qmltest/MyWindow.qml"));
//在创建组件时,会返回对象地址
std::unique_ptr<QObject> ct(
static_cast<QObject*>(com.create())
);
ct->setProperty("title","hi myWindow");
ct->findChild<QObject*>("myRectangle")->setProperty("color","green");

C++调用QML函数

在调用函数之前必须先获取对应的QML对象,如rootObject。先创建一个MyWindow.qml文件,写入几个函数测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import QtQuick
import QtQuick.Controls
Window {
width: 300
height: 100
visible: true
title: "myWindow"
Text
{
id:mytext
text: qsTr("这是一段文字")
}
function changeMyText1()
{
mytext.text=qsTr("C++调用了无参函数")
}
function changeMyText2(str:string)
{
mytext.text=str
}
function getString():string //这里返回值类型建议指定
{
return "qml函数返回的字符串"
}
}

注:QML不支持函数重载!!

  • 在C++文件调用无参函数changeMyText1()
1
QMetaObject::invokeMethod(rootObject,"changeMyText1"); 
  • 在C++文件调用有参函数changeMyText2(str:string)
1
QMetaObject::invokeMethod(rootObject,"changeMyText2",QString("C++调用了有参函数"));
  • C++文件里获取函数的返回值getString()
1
2
QString retValue; //用于接受函数的返回值
QMetaObject::invokeMethod(rootObject,"getString", qReturnArg(retValue));
invokeMethod函数介绍
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//invokeMethod函数有四个重载版本,下面我依次介绍。
//这四个重载函数共同点:返回值都是bool类型,表示调用是否成功,并且参数是模版类型,并且可以是多个。

[static, since 6.5]
template <typename... Args> bool QMetaObject::invokeMethod(QObject *obj, const char *member, Args &&... args)
//应用:无返回值的有参函数或者无参函数
//第一个参数:表示函数调用的qml对象。
//第二个参数:表示函数名,是个字符串(char*表示字符数组,以'\0'结尾)。
//第三个以及后面的多个参数:都表示传入函数的参数。

[static, since 6.5]
template <typename ReturnArg, typename... Args> bool QMetaObject::invokeMethod(QObject *obj, const char *member, QTemplatedMetaMethodReturnArgument<ReturnArg> ret, Args &&... args)
//应用:有返回值的有参函数或者无参函数
//第一个参数:表示函数调用的qml对象。
//第二个参数:表示函数名,是个字符串。
//第三个参数:这是个传出参数,是函数的返回值,需要函数外定义变量来接受传出的值。
//第四个以及后面的多个参数:都表示传入函数的参数。

[static, since 6.5]
template <typename... Args> bool QMetaObject::invokeMethod(QObject *obj, const char *member, Qt::ConnectionType type, Args &&... args)
//应用:开发者需要指定函数的调用方式(比如需要同步和异步),无返回值的有参或者无参函数。
//第一个参数:表示函数调用的qml对象。
//第二个参数:表示函数名,是个字符串。
//第三个参数:枚举类型,表示函数的调用方式
//第四个以及后面的多个参数:都表示传入函数的参数。

[static, since 6.5]
template <typename ReturnArg, typename... Args> bool QMetaObject::invokeMethod(QObject *obj, const char *member, Qt::ConnectionType type, QTemplatedMetaMethodReturnArgument<ReturnArg> ret, Args &&... args)
//应用:开发者需要指定函数的调用方式,有返回值的有参或者无参函数。
//第一个参数:表示函数调用的qml对象。
//第二个参数:表示函数名,是个字符串。
//第三个参数:枚举类型,表示函数的调用方式
//第四个参数:这是个传出参数,是函数的返回值,需要函数外定义变量来接受传出的值。
//第五个以及后面的多个参数:都表示传入函数的参数。


//总结(其实看看这个就行了)
QMetaObject::invokeMethod(相应的qml对象,函数名,调用方式(可选),返回值(可选),参数1(可选),参数2(可选),……,参数n(可选))
invokeMethod函数传参类型以及返回值类型需要注意
  • 在调用有参函数*changeMyText2(str:string)*时,为什么要加QString进行类型转换?
1
2
3
4
//传入参数为 QString类型
QMetaObject::invokeMethod(rootObject,"changeMyText2",QString("C++调用了有参函数"));
//传入参数为 const char[]类型
QMetaObject::invokeMethod(rootObject,"changeMyText2","C++调用了有参函数");

因为字符数组不是Qt的元对象系统,在编译时不是已知的,所以要使用QString进行转换。

建议使用Q_ARG宏来传递。

1
QMetaObject::invokeMethod(rootObject,"changeMyText2",Q_ARG(QString,"C++调用了有参函数");
  • 如果qml中函数没有指定返回值类型时,默认返回的是Qt的通用类型QVariant
1
2
3
4
5
6
7
8
9
10
//qml文件
function getString() //没有指定返回值类型
{
return "123"
}

//C++文件
QVariant retValue;
QMetaObject::invokeMethod(rootObject,"getString", qReturnArg(retValue));
qDebug()<<retValue.toString();

C++接收QML信号

在连接信号和槽前必须先获取对应的QML对象,在MyWindow.qml文件里定义一个信号和按钮以及自增变量count++,每点击按钮,都会把qml里定义count的值,通过信号传递给C++,C++获取值并打印。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//MyWindow.qml
import QtQuick
import QtQuick.Controls
Window {
width: 300
height: 100
visible: true
title: "myWindow"
property int count: 0 //定义变量
signal signalForCpp(count:int) //定义信号
Button{
text: qsTr("发送信号")
width: 100
height: 50
onClicked:
{
signalForCpp(count++) //发送信号
}
}
}

创建一个C++类MyClass定义槽函数,用来接受信号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//myclass.cpp
#ifndef MYCLASS_H
#define MYCLASS_H

#include <QObject>
#include<QDebug>
class MyClass : public QObject //如果想使用信号和槽机制,必须继承QObject,且有Q_OBJECT宏
{
Q_OBJECT
public:
explicit MyClass(QObject *parent = nullptr);

public slots:
void CppSlot(int count) //槽函数
{
qDebug()<<"count: "<<count;
}
};

#endif // MYCLASS_H

在main.cpp文件里,连接信号和槽

1
2
3
4
5
6
7
8
9
10
MyClass myclass;

QObject::connect(
//信号
rootObject, //信号发出的对象
SIGNAL(signalForCpp(int)), //信号
//槽
&myclass, //信号接受的对象
SLOT(CppSlot(int)) //槽函数
);

C++类转换成QML类型

把C++的类暴露给qml,可以在qml文件里创建与cpp类一致的类型,并且可以调用类里面的成员函数,访问成员变量,使用信号和槽等。如,在MyWindow.qml文件里可以使用自定义类型:

1
2
3
4
5
6
7
8
9
10
11
12
//MyWindow.qml
Window {
width: 300
height: 100
visible: true
title: "myWindow"

CppType //自定义类型,对应C++的一个自定义类
{
id:ct
}
}

自定义一个C++类CppType,要在qml文件里创建CppType类型,并且使用自定义的属性,并调用成员函数。

自定义的类要想让qml兼容,必须满足:

  • 继承自QObject
  • 添加Q_OBJECT,启用Qt元对象
  • 添加QML_ELEMENT,将类暴露给qml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//cpptype.h
#ifndef CPPTYPE_H
#define CPPTYPE_H

#include <QObject>
#include <QQmlEngine>

class CppType : public QObject
{
Q_OBJECT
QML_ELEMENT
public:
explicit CppType(QObject *parent = nullptr);
signals:
};

#endif // CPPTYPE_H

1
2
3
4
5
6
//cpptype.cpp
#include "cpptype.h"

CppType::CppType(QObject *parent)
: QObject{parent}
{}

通过CMake模块化注册(建议使用)

  • 使用CMake在构建Qt项目的时候,把cpp源文件编译成一个模块,在某个qml需要创建时,只需要导入该qml模块即可。这样模块化设计,降低了耦合,便于维护和管理。
1
2
3
4
5
6
7
8
9
10
qt_add_executable(appqmltest
main.cpp
cpptype.h cpptype.cpp
qml.qrc
)

qt_add_qml_module(appqmltest //依赖的库
URI com.CppType //模块名
VERSION 1.0 //版本
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import QtQuick
import QtQuick.Controls
import com.CppType 1.0

Window {
width: 300
height: 100
visible: true
title: "myWindow"
CppType
{

}
}

  • QtCreate存在问题,如果CMake执行后,CppType不能识别,请关闭QtCreate,然后重新导入项目即可。

通过qmlRegisterType注册

  • 在程序初始化时,使用qmlRegisterType函数将C++类注册到QML类型系统中。
1
2
3
4
5
6
7
//main.cpp

qmlRegisterType<CppType>("com.CppType", 1, 0, "CppType");

//加载qml文件
QQmlApplicationEngine engine;
engine.load("qrc:/MyWindow.qml");
  • “com.CppType”是模块名称,1和0分别是主版本号和次版本号,“CppType”是在QML中使用的类型名称。

  • 一定要在加载qml窗口之前使用qmlRegisterType函数。