Qt中QML和C++混合编程

使用QML开发界面

加载QML的3种方式

支持文件路径资源(资源文件)路径网络路径

  • 使用QQmlApplicationEngine

这个类结合了QQmlEngine和QQmlComponent

cpp 复制代码
QGuiApplication app(argc,argv);

//1、使用QQmlApplicationEngine加载qml代码,他结合了QQmlEngine和QQmlComponent
//构造函数指定qml文件的路径
QQmlApplicationEngine appeng("qrc:/Main.qml");

//获取根节点并设置参数
QList < QObject * > objs = appeng.rootObjects();

//假设qml中的根节点是Window,那么其实例化对象的类型是QQuickWindow
//因此将其转为实际的类型QQuickWindow*
auto win = (QQuickWindow * ) objs[0];

//修改其坐标和标题
win -> setX(30);
win -> setY(30);
win -> setTitle("你好世界");

return app.exec();
  • 使用QQuickView

要求:qml文件的根节点不能是Window及其派生元素,因为QQuickView 会自己创建根窗口,

此外,还需要调用其show()方法,窗口才会显示出来

cpp 复制代码
QGuiApplication app(argc,argv);

//使用QQuickView
//这种方式的话qml代码的根类型不能是Window,他会自动创建根窗口
QQuickView view;
//设置qml文件路径
view.setSource(QUrl("qrc:/MyItem.qml"));
//需要手动调用show才会显示
view.show();

return app.exec();
  • 使用QQmlComponent和QQmlEngine
  1. QQmlComponent加载QQmlEngine引擎
  2. QQmlComponent设置qml文件路径
  3. 调用QQmlComponent的**create()**方法(返回根节点的实例化对象的指针),创建实例后才会显示窗口
  4. 以通过QQmlComponent的isError()方法判断qml文件是否出错
  5. 以及errorString()方法获取具体的错误信息
cpp 复制代码
QGuiApplication app(argc,argv);

//使用QQmlComponent和QQmlEngine
QQmlEngine eng;
//1、组件加载引擎
QQmlComponent com( & eng);
//2、加载qml文件
com.loadUrl(QUrl("qrc:/Main.qml"));

//获取错误信息,并打印
if (com.isError()) {
    qDebug() << com.errorString();
}

//4、然后创建这个组件才会实例化并显示
//创建后会返回根节点的指针(假设根节点是Window,实例化之后就是QQuickWindow)
//使用智能指针管理这个实例化的组件
std::shared_ptr < QQuickWindow > p((QQuickWindow * ) com.create());

//设置根节点的一些属性
p -> setTitle("Main.qml");
p -> setColor(Qt::red);

return app.exec();

查找子节点并读取和修改节点的属性

  1. 先获取到根节点的实例的指针
  2. qml中的子节点需要设置objectName属性
  3. 调用findChild<>()方法根据objectName来查找对应的子节点,获取到子节点实例的指针
  4. 找到了则调用property()和setProperty()方法来读取和设置对应的属性
cpp 复制代码
//加载引擎和qml文件
QQmlEngine eng;
QQmlComponent com( & eng);
com.loadUrl(QUrl("qrc:/Main.qml"));

//假设qml中根节点是Window,实例化之后就是QQuickWindow
std::shared_ptr < QQuickWindow > p((QQuickWindow * ) com.create());

//通过findChild方法(模板方法)访问qml里面的节点并修改属性
//需要先给qml中的节点设置objectName属性,然后根据这个objectName进行查找
//假设查找objectName为"mytxt"的子节点
auto mytxt = p -> findChild < QObject * > ("mytxt");

//非空则找到了
if (mytxt != nullptr) {
    qDebug() << "找到了";
    //读取属性,返回的是QVariant
    auto width = mytxt -> property("width");
    qDebug() << "mytxt的宽度是:" << width.toInt();

    //修改属性
    mytxt -> setProperty("text", "你好世界");
}

递归遍历所有节点

可以调用自带的void QObject::dumpObjectTree() const,这个函数可以打印出所有的子节点

