当前位置: 首页 > article >正文

Qt中QML和C++混合编程

使用QML开发界面

加载QML的3种方式

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

  • 使用QQmlApplicationEngine

这个类结合了QQmlEngine和QQmlComponent

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()方法,窗口才会显示出来

QGuiApplication app(argc,argv);

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

return app.exec();
  • 使用QQmlComponentQQmlEngine
  1. QQmlComponent加载QQmlEngine引擎
  2. QQmlComponent设置qml文件路径
  3. 调用QQmlComponent的create()方法(返回根节点的实例化对象的指针),创建实例后才会显示窗口
  4. 以通过QQmlComponent的isError()方法判断qml文件是否出错
  5. 以及errorString()方法获取具体的错误信息
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()方法来读取设置对应的属性
//加载引擎和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,这个函数可以打印出所有的子节点

手写:

//参数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文件:

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中无参数,无返回值的函数 
QQmlApplicationEngine appeng("qrc:/Main.qml");
QList < QObject * > objs = appeng.rootObjects();
auto win = (QQuickWindow * ) objs[0];

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

两端的参数类型要对应好

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的第三个参数传进去

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类型

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中带返回值,带参数的函数

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

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(只不过实际类型是数组)

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

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中接受信号的对象和槽函数

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:
};

 连接槽函数

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的类型

#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这个模板函数进行注入

template <typename T> 
int qmlRegisterType(const char *uri, 
                    int versionMajor, 
                    int versionMinor, 
                    const char *qmlName)
  • 模板参数:自定义的cpp类
  • 参数1:qml中使用import时的导入名
  • 参数2:主版本号
  • 参数3:子版本号
  • 参数4:qml使用这个类时的元素名 
//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的各种语法和使用方式

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类的实例
CppType cppType;

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

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

(这种方式无法访问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元素交互,就是前面说的那些方法了

#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文件

import QtQuick 2.15
import QtQuick.Controls

Rectangle{
    color: "red"

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

 

Q_PROPERTY的一些说明

定义:

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


http://www.kler.cn/a/414410.html

相关文章:

  • 极狐GitLab 17.6 正式发布几十项与 DevSecOps 相关的功能【六】
  • 二刷代码随想录第15天
  • DICOM医学影像应用篇——窗宽窗位概念、原理及实现详解
  • 第六届国际科技创新学术交流大会暨信息技术与计算机应用学术会议(ITCA 2024)
  • 如何保护LabVIEW程序免遭反编译
  • python excel接口自动化测试框架!
  • 华为光学博士面试经验
  • 【AI系统】从 CUDA 对 AI 芯片思考
  • 未来已来?AI技术革新改变我们的生活
  • vscode自动打印日志插件
  • 【k8s深入理解之 Scheme 补充-1】理解 Scheme 中资源的注册以及 GVK 和 go 结构体的映射
  • 同时在github和gitee配置密钥
  • 力扣第 71 题 简化路径
  • 电脑模拟器端口号及相关的操作命令
  • 云计算基础-期末复习
  • 【Linux】文件管理
  • 华为Mate 70系列,行走在AI山脊
  • P1390 公约数的和
  • (73)脉冲幅度调制PAM调制解调通信系统的MATLAB仿真
  • 力扣hot100-->前缀和/前缀书/LRU缓存
  • 文本的预处理(pytorch)
  • Ubuntu环境中RocketMQ安装教程
  • ROS VSCode调试方法
  • Linux 命令详解之 tail 命令
  • 【计算机视觉】图像基本操作
  • C++和C中的volatile 关键字