Qt/QML 插件系统

Qt/QML 插件系统

本文将简要介绍一下 Qt 和 QML 的插件系统,并用几个简单的示例介绍 QML 的几种插件的创建方法。由于时间所限,有些地方可能讲述的不是很到位,欢迎沟通指正。

1. 插件概述

1.1. 什么是插件

插件(Plug-in,又称 addin、add-in、addon 或 add-on,又译外挂)是一种遵循一定规范的应用程序接口编写出来的程序。其只能运行在程序规定的系统平台下(可能同时支持多个平台),而不能脱离指定的系统单独运行。

1.2. 插件系统组成

  • 主系统 —— 通过插件管理器加载插件,并创建插件对象。一旦插件对象被创建,主系统就会获得插件相应的指针或引用,它可以像任何其他对象一样使用。

  • 插件管理器 —— 用于管理插件的生命周期,并将其暴露给主系统使用。它负责查找并加载插件,初始化它们,并且能够进行卸载。它还应该让主系统迭代加载的插件或注册的插件对象。

  • 插件 —— 插件本身应符合插件管理器的协议,并提供符合主系统期望的对象。

1.3. 为什么要使用插件

  • 为了将模块从框架中剥离出来,降低框架和功能间的耦合度,功能的实现作为模块单独开发,而不是功能实现相关的复杂代码与框架揉合在一起。

  • 解决需求不断变化的软件设计场景。

  • 面向未来,可以通过插件来扩展应用程序的功能(例如 vscode、qtcreator 的主流 IDE 的插件)。

2. 插件和动态库区别

2.1. 使用场景

  • 动态库:解决静态库编译时链接符号表导致的程序占用空间大,库升级时相关可执行程序需要重新编译等问题。

  • 插件:对于软件使用的不同场景,功能有所区别时,有选择定制和加载不同的插件,另外插件能降低模块和主功能间的耦合关系。

2.2. 生命周期

  • 动态库:程序启动时加载,程序运行时必须保证 .dll/.so 存在,否则无法正常启动。

  • 插件:程序运行时到需要的时候加载,程序运行时如果 .dll/.so 不存在,也可以正常启动,只是相应插件的功能无法正常加载和使用而已。

2.3. 耦合度

  • 动态库:编译时必须指定动态库依赖关系。

  • 插件:编译时主程序不知道插件的存在。

3. Qt 中插件的分类

3.1. 纯 C++/Qt 插件

3.1.1. 高级 API

高级 API 用于扩展 Qt 本身。

要扩展 Qt,需要继承 Qt 的插件基类,实现基类的函数和添加宏,最后在将编译好的插件放置在 Qt 安装目录下对应的插件目录中。需要注意的是,若不将自定义的插件放置在对应的插件子目录中,Qt 是不会加载该插件的。

以下是 Qt 提供的插件基类:

基类 插件目录名称 Qt 模块 Key 区分大小写
QAccessibleBridgePlugin accessiblebridge Qt GUI Case Sensitive
QImageIOPlugin imageformats Qt GUI Case Sensitive
QPictureFormatPlugin (obsolete) pictureformats Qt GUI Case Sensitive
QAudioSystemPlugin audio Qt Multimedia Case Insensitive
QDeclarativeVideoBackendFactoryInterface video/declarativevideobackend Qt Multimedia Case Insensitive
QGstBufferPoolPlugin video/bufferpool Qt Multimedia Case Insensitive
QMediaPlaylistIOPlugin playlistformats Qt Multimedia Case Insensitive
QMediaResourcePolicyPlugin resourcepolicy Qt Multimedia Case Insensitive
QMediaServiceProviderPlugin mediaservice Qt Multimedia Case Insensitive
QSGVideoNodeFactoryPlugin video/videonode Qt Multimedia Case Insensitive
QBearerEnginePlugin bearer Qt Network Case Sensitive
QPlatformInputContextPlugin platforminputcontexts Qt Platform Abstraction Case Insensitive
QPlatformIntegrationPlugin platforms Qt Platform Abstraction Case Insensitive
QPlatformThemePlugin platformthemes Qt Platform Abstraction Case Insensitive
QGeoPositionInfoSourceFactory position Qt Positioning Case Sensitive
QPlatformPrinterSupportPlugin printsupport Qt Print Support Case Insensitive
QSGContextPlugin scenegraph Qt Quick Case Sensitive
QScriptExtensionPlugin script Qt Script Case Sensitive
QSensorGesturePluginInterface sensorgestures Qt Sensors Case Sensitive
QSensorPluginInterface sensors Qt Sensors Case Sensitive
QSqlDriverPlugin sqldrivers Qt SQL Case Sensitive
QIconEnginePlugin iconengines Qt SVG Case Insensitive
QAccessiblePlugin accessible Qt Widgets Case Sensitive
QStylePlugin styles Qt Widgets Case Insensitive