手写:

cpp 复制代码
//参数1:某个节点的实例的指针
void printAllNode(QObject* obj,int level=0){
    if(obj==nullptr) return;

    QString head="";
    for(int i=0;i<level;++i)
    {
        head+="-";
    }
    QString str=head;
    str+="className:";
    str+=obj->metaObject()->className();
    str+=" ";
    str+="objectName:";
    str+=obj->objectName();
    str+=" ";
    str+=obj->property("width").toString();
    str+=":";
    str+=obj->property("height").toString();
    qDebug()<<str;

    //获取子节点
    auto subs=obj->children();
    //递归遍历子节点
    for(auto itor:subs)
    {
        printAllNode(itor,level+1);
    }
};

cpp和qml中的类型对应

  • 基础类型对应关系

如果qml中函数的参数的类型是这些基础类型,那么cpp中可以传下面的类型

QVariant var

  • 数组类型对应关系

如果qml中函数的参数的实际类型是数组,那么cpp中可以传下面的类型

  • 对象类型对应关系

如果qml中函数的参数的实际类型是js对象,那么cpp中可以传下面的类型

QVariantMap

cpp端直接调用qml端的函数

使用静态方法QMetaObject::invokeMethod 来调用qml中函数

  • 参数1:节点指针

一定要获取到qml函数所在节点的实例化对象的指针

只获取到父节点或者祖宗节点都不行,会找不到这个函数

  • 参数2:qml函数名

qml文件:

cpp 复制代码
Window{

    id:root
    width: 400
    height: 300
    visible: true
    title: "main.qml"

    //qml自定义信号
    signal sig1(msg:string)


    //无参数 无返回值
    function qmlFunc1()
    {
        print("call qmlFunc1")
    }

    //参数为int 或string或var   无返回值
    function qmlFunc2(index:int,str:string,param:var)
    {
        print("call qmlFunc2;",index,"  ",str,"  ",param)
    }

    //有返回值,没有显示指定返回值类型,则返回值类型为var
    function qmlFunc3()
    {
        print("call qmlFunc3")

        return "a string"
    }

    //有返回值,显示指定类型为string
    function qmlFunc4():string
    {
        print("call qmlFunc4")

        return "a string"
    }

    //有参数,有返回值
    function qmlFunc5(cnt:var):var
    {
        print("call qmlFunc5:",cnt)

        return "a string"
    }

    //参数是数组
    function qmlFunc6(arr:var)
    {
        print("qmlFunc6")
        //遍历这个数组
        for(var i=0;i<arr.length;++i)
        {
            print(arr[i]," ")
        }
    }

    //参数是js对象
    function qmlFunc7(obj:var)
    {
        print("qmlFunc7")
        //遍历这个对象
        for(var key in obj)
        {
            print("key:",key," value:",obj[key]);
        }
    }

    Text {
        id: txt

        objectName: "mytxt"
        text: qsTr("text")
        font.pixelSize: 25

        Rectangle{
            anchors.fill: txt
            color: "red"
            z:-1
        }
    }

    Button{
        id:btn
        objectName: "btn"

        anchors.centerIn: parent
        width: 100
        height: 30
        text: "test qml"
        onClicked: {
            root.sig1("signal1 from qml")
        }
    }

}
调用qml中无参数,无返回值的函数
cpp 复制代码
QQmlApplicationEngine appeng("qrc:/Main.qml");
QList < QObject * > objs = appeng.rootObjects();
auto win = (QQuickWindow * ) objs[0];

//参数1:qml函数所在节点的实例化对象的指针
//参数2:qml函数名
QMetaObject::invokeMethod(win, "qmlFunc1");
调用qml中带参数,无返回值的函数

两端的参数类型要对应好

cpp 复制代码
QQmlApplicationEngine appeng("qrc:/Main.qml");
QList < QObject * > objs = appeng.rootObjects();
auto win = (QQuickWindow * ) objs[0];

