Qt/QML 插件系統(tǒng)

Qt/QML 插件系統(tǒng)

本文將簡(jiǎn)要介紹一下 Qt 和 QML 的插件系統(tǒng)黍檩,并用幾個(gè)簡(jiǎn)單的示例介紹 QML 的幾種插件的創(chuàng)建方法疲酌。由于時(shí)間所限涮毫,有些地方可能講述的不是很到位翘魄,歡迎溝通指正蜓肆。

1. 插件概述

1.1. 什么是插件

插件(Plug-in颜凯,又稱 addin、add-in仗扬、addon 或 add-on症概,又譯外掛)是一種遵循一定規(guī)范的應(yīng)用程序接口編寫出來(lái)的程序。其只能運(yùn)行在程序規(guī)定的系統(tǒng)平臺(tái)下(可能同時(shí)支持多個(gè)平臺(tái))早芭,而不能脫離指定的系統(tǒng)單獨(dú)運(yùn)行彼城。

1.2. 插件系統(tǒng)組成

  • 主系統(tǒng) —— 通過(guò)插件管理器加載插件,并創(chuàng)建插件對(duì)象退个。一旦插件對(duì)象被創(chuàng)建募壕,主系統(tǒng)就會(huì)獲得插件相應(yīng)的指針或引用,它可以像任何其他對(duì)象一樣使用语盈。

  • 插件管理器 —— 用于管理插件的生命周期舱馅,并將其暴露給主系統(tǒng)使用。它負(fù)責(zé)查找并加載插件刀荒,初始化它們代嗤,并且能夠進(jìn)行卸載棘钞。它還應(yīng)該讓主系統(tǒng)迭代加載的插件或注冊(cè)的插件對(duì)象。

  • 插件 —— 插件本身應(yīng)符合插件管理器的協(xié)議干毅,并提供符合主系統(tǒng)期望的對(duì)象宜猜。

1.3. 為什么要使用插件

  • 為了將模塊從框架中剝離出來(lái),降低框架和功能間的耦合度溶锭,功能的實(shí)現(xiàn)作為模塊單獨(dú)開發(fā)宝恶,而不是功能實(shí)現(xiàn)相關(guān)的復(fù)雜代碼與框架揉合在一起。

  • 解決需求不斷變化的軟件設(shè)計(jì)場(chǎng)景趴捅。

  • 面向未來(lái)垫毙,可以通過(guò)插件來(lái)擴(kuò)展應(yīng)用程序的功能(例如 vscode、qtcreator 的主流 IDE 的插件)拱绑。

2. 插件和動(dòng)態(tài)庫(kù)區(qū)別

2.1. 使用場(chǎng)景

  • 動(dòng)態(tài)庫(kù):解決靜態(tài)庫(kù)編譯時(shí)鏈接符號(hào)表導(dǎo)致的程序占用空間大综芥,庫(kù)升級(jí)時(shí)相關(guān)可執(zhí)行程序需要重新編譯等問(wèn)題。

  • 插件:對(duì)于軟件使用的不同場(chǎng)景猎拨,功能有所區(qū)別時(shí)膀藐,有選擇定制和加載不同的插件,另外插件能降低模塊和主功能間的耦合關(guān)系红省。

2.2. 生命周期

  • 動(dòng)態(tài)庫(kù):程序啟動(dòng)時(shí)加載额各,程序運(yùn)行時(shí)必須保證 .dll/.so 存在,否則無(wú)法正常啟動(dòng)吧恃。

  • 插件:程序運(yùn)行時(shí)到需要的時(shí)候加載虾啦,程序運(yùn)行時(shí)如果 .dll/.so 不存在,也可以正常啟動(dòng)痕寓,只是相應(yīng)插件的功能無(wú)法正常加載和使用而已傲醉。

2.3. 耦合度

  • 動(dòng)態(tài)庫(kù):編譯時(shí)必須指定動(dòng)態(tài)庫(kù)依賴關(guān)系。

  • 插件:編譯時(shí)主程序不知道插件的存在呻率。