定义一个样式扩展的示例:

继承 QStypePlugin 基类,实现其中的 create 函数并添加 Q_PLUGIN_METADATA 宏,其他插件实现时可能
还需要实现其他函数,具体参考 Qt 文档。

mystypeplugin.h 文件:

class MyStylePlugin : public QStylePlugin
{
    Q_OBJECT
    Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QStyleFactoryInterface" FILE "mystyleplugin.json")
public:
    QStyle *create(const QString &key);
};

mystypeplugin.cpp:

#include "mystyleplugin.h"

QStyle *MyStylePlugin::create(const QString &key)
{
    if (key.toLower() == "mystyle")
        return new MyStyle;
    return 0;
}

注意,由于 QStylePlugin 不区分大小写,create 函数的实现中使用了小写来判断,但是其他的插件基类区
分大小写时不可以转换成小写判断。

大多数插件都需要添加一个提供插件元信息的 JSON 文件,该文件由 Q_PLUGIN_METADATA 宏指定。JSON 文件中的设置的信息由插件基类决定,需要参考 Qt 的文档。

mystyleplugin.json:

{ "Keys": [ "mystyleplugin" ] }

有些插件无须显示的创建插件对象,例如数据库驱动程序、图像格式、文本编解码器等,Qt 会在需要的时
候自己创建,但是样式插件例外,需要显示的创建。

QApplication::setStyle(QStyleFactory::create("MyStyle"));

Qt 提供的 样式插件示例 展示了一个更加完整的如何实现扩展 QStylePlugin 基类插件的方法,大家可以试一下。

由于,通常开发过程我们很少会有扩展 Qt 的需求,所以此处不做赘述。

3.1.2. 低级 API

不仅 Qt 本身,Qt 应用程序也可以通过插件进行扩展。在这种情况下,插件可以提供任意的功能,而不限于数据库驱动程序、图像格式、文本编解码器、样式和其他扩展 Qt 功能的插件类型。

低级 API 可以制作应用级别的插件,但是需要在应用中使用 QPluginLoader 去检测并加载插件。

使用应用插件通常需要以下步骤:

  1. 定义一个插件接口,该接口只有虚函数
  2. 使用 Q_DECLARE_INTERFACE() 宏告知 Qt 的元对象系统该接口信息
  3. 使用 QPluginLoader 加载插件
  4. 使用 qobject_cast() 测试插件是否实现了给定的接口。

编写应用插件需要以下步骤:

  1. 声明一个继承自 QObject 和该插件要提供的接口的插件类
  2. 使用 Q_INTERFACES()
    宏告知 Qt 的元对象系统该插件信息
  3. 使用 Q_PLUGIN_METADATA() 宏导出插件
  4. 编译插件项目

例如,这是一个包含多个接口类的声明的头文件的内容:

#ifndef INTERFACES_H
#define INTERFACES_H

#include <QtPlugin>

QT_BEGIN_NAMESPACE
class QImage;
class QPainter;
class QWidget;
class QPainterPath;
class QPoint;
class QRect;
class QString;
class QStringList;
QT_END_NAMESPACE

//! [0]
class BrushInterface
{
public:
    virtual ~BrushInterface() {}

    virtual QStringList brushes() const = 0;
    virtual QRect mousePress(const QString &brush, QPainter &painter,
                             const QPoint &pos) = 0;
    virtual QRect mouseMove(const QString &brush, QPainter &painter,
                            const QPoint &oldPos, const QPoint &newPos) = 0;
    virtual QRect mouseRelease(const QString &brush, QPainter &painter,
                               const QPoint &pos) = 0;
};
//! [0]

//! [1]
class ShapeInterface
{
public:
    virtual ~ShapeInterface() {}

    virtual QStringList shapes() const = 0;
    virtual QPainterPath generateShape(const QString &shape,
                                       QWidget *parent) = 0;
};
//! [1]

