悟透Qt

暴露C++类型的属性给QML

2023-09-14  本文已影响0人  akuan

官方文档:Exposing Attributes of C++ Types to QML

QML 可以轻松地通过在 C++ 代码中定义的功能进行扩展。由于 QML 引擎与 Qt 元对象系统(Qt meta-object system)的紧密集成,任何由 QObject 派生的类或 Q_GADGET 类适当公开的功能都可以从 QML 代码中访问。这使得 C++ 的数据和函数可以直接从 QML 中访问,通常几乎不需要修改。

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

(此外,如果已使用 Q_ENUM 声明了枚举类型,则可用。有关更多详细信息,请参阅 Data Type Conversion Between QML and C++

通常情况下,无论 QObject 派生类是否已在 QML 类型系统中注册(registered with the QML type system),这些成员都可以从 QML 中访问。但是,如果要以需要引擎访问其他类型信息的方式使用类 — 例如,如果类本身要用作方法参数或属性,或者如果其中一个枚举类型要以这种方式使用 — 那么可能需要注册该类。建议为在 QML 中使用的所有类型进行注册,因为只有注册的类型可以在编译时进行分析。

对于 Q_GADGET 类型,需要进行注册,因为它们不是从已知的公共基类派生的,无法自动提供。如果没有注册,它们的属性和方法将无法访问。

还请注意,本文档中涵盖的一些重要概念在Writting QML Extensions with C++教程中有所示范。

有关 C++ 和不同的 QML 集成方法的更多信息,请参阅 C++ and QML integration overview

数据类型处理和所有权

从C++传输到QML的任何数据,无论是作为属性值、方法参数或返回值,还是信号参数值,都必须是QML引擎支持的类型。

默认情况下,引擎支持一些Qt C++类型,并且在从QML中使用时可以自动进行适当的转换。此外,已在QML类型系统中注册的C++类可以用作数据类型,如果已适当注册,它们的枚举类型也可以使用。有关更多信息,请参阅Data Type Conversion Between QML and C++

此外,当从C++传输数据到QML时,还将考虑数据所有权规则。有关更多详细信息,请参阅Data Ownership

暴露属性

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

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

例如,下面是一个具有author属性的Message类。根据Q_PROPERTY宏调用的规定,该属性可以通过author()方法进行读取,并通过setAuthor()方法进行写入:
(注意:不要对Q_PROPERTY类型使用typedef或using,因为这会让moc混淆。这可能导致某些类型比较失败。)

class Message : public QObject {
    Q_OBJECT
    Q_PROPERTY(QString author READ author WRITE setAuthor NOTIFY authorChanged)
public:
    void setAuthor(const QString &a) {
        if (a != m_author) {
            m_author = a;
            emit authorChanged();
        }
    }
    QString author() const {
        return m_author;
    }
signals:
    void authorChanged();
private:
    QString m_author;
};

如果在从C++加载名为MyItem.qml的文件时,将此类的实例设置为上下文属性set as a context property

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

    QQuickView view;
    Message msg;
    view.engine()->rootContext()->setContextProperty("msg", &msg);
    view.setSource(QUrl::fromLocalFile("MyItem.qml"));
    view.show();

    return app.exec();
}

然后,可以从MyItem.qml中读取author属性:

// MyItem.qml
import QtQuick 2.0

Text {
    width: 100; height: 100
    text: msg.author    // invokes Message::author() to get this value

    Component.onCompleted: {
        msg.author = "Jonah"  // invokes Message::setAuthor()
    }
}

为了与 QML 最大限度地实现互操作性,任何可写属性都应该有一个相关的 NOTIFY 信号,每当属性值发生变化时就会发出。这允许属性与属性绑定一起使用,这是 QML 的一个基本特性,它通过自动更新属性来强制属性之间的关系,无论其依赖项的值如何变化。

在上面的例子中,author属性的相关 NOTIFY 信号是 authorChanged,如 Q_PROPERTY() 宏调用中所指定的。这意味着每当信号被发出——就像在 Message::setAuthor() 中author更改时一样——这就通知 QML 引擎必须更新任何涉及author属性的绑定,并且引擎将通过再次调用 Message::author() 来更新文本属性。