3. Qt 中插件的分類

3.1. 純 C++/Qt 插件

3.1.1. 高級(jí) API

高級(jí) API 用于擴(kuò)展 Qt 本身硬毕。

要擴(kuò)展 Qt,需要繼承 Qt 的插件基類礼仗,實(shí)現(xiàn)基類的函數(shù)和添加宏吐咳,最后在將編譯好的插件放置在 Qt 安裝目錄下對(duì)應(yīng)的插件目錄中。需要注意的是藐守,若不將自定義的插件放置在對(duì)應(yīng)的插件子目錄中挪丢,Qt 是不會(huì)加載該插件的。

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

基類 插件目錄名稱 Qt 模塊 Key 區(qū)分大小寫
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

定義一個(gè)樣式擴(kuò)展的示例:

繼承 QStypePlugin 基類卢厂,實(shí)現(xiàn)其中的 create 函數(shù)并添加 Q_PLUGIN_METADATA 宏,其他插件實(shí)現(xiàn)時(shí)可能
還需要實(shí)現(xiàn)其他函數(shù)惠啄,具體參考 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 不區(qū)分大小寫,create 函數(shù)的實(shí)現(xiàn)中使用了小寫來(lái)判斷融柬,但是其他的插件基類區(qū)
分大小寫時(shí)不可以轉(zhuǎn)換成小寫判斷死嗦。

大多數(shù)插件都需要添加一個(gè)提供插件元信息的 JSON 文件,該文件由 Q_PLUGIN_METADATA 宏指定粒氧。JSON 文件中的設(shè)置的信息由插件基類決定越除,需要參考 Qt 的文檔。

mystyleplugin.json:

{ "Keys": [ "mystyleplugin" ] }

有些插件無(wú)須顯示的創(chuàng)建插件對(duì)象外盯,例如數(shù)據(jù)庫(kù)驅(qū)動(dòng)程序摘盆、圖像格式、文本編解碼器等饱苟,Qt 會(huì)在需要的時(shí)
候自己創(chuàng)建孩擂,但是樣式插件例外,需要顯示的創(chuàng)建箱熬。

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

Qt 提供的 樣式插件示例 展示了一個(gè)更加完整的如何實(shí)現(xiàn)擴(kuò)展 QStylePlugin 基類插件的方法类垦,大家可以試一下。

由于城须,通常開發(fā)過(guò)程我們很少會(huì)有擴(kuò)展 Qt 的需求蚤认,所以此處不做贅述。

3.1.2. 低級(jí) API

不僅 Qt 本身糕伐,Qt 應(yīng)用程序也可以通過(guò)插件進(jìn)行擴(kuò)展砰琢。在這種情況下,插件可以提供任意的功能赤炒,而不限于數(shù)據(jù)庫(kù)驅(qū)動(dòng)程序氯析、圖像格式、文本編解碼器莺褒、樣式和其他擴(kuò)展 Qt 功能的插件類型掩缓。

低級(jí) API 可以制作應(yīng)用級(jí)別的插件,但是需要在應(yīng)用中使用 QPluginLoader 去檢測(cè)并加載插件遵岩。

使用應(yīng)用插件通常需要以下步驟:

  1. 定義一個(gè)插件接口你辣,該接口只有虛函數(shù)
  2. 使用 Q_DECLARE_INTERFACE() 宏告知 Qt 的元對(duì)象系統(tǒng)該接口信息
  3. 使用 QPluginLoader 加載插件
  4. 使用 qobject_cast() 測(cè)試插件是否實(shí)現(xiàn)了給定的接口。

編寫應(yīng)用插件需要以下步驟:

  1. 聲明一個(gè)繼承自 QObject 和該插件要提供的接口的插件類
  2. 使用 Q_INTERFACES()
    宏告知 Qt 的元對(duì)象系統(tǒng)該插件信息
  3. 使用 Q_PLUGIN_METADATA() 宏導(dǎo)出插件
  4. 編譯插件項(xiàng)目