//! [2]
class FilterInterface
{
public:
    virtual ~FilterInterface() {}

    virtual QStringList filters() const = 0;
    virtual QImage filterImage(const QString &filter, const QImage &image,
                               QWidget *parent) = 0;
};
//! [2]

QT_BEGIN_NAMESPACE
//! [3] //! [4]
#define BrushInterface_iid "org.qt-project.Qt.Examples.PlugAndPaint.BrushInterface/1.0"

Q_DECLARE_INTERFACE(BrushInterface, BrushInterface_iid)
//! [3]

#define ShapeInterface_iid  "org.qt-project.Qt.Examples.PlugAndPaint.ShapeInterface/1.0"

Q_DECLARE_INTERFACE(ShapeInterface, ShapeInterface_iid)
//! [5]
#define FilterInterface_iid "org.qt-project.Qt.Examples.PlugAndPaint.FilterInterface/1.0"

Q_DECLARE_INTERFACE(FilterInterface, FilterInterface_iid)
//! [4] //! [5]
QT_END_NAMESPACE

#endif

下面是定义插件类的头文件:

#ifndef EXTRAFILTERSPLUGIN_H
#define EXTRAFILTERSPLUGIN_H

//! [0]
#include <interfaces.h>

#include <QObject>
#include <QtPlugin>
#include <QStringList>
#include <QImage>

class ExtraFiltersPlugin : public QObject, public FilterInterface
{
    Q_OBJECT
    Q_PLUGIN_METADATA(IID "org.qt-project.Qt.Examples.PlugAndPaint.FilterInterface" FILE "extrafilters.json")
    Q_INTERFACES(FilterInterface)

public:
    QStringList filters() const override;
    QImage filterImage(const QString &filter, const QImage &image,
                       QWidget *parent) override;
};
//! [0]

#endif

有关该实例的完整信息,可以查看 Plug & Paint Example

3.2. 开源的纯 QML 插件(qmldir)

3.2.1. 创建不带 url 前缀的 QML 插件

创建目录 MyPlugins(本例中我们在完整目录 /home/dongshuang/TestQMLPlugin/ 下创建),此目录是自己定义的,名称也可以随意定义,但是这个目录名称会作为模块名称。

在 MyPlugins 目录中创建和功能相关的 qml 文件(MyRect.qml):

import QtQuick 2.12
import QtQuick.Controls 2.12

Item {
    anchors.centerIn: parent
    Rectangle{
        width: 100
        height: 100
        color: "teal"
        Label {
            width: 50
            height: 20
            text: qsTr("TestRect")
        }
    }
}

在 qml 同级目录下创建一个名为 qmldir 的文件,并添加如下内容:

module MyExamplePlugins
TestRect 1.0 MyRect.qml

3.2.2. 创建带 url 前缀的 QML 插件

创建目录 NewPlugins (本例中我们在完整目录 /home/dongshuang/TestQMLPlugin/com/mycompany/test/ 下创建),此目录是自己定义的,名称也可以随意定义,但是这个目录名称会作为模块名称。

在 NewPlugins 目录中创建和功能相关的 qml 文件(NewRect.qml):

import QtQuick 2.12
import QtQuick.Controls 2.12

Item {
    Rectangle{
        width: 100
        height: 100
        color: "teal"
        Label {
            width: 50
            height: 20
            text: qsTr("NewRect")
        }
    }
}

在 qml 同级目录下创建一个名为 qmldir 的文件,并添加如下内容:

module NewExamplePlugins
NewRect 1.0 NewRect.qml

3.2.3. 使用 QML 插件

在 pro 文件中添加:

# 环境变量的设置只是为了让 ide 能够找到插件位置,进行高亮,自动补全等
QML_IMPORT_PATH += /home/dongshuang/TestQMLPlugin

在 main 函数中添加如下代码即可:

// 此处才是真正告诉程序去哪里加载插件
engine.addImportPath("/home/dongshuang/TestQMLPlugin");

在 main.qml 中的使用实例:

import QtQuick 2.12
import QtQuick.Window 2.12

import MyPlugins 1.0
import com.mycompany.test.NewPlugins 1.0

Window {
    visible: true
    width: 640
    height: 480
    title: qsTr("Hello World")

    TestRect {
    }

    NewRect {
    }
}

以上插件实际上是将源码目录直接打包发布的过程。

