QML与C++混合编程[官方文档汇编]

本文档详述了QML与C++的混合开发,讲解了如何通过QML引擎与Qt元对象系统集成,实现C++类的属性、方法和信号在QML中的访问。介绍了QML和C++的多种集成方法,包括QML类型注册、对象属性、方法、信号的公开,以及如何在C++中定义和使用QML类型。同时,文档还涵盖了数据类型转换、所有权规则、C++类注册为QML类型以及单例对象的注册等关键概念。
摘要由CSDN通过智能技术生成

概述:

QML架构实现的目的就是把UI设计与后台程序逻辑分离,实现快速的前端UI设计,提供简洁而功能丰富的动画效果。QML表象上更像Web开发中的前端脚手架,通过集成JavaScript的开发优势实现前端可视元素的编程控制。

QT整个架构的灵魂就是强大的元对象系统( meta object system),通过元对象系统提供的信号机制实现QT开发的解决方案。QML作为QT解决方案的重要组成模块,必然继承强大的元对象系统( meta object system)。我们通过元对象系统( meta object system)可以通过C++代码轻松集成、扩展QML。这样我们就能轻松的把前端UI(QML)与后端C++(后端程序逻辑)分离。

QML模块中的类允许我们从C++加载和处理QML对象,并且QML引擎与QT的元对象系统完美的集成。使得C++代码可以直接从QML调用,也就是我们在QML里可以轻易的调用C++代码功能。QML的这种能力使得我们能够开发混合应用程序,该混合应用程序将QML、JavaScript和C++代码混合在一起实现。我们甚至可以使用Web的开发思维,使用JSON数据格式链接前端与后端进行开发。更确切的称之为MVC开发思维。

 

QML和C++的混合开发:

QT给我们提供了以下几种方式实现QML和C++的混合开发:

  • 通过QML和JavaScript实现前端UI,使用C++实现后台逻辑,将用户界面代码与应用程序逻辑代码分开。
  • 通过QML引擎给出的接口环境,访问C++中的功能。例如:调用应用程序逻辑,使用从C++实现的数据模型或调用的第三方C++库中的某些方法。这种方式,可以让我们访问在C++中暴露的各种属性与方法。
  • 访问Qt QML 或Qt Quick C++ API中的功能。这中方式只是调用引擎自带的C++功能。
  • 从C++实现QML对象类型,在QML中像使用其他QML组件类型一样使用。以完整对象组件的方式。

要向QML提供一些C++数据或功能,必须从QObject派生类中获得。由于QML引擎与元对象系统的集成,任何QObject派生类的属性、方法和信号都可以从QML访问,如将C++类型的属性暴露为QML所描述的那样。一旦此类提供了所需的功能,就可以通过多种方式将其公开给QML:

  • 类可以注册为可实例化的QML类型,这样它就可以像QML代码中的任何普通QML对象类型一样被实例化和使用。[C++类注册为组件服务于QML:这将完整的暴露给QML,以组件的形式。]

  • 类可以注册为单例类型,这样类的单个实例可以从QML代码导入,从而允许从QML访问实例的属性、方法和信号。[C++类注册为组件服务于QML:这将完整的暴露给QML,以组件的形式。单例模式注册,C++类也必定是单例模式类,否则这种注册将无效。]

  • 类的实例可以作为上下文属性或上下文对象嵌入到QML代码中,从而允许从QML访问实例的属性、方法和信号。[C++类里的方法和属性嵌入到QML上下文中,C++类的实例化将在QML引擎初始化之前初始化。C++类不会完全暴露给QML。只有特定声明的方法和属性。]

这些是从QML代码访问C++功能的最常用方法;对于更多的选项和细节,请参见下面的文档翻译。此外,除了从QML访问C++功能之外,QT QML模块还提供了反向操作的方法,并从C++代码中操纵QML对象。

C++代码可以集成到C++应用程序或C++插件中,这取决于它是否被作为独立的应用程序或库来分发。插件可以与QML模块集成,然后可以在其他应用程序中导入和使用QML代码;

 

C++与QML之间正确的集成方法选择:

要快速确定哪种集成方法适合您的情况,可以使用以下流程图:

 

 


将C ++类的属性公开给QML:


由于QML引擎与QT元对象系统的集成,QML可以很容易地从C++扩展。此集成允许从QML访问任何QObject派生类的属性、方法和信号:可以读取和修改属性,可以从JavaScript表达式调用方法,并根据需要自动为信号创建信号处理程序。另外,QObject派生类的枚举值可以从QML访问。

QML可以用C++代码中定义的功能轻松扩展。由于QML引擎与Qt元对象系统紧密集成,QObject派生类适当公开的任何功能都可以从QML代码访问。这使得C++数据和函数可以直接从QML访问,通常很少或没有修改。

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

  • 属性
  • 方法(假设它们是公共插槽或用Q_INVOKABLE标记的方法)
  • 信号

此外,如果枚举已经用Q_ENUMS声明,则可以使用。有关更多详细信息,请参见QML和C ++之间的数据类型转换。

通常,无论QObject派生类是否已注册到QML类型系统,都可以从QML访问这些类。但是,如果要以:需要引擎访问其他类型信息的方式使用类(例如,如果要将类本身用作方法参数或属性,或者要以这种方式使用其枚举类型之一),则可能需要注册该类。(暂时没有全部领会其意思,反正注册就好了。可能表达的意思就是,如果想深层访问类的内部数据、或方法,或是要把类实例当作一般组件使用,必须注册。)

还要注意,本文档介绍的许多重要概念在“用C ++编写QML扩展”教程中得到了演示与证明。

数据类型处理和所有权:

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

默认情况下,引擎支持许多QT C++类型,并且可以从QML中自动转换它们。另外,用QML类型系统注册的C++类可以用作数据类型,如果它们的枚举可以适当地注册,它们也可以。请参阅QML和C++之间的数据类型转换,以获取更多信息。

此外,数据所有权规则考虑到当数据从C++传递到QML时。当数据从C ++传输到QML时,数据所有权始终由C ++保留。 该规则的例外情况是从显式C ++方法调用返回QObject时:在这种情况下,除非通过调用QQmlEngine将对象的所有权显式设置为与C ++一起使用,否则QML引擎假定该对象的所有权: 使用QQmlEngine :: CppOwnership指定的setObjectOwnership()。(暂时没能全部理解。)

此外,QML引擎尊重Qt C ++对象的常规QObject父所有权语义,并且永远不会删除具有父对象的QObject实例。

公开属性:

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

例如,下面是带有author属性的Message类。 如Q_PROPERTY宏调用所指定,此属性可通过author()方法读取,并且可通过setAuthor()方法写入:

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的文件时将此类的实例设置为上下文属性,则:

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的基本功能,它通过在属性的任何依赖关系值发生变化时自动更新属性来强制属性之间的关系。

在上面的示例中,如Q_PROPERTY()宏调用中所指定,author属性的关联NOTIFY信号为authorChanged。 这意味着每当发出信号时(如作者在Message :: setAuthor()中进行更改时一样),这会通知QML引擎必须更新所有涉及author属性的绑定,并且引擎将更新文本。 通过再次调用Message :: author()设置属性。

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

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

使用通知信号(NOTIFY)的注意事项:

为了防止循环或过度计算,开发人员应该确保只有在属性值实际更改时才会发出属性更改信号。此外,如果一个属性或一组属性很少使用,则允许对多个属性使用相同的通知信号。这样做时应小心,以确保性能不会受到影响。

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

CONSTANT属性只能用于其值在类构造函数中已设置并最终确定的属性。 想要在绑定中使用的所有其他属性应改为具有NOTIFY信号。

 

对象的类型属性:其实就是C++类中包含的其他类实例。

如果对象类型已在QML类型系统中正确注册,则可以从QML访问对象的类型属性。

例如,消息类型可能具有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。但是,出于这个目的,应该使用qqmlistproperty而不是QList<T>作为属性类型。这是因为QList不是QObject派生的类型,因此无法通过Qt元对象系统提供必要的QML属性特性,例如修改列表时的信号通知。

例如,下面的MessageBoard类具有一个QQmlListProperty类型的messages属性,该属性存储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的模板类类型(在本例中是消息)必须注册到QML类型系统。

 

分组属性:

任何只读对象类型属性都可以作为分组属性从QML代码中访问。这可用于公开一组相关属性,这些属性描述类型的一组属性。

例如,假设Message :: author属性的类型为MessageAuthor,而不是简单的字符串,其子属性为name和email:

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代码访问,如果它是:

  • 用Q_INVOKABLE()宏标记的公共方法

  • 作为公共Qt插槽的方法

例如,下面的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可以调用以下方法中所示的两个方法:

//C++代码
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();
}

//QML代码
// 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 ++函数,则将根据提供的参数的数量和类型来调用正确的函数。

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

 

公开信号(暴露信号:Signals):

可以从QML代码访问任何QObject派生类型的公共信号。

QML引擎会自动为QML使用的任何QObject派生类型的信号创建信号处理程序。 信号处理程序始终在<Signal>上命名,其中<Signal>是信号的名称,首字母大写。 信号传递的所有参数均可通过参数名称在信号处理程序中使用。

例如,假设MessageBoard类具有带有单个参数subject的newMessagePosted()信号:

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

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

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

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

类可能包含多个具有相同名称的信号,但是只有最终信号才能作为QML信号进行访问。 请注意,名称相同但参数不同的信号无法相互区分。

 


从C++中定义QML类型


可以在C ++中定义QML类型,然后在QML类型系统中注册。 这允许将C ++类实例化为QML对象类型,从而使自定义对象类型可以用C ++实现并集成到现有QML代码中。 C ++类也可能出于其他目的而注册:例如,可以将其注册为Singleton Type,以使单个类实例可以通过QML代码导入,或者可以注册,以启用非实例化的枚举值 类可从QML访问。

此外,Qt QML模块提供了定义与诸如附加属性和默认属性之类的QML概念集成的QML类型的机制。

使用C ++代码扩展QML时,可以在QML类型系统中注册C ++类,以使该类可用作QML代码中的数据类型。 尽管可以从QML访问任何QObject派生类的属性,方法和信号,如将C ++类型的属性暴露给QML中所讨论的那样,但直到将其注册到类型系统后,此类才能用作QML的数据类型。 另外,注册还可以提供其他功能,例如允许从QML将类用作可实例化的QML对象类型,或者允许从QML导入和使用该类的单例实例。

此外,Qt QML模块提供了用于实现QML特定功能的机制,例如C ++中的附加属性和默认属性。

(请注意,使用C ++编写QML扩展教程演示了本文档中涵盖的许多重要概念。)

向QML类型系统注册C ++类型:

可以将QObject派生的类注册到QML类型系统中,以使该类型可用作QML代码中的数据类型。

该引擎允许实例化和非实例化类型的注册。 注册可实例化的类型可使C ++类用作QML对象类型的定义,从而允许将其用于QML代码的对象声明中以创建此类型的对象。 注册还为引擎提供了其他类型的元数据,使该类型(以及该类声明的任何枚举)可用作属性值,方法参数和返回值以及在QML和C ++之间交换的信号参数的数据类型。

注册非实例化类型也以这种方式将类注册为数据类型,但是该类型不能用作从QML实例化为QML对象类型。 例如,如果某个类型具有应暴露给QML的枚举但该类型本身不应实例化,则此功能很有用。

有关选择正确方法将C ++类型公开给QML的快速指南,请参阅在C ++和QML之间选择正确的集成方法。参考上面的导图。

注册实例化对象类型:

任何QObject派生的C ++类都可以注册为QML对象类型的定义。 一旦在QML类型系统中注册了一个类,就可以像声明QML代码中的任何其他对象类型一样声明和实例化该类。 一旦创建,就可以从QML中操纵类实例。 正如将C ++类型的属性暴露给QML所解释的那样,可以从QML代码访问任何QObject派生类的属性,方法和信号。

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

例如,假设有一个具有author和creationDate属性的Message类:

class Message : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString author READ author WRITE setAuthor NOTIFY authorChanged)
    Q_PROPERTY(QDateTime creationDate READ creationDate WRITE setCreationDate NOTIFY creationDateChanged)
public:
    // ...
};

可以通过使用适当的类型名称空间和版本号调用qmlRegisterType()来注册此类型。 例如,要使该类型在版本1.0的com.mycompany.messaging命名空间中可用:

qmlRegisterType<Message>("com.mycompany.messaging", 1, 0, "Message");

可以在QML的对象声明中使用该类型,并且可以按照以下示例读取和写入其属性:

import com.mycompany.messaging 1.0

Message {
    author: "Amelie"
    creationDate: new Date()
}

注册非实例类型:

有时,可能需要向QML类型系统注册QObject派生的类,而不是将其注册为可实例化的类型。 例如,如果是C ++类,就是这种情况:

  • 是不应可实例化的接口类型
  • 是不需要公开给QML的基类类型
  • 声明一些枚举,这些枚举应可从QML访问,但其他枚举不应可实例化
  • 是应通过单实例提供给QML的类型,不应从QML实例化

Qt QML模块提供了几种注册非实例类型的方法:

  • qmlRegisterType()(不带参数)注册了不可实例化且无法从QML引用的C ++类型。 这使引擎可以强制从QML实例化任何继承的类型。

  • qmlRegisterInterface()注册现有的Qt接口类型。 该类型不能从QML实例化,并且不能使用它声明QML属性。 但是,从QML使用这种类型的C ++属性将执行预期的接口强制转换。

  • qmlRegisterUncreatableType()注册一个命名的C ++类型,该类型不可实例化,但应标识为QML类型系统的类型。 如果应从QML访问类型的枚举或附加属性,但该类型本身不可实例化,则此方法很有用。

  • qmlRegisterSingletonType()注册一个可以从QML导入的单例类型,如下所述。

请注意,向QML类型系统注册的所有C ++类型都必须是QObject派生的,即使它们不是不可实例化的。

用单例类型注册单例对象:

单例类型使属性,信号和方法可以在命名空间中公开,而无需客户端手动实例化对象实例。 特别是QObject单例类型是一种提供功能或全局属性值的高效便捷的方法。

请注意,单例类型没有关联的QQmlContext,因为它们在引擎中的所有上下文之间共享。 QObject单例类型实例由QQmlEngine构造和拥有,并且在销毁引擎时将销毁。

可以以与任何其他QObject或实例化类型类似的方式与QObject单例类型进行交互,除了将仅存在一个(引擎构造并拥有)实例,并且必须通过类型名称而不是id进行引用。 可以绑定QObject单例类型的Q_PROPERTY,并且可以在信号处理程序表达式中使用QObject模块API的Q_INVOKABLE函数。 这使单例类型成为实现样式或主题的理想方式,并且它们也可以代替导入“ ..pragma library”脚本来使用,以存储全局状态或提供全局功能。

一旦注册,就可以像导入QML的任何其他QObject实例一样导入和使用QObject单例类型。 下面的示例假定QObject单例类型已注册到版本为1.0的“ MyThemeModule”命名空间中,其中QObject具有QColor“ color” Q_PROPERTY:

import MyThemeModule 1.0 as Theme

Rectangle {
    color: Theme.color // binding.
}

QJSValue也可以作为单例类型公开,但是客户应注意,此类单例类型的属性无法绑定。

有关如何实现和注册新的单例类型以及如何使用现有的单例类型的更多信息,请参见qmlRegisterSingletonType()

注意:QML中已注册类型的枚举值应以大写字母开头。

类型修订和版本:

许多类型注册功能要求为注册的类型指定版本。 类型修订和版本允许新属性或方法存在于新版本中,同时保持与先前版本的兼容性。

考虑以下两个QML文件:

// main.qml
import QtQuick 1.0

Item {
    id: root
    MyType {}
}

 

// MyType.qml
import MyTypes 1.0

CppType {
    value: root.x
}

 

其中CppType映射到C ++类CppType。

如果CppType的作者在其类型定义的新版本中将root属性添加到CppType,则root.x现在将解析为其他值,因为root也是顶级组件的ID。 作者可以指定可以从特定的次要版本获得新的根属性。 这允许在不破坏现有程序的情况下将新属性和功能添加到现有类型。

REVISION标记用于将根属性标记为该类型的修订版1中添加的。 也可以使用Q_REVISION(x)宏将诸如Q_INVOKABLE,信号和插槽之类的方法标记为修订版本:

class CppType : public BaseType
{
    Q_OBJECT
    Q_PROPERTY(int root READ root WRITE setRoot NOTIFY rootChanged REVISION 1)

signals:
    Q_REVISION(1) void rootChanged();
};

要将新的类修订注册到特定版本,请使用以下功能:

template<typename T, int metaObjectRevision>
int qmlRegisterType(const char *uri, int versionMajor, int versionMinor, const char *qmlName)

要为MyTypes 1.1注册CppType版本1:

qmlRegisterType<CppType,1>("MyTypes", 1, 1, "CppType")

root仅在导入MyTypes 1.1版时可用。

出于相同的原因,在更高版本中引入的新类型应使用qmlRegisterType的次要版本参数。

该语言的功能允许在不破坏现有应用程序的情况下进行行为更改。 因此,QML模块的作者应始终记住记录次要版本之间的更改,并且QML模块的用户应在部署更新的import语句之前检查其应用程序是否仍正常运行。

您还可以使用qmlRegisterRevision()函数注册您的类型所依赖的基类的修订版:

template<typename T, int metaObjectRevision>
int qmlRegisterRevision(const char *uri, int versionMajor, int versionMinor)

template<typename T, int metaObjectRevision>
int qmlRegisterUncreatableType(const char *uri, int versionMajor, int versionMinor, const char *qmlName, const QString& reason)

template<typename T, typename E, int metaObjectRevision>
int qmlRegisterExtendedUncreatableType(const char *uri, int versionMajor, int versionMinor, const char *qmlName, const QString& reason)

例如,如果更改了BaseType并且现在具有修订版1,则可以指定您的类型使用新修订版:

qmlRegisterRevision<BaseType,1>("MyTypes", 1, 1);

当从其他作者提供的基类派生时,这很有用。 从Qt Quick模块扩展类时。

注意:QML引擎不支持对属性或分组和附加属性对象的信号的修订。

注册扩展对象:

将现有的类和技术集成到QML中时,通常需要对API进行调整,以使其更好地适应声明性环境。 尽管通常可以通过直接修改原始类来获得最佳结果,但是如果这不可能或由于其他一些原因而变得复杂,则扩展对象无需进行直接修改就可以实现有限的扩展可能性。

扩展对象将其他属性添加到现有类型。 扩展对象只能添加属性,不能添加信号或方法。 扩展类型定义允许程序员在注册类时提供其他类型,称为扩展类型。 从QML内部使用时,这些属性与原始目标类透明合并。 例如:

QLineEdit {
    leftMargin: 20
}

leftMargin属性是添加到现有C ++类型QLineEdit的新属性,而无需修改其源代码。

qmlRegisterExtendedType()函数用于注册扩展类型。 请注意,它有两种形式。

template<typename T, typename ExtendedT>
int qmlRegisterExtendedType(const char *uri, int versionMajor, int versionMinor, const char *qmlName)

template<typename T, typename ExtendedT>
int qmlRegisterExtendedType()

应该使用此函数代替常规的qmlRegisterType()变体。 这些参数与相应的非扩展注册函数相同,除了ExtendedT参数是扩展对象的类型之外。

扩展类是常规QObject,其构造函数采用QObject指针。 但是,扩展类的创建被延迟,直到访问第一个扩展属性。 创建扩展类,并将目标对象作为父级传递。 访问原始属性时,将使用扩展对象上的相应属性。

扩展对象示例演示了扩展对象的用法。[https://doc.qt.io/qt-5/qtqml-referenceexamples-extended-example.html]

定义特定于QML的类型和属性:

提供附加属性:

在QML语言语法中,存在附加属性和附加信号处理程序的概念,它们是附加到对象的附加属性。 本质上,此类属性是通过附加类型实现和提供的,并且这些属性可以附加到其他类型的对象。 这与由对象类型本身(或对象的继承类型)提供的普通对象属性形成对比。

例如,下面的项目使用附加的属性和附加的处理程序:

import QtQuick 2.0

Item {
    width: 100; height: 100

    focus: true
    Keys.enabled: false
    Keys.onReturnPressed: console.log("Return key was pressed")
}

在这里,Item对象能够访问和设置Keys.enabled和Keys.onReturnPressed的值。 这允许Item对象访问这些额外的属性,作为对其自身现有属性的扩展。

实现附加对象的步骤:

在考虑上述例子时,涉及几个方面:

  • 存在一个匿名附加对象类型的实例,该实例具有enable和returnPressed信号,该信号已附加到Item对象,以使其能够访问和设置这些属性。

  • Item对象是附加对象,附加对象类型的实例已附加到该对象。

  • Keys是附加类型,它为attachee提供一个命名限定符“Keys”,通过它可以访问附加对象类型的属性。

当QML引擎处理此代码时,它将创建附加对象类型的单个实例,并将此实例附加到Item对象,从而为它提供对实例的enabled和returnPressed属性的访问。

通过提供附加对象类型和附加类型的类,可以提供从C++提供附加对象的机制。对于附加的对象类型,提供一个QObject派生类,该类定义要使attachee对象可访问的属性。对于附加类型,提供一个QObject派生类:

  • 实现具有以下签名的静态qmlAttachedProperties():

static <AttachedPropertiesType> *qmlAttachedProperties(QObject *object);

此方法应返回附加对象类型的实例。

QML引擎调用此方法,以便将附加对象类型的实例附加到由对象参数指定的attachee。这个方法实现通常(虽然不是严格要求)将返回的实例作为对象的父对象,以防止内存泄漏。

此方法最多由引擎为每个attachee对象实例调用一次,因为引擎缓存返回的实例指针以供后续附加的属性访问。因此,在销毁attachee对象之前,不能删除附件对象。

  • 通过调用带有QML_HAS_ATTACHED_PROPERTIES标志的QML_DECLARE_TYPEINFO()宏,声明为附加类型。

实现附加对象:一个示例

例如,以前面示例中描述的消息类型为例:

class Message : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString author READ author WRITE setAuthor NOTIFY authorChanged)
    Q_PROPERTY(QDateTime creationDate READ creationDate WRITE setCreationDate NOTIFY creationDateChanged)
public:
    // ...
};

假设有必要在将消息发布到消息板上时在消息上触发信号,并在消息板上的消息到期时进行跟踪。 由于没有必要直接将这些属性添加到Message,因为这些属性与留言板上下文更相关,所以可以将它们实现为Message对象上的附加属性,这些属性是通过“ MessageBoard”限定符提供的。 就前面描述的概念而言,此处涉及的各方是:

  • 匿名附加对象类型的实例,它提供已发布的信号和过期的属性。此类型由下面的MessageBoardAttachedType实现
  • 一个消息对象,它将是attachee
  • 消息板类型,它将是消息对象用来访问附加属性的附加类型

下面是一个示例实现。首先,需要有一个附加的对象类型,该对象类型具有被附加者可以访问的必要属性和信号:

class MessageBoardAttachedType : public QObject
{
    Q_OBJECT
    Q_PROPERTY(bool expired READ expired WRITE setExpired NOTIFY expiredChanged)
public:
    MessageBoardAttachedType(QObject *parent);
    bool expired() const;
    void setExpired(bool expired);
signals:
    void published();
    void expiredChanged();
};

然后附加类型MessageBoard必须声明qmlAttachedProperties()方法,该方法返回由MessageBoardAttachedType实现的附加对象类型的实例。此外,消息板必须通过QML_DECLARE_TYPEINFO()宏声明为附加类型:

class MessageBoard : public QObject
{
    Q_OBJECT
public:
    static MessageBoardAttachedType *qmlAttachedProperties(QObject *object)
    {
        return new MessageBoardAttachedType(object);
    }
};
QML_DECLARE_TYPEINFO(MessageBoard, QML_HAS_ATTACHED_PROPERTIES)

现在,消息类型可以访问附加对象类型的属性和信号:

Message {
    author: "Amelie"
    creationDate: new Date()

    MessageBoard.expired: creationDate < new Date("January 01, 2015 10:45:00")
    MessageBoard.onPublished: console.log("Message by", author, "has been
published!")
}

此外,C++实现可以通过调用QMLATTHCHEDFRESTIOSObject()函数来访问已连接到任何对象的附加对象实例。

例如:

Message *msg = someMessageInstance();
MessageBoardAttachedType *attached =
        qobject_cast<MessageBoardAttachedType*>(qmlAttachedPropertiesObject<MessageBoard>(msg));

qDebug() << "Value of MessageBoard.expired:" << attached->expired();

 

属性修饰符类型

属性修饰符类型是一种特殊的QML对象类型。属性修饰符类型实例影响应用它的属性(QML对象实例的)。有两种不同的属性修饰符类型:

  • 属性值写入侦听器
  • 属性值源

属性值写入拦截器可用于筛选或修改写入属性的值。目前,唯一受支持的属性值写入拦截器是QtQuick导入提供的行为类型。

属性值源可用于随时间自动更新属性值。客户端可以定义自己的属性值源类型。QtQuick导入提供的各种属性动画类型是属性值源的示例。

可以通过“<modifier type>on<propertyName>”语法创建属性修饰符类型实例并将其应用于QML对象的属性,如下例所示:

import QtQuick 2.0

Item {
    width: 400
    height: 50

    Rectangle {
        width: 50
        height: 50
        color: "red"

        NumberAnimation on x {
            from: 0
            to: 350
            loops: Animation.Infinite
            duration: 2000
        }
    }
}

客户端可以注册自己的属性值源类型,但当前不能注册属性值写入侦听器。

属性值源

属性值源是QML类型,可以使用<PropertyValueSource>on<Property>语法随时间自动更新属性值。例如,QtQuick模块提供的各种属性动画类型都是属性值源的示例。

属性值源可以通过C++类实现QQMLPrimeTyValueSurCE并提供一个实现,该实现可以随着时间的推移将不同的值写入属性。当使用QML中的<property value source>on<property>语法将属性值源应用于属性时,引擎将为其提供对此属性的引用,以便可以更新属性值。

例如,假设有一个RandomNumberGenerator类可用作属性值源,因此当应用于QML属性时,它将每隔500毫秒将属性值更新为一个不同的随机数。此外,可以向该随机数生成器提供maxValue。这个类可以实现如下:

class RandomNumberGenerator : public QObject, public QQmlPropertyValueSource
{
    Q_OBJECT
    Q_INTERFACES(QQmlPropertyValueSource)
    Q_PROPERTY(int maxValue READ maxValue WRITE setMaxValue NOTIFY maxValueChanged);
public:
    RandomNumberGenerator(QObject *parent)
        : QObject(parent), m_maxValue(100)
    {
        QObject::connect(&m_timer, SIGNAL(timeout()), SLOT(updateProperty()));
        m_timer.start(500);
    }

    int maxValue() const;
    void setMaxValue(int maxValue);

    virtual void setTarget(const QQmlProperty &prop) { m_targetProperty = prop; }

signals:
    void maxValueChanged();

private slots:
    void updateProperty() {
        m_targetProperty.write(QRandomGenerator::global()->bounded(m_maxValue));
    }

private:
    QQmlProperty m_targetProperty;
    QTimer m_timer;
    int m_maxValue;
};

当QML引擎遇到使用RandomNumberGenerator作为属性值源时,它将调用RandomNumberGenerator::setTarget()为类型提供已应用值源的属性。当RandomNumberGenerator中的内部计时器每500毫秒触发一次时,它将向指定的属性写入一个新的数值。

RandomNumberGenerator类在QML类型系统中注册后,就可以从QML中将其用作属性值源。下面,它用于每500毫秒更改一个矩形的宽度:

import QtQuick 2.0

Item {
    width: 300; height: 300

    Rectangle {
        RandomNumberGenerator on width { maxValue: 300 }

        height: 100
        color: "red"
    }
}

在所有其他方面,属性值源都是常规的QML类型,可以有属性、信号方法等,但具有附加功能,可以使用<PropertyValueSource>on<property>语法更改属性值。

当一个属性值源对象被分配给一个属性时,QML首先尝试正常地分配它,就像它是一个普通的QML类型一样。仅当此赋值失败时,引擎才会调用setTarget()方法。这允许在上下文中使用类型,而不仅仅是作为值源。

指定QML对象类型的默认属性

任何注册为可实例化QML对象类型的QObject派生类型都可以选择指定该类型的默认属性。默认属性是如果对象的子对象未指定给任何特定属性,则自动将其指定给的属性。

可以通过调用具有特定“DefaultProperty”值的类的Q_CLASSINFO()宏来设置默认属性。例如,下面的MessageBoard类将其messages属性指定为该类的默认属性:

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

private:
    QList<Message *> m_messages;
};

这样,如果未将MessageBoard对象的子对象分配给特定属性,则可以将其自动分配给messages属性。例如:

MessageBoard {
    Message { author: "Naomi" }
    Message { author: "Clancy" }
}

如果未将消息设置为默认属性,则必须将任何消息对象显式分配给消息属性,如下所示:

MessageBoard {
    messages: [
        Message { author: "Naomi" },
        Message { author: "Clancy" }
    ]
}

(顺便说一下,Item::data属性是它的默认属性。添加到此数据属性的任何项对象也将添加到Item::children的列表中,因此使用默认属性可以为项声明可视子项,而无需将它们显式分配给children属性。)

使用Qt Quick模块定义视觉项目

使用Qt-Quick模块构建用户界面时,要可视化呈现的所有QML对象都必须派生自项类型,因为它是Qt-Quick中所有可视化对象的基类型。此项目类型由QQuestIt+C++类实现,QQuestC+++类由QT快速模块提供。因此,当需要在C++中实现可视类型,可以将其集成到基于QML的用户界面中时,应该对该类进行子类化。

有关详细信息,请参阅QQuickItem文档。此外,用C++教程编写QML扩展说明了QQuiTube为基础的可视化项目如何在C++中实现,并集成到QT基于快速的用户界面中。

接收对象初始化通知

对于某些自定义QML对象类型,在创建对象并设置其所有属性之前延迟特定数据的初始化可能是有益的。例如,如果初始化代价高昂,或者在初始化所有属性值之前不应执行初始化,则可能会出现这种情况。

Qt QML模块提供了QQmlParserStatus子类以实现这些目的。它定义了在组件实例化的不同阶段调用的许多虚拟方法。为了接收这些通知,C++类应该继承QQMLPARSSTATION,并且还使用QyIdFACESSUMER()宏通知QT元系统。

例如:

class MyQmlType : public QObject, public QQmlParserStatus
{
    Q_OBJECT
    Q_INTERFACES(QQmlParserStatus)
public:
    virtual void componentComplete()
    {
        // Perform some initialization here now that the object is fully created
    }
};

 

 


用C++编写QML扩展


QT C++与QML扩展教程。

QT QML模块提供了一组API,用于通过C++扩展来扩展QML。您可以编写扩展来添加自己的QML类型、扩展现有QT类型,或者调用不可从普通QML代码访问的C/C++函数。

本教程展示如何使用C++编写QML扩展,其中包括核心QML特征,包括属性、信号和绑定。它还展示了如何通过插件部署扩展。

本教程中涉及的许多主题在集成QML和C++及其文档子主题方面有进一步的详细记录。特别是,您可能会感兴趣的子主题,将C++类的属性暴露为QML,并从C++定义QML类型。

运行教程示例

本教程中的代码作为一个示例项目提供,其中的子项目与每个教程章节关联。在Qt Creator中,打开欢迎模式并从示例中选择教程。在编辑模式下,展开扩展qml项目,右键单击要运行的子项目(章节),然后选择运行。

第1章:创建新类型

扩展qml/chapter1基础

扩展QML时的常见任务是提供一种新的QML类型,该类型支持一些自定义功能,而不是内置的Qt Quick类型所提供的功能。 例如,可以执行此操作以实现特定的数据模型,或为类型提供自定义的绘画功能,或访问无法通过内置QML功能访问的系统功能(如网络编程)。

在本教程中,我们将展示如何使用Qt Quick模块中的C ++类扩展QML。 最终结果将是一个简单的饼图显示,该显示由几种自定义QML类型实现,这些QML类型通过诸如绑定和信号之类的QML功能连接在一起,并通过插件提供给QML运行时。

首先,让我们创建一个名为“PieChart”的新QML类型,它有两个属性:名称和颜色。我们将在名为“Charts”的可导入类型命名空间中提供它,版本为1.0。

我们希望这种PieChart类型可以从QML中使用,如下所示:

import Charts 1.0

PieChart {
    width: 100; height: 100
    name: "A simple pie chart"
    color: "red"
}