例如尘执,這是一個(gè)包含多個(gè)接口類的聲明的頭文件的內(nèi)容:

#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

有關(guān)該實(shí)例的完整信息舍哄,可以查看 Plug & Paint Example

3.2. 開源的純 QML 插件(qmldir)

3.2.1. 創(chuàng)建不帶 url 前綴的 QML 插件

創(chuàng)建目錄 MyPlugins(本例中我們?cè)谕暾夸?/home/dongshuang/TestQMLPlugin/ 下創(chuàng)建),此目錄是自己定義的誊锭,名稱也可以隨意定義表悬,但是這個(gè)目錄名稱會(huì)作為模塊名稱。

在 MyPlugins 目錄中創(chuàng)建和功能相關(guān)的 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 同級(jí)目錄下創(chuàng)建一個(gè)名為 qmldir 的文件丧靡,并添加如下內(nèi)容:

module MyExamplePlugins
TestRect 1.0 MyRect.qml

3.2.2. 創(chuàng)建帶 url 前綴的 QML 插件

創(chuàng)建目錄 NewPlugins (本例中我們?cè)谕暾夸?/home/dongshuang/TestQMLPlugin/com/mycompany/test/ 下創(chuàng)建)蟆沫,此目錄是自己定義的籽暇,名稱也可以隨意定義,但是這個(gè)目錄名稱會(huì)作為模塊名稱饭庞。

在 NewPlugins 目錄中創(chuàng)建和功能相關(guān)的 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 同級(jí)目錄下創(chuàng)建一個(gè)名為 qmldir 的文件戒悠,并添加如下內(nèi)容:

module NewExamplePlugins
NewRect 1.0 NewRect.qml

3.2.3. 使用 QML 插件

在 pro 文件中添加:

# 環(huán)境變量的設(shè)置只是為了讓 ide 能夠找到插件位置,進(jìn)行高亮,自動(dòng)補(bǔ)全等
QML_IMPORT_PATH += /home/dongshuang/TestQMLPlugin

在 main 函數(shù)中添加如下代碼即可:

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

在 main.qml 中的使用實(shí)例:

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 {
    }
}

以上插件實(shí)際上是將源碼目錄直接打包發(fā)布的過(guò)程。

3.3. 隱藏源碼的 QML 插件

在實(shí)際開發(fā)中舟山,我們更多的需要將源碼封裝打包绸狐,而不對(duì)外提供源碼累盗。那又應(yīng)該如何處理呢寒矿?

此時(shí)就需要借助 QQmlExtensionPlugin 這個(gè)類劫窒,以及 Qt 的資源管理系統(tǒng)了搞旭。下面我們用一個(gè)簡(jiǎn)單的實(shí)例來(lái)講解如何實(shí)現(xiàn)翎嫡。

3.3.1. 創(chuàng)建插件工程

首先,我們使用 Qt Creator 的新建一工程向?qū)В瑒?chuàng)建一個(gè) “Library > Qt Quick 2 Extension Plugin”插件工程衙伶。工程名字,我們可以叫 qrcmoduleplugin,Object class-name 可以隨便填剃袍,因?yàn)楹竺嫖覀円サ羲?URI 的名稱我們定為:com.mycompany.mymodule

3.3.2. 添加 qml 文件

之后我們?cè)?qrcmoduleplugin.pro 文件所在的目錄創(chuàng)建三個(gè) qml 文件:

ButtonBase.qml 文件的內(nèi)容如下:

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 文件的內(nèi)容如下:

import QtQuick 2.12

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

ButtonQrc2.qml 文件的內(nèi)容如下:

import QtQuick 2.12

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

3.3.3. 創(chuàng)建資源文件

之后我們?cè)?qrcmoduleplugin.pro 文件所在的目錄創(chuàng)建一個(gè)名為 qrcmoduleplugin.qrc 的資源文件黄刚,其內(nèi)容如下:

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

3.3.4. 修改工程文件的內(nèi)容