3.3. 隐藏源码的 QML 插件

在实际开发中,我们更多的需要将源码封装打包,而不对外提供源码。那又应该如何处理呢?

此时就需要借助 QQmlExtensionPlugin 这个类,以及 Qt 的资源管理系统了。下面我们用一个简单的实例来讲解如何实现。

3.3.1. 创建插件工程

首先,我们使用 Qt Creator 的新建一工程向导,创建一个 “Library > Qt Quick 2 Extension Plugin”插件工程。工程名字,我们可以叫 qrcmoduleplugin,Object class-name 可以随便填,因为后面我们要去掉它。 URI 的名称我们定为:com.mycompany.mymodule

3.3.2. 添加 qml 文件

之后我们在 qrcmoduleplugin.pro 文件所在的目录创建三个 qml 文件:

ButtonBase.qml 文件的内容如下:

import QtQuick 2.12

MouseArea {
    property alias border: bgObj.border
    property alias color: bgObj.color
    property alias font: txtObj.font
    property alias text: txtObj.text
    property alias textAnchors: txtObj.anchors
    implicitWidth: 100
    implicitHeight: 100
    objectName: "ButtonBase"

    Rectangle {
        id: bgObj
        anchors.fill: parent
        color: "honeydew"
    }

    Text {
        id: txtObj
        text: qsTr("ButtonBase")
    }
}

ButtonQrc.qml 文件的内容如下:

import QtQuick 2.12

ButtonBase {
    color: "lightcoral"
    text: qsTr("ButtonQrc")
}

ButtonQrc2.qml 文件的内容如下:

import QtQuick 2.12

ButtonBase {
    color: "slateblue"
    text: qsTr("ButtonQrc2")
}

3.3.3. 创建资源文件

之后我们在 qrcmoduleplugin.pro 文件所在的目录创建一个名为 qrcmoduleplugin.qrc 的资源文件,其内容如下:

<RCC>
    <qresource prefix="/component">
        <file>ButtonQrc.qml</file>
        <file>ButtonQrc2.qml</file>
        <file>ButtonBase.qml</file>
    </qresource>
</RCC>

3.3.4. 修改工程文件的内容

之后我们修改 qrcmoduleplugin.pro 文件,主要涉及到其中用数字标记的 5 处:

TEMPLATE = lib
TARGET = qrcmoduleplugin
QT += qml quick
CONFIG += plugin c++11

TARGET = $$qtLibraryTarget($$TARGET)
uri = com.mycompany.mymodule

# 1. 去掉其他的实现文件
SOURCES += \
        qrcmoduleplugin_plugin.cpp
# 2. 去掉其他的头文件
HEADERS += \
        qrcmoduleplugin_plugin.h

DISTFILES = qmldir

!equals(_PRO_FILE_PWD_, $$OUT_PWD) {
    copy_qmldir.target = $$OUT_PWD/qmldir
    copy_qmldir.depends = $$_PRO_FILE_PWD_/qmldir
    copy_qmldir.commands = $(COPY_FILE) "$$replace(copy_qmldir.depends, /, $$QMAKE_DIR_SEP)" "$$replace(copy_qmldir.target, /, $$QMAKE_DIR_SEP)"
    QMAKE_EXTRA_TARGETS += copy_qmldir
    PRE_TARGETDEPS += $$copy_qmldir.target
}

qmldir.files = qmldir
# 3. 增加安装资源文件
qrc.files = qrcmoduleplugin.qrc
unix {
    installPath = $$[QT_INSTALL_QML]/$$replace(uri, \., /)
    qmldir.path = $$installPath
    target.path = $$installPath
    # 4.指定安装资源文件位置
    qrc.path = $$installPath
    INSTALLS += target qmldir qrc
}

# 5. 添加资源文件到工程
RESOURCES += \
    qrcmoduleplugin.qrc

3.3.5. 修改插件类的实现

在这之后,我们修改 qrcmoduleplugin_plugin.cpp 文件,这里我们注册了两个 qml 文件,给外部使用,我们的 ButtonBase.qml 不会被暴露:

#include "qrcmoduleplugin_plugin.h"

#include <qqml.h>

void QrcmodulepluginPlugin::registerTypes(const char *uri)
{
    // @uri com.mycompany.mymodule
    qmlRegisterType(QUrl("qrc:/component/ButtonQrc.qml"), uri, 1, 0, "ButtonQrc");
    qmlRegisterType(QUrl("qrc:/component/ButtonQrc2.qml"), uri, 2, 0, "ButtonQrc");
}