要做到这一点,我们需要一个C++类来封装这个PyCurpe类型及其两个属性。由于QML广泛使用Qt的元对象系统,这个新类必须:

  • 从QObject继承
  • 使用Q_PROPERTY宏声明其属性

这是我们的PieChart类,在PieChart.h中定义:

#include <QtQuick/QQuickPaintedItem>
#include <QColor>

class PieChart : public QQuickPaintedItem
{
    Q_OBJECT
    Q_PROPERTY(QString name READ name WRITE setName)
    Q_PROPERTY(QColor color READ color WRITE setColor)

public:
    PieChart(QQuickItem *parent = 0);

    QString name() const;
    void setName(const QString &name);

    QColor color() const;
    void setColor(const QColor &color);

    void paint(QPainter *painter);

private:
    QString m_name;
    QColor m_color;
};

该类继承自QQuickPaintedItem,因为我们希望在使用QPainter API执行绘图操作时重写QQuickPaintedItem::paint()。如果类只是表示某种数据类型,而不是实际需要显示的项,那么它可以简单地从QObject继承。或者,如果我们想扩展现有的基于QObject的类的功能,它可以从该类继承。或者,如果我们想创建一个不需要使用QPainter API执行绘图操作的可视化项,我们可以将QQuickItem子类化。

PieChart类使用Q_PROPERTY宏定义了两个属性name和color,并重写QQuickPaintedItem::paint()。cpp中的类实现只是根据需要设置并返回m_name和m_color值,并实现paint()以绘制简单的饼图。它还关闭QGraphicsItem::ItemHasNoContents标志以启用绘制:

PieChart::PieChart(QQuickItem *parent)
    : QQuickPaintedItem(parent)
{
}
...
void PieChart::paint(QPainter *painter)
{
    QPen pen(m_color, 2);
    painter->setPen(pen);
    painter->setRenderHints(QPainter::Antialiasing, true);
    painter->drawPie(boundingRect().adjusted(1, 1, -1, -1), 90 * 16, 290 * 16);
}

既然我们已经定义了PieChart类型,我们将从QML中使用它。app.qml文件创建饼图项,并使用标准qml文本项显示饼图的详细信息:

import Charts 1.0
import QtQuick 2.0

Item {
    width: 300; height: 200

    PieChart {
        id: aPieChart
        anchors.centerIn: parent
        width: 100; height: 100
        name: "A simple pie chart"
        color: "red"
    }

    Text {
        anchors { bottom: parent.bottom; horizontalCenter: parent.horizontalCenter; bottomMargin: 20 }
        text: aPieChart.name
    }
}

请注意,尽管颜色在QML中指定为字符串,但它会自动转换为PieChart color属性的QColor对象。为各种其他基本类型提供了自动转换;例如,“640x480”之类的字符串可以自动转换为QSize值。

我们还将创建一个C++应用程序,使用QQueVIEW运行并显示App.QML。应用程序必须使用qmlRegisterType()函数注册PieChart类型,以允许从QML使用它。如果不注册类型,app.qml将无法创建PieChart。

下面是application main.cpp:

#include "piechart.h"
#include <QtQuick/QQuickView>
#include <QGuiApplication>

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

    qmlRegisterType<PieChart>("Charts", 1, 0, "PieChart");

    QQuickView view;
    view.setResizeMode(QQuickView::SizeRootObjectToView);
    view.setSource(QUrl("qrc:///app.qml"));
    view.show();
    return app.exec();
}

对qmlRegisterType()的此调用将PieChart类型注册为类型为“ PieChart”的类型名称空间,该名称空间的名称为“ Charts”,版本为1.0。

最后,我们编写一个.pro项目文件,其中包括文件和声明性库:

QT += qml quick

HEADERS += piechart.h
SOURCES += piechart.cpp \
           main.cpp

RESOURCES += chapter1-basics.qrc

DESTPATH = $$[QT_INSTALL_EXAMPLES]/qml/tutorials/extending-qml/chapter1-basics
target.path = $$DESTPATH

qml.files = *.qml
qml.path = $$DESTPATH

INSTALLS += target qml

现在我们可以构建并运行应用程序:

 

注意:您可能会看到一个警告表达式。。。取决于不可通知的属性:PieChart::name。发生这种情况是因为我们向writeable name属性添加了绑定,但尚未为其定义通知信号。因此,如果名称值更改,QML引擎将无法更新绑定。这将在以下章节中讨论。

本章将引用以下文件中的源代码:

第2章:C++方法与信号的连接

扩展qml/chapter2方法

假设我们希望PieChart有一个“clearChart()”方法来擦除图表,然后发出一个“chartCleared”信号。我们的app.qml可以调用clearChart()并接收如下chartCleared()信号:

import Charts 1.0
import QtQuick 2.0

Item {
    width: 300; height: 200

    PieChart {
        id: aPieChart
        anchors.centerIn: parent
        width: 100; height: 100
        color: "red"

        onChartCleared: console.log("The chart has been cleared")
    }

    MouseArea {
        anchors.fill: parent
        onClicked: aPieChart.clearChart()
    }

    Text {
        anchors { bottom: parent.bottom; horizontalCenter: parent.horizontalCenter; bottomMargin: 20 }
        text: "Click anywhere to clear the chart"
    }
}

为此,我们将CultCARTHER()方法和CARTCAREAREDE()信号添加到C++类:

class PieChart : public QQuickPaintedItem
{
    ...
public:
    ...
    Q_INVOKABLE void clearChart();

signals:
    void chartCleared();
    ...
};

Q_INVOKABLE的使用使clearChart()方法可用于Qt元对象系统,进而可用于QML。 请注意,它可以被声明为Qt插槽,而不是使用Q_INVOKABLE,因为也可以从QML调用该插槽。 这两种方法都是有效的。

clearChart()方法只需将颜色更改为Qt::transparent,重新绘制图表,然后发出chartCleared()信号:

void PieChart::clearChart()
{
    setColor(QColor(Qt::transparent));
    update();

    emit chartCleared();
}

现在,当我们运行应用程序并单击窗口时,饼图消失,应用程序输出:

qml: The chart has been cleared

本章将引用以下文件中的源代码:

第3章:添加属性绑定

扩展qml/chapter3绑定

属性绑定是QML的一个强大功能,它允许自动同步不同类型的值。当属性值更改时,它使用信号通知和更新其他类型的值。

让我们为颜色属性启用属性绑定。这意味着如果我们有这样的代码:

import Charts 1.0
import QtQuick 2.0

Item {
    width: 300; height: 200

    Row {
        anchors.centerIn: parent
        spacing: 20

        PieChart {
            id: chartA
            width: 100; height: 100
            color: "red"
        }

        PieChart {
            id: chartB
            width: 100; height: 100
            color: chartA.color
        }
    }

    MouseArea {
        anchors.fill: parent
        onClicked: { chartA.color = "blue" }
    }

    Text {
        anchors { bottom: parent.bottom; horizontalCenter: parent.horizontalCenter; bottomMargin: 20 }
        text: "Click anywhere to change the chart color"
    }
}

 

“color:chartA.color”语句将chartB的颜色值绑定到chartA的颜色。每当chartA的颜色值更改时,chartB的颜色值将更新为相同的值。单击窗口时,MouseArea中的onclick处理程序将更改chartA的颜色,从而将两个图表都更改为蓝色。

很容易为颜色属性启用属性绑定。我们在它的Q_PROPERTY()声明中添加了一个NOTIFY特性,以指示每当值改变时都会发出一个“colorChanged”信号。

class PieChart : public QQuickPaintedItem
{
    ...
    Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged)
public:
    ...
signals:
    void colorChanged();
    ...
};

然后,我们在setPieSlice()中发出这个信号:

void PieChart::setColor(const QColor &color)
{
    if (color != m_color) {
        m_color = color;
        update();   // repaint with the new color
        emit colorChanged();
    }
}

在发出colorChanged()之前,setColor()必须检查颜色值是否已实际更改。这样可以确保信号不会不必要地发出,并防止其他类型响应值更改时出现循环。

绑定的使用对QML至关重要。如果可以实现属性,则应始终为它们添加通知信号,以便可以在绑定中使用属性。无法绑定的属性不能自动更新,也不能在QML中灵活使用。此外,由于在QML使用中经常调用和依赖绑定,如果未实现绑定,则自定义QML类型的用户可能会看到意外的行为。

