将C++对象暴露给QML

简述

QML 可以很容易地通过 C++ 代码中定义的功能进行扩展。由于 QML 引擎与 Qt 元对象系统的紧密集成,QObject 派生类适当暴露的任何功能都可以从 QML 代码访问,这使得 C++ 中的数据和函数可以直接从 QML 中访问,通常不需要太多修改,甚至不用修改。

通过元对象系统,QML 引擎具有内省 QObject 实例的能力。这意味着,任何 QML 代码都可以访问 QObject 派生类 的以下成员:

  • 属性
  • 函数
  • 信号

一般来说,无论 QObject 派生类是否被注册到 QML 类型系统,这些成员都可以从 QML 中访问。但是,如果 QML 引擎需要访问这个类的附加类型信息(例如,如果类本身被用作一个函数参数或属性,或者以这种方式使用它的一个枚举类型),那么该类可能需要被注册。

版权所有:一去丶二三里,转载请注明出处:http://blog.csdn.net/liang19890820

暴露属性

任何的 QObject 派生类都可以使用 Q_PROPERTY() 宏来指定属性。属性是类的数据成员,具有关联的 READ 读取函数以及可选的 WRITE 写入函数。

QObject 派生类的所有属性都可以从 QML 访问。

基本类型属性

例如,下面是包含 name 属性的 Person 类。正如 Q_PROPERTY 宏所指定那样,该属性可通过 name() 函数来读取,setName() 来写入:

// person.h
#ifndef PERSON_H
#define PERSON_H

#include <QObject>
#include <qDebug>

// 人
class Person : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged)

public:
    void setName(const QString &name) {
        if (name != m_name) {
            m_name = name;
            emit nameChanged();
            qDebug() << "Name changed: " << name;
        }
    }
    QString name() const {
        return m_name;
    }

signals:
    void nameChanged();

private:
    QString m_name;  // 姓名
};

#endif // PERSON_H

为了访问实例中的数据,需要设置上下文属性,将数据暴露给由 QML 引擎实例化的 QML 组件。

通过调用 QQmlContext::setContextProperty() 来定义和更新上下文属性,允许以名称将数据显式地绑定到上下文。

// main.cpp
#include <QGuiApplication>
#include <QQuickView>
#include <QQmlEngine>
#include <QQmlContext>
#include "person.h"

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    QQuickView view;
    Person person;
    // 设置上下文属性
    view.engine()->rootContext()->setContextProperty("person", &person);
    view.setSource(QUrl("qrc:/main.qml"));
    view.show();

    return app.exec();
}

然后,就可以从 QML 中访问了:

// main.qml
import QtQuick 2.3

Rectangle {
    width: 200; height: 150

    Text {
        anchors.centerIn: parent
        text: person.name  // 调用 Person::name() 来获取值

        Component.onCompleted: {
            person.name = "Qter"  // 调用 Person::setName()
        }
    }
}

加载完成,name 的值就会被显示出来,如下所示:

将C++对象暴露给QML

为了尽可能增强与 QML 的交互性,任何可写的属性都应该有一个相关的 NOTIFY 信号,只要属性值发生改变就发出该信号。这允许该属性应用于属性绑定,属性绑定是 QML 的一个重要特性,每当其依赖的任何关系值发生改变,就会通过自动更新属性来强制执行属性之间的关系。

上述示例中,name 属性相关的 NOTIFY 信号是 nameChanged。这意味着无论何时发出该信号(就像在 Person::setName() 改变 name 时一样),将会通知 QML 引擎,必须更新涉及 name 属性的任何绑定。反过来,引擎将通过调用 Person::name() 来更新 text 属性。

如果 name 属性是可写的,但没有相关的 NOTIFY 信号,则 name 值将由 Person::name() 返回的初始值来初始化,但当该属性后续发生任何更改时不会进行相应的更新。此外,绑定到该属性的任何尝试都会产生运行时警告。

信号命名建议: 将 NOTIFY 信号命名为 <property>Changed 的形式,其中 <property> 是属性的名称。由 QML 引擎生成的关联的属性更改信号处理程序将始终采用 on<Property>Changed 的形式,而无需关心相关 C++ 信号的名称,因此建议信号名称遵循此约定,以避免任何混淆。

使用 Notify 信号的注意事项

为了防止循环或过度评估,应确保属性更改信号仅在属性值更改时发出。此外,如果一个属性或属性组不常被用到,则允许对若干属性使用相同的 NOTIFY 信号。不过,使用时应注意,确保性能不会受到影响。