3.3.6. 拷贝插件资源到指定目录

之后,我们构建工程,完成之后,就可以在构建目录下生成一系列的文件,我们只要拷贝 libqrcmoduleplugin.soqmldir 这两个文件到目录 /home/dongshuang/TestQMLPlugin/com/mycompany/mymodule 中即可,我们可以看到这个目录结构其实是和我们之前定义的 URI (我们的定义为:com.mycompany.mymodule)有一定的关联的。而 /home/dongshuang/TestQMLPlugin/ 这个目录是上节中我们用到的目录,没错,我们之后还会使用上节介绍的例子进行测试。

3.3.7. 生成 .qmltypes 文件

在命令行运行如下两条命令:

$ cd /home/dongshuang/TestQMLPlugin
$ qmlplugindump com.mycompany.mymodule 1.0 /home/dongshuang/TestQMLPlugin > /home/dongshuang/TestQMLPlugin/com/mycompany/mymodule/mymodule.qmltypes

注:如果 qmlplugindump 找不到,需要添加 Qt 的环境变量。

之后,我们就可以在 /home/dongshuang/TestQMLPlugin/com/mycompany/mymodule 目录中生成 mymodule.qmltypes 文件:

import QtQuick.tooling 1.2

// This file describes the plugin-supplied types contained in the library.
// It is used for QML tooling purposes only.
//
// This file was auto-generated by:
// 'qmlplugindump com.mycompany.mymodule 1.0 /home/dongshuang/TestQMLPlugin'

Module {
    dependencies: ["QtQuick 2.12"]
    Component {
        prototype: "QQuickMouseArea"
        name: "ButtonQrc 1.0"
        exports: ["ButtonQrc 1.0"]
        exportMetaObjectRevisions: [0]
        isComposite: true
        defaultProperty: "data"
        Property { name: "border"; type: "QQuickPen"; isReadonly: true; isPointer: true }
        Property { name: "color"; type: "QColor" }
        Property { name: "font"; type: "QFont" }
        Property { name: "text"; type: "string" }
        Property { name: "textAnchors"; type: "QQuickAnchors"; isReadonly: true; isPointer: true }
    }
    Component {
        prototype: "QQuickMouseArea"
        name: "ButtonQrc 2.0"
        exports: ["ButtonQrc 2.0"]
        exportMetaObjectRevisions: [0]
        isComposite: true
        defaultProperty: "data"
        Property { name: "border"; type: "QQuickPen"; isReadonly: true; isPointer: true }
        Property { name: "color"; type: "QColor" }
        Property { name: "font"; type: "QFont" }
        Property { name: "text"; type: "string" }
        Property { name: "textAnchors"; type: "QQuickAnchors"; isReadonly: true; isPointer: true }
    }
}

3.3.8. 修改 qmldir 文件

之后我们修改 /home/dongshuang/TestQMLPlugin/com/mycompany/mymodule 目录中的 qmldir 文件为如下内容:

module com.mycompany.mymodule
plugin qrcmoduleplugin
typeinfo mymodule.qmltypes

3.3.9. 使用示例

之后,我们修改上节中的示例,将 main.qml 改成如下:

import QtQuick 2.12
import QtQuick.Window 2.12

import MyPlugins 1.0
import com.mycompany.mymodule 1.0

Window {
    visible: true
    width: 640
    height: 480
    title: qsTr("Hello World")

    TestRect {
    }

    ButtonQrc {
        anchors.centerIn: parent
    }
}

上面的代码我们可以再试下不改变别的内容,而只是把 import com.mycompany.mymodule 1.0 这一句中的 1.0 改成 2.0,再运行试试效果。这也是 QML 插件处理不同版本的插件的测试。

3.4. 包含 C++ 的 QML 插件

现实开发中我们的 QML 插件,可能需要 C++ 的功能的支持,因此包含 C++ 的 QML 插件也是需要我们学习和掌握的。有了上面的两节示例的基础,其实 C++ 的部分看起来就很简单了,这部分大家可以直接参考 QML Plugin Example 这个示例。

3.4.1. 创建 MinuteTimer 类