本章将引用以下文件中的源代码:

第4章:使用自定义属性类型

扩展qml/chapter4自定义属性类型

PieChart类型当前具有字符串类型属性和颜色类型属性。它可能有许多其他类型的属性。例如,它可以有一个int type属性来存储每个图表的标识符:

// C++
class PieChart : public QQuickPaintedItem
{
    Q_PROPERTY(int chartId READ chartId WRITE setChartId NOTIFY chartIdChanged)
    ...

public:
    void setChartId(int chartId);
    int chartId() const;
    ...

signals:
    void chartIdChanged();
};

// QML
PieChart {
    ...
    chartId: 100
}

除了int之外,我们还可以使用其他各种属性类型。QML自动支持QColor、QSize和QRect等许多Qt数据类型。(参见QML和C++文档之间的数据类型转换,以获取完整列表)。

如果要创建默认情况下QML不支持其类型的属性,则需要向QML引擎注册该类型。

例如,让我们用一个名为“PieSlice”的类型替换属性的使用,该类型有一个color属性。我们不指定颜色,而是指定一个PieSlice值,该值本身包含一种颜色:

import Charts 1.0
import QtQuick 2.0

Item {
    width: 300; height: 200

    PieChart {
        id: chart
        anchors.centerIn: parent
        width: 100; height: 100

        pieSlice: PieSlice {
            anchors.fill: parent
            color: "red"
        }
    }

    Component.onCompleted: console.log("The pie is colored " + chart.pieSlice.color)
}

与PieChart类似,这个新的PieSlice类型继承自QQuickPaintedItem,并使用Q_PROPERTY()声明其属性:

class PieSlice : public QQuickPaintedItem
{
    Q_OBJECT
    Q_PROPERTY(QColor color READ color WRITE setColor)

public:
    PieSlice(QQuickItem *parent = 0);

    QColor color() const;
    void setColor(const QColor &color);

    void paint(QPainter *painter);

private:
    QColor m_color;
};

为了在PieChart中使用它,我们修改了颜色属性声明和相关的方法签名

class PieChart : public QQuickItem
{
    Q_OBJECT
    Q_PROPERTY(PieSlice* pieSlice READ pieSlice WRITE setPieSlice)
    ...
public:
    ...
    PieSlice *pieSlice() const;
    void setPieSlice(PieSlice *pieSlice);
    ...
};

在实现setPieSlice()时有一件事需要注意。PieSlice是一个可视项,因此必须使用QQuickItem::setParentItem()将其设置为PieChart的子项,以便PieChart知道在绘制其内容时绘制此子项:

void PieChart::setPieSlice(PieSlice *pieSlice)
{
    m_pieSlice = pieSlice;
    pieSlice->setParentItem(this);
}

与PieChart类型一样,PieSlice类型必须使用QML中要使用的qmlRegisterType()进行注册。与PieChart一样,我们将向“Charts”类型命名空间(版本1.0)添加类型:

int main(int argc, char *argv[])
{
    ...
    qmlRegisterType<PieSlice>("Charts", 1, 0, "PieSlice");
    ...
}

本章将引用以下文件中的源代码:

第5章:使用列表属性类型

扩展qml/chapter5列表属性

现在,一个馅饼图只能有一个馅饼。理想情况下,一个图表应该有多个切片,具有不同的颜色和大小。为此,我们可以使用一个slices属性来接受PieSlice项的列表:

import Charts 1.0
import QtQuick 2.0

Item {
    width: 300; height: 200

    PieChart {
        anchors.centerIn: parent
        width: 100; height: 100

        slices: [
            PieSlice {
                anchors.fill: parent
                color: "red"
                fromAngle: 0; angleSpan: 110
            },
            PieSlice {
                anchors.fill: parent
                color: "black"
                fromAngle: 110; angleSpan: 50
            },
            PieSlice {
                anchors.fill: parent
                color: "blue"
                fromAngle: 160; angleSpan: 100
            }
        ]
    }
}

为此,我们用声明为QQmlListProperty类型的slices属性替换PieChart中的pieSlice属性。 QQmlListProperty类允许在QML扩展中创建列表属性。 我们用slices()函数代替了pieSlice()函数,该函数返回一个切片列表,并添加一个内部append_slice()函数(在下面讨论)。 我们还使用QList将切片的内部列表存储为m_slices:

class PieChart : public QQuickItem
{
    Q_OBJECT
    Q_PROPERTY(QQmlListProperty<PieSlice> slices READ slices)
    ...
public:
    ...
    QQmlListProperty<PieSlice> slices();

private:
    static void append_slice(QQmlListProperty<PieSlice> *list, PieSlice *slice);

    QString m_name;
    QList<PieSlice *> m_slices;
};

尽管slices属性没有关联的写函数,但由于qqmlistproperty的工作方式,它仍然可以修改。在PieChart实现中,我们实现PieChart::slices()以返回QQmlListProperty值,并指示每当QML请求向列表中添加项时,都将调用内部PieChart::append_slice()函数:

QQmlListProperty<PieSlice> PieChart::slices()
{
    return QQmlListProperty<PieSlice>(this, nullptr, &PieChart::append_slice, nullptr, nullptr, nullptr);
}

void PieChart::append_slice(QQmlListProperty<PieSlice> *list, PieSlice *slice)
{
    PieChart *chart = qobject_cast<PieChart *>(list->object);
    if (chart) {
        slice->setParentItem(chart);
        chart->m_slices.append(slice);
    }
}

函数的作用是:像以前一样简单地设置父项,并将新项添加到m_slices列表中。如您所见,QQmlListProperty的append函数是用两个参数调用的:list属性和要追加的项。

PieSlice类也被修改为包含fromAngle和angleSpan属性,并根据这些值绘制切片。如果您已经阅读了本教程中的前几页,则这是一个简单的修改,因此此处不显示代码。

本章将引用以下文件中的源代码:

第6章:编写扩展插件

扩展qml/chapter6插件

目前,PyScice和PixLice类型被App.QML使用,它在QC++应用中使用QQuQuew显示。使用QML扩展的另一种方法是创建一个插件库,使其作为一个新的QML导入模块提供给QML引擎。这允许PieChart和PieSlice类型注册到一个类型命名空间中,该命名空间可由任何QML应用程序导入,而不是限制这些类型仅由一个应用程序使用。

创建插件的步骤在创建QML插件的C++语言中描述。首先,我们创建一个名为ChartsPlugin的插件类。它将QQmlExtensionPlugin子类化,并在继承的registerTypes()方法中注册我们的QML类型。

以下是ChartsPlugin.h中的ChartsPlugin定义:

#include <QQmlExtensionPlugin>

class ChartsPlugin : public QQmlExtensionPlugin
{
    Q_OBJECT
    Q_PLUGIN_METADATA(IID QQmlExtensionInterface_iid)

public:
    void registerTypes(const char *uri);
};

以及在chartsplugin.cpp中的实现:

#include "piechart.h"
#include "pieslice.h"
#include <qqml.h>

void ChartsPlugin::registerTypes(const char *uri)
{
    qmlRegisterType<PieChart>(uri, 1, 0, "PieChart");
    qmlRegisterType<PieSlice>(uri, 1, 0, "PieSlice");
}

然后,我们编写一个.pro项目文件,该文件将项目定义为一个插件库,并使用DESTDIR指定库文件应构建到../Charts目录中。

TEMPLATE = lib
CONFIG += plugin
QT += qml quick

DESTDIR = ../Charts
TARGET = $$qtLibraryTarget(chartsplugin)

HEADERS += piechart.h \
           pieslice.h \
           chartsplugin.h

SOURCES += piechart.cpp \
           pieslice.cpp \
           chartsplugin.cpp

DESTPATH=$$[QT_INSTALL_EXAMPLES]/qml/tutorials/extending-qml/chapter6-plugins/Charts

target.path=$$DESTPATH
qmldir.files=$$PWD/qmldir
qmldir.path=$$DESTPATH
INSTALLS += target qmldir

CONFIG += install_ok  # Do not cargo-cult this!

OTHER_FILES += qmldir

# Copy the qmldir file to the same folder as the plugin binary
cpqmldir.files = qmldir
cpqmldir.path = $$DESTDIR
COPIES += cpqmldir