之后我們修改 qrcmoduleplugin.pro 文件,主要涉及到其中用數(shù)字標(biāo)記的 5 處:

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

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

# 1. 去掉其他的實(shí)現(xiàn)文件
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. 修改插件類的實(shí)現(xiàn)

在這之后民效,我們修改 qrcmoduleplugin_plugin.cpp 文件憔维,這里我們注冊(cè)了兩個(gè) qml 文件,給外部使用畏邢,我們的 ButtonBase.qml 不會(huì)被暴露:

#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. 拷貝插件資源到指定目錄

之后业扒,我們構(gòu)建工程,完成之后舒萎,就可以在構(gòu)建目錄下生成一系列的文件程储,我們只要拷貝 libqrcmoduleplugin.soqmldir 這兩個(gè)文件到目錄 /home/dongshuang/TestQMLPlugin/com/mycompany/mymodule 中即可,我們可以看到這個(gè)目錄結(jié)構(gòu)其實(shí)是和我們之前定義的 URI (我們的定義為:com.mycompany.mymodule)有一定的關(guān)聯(lián)的臂寝。而 /home/dongshuang/TestQMLPlugin/ 這個(gè)目錄是上節(jié)中我們用到的目錄章鲤,沒錯(cuò),我們之后還會(huì)使用上節(jié)介紹的例子進(jìn)行測(cè)試咆贬。

3.3.7. 生成 .qmltypes 文件

在命令行運(yùn)行如下兩條命令:

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

注:如果 qmlplugindump 找不到败徊,需要添加 Qt 的環(huán)境變量。

之后素征,我們就可以在 /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 文件為如下內(nèi)容:

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

3.3.9. 使用示例

之后集嵌,我們修改上節(jié)中的示例,將 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
    }
}

上面的代碼我們可以再試下不改變別的內(nèi)容御毅,而只是把 import com.mycompany.mymodule 1.0 這一句中的 1.0 改成 2.0根欧,再運(yùn)行試試效果。這也是 QML 插件處理不同版本的插件的測(cè)試端蛆。

3.4. 包含 C++ 的 QML 插件

現(xiàn)實(shí)開發(fā)中我們的 QML 插件凤粗,可能需要 C++ 的功能的支持,因此包含 C++ 的 QML 插件也是需要我們學(xué)習(xí)和掌握的。有了上面的兩節(jié)示例的基礎(chǔ)嫌拣,其實(shí) C++ 的部分看起來(lái)就很簡(jiǎn)單了柔袁,這部分大家可以直接參考 QML Plugin Example 這個(gè)示例。

3.4.1. 創(chuàng)建 MinuteTimer 類

MinuteTimer 類主要作用是創(chuàng)建 QBasicTimer 對(duì)象异逐,并啟動(dòng) QBasicTimer 的 start 方法捶索,之后監(jiān)聽 QBasicTimer 在 time out 之后發(fā)出的 timerEvent 事件來(lái)產(chǎn)生時(shí)間變化的信號(hào),同時(shí)計(jì)算和更新當(dāng)前的 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 這個(gè)類腥例,該類是 Qt 在內(nèi)部使用的一個(gè)快速、輕量級(jí)的低級(jí)類酝润。如果希望在應(yīng)用程序中使用計(jì)時(shí)器燎竖,我們建議使用更高級(jí)別的 QTimer 類而不是這個(gè)類。注意要销,這個(gè)計(jì)時(shí)器是一個(gè)重復(fù)計(jì)時(shí)器构回,除非調(diào)用 stop() 函數(shù),否則它將發(fā)送后續(xù)計(jì)時(shí)器事件疏咐。

3.4.2. 創(chuàng)建 TimeModel 類

下面是創(chuàng)建用于暴露給 QML 使用的 TimeModel 類纤掸,它主要是對(duì) MinuteTimer 類進(jìn)行單利化的管理和封裝,畢竟時(shí)間應(yīng)該是一樣的凳鬓,對(duì)吧茁肠。其代碼如下:

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. 注冊(cè) TimeModel 類到 QML 插件