MinuteTimer 类主要作用是创建 QBasicTimer 对象,并启动 QBasicTimer 的 start 方法,之后监听 QBasicTimer 在 time out 之后发出的 timerEvent 事件来产生时间变化的信号,同时计算和更新当前的 hour 和 minute 值。

class MinuteTimer : public QObject
{
    Q_OBJECT
public:
    MinuteTimer(QObject *parent) : QObject(parent)
    {
    }

    void start()
    {
        if (!timer.isActive()) {
            time = QTime::currentTime();
            timer.start(60000-time.second()*1000, this);
        }
    }

    void stop()
    {
        timer.stop();
    }

    int hour() const { return time.hour(); }
    int minute() const { return time.minute(); }

signals:
    void timeChanged();

protected:
    void timerEvent(QTimerEvent *) override
    {
        QTime now = QTime::currentTime();
        if (now.second() == 59 && now.minute() == time.minute() && now.hour() == time.hour()) {
            // just missed time tick over, force it, wait extra 0.5 seconds
            time = time.addSecs(60);
            timer.start(60500, this);
        } else {
            time = now;
            timer.start(60000-time.second()*1000, this);
        }
        emit timeChanged();
    }

private:
    QTime time;
    QBasicTimer timer;
};

上述代码使用了 QBasicTimer 这个类,该类是 Qt 在内部使用的一个快速、轻量级的低级类。如果希望在应用程序中使用计时器,我们建议使用更高级别的 QTimer 类而不是这个类。注意,这个计时器是一个重复计时器,除非调用 stop() 函数,否则它将发送后续计时器事件。

3.4.2. 创建 TimeModel 类

下面是创建用于暴露给 QML 使用的 TimeModel 类,它主要是对 MinuteTimer 类进行单利化的管理和封装,毕竟时间应该是一样的,对吧。其代码如下:

class TimeModel : public QObject
{
    Q_OBJECT
    Q_PROPERTY(int hour READ hour NOTIFY timeChanged)
    Q_PROPERTY(int minute READ minute NOTIFY timeChanged)

public:
    TimeModel(QObject *parent=nullptr) : QObject(parent)
    {
        if (++instances == 1) {
            if (!timer)
                timer = new MinuteTimer(QCoreApplication::instance());
            connect(timer, &MinuteTimer::timeChanged, this, &TimeModel::timeChanged);
            timer->start();
        }
    }

    ~TimeModel() override
    {
        if (--instances == 0) {
            timer->stop();
        }
    }

    int minute() const { return timer->minute(); }
    int hour() const { return timer->hour(); }

signals:
    void timeChanged();

private:
    QTime t;
    static MinuteTimer *timer;
    static int instances;
};

int TimeModel::instances=0;
MinuteTimer *TimeModel::timer=nullptr;

3.4.3. 注册 TimeModel 类到 QML 插件

在接下来,就是将 TimeModel 类注册给 QML 插件:

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

public:
    void registerTypes(const char *uri) override
    {
        Q_ASSERT(uri == QLatin1String("TimeExample"));
        qmlRegisterType<TimeModel>(uri, 1, 0, "Time");
    }
};

这段代码中,作者没有使用 C++ 中的类名,而是给暴露到 QML 插件中的类另起了一个简单和一目了然的名字 Time。

3.4.4. 改造示例内容

接下来我们像上节一样改造一下这个示例,使其 qml 文件和图片资源都能一起发布到 libqmlqtimeexampleplugin.so 文件中,而不是独立的文件。

3.4.4.1. 添加 res.qrc 文件在 pro 目录

res.qrc 的内容如下:

<RCC>
    <qresource prefix="/">
        <file>imports/TimeExample/center.png</file>
        <file>imports/TimeExample/clock.png</file>
        <file>imports/TimeExample/Clock.qml</file>
        <file>imports/TimeExample/hour.png</file>
        <file>imports/TimeExample/minute.png</file>
    </qresource>
</RCC>

3.4.4.2. 修改 qmlextensionplugins.pro 文件的内容

将其改成如下的样子:

TEMPLATE = lib
CONFIG += plugin
QT += qml

DESTDIR = imports/TimeExample
TARGET  = qmlqtimeexampleplugin

SOURCES += plugin.cpp

qrc.files = qrcmoduleplugin.qrc

qml.files = plugins.qml \
    imports/TimeExample/qmldir