如果author属性是可写的但没有相关的 NOTIFY 信号,则文本值将使用 Message::author() 返回的初始值进行初始化,但不会根据此属性的任何后续更改进行更新。此外,任何尝试从 QML 绑定到该属性的操作都将从引擎产生运行时警告。

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

使用通知信号的注意事项

为了防止循环或过度评估,开发人员应确保当属性值实际更改时才发出属性更改信号。此外,如果某个属性或一组属性很少使用,则允许为几个属性使用相同的NOTIFY信号。这应该小心进行,以确保性能不会受到影响。

NOTIFY信号的存在确实会产生一些开销。有些情况下,属性的值在对象构造时设置,并且随后不会更改。这种情况最常见的情况是当类型使用分组属性时,分组属性对象只分配一次,并且只在删除对象时释放。在这些情况下,可以在属性声明中添加CONSTANT属性而不是NOTIFY信号。

CONSTANT属性应仅用于其值仅在类构造函数中设置并完成的属性。所有其他要在绑定中使用的属性都应具有NOTIFY信号。

➊具有对象类型的属性

只要对象类型已经适当地在QML类型系统中注册,就可以从QML访问对象类型的属性。
例如,Message类型可能具有MessageBody*类型的body属性:

class Message : public QObject {
    Q_OBJECT
    Q_PROPERTY(MessageBody* body READ body WRITE setBody NOTIFY bodyChanged)
public:
    MessageBody* body() const;
    void setBody(MessageBody* body);
};

class MessageBody : public QObject {
    Q_OBJECT
    Q_PROPERTY(QString text READ text WRITE text NOTIFY textChanged)
// ...
}

假设Message类型已经在QML类型系统中注册,允许它作为QML代码中的对象类型使用:

Message {
    // ...
}

如果MessageBody类型也在类型系统中注册,就可以从QML代码中将MessageBody分配给Message的body属性:

Message {
    body: MessageBody {
        text: "Hello, world!"
    }
}
➋具有对象列表类型的属性

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

例如,下面的 MessageBoard 类具有一个 messages 属性,其类型为 QQmlListProperty,用于存储 Message 实例列表:

class MessageBoard : public QObject {
    Q_OBJECT
    Q_PROPERTY(QQmlListProperty<Message> messages READ messages)
public:
    QQmlListProperty<Message> messages();

private:
    static void append_message(QQmlListProperty<Message> *list, Message *msg);

    QList<Message *> m_messages;
};

MessageBoard::messages()函数简单地从其QList <T> m_messages成员创建并返回QQmlListProperty,根据QQmlListProperty构造函数所需的适当列表修改函数进行传递:

QQmlListProperty<Message> MessageBoard::messages() {
    return QQmlListProperty<Message>(this, 0, &MessageBoard::append_message);
}

void MessageBoard::append_message(QQmlListProperty<Message> *list, Message *msg) {
    MessageBoard *msgBoard = qobject_cast<MessageBoard *>(list->object);
    if (msg)
        msgBoard->m_messages.append(msg);
}

请注意,QQmlListProperty 的模板类类型(在本例中为 Message)必须在 QML 类型系统中注册。

➌分组属性

任何只读的对象类型属性都可以作为分组属性从QML代码中访问。这可以用于公开一组相关的属性,用于描述类型的一组属性。
例如,假设Message::author属性是MessageAuthor类型而不是简单的字符串,具有名称和电子邮件的子属性:

class MessageAuthor : public QObject {
    Q_PROPERTY(QString name READ name WRITE setName)
    Q_PROPERTY(QString email READ email WRITE setEmail)
public:
    ...
};

class Message : public QObject {
    Q_OBJECT
    Q_PROPERTY(MessageAuthor* author READ author)
public:
    Message(QObject *parent)
        : QObject(parent), m_author(new MessageAuthor(this)) {
    }
    MessageAuthor *author() const {
        return m_author;
    }
private:
    MessageAuthor *m_author;
};

可以使用QML中的分组属性语法来编写author属性,如下所示:

Message {
    author.name: "Alexandra"
    author.email: "alexandra@mail.com"
}

作为分组属性公开的类型与对象类型属性不同,分组属性是只读的,并且在构造时由父对象初始化为有效值。分组属性的子属性可以从QML中修改,但是分组属性对象本身永远不会改变,而对象类型属性可以随时从QML分配新的对象值。因此,分组属性对象的生命周期严格由C++父实现控制,而对象类型属性可以通过QML代码自由创建和销毁。