在Windows或Linux上构建这个示例时,Charts目录将与使用新导入模块的应用程序位于同一级别。这样,QML引擎将找到我们的模块,因为QML导入的默认搜索路径包括应用程序可执行文件的目录。在macOS上,插件二进制文件被复制到应用程序包中的内容/插件;此路径在第6章PlugIns/app.pro中设置:

osx {
    charts.files = $$OUT_PWD/Charts
    charts.path = Contents/PlugIns
    QMAKE_BUNDLE_DATA += charts
}

在Windows或Linux上构建这个示例时,Charts目录将与使用新导入模块的应用程序位于同一级别。这样,QML引擎将找到我们的模块,因为QML导入的默认搜索路径包括应用程序可执行文件的目录。在macOS上,插件二进制文件被复制到应用程序包中的内容/插件;此路径在第6章PlugIns/app.pro中设置:

osx {
    charts.files = $$OUT_PWD/Charts
    charts.path = Contents/PlugIns
    QMAKE_BUNDLE_DATA += charts
}

为此,我们还需要将此位置添加为main.cpp中的QML导入路径:

  QQuickView view;
#ifdef Q_OS_OSX
    view.engine()->addImportPath(app.applicationDirPath() + "/../PlugIns");
#endif
    ...

当有多个应用程序使用相同的QML导入时,定义自定义导入路径也很有用。

pro文件还包含额外的魔力,以确保模块定义qmldir文件始终复制到与插件二进制文件相同的位置。

qmldir文件声明模块名和模块提供的插件:

module Charts
plugin chartsplugin

现在我们有了一个可以导入到任何应用程序的QML模块,只要QML引擎知道在哪里可以找到它。该示例包含加载app.qml的可执行文件,该文件使用import Charts 1.0语句。或者,可以使用qmlscene工具加载QML文件,将导入路径设置为当前目录,以便找到qmldir文件:

qmlscene -I . app.qml

模块“Charts”将由QML引擎加载,该模块提供的类型将可用于导入它的任何QML文档。

本章将引用以下文件中的源代码:

第七章总结

在本教程中,我们展示了创建QML扩展的基本步骤:

  • 通过子类化QObject并使用qmlRegisterType()注册它们来定义新的QML类型
  • 使用Q_INVOKABLE或Qt插槽添加可调用方法,并使用onSignal语法连接到Qt信号
  • 通过定义NOTIFY信号添加属性绑定
  • 如果内置类型不够,请定义自定义属性类型
  • 使用QQmlListProperty定义列表属性类型
  • 通过定义Qt插件并编写qmldir文件来创建插件库

QML和C ++集成文档显示了可以添加到QML扩展中的其他有用功能。 例如,我们可以使用默认属性来允许添加切片,而无需使用slices属性:

PieChart {
    PieSlice { ... }
    PieSlice { ... }
    PieSlice { ... }
}

或使用属性值源不定期地随机添加和删除切片:

PieChart {
    PieSliceRandomizer on slices {}
}

 


使用上下文属性将C++对象嵌入QML


将QML对象加载到C ++应用程序时,直接嵌入一些可在QML代码中使用的C ++数据会很有用。 例如,这使得可以对嵌入式对象调用C ++方法,或将C ++对象实例用作QML视图的数据模型。

QQmlContext类使将C ++数据注入QML对象成为可能。 此类将数据公开给QML对象的上下文,以便可以直接从QML代码范围内引用数据。

设置一个简单的上下文属性

例如,这里有一个QML项,它引用当前作用域中不存在的currentDateTime值:

// MyItem.qml
import QtQuick 2.0

Text { text: currentDateTime }

此currentDateTime值可以由加载QML组件的C ++应用程序使用QQmlContext :: setContextProperty()直接设置:

QQuickView view;
view.rootContext()->setContextProperty("currentDateTime", QDateTime::currentDateTime());
view.setSource(QUrl::fromLocalFile("MyItem.qml"));
view.show();

注意:由于在QML中求值的所有表达式都是在特定上下文中求值的,因此,如果修改了上下文,则该上下文中的所有绑定都将重新求值。 因此,应在应用程序初始化之外小心使用上下文属性,因为这可能会导致应用程序性能下降。

将对象设置为上下文属性

上下文属性可以包含QVariant或QObject*值。这意味着,也可以使用这种方法注入自定义C++对象,并且可以在QML中直接修改和读取这些对象。在这里,我们修改上面的示例以嵌入QObject实例而不是QDateTime值,QML代码调用对象实例上的方法:

//C++代码
class ApplicationData : public QObject
{
    Q_OBJECT
public:
    Q_INVOKABLE QDateTime getCurrentDateTime() const {
        return QDateTime::currentDateTime();
    }
};

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

    QQuickView view;

    ApplicationData data;
    view.rootContext()->setContextProperty("applicationData", &data);

    view.setSource(QUrl::fromLocalFile("MyItem.qml"));
    view.show();

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

Text { text: applicationData.getCurrentDateTime() }

(请注意,可以通过Qt.formatDateTime()和关联的函数来格式化从C ++返回到QML的日期/时间值。)

例如,如果ApplicationData有一个称为dataChanged()的信号,则可以在Connections对象中使用onDataChanged处理程序连接此信号:

Text {
    text: applicationData.getCurrentDateTime()

    Connections {
        target: applicationData
        onDataChanged: console.log("The application data changed!")
    }
}

上下文属性对于在QML视图中使用基于C ++的数据模型很有用。 请参阅以下示例:

演示在QML视图中使用QStringList,基于QList <QObject *>的模型和QAbstractItemModel。

有关更多信息,请参见QQmlContext文档。

 


C++与QML对象的交互


可以从C++实例化QML对象类型并检查它们,以便访问它们的属性,调用它们的方法并接收它们的信号通知。这是可能的,因为所有QML对象类型都是使用QObject派生类实现的,这使得QML引擎能够通过Qt元对象系统动态加载和内省对象。

警告:尽管可以从C ++访问QML对象并对其进行操作,但除测试和原型设计目的外,不建议使用这种方法。 QML和C ++集成的优势之一是能够以与C ++逻辑和数据集后端分离的方式在QML中实现UI,如果C ++方面开始直接操作QML,则此操作将失败。 这种方法还使得更改QML UI变得困难,而又不影响其C ++对应版本。

所有QML对象类型都是QObject派生类型,无论它们是由引擎内部实现的还是由第三方源定义的。这意味着QML引擎可以使用Qt元对象系统动态实例化任何QML对象类型并检查创建的对象。

这对于从C++代码创建QML对象、显示可可视化渲染的QML对象、或者将非可视QML对象数据集成到C++应用程序中是有用的。一旦创建了QML对象,就可以从C++中检查它,以便读取和写入属性、调用方法和接收信号通知。

从C++加载QML对象

QML文档可以用QQmlComponent或QQuickView加载。QQML组件将QML文档加载为C++对象,然后可以从C++代码中修改。QQuickView也这样做,但是由于QQuickView是QWindow派生的类,加载的对象也将呈现为可视化显示;QQuickView通常用于将可显示的QML对象集成到应用程序的用户界面中。

例如,假设有一个MyItem.qml文件如下所示:

import QtQuick 2.0

Item {
    width: 100; height: 100
}

可以使用以下C ++代码将此QQmlComponent或QQuickView装入此QML文档。 使用QQmlComponent要求调用QQmlComponent :: create()创建组件的新实例,而QQuickView自动创建组件的实例,可通过QQuickView :: rootObject()访问该实例:

// Using QQmlComponent
QQmlEngine engine;
QQmlComponent component(&engine,
        QUrl::fromLocalFile("MyItem.qml"));
QObject *object = component.create();
...
delete object;

 

// Using QQuickView
QQuickView view;
view.setSource(QUrl::fromLocalFile("MyItem.qml"));
view.show();
QObject *object = view.rootObject();

此对象是已创建的MyItem.qml组件的实例。 现在,您可以使用QObject :: setProperty()或QQmlProperty :: write()修改项目的属性:

object->setProperty("width", 500);
QQmlProperty(object, "width").write(500);

QObject :: setProperty()和QQmlProperty :: write()之间的区别在于,后者除了设置属性值外还将删除绑定。 例如,假设上面的宽度分配是对高度的绑定:

width: height