//qml中的var类型对应qt里面的QVariant
//qml中的string类型对应qt里面的QString
//后面传对应的参数
QMetaObject::invokeMethod(win, "qmlFunc2",
    100, QString("你好"), QVariant(123));
调用qml中带返回值的函数,且没有显示指定返回值的类型

用QVariant接收返回值

然后用qReturnArg包裹

作为QMetaObject::invokeMethod的第三个参数传进去

cpp 复制代码
QQmlApplicationEngine appeng("qrc:/Main.qml");
QList < QObject * > objs = appeng.rootObjects();
auto win = (QQuickWindow * ) objs[0];

//调用qml函数,有返回值的函数(但是没有显示指定返回值类型),使用QVariant接收返回值
//用qReturnArg包裹下返回值
QVariant ret;
QMetaObject::invokeMethod(win, "qmlFunc3", qReturnArg(ret));
//转为实际的类型
qDebug() << ret.toString();
调用qml中带返回值的函数,且显示指定返回值的类型

如果qml中的函数显示指定了类型,那么就用Cpp端对应的类型接收

比如这里qml中的函数显示指定了返回值的函数时string类型

cpp 复制代码
QQmlApplicationEngine appeng("qrc:/Main.qml");
QList < QObject * > objs = appeng.rootObjects();
auto win = (QQuickWindow * ) objs[0];

//调用qml函数,有返回值的函数(显示指定了返回值类型),直接使用qt中对应的类型接收
QString ret2;
QMetaObject::invokeMethod(win, "qmlFunc4", qReturnArg(ret2));
qDebug() << ret2;
调用qml中带返回值,带参数的函数

显然就是综合前面的来调用

cpp 复制代码
QQmlApplicationEngine appeng("qrc:/Main.qml");
QList < QObject * > objs = appeng.rootObjects();
auto win = (QQuickWindow * ) objs[0];

//调用带参数,带返回值的函数
//参数3:用来接收返回值
//后面的参数就传qml函数所需的参数
QVariant ret3;
QMetaObject::invokeMethod(win, "qmlFunc5", qReturnArg(ret3), QVariant(4));
qDebug() << ret3.toString();
调用qml中参数实际类型是数组的函数

cpp端可以传QVariantList等等(见上面的类型对应)

但是传参的时候仍然要用QVariant包裹,

因为qml中的函数参数类型是var(只不过实际类型是数组)

cpp 复制代码
QQmlApplicationEngine appeng("qrc:/Main.qml");
QList < QObject * > objs = appeng.rootObjects();
auto win = (QQuickWindow * ) objs[0];

//qml中的函数参数实际类型是js数组
//cpp端则传QVariantList...等等
//调用时要用 QVariant包裹
QVariantList arr {1,2,3,4};
QMetaObject::invokeMethod(win, "qmlFunc6",QVariant(arr));

//传vector<int>
std::vector < int > arr2 {1,2,3,4};
QMetaObject::invokeMethod(win, "qmlFunc6",QVariant::fromValue(arr2));
调用qml中参数实际类型是js对象的函数

cpp端只能传QVariantMap

cpp 复制代码
QQmlApplicationEngine appeng("qrc:/Main.qml");
QList < QObject * > objs = appeng.rootObjects();
auto win = (QQuickWindow * ) objs[0];

//qml中的函数参数实际类型是js对象
//cpp端则传QVariantMap
QVariantMap maps;
maps["name"] = "张三";
maps["age"] = 18;
QMetaObject::invokeMethod(win, "qmlFunc7",QVariant::fromValue(maps));

cpp端接收qml端的信号

  • 仍然是通过connect函数连接,且只支持qt4的写法,也不支持lambda表达式
  • 需要获取到信号所在节点的实例化对象的指针 (即信号的发送者)
  • qml中信号的参数类型在连接时要转成cpp的类型

先定义cpp中接受信号的对象和槽函数

cpp 复制代码
class MyClass : public QObject
{
    Q_OBJECT
public:
    explicit MyClass(QObject *parent = nullptr) : QObject{parent}
    {

    }
public slots:
    void cppSlot(QString msg)
    {
        qDebug()<<"CppSlot:"<<msg;
    }
    void cppSlot2()
    {
        qDebug()<<"按钮点击了";
    }

signals:
};