NOTIFY 信号确实会有较小的开销。有时,有些属性的值仅在对象构造阶段设置,随后就再也不会改变。最常见的情况是当一个类型使用分组属性时,分组属性对象被分配一次,并且只有在销毁对象时才会释放。在这种情况下,属性声明时应该使用 CONSTANT 属性,而不是 NOTIFY 信号。

CONSTANT 属性应该仅用于那些仅在类构造函数中设值置,并且随后不再改变的属性,而所有可能会在绑定中使用的其他属性应该使用 NOTIFY 信号。

对象类型属性

如果对象类型已经被注册到 QML 类型系统,则可以从 QML 访问对象类型属性。

例如,Person 类型有一个 IDCard * 类型的属性:

// person.h
#ifndef PERSON_H
#define PERSON_H

#include <QObject>
#include <qDebug>

// 身份证
class IDCard : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString number READ number WRITE setNumber)
    Q_PROPERTY(QString validDate READ validDate WRITE setValidDate NOTIFY validDateChanged)

public:
    void setNumber(const QString &number) {
        if (number != m_number)
            m_number = number;
    }
    QString number() const {
        return m_number;
    }

    void setValidDate(const QString &validDate) {
        if (validDate != m_validDate) {
            m_validDate = validDate;
            emit validDateChanged();
            qDebug() << "Valid date changed: " << validDate;
        }
    }
    QString validDate() const {
        return m_validDate;
    }

signals:
    void validDateChanged();

private:
    QString m_number;  // 身份证号码
    QString m_validDate;  // 有效日期
};

// 人
class Person : public QObject
{
    Q_OBJECT
    Q_PROPERTY(IDCard* idCard READ idCard WRITE setIDCard NOTIFY idCardChanged)

public:
    IDCard* idCard() const {
        return m_idCard;
    }
    void setIDCard(IDCard *idCard) {
        if (idCard != m_idCard) {
            m_idCard = idCard;
            emit idCardChanged();
            qDebug() << "ID card changed: " << idCard->number();
        }
    }

signals:
    void idCardChanged();

private:
    IDCard *m_idCard;  // 身份证
};

#endif // PERSON_H

任何 QObject 的派生类都可以被注册为 QML 对象类型。一旦使用 QML 类型系统注册,该类就可以像 QML 中的任何其他对象类型(例如:Item)一样被声明和实例化。一旦被创建,类实例便可以从 QML 中操作,作为 C++ 类型的属性暴露给 QML 解释,任何 QObject 派生类的属性、方法和信号都可以从 QML 代码访问。

要将 QObject 派生类注册为可实例化的 QML 对象类型,需要调用 qmlRegisterType() 将类注册为特定类型命名空间中的 QML 类型。然后客户端可以导入该命名空间,以便使用该类型。

例如,将 C++ 类型 Person 注册为名为 Person(双引号中的名称 - 尽量见名知义,对象类型首字母大写,例如:Per) 的 QML 类型,其在版本号为 1.0 的 People 命名空间中可用:

qmlRegisterType<Person>("People", 1, 0, "Person");

由于 IDCard 是 Person 的对象类型属性,所以要访问 IDCard 的附加信息,也需要被注册,完整的代码如下:

// main.cpp
#include <QGuiApplication>
#include <QQuickView>
#include "person.h"

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    // 使用 QML 类型系统注册
    qmlRegisterType<Person>("People", 1, 0, "Person");
    qmlRegisterType<IDCard>("People", 1, 0, "IDCard");

    QQuickView view;
    view.setSource(QUrl("qrc:/main.qml"));
    view.show();
    view.setIcon(QIcon(":/logo.png"));
    view.setTitle(QStringLiteral("将C++对象暴露给QML"));

    return app.exec();
}

一旦被注册,通过导入指定的类型命名空间和版本号便可以在 QML 中使用该类型:

// main.qml
import QtQuick 2.3
import People 1.0

Rectangle {
    width: 200; height: 150

    Text {
        anchors.centerIn: parent
        text: person.idCard.validDate  // 先调用 Person::idCard() 来获取 IDCard,再调用 IDCard::validDate()
    }

    Person {
        id: person
        idCard: IDCard {  // 调用 Person::setIDCard()
            number: "610122..."  // 调用 IDCard::setNumber()
            validDate: "2008.10.01-2018.10.01"   // 调用 IDCard::setValidDate()
        }
    }
}

这里,我们只显示身份证的有效日期,如下所示:

将C++对象暴露给QML

分组属性

如果对象类型属性是只读的,则可以在 QML 中作为分组属性来访问,这种方式可用于暴露一个类型的一组相关属性。

修改上述示例:

// person.h
#ifndef PERSON_H
#define PERSON_H

#include <QObject>

// IDCard 类同上... 

// 人
class Person : public QObject
{
    Q_OBJECT
    Q_PROPERTY(IDCard* idCard READ idCard)

public:
    IDCard *idCard() {
        return &m_idCard;
    }

private:
    IDCard m_idCard;  // 身份证
};

#endif // PERSON_H

这时,就不能再这么使用了:

// 错误的用法
Person {
    id: person
    idCard: IDCard { 
        number: "610122..." 
        validDate: "2008.10.01-2018.10.01"
    }
}

因为 idCard 是一个只读的对象属性,而非可写的,所以会提示如下错误:

Invalid property assignment: “idCard” is a read-only property

正确的姿势是使用分组属性语法,为 idCard 属性赋值:

// 组表示法
Person {
    id: person
    idCard {
        number: "610122..."
        validDate: "2008.10.01-2018.10.01"
    }
}

或者:

// 点表示法
Person {
    id: person
    idCard.number: "610122..."
    idCard.validDate: "2008.10.01-2018.10.01"
}

对象类型属性与分组属性的区别:

  • 分组属性:只读,只能在构造时由父对象初始化为一个有效值,生命周期由 C++ 父对象严格控制。其子属性可以从 QML 中修改,但是分组属性对象本身不能改变。
  • 对象类型属性:可以随时被分配新的对象值,通过 QML 代码*创建和销毁。

对象列表类型属性

如果属性包含 QObject 派生类列表,也可以暴露给 QML。但是,属性类型应该使用 QQmlListProperty,而非 QList<T>。这是因为 QList 不是 QObject 的派生类型,因此不能通过 Qt 元对象系统提供必要的 QML 属性特性。例如,当列表被修改时的信号通知。

QQmlListProperty 是一个模板类,可以很方便的由一个 QList 值构造。

例如,Company 类有一个类型为 QQmlListProperty 的 persons 属性,存储了一个 Person 实例列表:

// person.h
#ifndef PERSON_H
#define PERSON_H

#include <QObject>
#include <QQmlListProperty>
#include <QDebug>

// 人
class Person : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged)

public:
    void setName(const QString &name) {
        if (name != m_name) {
            m_name = name;
            emit nameChanged();
            qDebug() << "Name changed: " << name;
        }
    }
    QString name() const {
        return m_name;
    }

signals:
    void nameChanged();

private:
    QString m_name;  // 姓名
};

// 公司
class Company : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QQmlListProperty<Person> persons READ persons)

public:
    Company(QObject *parent = 0);
    QQmlListProperty<Person> persons();
    int personCount() const;
    Person *person(int) const;

private:
    QList<Person *> m_persons;
};

#endif // PERSON_H

注册同上:

// main.cpp
...
qmlRegisterType<Company>("People", 1,0, "Company");
qmlRegisterType<Person>("People", 1,0, "Person");
...

在 QML 中,我们定义一个 ListView 用于显示列表中人的姓名:

// main.qml
import QtQuick 2.7
import QtQuick.Controls 2.0
import People 1.0

Rectangle {
    width: 200; height: 150

    Component {
        id: contactDelegate
        Item {
            width: 180; height: 40
            Row {
                spacing: 5
                leftPadding : 5
                topPadding : 5
                bottomPadding : 5
                Image {  // 头像
                    source: "logo.png"
                    sourceSize.width: 25
                    sourceSize.height: 25
                }
                Text {  // 名字
                    text: name
                    height: 25
                    horizontalAlignment : Text.AlignHCenter
                    verticalAlignment : Text.AlignVCenter
                }
            }
        }
    }

    ListView {
        id: view
        anchors.fill: parent
        model: company.persons
        delegate: contactDelegate
        highlight: Rectangle { color: "lightsteelblue"; radius: 5 }
        focus: true
    }

    Company {
        id: company
        persons: [
            Person { name: "Qter" },
            Person { name: "Pythoner" },
            Person { name: "Linuxer" }
        ]
    }

    // 输出信息
    Component.onCompleted: {
        for (var i = 0; i < company.persons.length; i++)
            console.log("Author: ", i, company.persons[i].name)
    }
}

效果如下:

将C++对象暴露给QML

