快速入门c++编写qml插件

#精进 #编程

c++和qml之间可以互相调用。利用Qt QML module, c++能够调用qml写的类,因为qml的引擎集成在qt的元对象系统中,所以qml可以调用c++写的类。

我们可以有三种方法,可以在qml中调用c++写的类

  1. 在qml运行的上下文环境中添加一个类的实例,这样qml中便能使用实例
  2. 在qml引擎(engine)中注册一个类,之后便能在qml中声明这样的对象
  3. 在一个插件类中注册需要的类,将其编译成动态库,之后qml从动态库中import所需要的类

所谓c++插件指的是第三种方式。

几个相关的类

QQmlEngine

QQmlEngine类提供一个环境来实例化QML组件。每一个QML组件都是在QQmlContext中实例化的,QQmlContext能将数据传给QML组件,QQmlContext是由QQmlEngine分层级来管理的。

QQmlEngine engine;
QQmlComponent component(&engine);
component.setData("import QtQuick 2.0\nText { text: \"Hello world!\" }", QUrl());
QQuickItem *item = qobject_cast<QQuickItem *>(component.create());

//add item to view, etc

QQmlContext

所有QML组件的实例都存在QQmlContext中,因此QQmlContext需要提供qml值的解释环境。正如我们在一个普通qml文件中看见的那样,我们可以在一个qml对象里面,定义很多其他的qml对象,因此一个qml文件可以看成一颗qml对象树。

import QtQuick 2.0

Item {
    Rectangle {
        id: rect1
        x: 12; y: 12
        width: 76; height: 96
        color: "lightsteelblue"
    }
    Rectangle {
        id: rect2
        x: 112; y: 12
        width: 76; height: 96
        border.color: "lightsteelblue"
        border.width: 4
        radius: 8
    }
}

每一个qml对象都有一个上下文来提供其值的解释。我们可以使QQmlEngine::rootContext() 来接触最顶层的上下文。

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

    QQmlEngine engine;
    Message msg;
    engine.rootContext()->setContextProperty("msg", &msg);
    QQmlComponent component(&engine, QUrl::fromLocalFile("MyItem.qml"));
    component.create();

    return app.exec();
}

假如我们已经在其他地方对类Message进行了定义,那么上面这段代码这就是直接在qml运行的上下文中添加了一个类的实例,从而可以在qml中直接用msg这个类,这便是qml调用c++类的第一种类型。

engine.rootContext()->setContextProperty("msg", &msg);

分析这条语句:&msg是需要添加到上下文的类的指针,“msg”是在qml中这个类的新名字,也就是在qml中,用“msg”来指代&msg指针所指向的类。 ​

##如何定义qt类

我们知道qt引以为豪的信号槽机制是通过继承QObject类来实现的,qt的元对象系统将c++传统的类进行了扩展,添加了属性、信号和槽。正如我们在编写qml时所了解的那样,一个qml对象有属性、信号、槽和方法。这样qml对象和qt中继承QObject的对象可以建立一种对应的关系。用了qt特性的类,都需要在定义类的开始加入Q_OBJECT宏,这个宏将赋予这个类很多qt的特性,例如属性,信号槽,类的反思,即对类名的敏感。我们可以写一个qt中典型的类:


people.h文件

#ifndef PEOPLE_H
#define PEOPLE_H

#include <QObject>

class People : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged)
    Q_PROPERTY(QString idnum READ idnum WRITE setIdnum NOTIFY idnumChanged)

public:
    explicit People(QObject *parent = 0);

    QString name() const;
    QString idnum() const;
    void setName(const QString &s);
    void setIdnum(const QString &s);

    Q_INVOKABLE void sayHello();

signals:
    void nameChanged();
    void idnumChanged();

public slots:
    void sayMyName();

private:
    QString m_name;
    QString m_idnum;
};

#endif // PEOPLE_H

这段代码值得注意的是:

Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged)

这段代码定义这个类有一个叫name的属性,我们可以读属性的值,也可以改写属性的值,由于qml中可以将属性的值进行绑定,所以当一个属性的值变化时,需要发出改变此值的信号,方便和该值绑定的其他属性捕捉该信号,更新其他属性的值。属性值的绑定值指的是,将一个属性A和属性B绑定,则属性B的值就是属性A的值,当属性B的值发生改变,属性A的值也随之发生改变。例如


在main.qml文件中

import QtQuick 2.3
import QtQuick.Window 2.2

import Module 1.0

Window {
    visible: true
    Rectangle {
        id: rect1
        width: 100; height: 100
        color: "yellow"
    }

    Rectangle {
        id: rect2
        width: 100; height: 100
        color: rect1.color
        anchors.top: rect1.bottom
    }

    People {
        id: p0
        name: "David"
        idnum: "123456"
    }

    Text {
        text: p0.idnum
    }	
}

这里就将rect2.color和rect1.color绑定起来了,改变了rect1.color的值,就改变了rect2.color的值。

定义属性还需要定义属性怎么读,怎么写,怎么发出改变的信号。

public:
    explicit People(QObject *parent = 0);

    QString name() const;
    QString id() const;
    void setName(const QString &s);
    void setId(const QString &s);

    Q_INVOKABLE void sayHello();

signals:
    void nameChanged();
    void idChanged();

具体的定义在people.cpp文件中 ​ #include “people.h” #include