连接槽函数

cpp 复制代码
QQmlApplicationEngine appeng("qrc:/Main.qml");
QList < QObject * > objs = appeng.rootObjects();
auto win = (QQuickWindow * ) objs[0];

//定义接收的对象
MyClass obj;

//绑定接收对象的槽函数
//qml中信号所在节点的实例化对象的地址  具体的信号 接收对象的地址  接收对象的槽函数
//注意参数类型要对应,比如这里qml中的信号参数是string 这里变成QString
QObject::connect(win, SIGNAL(sig1(QString)), & obj, SLOT(cppSlot(QString)));

//绑定qml自带的信号:Button元素的clicked信号
auto btn = win -> findChild < QObject * > ("btn");

if (btn != nullptr) {
    QObject::connect(btn, SIGNAL(clicked()), & obj, SLOT(cppSlot2()));
}

qml端接收cpp端的信号

方式1:和上面一样,只不过发送者变成cpp里面的对象,接收者变成qml里面的节点

方式2:cpp端自己接收信号,绑定cpp端的槽函数,然后在槽函数中通过invokeMethod来调用qml中的函数

cpp扩展qml类型

自定义一个类,继承自QObject或者QQuickItem或者QQuickPaintedItem,这样才能被qml使用

类中添加宏Q_OBJECT,使他支持信号槽

类中添加宏QML_ELEMENT,使他能够在qml文件中使用

使用Q_PROPERTY定义各种属性,给属性提供读取函数和属性改变信号,这些属性可以在qml中使用

在成员方法前面加上Q_INVOKABLE,这样就可以将cpp的函数暴露给qml了,qml中可以调用

如果有定义的枚举,使用强类型枚举,且使用Q_ENUM注册

下面是一个cpp类扩展了qml的类型

cpp 复制代码
#ifndef CPPTYPE_H
#define CPPTYPE_H

#include <QObject>
#include <QQmlEngine>

//1、继承QObject或他的派生类  只有这样才能给qml使用
class CppType : public QObject
{
    //2、添加Q_OBJEC支持信号槽
    Q_OBJECT
    //3、添加QML_ELEMENT使得他能够在qml中使用
    QML_ELEMENT

    //4、设置属性  提供读写函数  和  属性改变信号
    Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged)

    //属性还可以绑定数据成员  此时不用提供读写函数  和  手动发射属性改变信号了(属性改变信号还是要提供)  他自己会处理
    Q_PROPERTY(int age MEMBER age_ NOTIFY ageChanged)
public:
     //cpp定义的枚举
    enum class MyEnum{
        Value1,
        Value2
    };
    //注册到元对象中
    Q_ENUM(MyEnum)

    explicit CppType(QObject *parent = nullptr) : QObject{parent}
    {

    }
    ~CppType()=default;

    //提供属性读取函数
    QString name(){return objectName();}

    //提供属性修改函数
    void setName(QString name)
    {
        //前后属性一样就不用改了
        if(name==objectName())
        {
            return;
        }

        setObjectName(name);

        //发射属性改变信号
        emit nameChanged();
    }

    //5、将成员函数暴露给qml,qml中调用
    //函数前面加Q_INVOKABLE就可以暴漏出去
    Q_INVOKABLE void cppFunc1()
    {
        qDebug()<<"call cppFunc1";
    }

    //函数参数3种类型 基础  数组  js对象
    Q_INVOKABLE QString cppFunc2(int index,std::vector<int> arr,QVariantMap maps)
    {
        qDebug()<<"call cppFunc2";
        qDebug()<<"index:"<<index;
        for(const auto& itor:arr)
        {
            qDebug()<<itor;
        }

        for(const auto& key:maps.keys())
        {
            qDebug()<<"key:"<<key<<" value:"<<maps[key];
        }

        return "from cpp";

    }

