zl程序教程

您现在的位置是:首页 >  后端

当前栏目

【QML与C++混合编程】在 QML 中使用 C++ 类和对象(一)

C++对象编程 混合 qml 使用
2023-09-27 14:29:13 时间

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章

Qt Quick 之 QML 与 C++ 混合编程详解

QML与C++交互

QML与C++集成<二>——<使用C++属性及注册QML类型>

Qt中如何注册一个C++类到qml