People::People(QObject *parent) : QObject(parent)
{

}

QString People::name() const
{
    return m_name;
}

QString People::id() const
{
    return m_id;
}

void People::setName(const QString &s)
{
    if (s != m_name) {
        m_name = s;
        emit nameChanged();
    }
}

void People::setId(const QString &s)
{
    if (s != m_id) {
        m_id = s;
        emit idChanged();
    }
}

void People::sayHello()
{
    qDebug() << "Hello, world!";
}

void People::sayMyName()
{
    qDebug() << m_name;
}

我们还可以在类里面定义信号槽、函数(前面需要添加Q_INVOKABLE宏)

public:
    explicit People(QObject *parent = 0);

    QString name() const;
    QString id() const;
    void setName(const QString &s);
    void setId(const QString &s);

    Q_INVOKABLE void sayHello();

signals:
    void nameChanged();
    void idChanged();

具体定义在上面提到的people.cpp里。

在qml引擎中注册类

文件结构是这样的


image

main.cpp文件

#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QtQuick/QQuickView>
#include "people.h"

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

    QQmlApplicationEngine engine;
    qmlRegisterType<People>("Module", 1, 0, "People");
    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));

    return app.exec();
}

为了能够让qml中能够使用c++定义的类,需要在main.cpp中添加一些代码:

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

这段代码的意思是将People类型注册到qmlengine中,将这个类命名为”People”,放在名为”Module”的库中,版本为1.0。声明不同版本号的意义在开发的过程中我们会对类进行进行,会有不同版本的类,我们将不同版本的类放在不同版本的库中,方便使用。

编写插件

所谓插件就是一个定义好接口的动态库。我们首先要生成插件,当要使用插件的时候需要先安装好插件。

生成插件

如果我们使用QtCreator那我们可以使用向导来建立一个插件。在Libary下点击Qt Quick 2 Extension Plugin,输入插件名和要注册的类名之后得到如下的文件结构:

image

其中add_plugin.h和add_plugin.cpp中定义了名为Adder的插件(输入的插件名),myadder.h和myadder.cpp定义了我们要注册的类Myadder(输入的要注册的类名)。

Adder这个类继承QQmlExtensionPlugin。其一般形式如下:

在adder_plugin.h文件中

#ifndef ADDER_PLUGIN_H
#define ADDER_PLUGIN_H

#include <QQmlExtensionPlugin>

class AdderPlugin : public QQmlExtensionPlugin
{
    Q_OBJECT
    Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface")

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

#endif // ADDER_PLUGIN_H

在adder_plugin.cpp文件中

#include "adder_plugin.h"
#include "myadder.h"

#include <qqml.h>

void AdderPlugin::registerTypes(const char *uri)
{
    // @uri operators
    qmlRegisterType<MyAdder>(uri, 1, 0, "MyAdder");
}

其中

Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface") 声明这个类为qml的插件。

void AdderPlugin::registerTypes(const char *uri)
{
    // @uri operators
    qmlRegisterType<MyAdder>(uri, 1, 0, "MyAdder");
} 将MyAdder这个类注册到qml引擎中,将其放在uri的1.0版本的库中,qml通过"MyAdder"这个名字来用这个类。uri就是库名,uri是在qmldir文件中声明的。

在qmldir文件中

module operators
plugin Adder

第一行是将uri指定为“operators”,第二行指定这个qmldir所代表的插件为Adder。qmldir文件的作用是将插件的库(uri地址)暴露出来,方便调用的时候寻址。

方便起见,我们可以在pro文件里添加这样的语句

DESTDIR = ../operators
# Copy the qmldir file to the same folder as the plugin binary
QMAKE_POST_LINK += $$QMAKE_COPY $$replace($$list($$quote($$PWD/qmldir) $$DESTDIR), /, $$QMAKE_DIR_SEP)

第一行的意思是: 这个插件编译出来的库的地址是上一级目录的operators。

第二行的意思是: 将qmldir文件拷贝到上面的地址,这样让编译出来的库和qmldir文件在同一个目录下。在同一个目录下的意义是,当调用插件时,解析了qmldir文件,去uri指定的地址,也就是当前目录下寻找库文件。

编译该工程,我们得到


image

在mac os 系统下编译,该目录有


image

这样我们就得到插件了,理论上我们可以将得到的文件夹放到任意的位置,只要使用插件的时候导入正确的地址就可以了。

###使用插件 我们在QtCreator中建立一个普通的qt quick application来使用该插件。

image

我们可以在main.qml中通过

import operators 1.0

之后,来使用MyAdder这个类,使用的方法和一般的qml对象一样:

MyAdder {
   id: adder
}

为了能够在qml文件中顺利import operators 1.0,我们要在main.cpp中做点修改。

main.cpp文件

#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QDebug>

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

    QQmlApplicationEngine engine;
    engine.addImportPath("../");

    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));

    return app.exec();
}

添加了这个语句

engine.addImportPath("../");

意思是,添加一个路径,在这个路径下搜索按qmldir中uri的格式来寻找插件。因为我们默认使用影子编译,即编译出来的文件单独在一个文件夹,和源文件分离。编译的地址默认为影子编译编译出来的文件夹,而我们一般讲插件这么放

image

所以我们一般这样

engine.addImportPath("../");

添加语句。

这样我们就能在这个工程下顺利使用我们的插件了。