signals:
    //提供属性改变信号
    void nameChanged();
    void ageChanged();

    //其他信号   他的信号处理器在qml中: onOtherSignal,遵循qml的规则on+信号名
    void otherSingal();
private:
    int age_=0;
};

#endif // CPPTYPE_H

然后在加载qml文件之前,

需要将这个类注入到qml中,

注入完成之后,qml中就可以使用这个cpp扩展的类型了

使用qmlRegisterType这个模板函数进行注入

cpp 复制代码
template <typename T> 
int qmlRegisterType(const char *uri, 
                    int versionMajor, 
                    int versionMinor, 
                    const char *qmlName)
  • 模板参数:自定义的cpp类
  • 参数1:qml中使用import时的导入名
  • 参数2:主版本号
  • 参数3:子版本号
  • 参数4:qml使用这个类时的元素名
cpp 复制代码
//qml调用cpp  即使用cpp扩展qml类型
//一定要在加载qml文件之前将cpp扩展的类型注入到qml中
//模板参数:cpp扩展的类
//参数1:qml中使用import时的导入名
//参数2:主版本号
//参数3:子版本号
//参数4:qml使用这个类时的元素名
qmlRegisterType < CppType > ("CppType", 1, 0, "CppType");

QQmlApplicationEngine appeng;
appeng.load("qrc:/Main2.qml");

然后就可以把他当成一个qml的类型在qml中使用了,遵循qml的各种语法和使用方式

cpp 复制代码
import QtQuick 2.15
import QtQuick.Controls

//需要先import,这里要和之前qmlRegisterType传入的参数对应
import CppType 1.0

Window{

    id:root
    width: 400
    height: 300
    visible: true
    title: "main2.qml"

    Button{
        width: 100
        height: 30
        text: "btn"
        onClicked: {
            cpp1.name="aaa"
            cpp1.age=20

            //调用cpp扩展类型的函数
            cpp1.cppFunc1()

            //可以直接这样传
            //cpp1.cppFunc2(3,[1,2,3],{name:"张三",age:18})

            //js数组
            var arr=[1,2,3]
            //js对象
            var obj={name:"张三",age:18}
            cpp1.cppFunc2(3,arr,obj)

            //访问cpp里面的枚举
            print(CppType.MyEnum.Value1)
        }
    }

    //使用cpp扩展的qml类型
    CppType{
        id:cpp1

        name:"cpptype"

        age:18

        onAgeChanged: {
            print("年龄:",age)
        }

        onNameChanged: {
            print("姓名:",name)
        }

        onOtherSingal: {

        }

    }
}

如果我们在qml中仅仅只是想使用cpp里面的一些函数和属性,

而不是直接使用cpp扩展的整个类型,

那么我们可以不用将这个类型注入到qml中,

而是在加载qml文件之前实例化这个cpp类型的对象

然后设置上下文属性

cpp 复制代码
//创建要使用的cpp类的实例
CppType cppType;

QQmlApplicationEngine appeng;
//在加载qml文件之前获取上下文环境
//设置上下文属性
//参数1:在qml中使用实例名称
//参数2:对应的实例地址
appeng.rootContext() -> setContextProperty("cppType", & cppType);
appeng.load("qrc:/Main3.qml");

qml中直接通过对象名.函数名 或者对象名.属性名直接调用

(这种方式无法访问cpp里面定义的枚举类型)

cpp 复制代码
import QtQuick 2.15
import QtQuick.Controls

Window{

    id:root
    width: 400
    height: 300
    visible: true
    title: "window"

    Button{
        width: 100
        height: 30
        text: "btn"

        //还可以直接自定义属性来绑定cpp对象的属性
        property int age:cppType.age

        onAgeChanged: {
            print("age变化:",age)
        }

        onClicked: {

            //直接使用cpp的函数,通过对象名.函数名调用(这里的函数名就是setContextProperty中设置的名称)
            cppType.cppFunc1()
            
            //直接访问里面的属性
            cppType.age=10
        }
    }
}

QWidget中局部使用qml

可以使用QQuickWidget