在接下來(lái),就是將 TimeModel 類注冊(cè)給 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 插件中的類另起了一個(gè)簡(jiǎn)單和一目了然的名字 Time。

3.4.4. 改造示例內(nèi)容

接下來(lái)我們像上節(jié)一樣改造一下這個(gè)示例仅孩,使其 qml 文件和圖片資源都能一起發(fā)布到 libqmlqtimeexampleplugin.so 文件中托猩,而不是獨(dú)立的文件。

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

res.qrc 的內(nèi)容如下:

<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 文件的內(nèi)容

將其改成如下的樣子:

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 實(shí)現(xiàn)

將其改成如下形式:

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 文件的內(nèi)容

接下來(lái)我們修改名叫 Clock.qml 的 QML 文件辽慕,它主要用于時(shí)間顯示京腥,其內(nèi)部主要是使用 SpringAnimation 實(shí)現(xiàn)的時(shí)鐘,我們的修改點(diǎn)主要是圖片的資源改為使用 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 文件的內(nèi)容

修改工程目錄中的 qmldir 文件的內(nèi)容為:

module TimeExample
plugin qmlqtimeexampleplugin

3.4.5. 構(gòu)建并拷貝資源到指定目錄

接下來(lái)構(gòu)建項(xiàng)目溅蛉,然后找到構(gòu)建目錄公浪,將其中的 libqmlqtimeexampleplugin.so 文件。以及工程目錄中的 qmldir 文件拷貝船侧。然后復(fù)制到 /home/dongshuang/TestQMLPlugin/TimeExample 這個(gè)目錄欠气。

3.4.6. 接著使用 3.2 節(jié)中的項(xiàng)目中 main.qml 進(jìn)行測(cè)試

修改 main.qml 的內(nèi)容,其內(nèi)容其實(shí)是參考示例代碼中的 plugins.qml 文件的內(nèi)容:

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 的內(nèi)容改為:

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è) C++ 類插件的功能预柒,以及將 qml 文件和圖片資源文件一起打包發(fā)布的示例。

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
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請(qǐng)通過(guò)簡(jiǎn)信或評(píng)論聯(lián)系作者宜鸯。
  • 序言:七十年代末憔古,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子淋袖,更是在濱河造成了極大的恐慌鸿市,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件适贸,死亡現(xiàn)場(chǎng)離奇詭異灸芳,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)拜姿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)冯遂,“玉大人蕊肥,你說(shuō)我怎么就攤上這事「蚣。” “怎么了壁却?”我有些...
    開封第一講書人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)裸准。 經(jīng)常有香客問(wèn)我展东,道長(zhǎng),這世上最難降的妖魔是什么炒俱? 我笑而不...
    開封第一講書人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任盐肃,我火速辦了婚禮,結(jié)果婚禮上权悟,老公的妹妹穿的比我還像新娘砸王。我一直安慰自己,他們只是感情好峦阁,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開白布谦铃。 她就那樣靜靜地躺著,像睡著了一般榔昔。 火紅的嫁衣襯著肌膚如雪驹闰。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 48,970評(píng)論 1 284
  • 那天撒会,我揣著相機(jī)與錄音嘹朗,去河邊找鬼。 笑死茧彤,一個(gè)胖子當(dāng)著我的面吹牛骡显,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼惫谤,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼壁顶!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起溜歪,我...
    開封第一講書人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤若专,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后蝴猪,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體调衰,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年自阱,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了嚎莉。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡沛豌,死狀恐怖趋箩,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情加派,我是刑警寧澤叫确,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布,位于F島的核電站芍锦,受9級(jí)特大地震影響竹勉,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜娄琉,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一次乓、第九天 我趴在偏房一處隱蔽的房頂上張望种远。 院中可真熱鬧辣卒,春花似錦、人聲如沸涩拙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至主卫,卻和暖如春逃默,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背簇搅。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工完域, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人瘩将。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓吟税,卻偏偏與公主長(zhǎng)得像凹耙,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子肠仪,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345