暴露方法(包括 Qt 槽)

任何 QObject 派生类型的方法都可以从 QML 代码中访问,如果它是:

例如,下面的 MessageBoard 类具有已使用 Q_INVOKABLE 宏标记的 postMessage() 方法,以及作为公共槽的 refresh() 方法:

class MessageBoard : public QObject {
    Q_OBJECT
public:
    Q_INVOKABLE bool postMessage(const QString &msg) {
        qDebug() << "Called the C++ method with" << msg;
        return true;
    }

public slots:
    void refresh() {
        qDebug() << "Called the C++ slot";
    }
};

如果将MessageBoard的一个实例设置为文件MyItem.qml的上下文数据,那么MyItem.qml可以像下面的示例那样调用这两个方法:

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

    MessageBoard msgBoard;
    QQuickView view;
    view.engine()->rootContext()->setContextProperty("msgBoard", &msgBoard);
    view.setSource(QUrl::fromLocalFile("MyItem.qml"));
    view.show();

    return app.exec();
}
// MyItem.qml
import QtQuick 2.0

Item {
    width: 100; height: 100

    MouseArea {
        anchors.fill: parent
        onClicked: {
            var result = msgBoard.postMessage("Hello from QML")
            console.log("Result of postMessage():", result)
            msgBoard.refresh();
        }
    }
}

如果一个C++方法有一个QObject*类型的参数,那么可以使用对象ID或JavaScript var值引用对象从QML中传递参数值。

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

从C++方法返回的值在从QML中的JavaScript表达式访问时转换为JavaScript值。

C++方法和'this'对象

您可能希望从一个对象中检索C++方法,并在不同的对象上调用它。考虑以下示例,在名为Example的QML模块中:

class Invokable : public QObject {
    Q_OBJECT
    QML_ELEMENT
public:
    Invokable(QObject *parent = nullptr) : QObject(parent) {}

    Q_INVOKABLE void invoke() {
        qDebug() << "invoked on " << objectName(); }
    };
import QtQml
import Example

Invokable {
    objectName: "parent"
    property Invokable child: Invokable {}
    Component.onCompleted: child.invoke.call(this)
}

如果你从一个合适的 main.cpp 文件中加载 QML 代码,它应该会打印出 "invoked on parent"。然而,由于一个长期存在的 bug,它并没有。历史上,基于 C++ 的方法的 'this' 对象与方法密不可分。改变现有代码的这种行为将会导致微妙的错误,因为 'this' 对象在许多地方是隐含的。自 Qt 6.5 开始,你可以明确地选择正确的行为并允许 C++ 方法接受一个 'this' 对象。为此,请在你的 QML 文档中添加以下编译指示:

pragma NativeMethodBehavior: AcceptThisObject

加上这行代码后,上面的示例将按预期工作。

暴露信号

任何 QObject 派生类型的公共信号都可以从 QML 代码中访问。QML 引擎会自动为从 QML 使用的任何 QObject 派生类型的信号创建信号处理程序。信号处理程序始终命名为 on<Signal>,其中 <Signal> 是信号的名称,第一个字母大写。通过参数名称,信号传递的所有参数都在信号处理程序中可用。例如,假设 MessageBoard 类具有一个带有单个参数 subject 的 newMessagePosted() 信号:

class MessageBoard : public QObject {
    Q_OBJECT
public:
   // ...
signals:
   void newMessagePosted(const QString &subject);
};

如果MessageBoard类型已在QML类型系统中注册,则在QML中声明的MessageBoard对象可以使用名为onNewMessagePosted的信号处理程序接收newMessagePosted()信号,并检查subject参数值:

MessageBoard {
    onNewMessagePosted: (subject)=> console.log("New message received:", subject)
}

与属性值和方法参数一样,信号参数必须具有 QML 引擎支持的类型;请参阅 QML和C++之间的数据类型转换。(使用未注册的类型不会生成错误,但是无法从处理程序中访问参数值。)

类可以具有多个具有相同名称的信号,但是只有最终信号可以作为 QML 信号访问。请注意,具有相同名称但具有不同参数的信号无法彼此区分。

上一篇下一篇

猜你喜欢

热点阅读