使用QQuickWidget来加载qml文件,就可以显示在QWidget中

qml中的根节点不能是Window,一般使用item或者其派生元素

QQuickWidget有两种resizeMode

  • QQuickWidget::SizeViewToRootObject

将QQuickWidge的大小调整为和qml根元素的大小一致,默认就是这种模式,

在这种情况下,QQuickWidge的大小可以不用设置(只设置位置)

qml根元素的大小需要显式地设置,不设置就是0,QQuickWidget的宽高也会跟着缩小到0,导致显示不出来

  • QQuickWidget::SizeRootObjectToView

将qml根元素的大小调整为和QQuickWidge的大小一致,

在这种情况下,QQuickWidget的大小要显式的设置(或者添加进布局之中)

qml根元素的大小可以不用设置(设置了也不生效),会调整为和QQuickWidge的大小一致

resizeMode的模式设置一定要在加载qml文件之前设置,否则会不生效!

这之后就可以在QQuickWidget显示qml的元素了,如何和里面的qml元素交互,就是前面说的那些方法了

cpp 复制代码
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include<QQuickWidget>

class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr) : QWidget(parent)
    {
        resize(800,600);

        QQuickWidget* w=new QQuickWidget(this);
        //设置QuickWidget的几何属性
        w->setGeometry(10,10,300,300);
        //resize模式的设置一定要在加载qml文件(setSource)之前设置,否则会无效
        //resizeMode设置:qml根元素的大小调整为和QQuickWidget一致
        w->setResizeMode(QQuickWidget::SizeRootObjectToView);

        w->setSource(QUrl("qrc:/Main.qml"));


    }

    ~Widget()=default;
};
#endif // WIDGET_H

qml文件

cpp 复制代码
import QtQuick 2.15
import QtQuick.Controls

Rectangle{
    color: "red"

    Slider{
        width: parent
        height: 50
        anchors.centerIn: parent
    }
}

Q_PROPERTY的一些说明

定义:

cpp 复制代码
Q_PROPERTY(type name
(READ getFunction[WRITE setFunction] | 
MEMBER memberName[(READ getFunction | WRITE setFunction)])
[RESET resetFunction]
[NOTIFY notifySignal]
[REVISION int | REVISION(int[, int])]
[DESIGNABLE bool]
[SCRIPTABLE bool]
[STORED bool]
[USER bool]
[BINDABLE bindableProperty]
[CONSTANT]
[FINAL]
[REQUIRED])

一般需要注意的字段就几个点

  • 属性是只读的,需要加CONSTANT
  • 属性绑定了成员变量,可以不用提供属性读写函数,不用手动发射属性改变信号
  • 需要在属性写入函数中发射属性改变信号(如果前后值不一样,即真正修改后才发射)

快速给数据成员设置属性

  1. 右键数据成员
  2. 点击重构
  3. 点击生成Q_PROPERTY
相关推荐
Code out the future12 分钟前
【C++——临时对象,const T&】
开发语言·c++
sam-zy32 分钟前
MFC用List Control 和Picture控件实现界面切换效果
c++·mfc
爱码小白1 小时前
PyQt5 学习方法之悟道
开发语言·qt·学习方法
aaasssdddd961 小时前
C++的封装(十四):《设计模式》这本书
数据结构·c++·设计模式
发呆小天才O.oᯅ1 小时前
YOLOv8目标检测——详细记录使用OpenCV的DNN模块进行推理部署C++实现
c++·图像处理·人工智能·opencv·yolo·目标检测·dnn
qincjun2 小时前
文件I/O操作:C++
开发语言·c++
星语心愿.2 小时前
D4——贪心练习
c++·算法·贪心算法
汉克老师2 小时前
2023年厦门市第30届小学生C++信息学竞赛复赛上机操作题(三、2023C. 太空旅行(travel))
开发语言·c++
single5942 小时前
【c++笔试强训】(第四十一篇)
java·c++·算法·深度优先·图论·牛客
yuanbenshidiaos2 小时前
C++-----函数与库
开发语言·c++·算法