如果在object-> setProperty(“ width”,500)调用之后更改了Item的高度,则该宽度将再次更新,因为绑定保持活动状态。 但是,如果在调用QQmlProperty(object,“ width”)。write(500)之后更改高度,则宽度将不会更改,因为绑定不再存在。

另外,您可以将对象强制转换为实际类型,并在编译时安全地调用方法。 在这种情况下,MyItem.qml的基础对象是一个Item,由QQuickItem类定义:

QQuickItem *item = qobject_cast<QQuickItem*>(object);
item->setWidth(500);

还可以使用QMetaObject::invokeMethod()和QObject::connect()连接到组件中定义的任何信号或调用方法。有关更多详细信息,请参见下面的调用QML方法和连接QML信号。

按对象名访问加载的QML对象

QML组件本质上是带有子对象的对象树,这些子对象具有同级和自己的子级。 可以使用带有QObject :: findChild()的QObject :: objectName属性来定位QML组件的子对象。 例如,如果MyItem.qml中的根项目有一个Rectangle子项目:

import QtQuick 2.0

Item {
    width: 100; height: 100

    Rectangle {
        anchors.fill: parent
        objectName: "rect"
    }
}

子对象可以这样定位:

QObject *rect = object->findChild<QObject*>("rect");
if (rect)
    rect->setProperty("color", "red");

请注意,一个对象可能有多个具有相同objectName的子对象。 例如,ListView创建其委托的多个实例,因此,如果使用特定的objectName声明其委托,则ListView将有多个具有相同objectName的子代。 在这种情况下,可以使用QObject :: findChildren()查找具有匹配objectName的所有子项。

警告:尽管可以从C ++访问QML对象并对其进行操作,但除测试和原型设计目的外,不建议使用这种方法。 QML和C ++集成的优势之一是能够以与C ++逻辑和数据集后端分离的方式在QML中实现UI,如果C ++方面开始直接操作QML,则此操作将失败。 这种方法还使得更改QML UI变得困难,而又不影响其C ++对应版本。

从C++访问QML对象类型的成员

属性

在QML对象中声明的任何属性都可以从C++自动访问。给定这样的QML项:

// MyItem.qml
import QtQuick 2.0

Item {
    property int someNumber: 100
}

可以使用QQmlProperty或QObject :: setProperty()和QObject :: property()设置和读取someNumber属性的值:

QQmlEngine engine;
QQmlComponent component(&engine, "MyItem.qml");
QObject *object = component.create();

qDebug() << "Property value:" << QQmlProperty::read(object, "someNumber").toInt();
QQmlProperty::write(object, "someNumber", 5000);

qDebug() << "Property value:" << object->property("someNumber").toInt();
object->setProperty("someNumber", 100);

您应该始终使用QObject :: setProperty(),QQmlProperty或QMetaProperty :: write()来更改QML属性值,以确保使QML引擎知道该属性更改。 例如,假设您有一个带有buttonText属性的自定义类型PushButton,该属性在内部反映了m_buttonText成员变量的值。 像这样直接修改成员变量不是一个好主意:

//bad code
QQmlComponent component(engine, "MyButton.qml");
PushButton *button = qobject_cast<PushButton*>(component.create());
button->m_buttonText = "Click me";

由于该值是直接更改的,所以这会绕过Qt的元对象系统,QML引擎不会意识到属性的更改。这意味着不会更新buttonText的属性绑定,也不会调用任何onButtonTextChanged处理程序。

调用QML方法

所有QML方法都公开给元对象系统,并且可以使用QMetaObject :: invokeMethod()从C ++调用。 您可以在冒号字符后指定参数的类型和返回值,如下面的代码片段所示。 例如,当您要将具有特定签名的C ++信号连接到QML定义的方法时,这可能会很有用。 如果省略类型,则C ++签名将使用QVariant。

这是一个使用QMetaObject :: invokeMethod()调用QML方法的C ++应用程序:

// MyItem.qml
import QtQuick 2.0

Item {
    function myQmlFunction(msg: string) : string {
        console.log("Got message:", msg)
        return "some return value"
    }
}
// main.cpp
QQmlEngine engine;
QQmlComponent component(&engine, "MyItem.qml");
QObject *object = component.create();

QString returnedValue;
QString msg = "Hello from C++";
QMetaObject::invokeMethod(object, "myQmlFunction",
        Q_RETURN_ARG(QString, returnedValue),
        Q_ARG(QString, msg));

qDebug() << "QML function returned:" << returnedValue;
delete object;

注意冒号后面指定的参数和返回类型。 您可以使用基本类型和对象类型作为类型名称。

如果在QML中省略了该类型,则在调用QMetaObject :: invokeMethod时必须使用Q_RETURN_ARG()和Q_ARG()将QVariant指定为类型。

连接到QML信号

所有QML信号都可自动用于C ++,并且可以像任何普通Qt C ++信号一样使用QObject :: connect()连接到它。 作为回报,QML对象可以使用信号处理程序接收任何C ++信号。

这是一个QML组件,带有一个名为qmlSignal的信号,该信号通过字符串类型的参数发出。 该信号使用QObject :: connect()连接到C ++对象的插槽,以便在发出qmlSignal时调用cppSlot()方法

// MyItem.qml
import QtQuick 2.0

Item {
    id: item
    width: 100; height: 100

    signal qmlSignal(msg: string)

    MouseArea {
        anchors.fill: parent
        onClicked: item.qmlSignal("Hello from QML")
    }
}

 

class MyClass : public QObject
{
    Q_OBJECT
public slots:
    void cppSlot(const QString &msg) {
        qDebug() << "Called the C++ slot with message:" << msg;
    }
};

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

    QQuickView view(QUrl::fromLocalFile("MyItem.qml"));
    QObject *item = view.rootObject();

    MyClass myClass;
    QObject::connect(item, SIGNAL(qmlSignal(QString)),
                     &myClass, SLOT(cppSlot(QString)));

    view.show();
    return app.exec();
}

将信号参数中的QML对象类型转换为指向C ++中的类的指针:

// MyItem.qml
import QtQuick 2.0

Item {
    id: item
    width: 100; height: 100

    signal qmlSignal(anObject: Item)

    MouseArea {
        anchors.fill: parent
        onClicked: item.qmlSignal(item)
    }
}
class MyClass : public QObject
{
    Q_OBJECT
public slots:
    void cppSlot(QQuickItem *item) {
       qDebug() << "Called the C++ slot with item:" << item;

       qDebug() << "Item dimensions:" << item->width()
                << item->height();
    }
};

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

    QQuickView view(QUrl::fromLocalFile("MyItem.qml"));
    QObject *item = view.rootObject();

    MyClass myClass;
    QObject::connect(item, SIGNAL(qmlSignal(QVariant)),
                     &myClass, SLOT(cppSlot(QVariant)));

    view.show();
    return app.exec();
}

 

 

本文适合于对Qt Quick有基本了解的读者。首先回答一个比较常会被问到的问题:什么是QML,它与Quick的关系是什么? Qt Quick是Qt User Interface Creation Kit的缩写,而QML是Qt Quick最重要的组成部分,Qt Quick结合了如下技术: 组件集合,其大部分是关于图形界面的 基于JavaScript陈述性语言:QML (Qt Meta-Object Language的缩写) 用于管理组件并与组件交互的C++ API - QtDeclarative模块 言归正传:通过Qt Creator,我们可以轻松生成一个Qt Quick的应用工程,从而为QML生成应用程序框架。具体操作详见:创建qt quick (qml) 应用程序。 C++QML的交互是通过注册C++对象QML环境得以实现的: 在C++实现,非可视化的型别均为QObject的子类,可视化的类型均为QDeclarativeItem的子类。注意:QDeclarativeItem等同于QML的Item类。 如果用户想要定义自己的型别,做法如下: 在C++,实现派生于QObject或QDeclarativeItem的子类,它是新定义item的实体对象; 在C++,将1实现的新item类型注册给QML; 在QML,导入含有1定义的新item的模块; 在QML,向使用标准的item一样使用新定义的item 现举例说明,我们现尝试使用用Qt C++实现的MyButton对象(如下qml代码),它有自己的属性、方法以及信号的handler。用法如下(它与使用其它标准的QML item一样),所需要做的是 需要导入包含MyButton的对应模块名称及其版本“MyItems 1.0 ”。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值