qml.path += $$[QT_INSTALL_EXAMPLES]/qml/qmlextensionplugins
target.path += $$[QT_INSTALL_EXAMPLES]/qml/qmlextensionplugins/imports/TimeExample
qrc.path += $$[QT_INSTALL_EXAMPLES]/qml/qmlextensionplugins/res.qrc

INSTALLS += target qml qrc

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

RESOURCES += \
    res.qrc

3.4.4.3. 修改 plugin.cpp 文件中的 QExampleQmlPlugin 实现

将其改成如下形式:

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

public:
    void registerTypes(const char *uri) override
    {
        Q_ASSERT(uri == QLatin1String("TimeExample"));
        qmlRegisterType<TimeModel>(uri, 1, 0, "Time");
        qmlRegisterType(QUrl("qrc:/imports/TimeExample/Clock.qml"), uri, 1, 0, "Clock");
    }
};

3.4.4.4 修改 Clock.qml 文件的内容

接下来我们修改名叫 Clock.qml 的 QML 文件,它主要用于时间显示,其内部主要是使用 SpringAnimation 实现的时钟,我们的修改点主要是图片的资源改为使用 qrc 文件中的资源:

import QtQuick 2.12

Rectangle {
    id: clock
    width: 200; height: 200; color: "gray"

    property alias city: cityLabel.text
    property variant hours
    property variant minutes
    property variant shift : 0

    Image { id: background; source: "qrc:/imports/TimeExample/clock.png" }

    Image {
        x: 92.5; y: 27
        source: "qrc:/imports/TimeExample/hour.png"
        transform: Rotation {
            id: hourRotation
            origin.x: 7.5; origin.y: 73;
            angle: (clock.hours * 30) + (clock.minutes * 0.5)
            Behavior on angle {
                SpringAnimation{ spring: 2; damping: 0.2; modulus: 360 }
            }
        }
    }

    Image {
        x: 93.5; y: 17
        source: "qrc:/imports/TimeExample/minute.png"
        transform: Rotation {
            id: minuteRotation
            origin.x: 6.5; origin.y: 83;
            angle: clock.minutes * 6
            Behavior on angle {
                SpringAnimation{ spring: 2; damping: 0.2; modulus: 360 }
            }
        }
    }

    Image {
        anchors.centerIn: background; source: "qrc:/imports/TimeExample/center.png"
    }

    Text {
        id: cityLabel; font.bold: true; font.pixelSize: 14; y:200; color: "white"
        anchors.horizontalCenter: parent.horizontalCenter
    }
}

3.4.4.5 修改 qmldir 文件的内容

修改工程目录中的 qmldir 文件的内容为:

module TimeExample
plugin qmlqtimeexampleplugin

3.4.5. 构建并拷贝资源到指定目录

接下来构建项目,然后找到构建目录,将其中的 libqmlqtimeexampleplugin.so 文件。以及工程目录中的 qmldir 文件拷贝。然后复制到 /home/dongshuang/TestQMLPlugin/TimeExample 这个目录。

3.4.6. 接着使用 3.2 节中的项目中 main.qml 进行测试

修改 main.qml 的内容,其内容其实是参考示例代码中的 plugins.qml 文件的内容:

import TimeExample 1.0 // import types from the plugin

Clock { // this class is defined in QML (imports/TimeExample/Clock.qml)

    Time { // this class is defined in C++ (plugin.cpp)
        id: time
    }

    hours: time.hour
    minutes: time.minute

}

我们将 main.qml 的内容改为:

import QtQuick 2.12
import QtQuick.Window 2.12

import MyPlugins 1.0
import TimeExample 1.0

Window {
    visible: true
    width: 640
    height: 480
    title: qsTr("Hello World")

    TestRect {
    }

    Clock { // this class is defined in QML (imports/TimeExample/Clock.qml)
        anchors.centerIn: parent
        Time { // this class is defined in C++ (plugin.cpp)
            id: time
        }

        hours: time.hour
        minutes: time.minute
    }
}

至此,我们完成了在 QML 插件中注册 C++ 类插件的功能,以及将 qml 文件和图片资源文件一起打包发布的示例。

4. 参考文章

  1. Module Definition qmldir Files
  2. QML Plugin Example
  3. Identified Modules

5. 源代码

  1. qrcmoduleplugin.zip 密码: g13f
  2. qmlextensionplugins.zip 密码: qfvl
  3. TestQMLPlugin.zip 密码: ck2q
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271