注意: QQmlListProperty 的模板类类型 - 在这种情况下,是 Person - 必须向 QML 类型系统注册。

暴露函数

QML 可以访问 QObject 派生类的函数,但是函数需要满足以下条件之一:

  • 使用 Q_INVOKABLE() 宏标记的 public 函数
  • public 槽函数

现在,为人添加一些基本的行为。例如:eat、walk。。。吃饱了才有力气减肥,每天三万步,身体倍棒,吃嘛嘛香。

// person.h
#ifndef PERSON_H
#define PERSON_H

#include <QObject>
#include <qDebug>

// 人
class Person : public QObject
{
    Q_OBJECT

public:
    Q_INVOKABLE bool eat(const QString &food) {  // 吃饭
        qDebug() << "Food: " << food;
        return true;
    }

public slots:
    void walk() {  // 走路
        qDebug() << "Thirty thousand steps";
    }
};

#endif // PERSON_H

和前面一样,要在 QML 中使用 Person,需要将其设置为 main.qml 的上下文属性:

// main.cpp
...
Person person;
QQuickView view;
// 设置上下文属性
view.engine()->rootContext()->setContextProperty("person", &person);
...

然后,就可以在 QML 中使用这个实例访问这两个函数:

// main.qml
import QtQuick 2.3
import QtQuick.Controls 2.0

Rectangle {
    width: 200; height: 150

    Label {
        id: label
        anchors.centerIn: parent
        text: "Tip"
    }

    MouseArea {
        anchors.fill: parent
        onClicked: {
            // Q_INVOKABLE 标记的函数
            var result = person.eat("spinach")

            // 输出返回值 result
            label.text = "Result: " +  result

            // 槽函数
            person.walk();
        }
    }
}

点击鼠标,标签上显示函数的返回值,如下所示:

将C++对象暴露给QML

如果 C++ 函数的参数包含 QObject* 类型,参数值可以从 QML 中传递,使用一个对象 id 或引用该对象的一个 JavaScript var 值。

QML 支持重载的 C++ 函数调用,如果函数具有相同名称和不同参数,则将根据提供的参数的数量和类型调用正确的函数。

当从 QML 中的 JavaScript 表达式访问 C++ 函数时,函数的返回值将转换为对应的 JavaScript 值。

暴露信号

QML 代码可以访问 QObject 派生类的任何 public 信号。

QML 引擎会为 QObject 派生类的信号自动地创建一个名为 on<Signal> 的信号处理程序,其中 <Signal> 是信号的名称,首字母大写。信号传递的所有参数在信号处理程序中都是可用的,通过参数名来访问。

前面,我们为人添加了走路的行为,其实三万步也是蛮多了。走完之后,人会发出一个信号:我累了,需要休息,具体的休息时间由参数 minute 决定:

// person.h
#ifndef PERSON_H
#define PERSON_H

#include <QObject>
#include <qDebug>

// 人
class Person : public QObject
{
    Q_OBJECT

signals:
    void tired(int minute);  // 累了

public slots:
    void walk() {  // 走路
        qDebug() << "Thirty thousand steps";
        emit tired(30);
    }
};

#endif // PERSON_H

注册就不再赘述了,和前面类似:

// main.cpp
...
qmlRegisterType<Person>("People", 1, 0, "Person");
...

QML 中声明的 Person 对象可以使用名为 onTired 的信号处理程序来接收 tired() 信号,并使用 minute 参数值:

// main.qml
import QtQuick 2.3
import QtQuick.Controls 2.0
import People 1.0

Rectangle {
    width: 200; height: 150

    Label {
        id: label
        anchors.centerIn: parent
        text: "Tip"
    }

    MouseArea {
        anchors.fill: parent
        onClicked: {
            // 槽函数
            person.walk();
        }
    }

    Person {
        id: person
        onTired: {  // 信号处理程序
            label.text = "Rest: " +  minute
        }
    }
}

点击鼠标,标签上显示信号的参数值,如下所示:

将C++对象暴露给QML

与属性值和方法参数一样,信号的参数的类型也必须能够被 QML 引擎支持。值得注意的是,使用未注册的类型不会生成错误,但是参数值不能从处理程序中访问。

注意: 如果类中包含多个名称相同的信号,只有最后一个信号可以被 QML 访问,因为具有相同名称不同参数的信号无法被区分开来。

上一篇:如何在S/4HANA里创建Custom Business object并实现自定义逻辑


下一篇:《数据结构与抽象:Java语言描述(原书第4版)》一2.1.7 删除项的方法