Bootstrap

Qt使用插件QPluginLoader 机制开发

简介:

插件(Plug-in,又称addin、add-in、addon或add-on,又译外挂)是一种遵循一定规范的应用程序接口编写出来的程序。

Qt 提供了2种APIs来创建插件:

一种高级API,用于为Qt本身编写插件:自定义数据库驱动程序,图像格式,文本编解码器,自定义样式等。

一种用于扩展Qt应用程序的低级API。也就是说扩展我们自己使用Qt编写的程序。这要求应用程序使用 QPluginLoader 检测和加载插件。在这种情况下,插件可以提供任意功能,不限于数据库驱动程序、图像格式、文本编解码器、样式以及扩展Qt功能的其他类型的插件。本文主要通过示例来展示第二种方式创建插件。

原理:

重载了虚函数的dll,这跟抽象工厂类类似,这便是插件的原理。qt的插件可以说是一种动态库

Qt插件特点:

核心技术:QPluginLoader

Qt插件存储在共享库(DLL)中(即以动态库的方式存在),与使用QLibrary访问的共享库相比,它具有以下优势:

面向Interface编程,内部封装,模块和整体流程开发分离,提高开发效率。

QPluginLoader 检查插件是否与应用程序相同版本的 Qt 链接。

QPluginLoader 提供对根组件对象 (instance()) 的直接访问,而不是强制您手动解析 C 函数

插件

插件主要面向接口编程,无需访问.lib文件,热插拔、利于团队开发。即使在程序运行时.dll不存在,也可以正常启动,只是相应插件功能无法正常使用而已;

动态库

动态库需要访问.lib文件,而且在程序运行时必须保证.lib存在,否则无法正常启动

开发测试环境:QT5.15.2 + MSVC 2019

在容器端:

  1. 定义一组接口(只有纯虚函数的类)。
  2. 在QT_BEGIN_NAMESPACE和QT_END_NAMESPACE之间使用Q_DECLARE_INTERFACE()宏告诉Qt的元对象系统有关接口的信息。
  3. 在程序中使用 QPluginLoader加载插件。
  4. 使用qobject_cast()测试插件是否实现给定的接口。

Q_DECLARE_INTERFACE

Q_DECLARE_INTERFACE(类名,标识符)

此宏用于把标识符与类名接口关联起来。这个标识符是唯一的,这个宏通常在被放到一个类被定后的位置。

这个是Qt实现插件必须要有的一个宏, 把标识符与类名接口关联起来。

在插件端:

声明一个插件类,该类继承自QObject和插件要提供的接口类。

使用Q_INTERFACES()宏告诉Qt的元对象系统有关接口的信息。

使用Q_PLUGIN_METADATA()宏导出插件。

Q_PLUGIN_METADATA
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.Examples.SubPlugin" FILE "SubPlugin.json")

这个宏第一次参数定义了一个uuid,保证唯一即可,第二个json是必须要有的,当无法找到指定的文件时,moc 会出现错误,即使是空的文件也行

SubPlugin.json文件存放在源代码目录下,可以记录一下插件的信息(如插件名称、插件版本、插件依赖库),如下

{

    "name": "testSubPlugin",

    "version": "1.0",

    "dependencies": []

}

这个宏被用于声明元数据,这个元数据是被实例化插件的一部分。

这个宏需要通过对象声明被实例化接口的IID,并且要引用包含元数据内容的文件。

需要获取这些元数据(即上面的json数据),可在在通过下面的办法。

        QPluginLoader *loader = new QPluginLoader(this);

        loader->setFileName(plugin.absoluteFilePath());

        qDebug()<<loader->metaData().keys();

        QJsonObject json = loader->metaData().value("MetaData").toObject();

        QVariant var = json.value("name").toVariant();

Q_INTERFACES

Q_INTERFACES(AppInterface)   //声明这个插件实现是基于哪个插件接口的,使qobject_cast()能正确进行QObject*到接口指针的转换

QPluginLoader 

~QPluginLoader()

销毁 QPluginLoader 对象。

除非显式调用 unload(),否则插件会一直保留在内存中,直到应用程序终止。

Load

加载插件,如果插件加载成功则返回true; 否则返回false。

unload

卸载插件,如果插件可以卸载则返回true,否则返回false。

这在应用程序终止时自动发生,因此通常不需要调用此函数。

如果 QPluginLoader 的其他实例正在使用相同的插件,则调用将失败,并且只有在每个实例都调用了 unload() 时才会发生卸载。

不要尝试删除根组件。 而是依靠 unload() 会在需要时自动删除它

Instance

返回插件的根组件对象,组件对象是一个 QObject。 使用 qobject_cast() 可将其转成所需的对象。如果根组件对象被销毁,则调用此函数会创建一个新实例。

该函数返回的根组件在 QPluginLoader 销毁时不会被删除。 如果要确保删除根组件,则应在不再需要访问核心组件时立即调用 unload()。 当库最终卸载时,根组件将自动删除。

metaData

返回此插件的元数据。 元数据是在编译插件时使用 Q_PLUGIN_METADATA() 宏以 json 格式指定的数据。

例子

首先创建一个Qt Subdirs Project工程PluginDemo,并添加一个app应用程序端并在app.pro添加输出目录DESTDIR = $$PWD/../bin

  1. 然后添加一个appinterface.h的头文件,该类作为插件接口类。
#ifndef APPINTERFACE_H

#define APPINTERFACE_H

#include <QList>

#include <QAction>

class AppInterface

{

public:

    virtual ~AppInterface() {}

    // 插件的名字

    virtual QString name() const = 0;

    // 插件返回的QAction列表

    virtual QList<QAction *> actions() const = 0;

};

QT_BEGIN_NAMESPACE

Q_DECLARE_INTERFACE(AppInterface, "plugindemo_app_interface")

QT_END_NAMESPACE

#endif // APPINTERFACE_H
  1. 在工程上右键,选择新子项目,选择Library->C++ Library类型选择“shared Library”,qt module 选择“widgets”.插件名称为subplugin.
  2. 修改subplugin.pro

添加CONFIG+=plugin  //目的就是为是该dll作为插件

target.path = $$PWD/../bin/pluginsINSTALL+=target //把插件dll拷贝到容器的bin目录下。需构建步骤添加 make install

  1. 实现接口类SubPlugin.h,注意添加Q_INTERFACESQ_PLUGIN_METADATA
#ifndef SUBPLUGIN_H

#define SUBPLUGIN_H

#include "subPlugin_global.h"

#include "../app/appinterface.h"

#include <QObject>

class SUBPLUGIN_EXPORT SubPlugin : public QObject, public AppInterface

{

public:

    SubPlugin();

    Q_OBJECT

    Q_INTERFACES(AppInterface)   //声明这个插件实现是基于哪个插件接口的

    Q_PLUGIN_METADATA(IID "org.qt-project.Qt.Examples.SubPlugin" FILE "SubPlugin.json")  //使用Q_PLUGIN_METADATA宏导出插件

    // AppInterface interface

public:

    virtual QString name() const override;

    virtual QList<QAction *> actions() const override;

};



#endif // SUBPLUGIN_H



#include "subplugin.h"

SubPlugin::SubPlugin()

{

}

QString SubPlugin::name() const

{

    return "SubPlugin";

}

QList<QAction *> SubPlugin::actions() const

{

    QAction *aboutQt = new QAction;

    aboutQt->setText(tr("About Qt"));

    QList<QAction *> result;

    result << aboutQt;

    return result;

}
  1. 在容器的Dialog.cpp里面添加加载插件的代码
#include "dialog.h"

#include "ui_dialog.h"

#include "appinterface.h"





#include <QDebug>

#include <QPluginLoader>

#include <QFileInfo>

#include <QDir>

#include <QPushButton>





Dialog::Dialog(QWidget *parent)

    : QDialog(parent)

    , ui(new Ui::Dialog)

{

    loadPlugins();

    ui->setupUi(this);

}



Dialog::~Dialog()

{

    delete ui;

}



void Dialog::loadPlugins()

{

    QString pluginPath = QCoreApplication::applicationDirPath() + "/plugins";

    QStringList filters;

#if defined(Q_OS_LINUX)

    filters << "*.so";

#elif defined(Q_OS_WIN)

    filters << "*.dll";

#endif

    QFileInfoList plugins = QDir(pluginPath).entryInfoList(filters,

                                                           QDir::Files | QDir::NoSymLinks);

    for (int i = 0; i < plugins.count(); ++i) {

        QFileInfo plugin = plugins.at(i);



        QPluginLoader *loader = new QPluginLoader(this);

        loader->setFileName(plugin.absoluteFilePath());

        qDebug()<<loader->metaData().keys();

        QJsonObject json = loader->metaData().value("MetaData").toObject();

        QVariant var = json.value("name").toVariant();

        bool result = loader->load();

        qDebug() << QString("load plguin %1 result: ").arg(plugin.absoluteFilePath()) << result;

        if (result) {

            QObject *pluginInstance = loader->instance();

            AppInterface *appInterface = qobject_cast<AppInterface *>(pluginInstance);

            if (appInterface != nullptr) {

                qDebug() << "add actions from plugin:" << appInterface->name();

                QList<QAction *> actions = appInterface->actions();

                for (int k = 0; k < actions.count(); ++k) {

                    QAction *ac = actions.at(k);

                    QString strtext = ac->text();

                }

            }

        } else {

            qDebug() << loader->errorString();

        }

    }

}

插件管理

自定义pluginmanager类,封装成dll

管理所有插件,包括加载、卸载等。并暴露方法给主程序。

插件管理器代码

插件接口

#ifndef PLUGININTERFACE_H

#define PLUGININTERFACE_H

#include <QtWidgets/qwidget.h>

#include <QString>



class PluginInterface

{

public:

    virtual ~PluginInterface() {}

    // 插件返回的QAction列表

    virtual QString datawrite() = 0;

};



Q_DECLARE_INTERFACE(PluginInterface, "PluginManager.PluginInterface")

#endif // PLUGININTERFACE_H

插件管理器

#ifndef PLUGINMANAGER_H

#define PLUGINMANAGER_H



#include "PluginManager_global.h"

#include <QObject>

#include <QPluginLoader>



class PLUGINMANAGER_EXPORT PluginManager:public QObject

{

     Q_OBJECT

private:

    PluginManager();

 public:

    static PluginManager *getinstance();



     //加载所有插件

     void loadAllPlugins();



    //卸载所有插件

    void unloadAllPlugins();



     QString datawrite();



 private:

    static PluginManager *m_instance;



     QHash<QString, QPluginLoader *>m_loaders; //插件路径--QPluginLoader实例



};



#endif // PLUGINMANAGER_H





#include "pluginmanager.h"

#include <QDebug>

#include <QPluginLoader>

#include <QFileInfo>

#include <QDir>

#include <QCoreApplication>



#include"PluginInterface.h"



PluginManager* PluginManager::m_instance=nullptr;

PluginManager::PluginManager()

{

}



PluginManager *PluginManager::getinstance()

{

    if(m_instance==nullptr)

        m_instance= new PluginManager();

    return m_instance;

}



void PluginManager::loadAllPlugins()

{

    QString pluginPath = QCoreApplication::applicationDirPath() + "/plugins";

    QStringList filters;

#if defined(Q_OS_LINUX)

    filters << "*.so";

#elif defined(Q_OS_WIN)

    filters << "*.dll";

#endif

    QFileInfoList plugins = QDir(pluginPath).entryInfoList(filters,

                                                           QDir::Files | QDir::NoSymLinks);

    for (int i = 0; i < plugins.count(); ++i)

    {

        QFileInfo plugin = plugins.at(i);

        QPluginLoader *loader = new QPluginLoader(this);

        loader->setFileName(plugin.absoluteFilePath());

        qDebug()<<loader->metaData().keys();

        QJsonObject json = loader->metaData().value("MetaData").toObject();

        QVariant var = json.value("name").toVariant();

        bool result = loader->load();

        qDebug() << QString("load plguin %1 result: ").arg(plugin.absoluteFilePath()) << result;

        if (result)

        {

            m_loaders.insert(plugin.absoluteFilePath(), loader);

            QObject *pluginInstance = loader->instance();

            PluginInterface *appInterface = qobject_cast<PluginInterface *>(pluginInstance);

            if (appInterface != nullptr)

            {



            }

        }

        else

        {

            qDebug() << loader->errorString();

        }

    }



}



void PluginManager::unloadAllPlugins()

{

    for(auto filepath : m_loaders.keys())

    {

        QPluginLoader *loader = m_loaders.value(filepath);

        //卸载插件,并从内部数据结构中移除

        if(loader->unload())

        {

            m_loaders.remove(filepath);

            delete loader;

            loader = nullptr;

        }

    }



}



QString PluginManager::datawrite()

{

    QString str;

    for(auto filepath : m_loaders.keys())

    {

        QPluginLoader *loader = m_loaders.value(filepath);

        PluginInterface *Plugin = dynamic_cast<PluginInterface*>(loader->instance());

        if(Plugin)

            str = Plugin->datawrite();

    }

    return str;

}

插件代码

#ifndef LOGPLUGIN_H

#define LOGPLUGIN_H

#include "logPlugin_global.h"

#include "../PluginManager/PluginInterface.h"

#include <QObject>

class LOGPLUGIN_EXPORT LogPlugin: public QObject,public PluginInterface

{

    Q_OBJECT

    Q_INTERFACES(PluginInterface)   //声明这个插件实现是基于哪个插件接口的

    Q_PLUGIN_METADATA(IID "org.qt-LogPlugin" FILE "LogPlugin.json")  //使用Q_PLUGIN_METADATA宏导出插件

public:

    LogPlugin();

    virtual QString datawrite() override;

};



#endif // LOGPLUGIN_H





#ifndef DBPLUGIN_H

#define DBPLUGIN_H



#include "DbPlugin_global.h"

#include "../PluginManager/PluginInterface.h"

#include <QObject>

class DBPLUGIN_EXPORT DbPlugin : public QObject,public PluginInterface

{

public:   

    Q_OBJECT

    Q_INTERFACES(PluginInterface)   //声明这个插件实现是基于哪个插件接口的

    Q_PLUGIN_METADATA(IID "org.qt-DbPlugin" FILE "DbPlugin.json")  //使用Q_PLUGIN_METADATA宏导出插件

public:

    DbPlugin();

    virtual QString datawrite() override;



};



#endif // DBPLUGIN_H

App代码

#include "dialog.h"

#include "ui_dialog.h"

#include <QDebug>

#include <QPluginLoader>

#include <QFileInfo>

#include <QDir>

#include <QPushButton>

#include "../PluginManager/pluginmanager.h"

Dialog::Dialog(QWidget *parent)

    : QDialog(parent)

    , ui(new Ui::Dialog)

{

    ui->setupUi(this);

}



Dialog::~Dialog()

{

    delete ui;

}



void Dialog::on_pushButton_clicked()

{

    PluginManager::getinstance()->loadAllPlugins();

    PluginManager::getinstance()->datawrite();

    PluginManager::getinstance()->unloadAllPlugins();

}

插件通信

插件的通信通过插件管理器来管理,插件管理器转发插件的消息

  1. 在插件管理器的接口头文件增加一个插件间通信的结构体,增加插件发送消息和接收消息的纯虚函数

Q_DECLARE_METATYPE(Type)

作用:向Qt元系统注册一些非基本类型

这个宏能使Qt元系统的QMetaType知道Type类型,但前提是Type类型必须提供一个公有的默认构造函数、公有的默认拷贝构造函数和公有的默认析构函数

  1. 在插件重写插件管理器声明的接收函数插件管理器声明的发送函数声明为信号。

  1. 在插件管理器的加载插件处加上connect函数,是信号槽关联起来。其中把插件发送信号关联插件管理器的接收槽函数。

同时让插件管理器的接收槽函数转发给对应的插件

  1. 在app程序中调用调用插件的信号从而发送消息

完整代码参见:

链接:https://pan.baidu.com/s/1fWK_oSYQTRNip4FLnTOLwg

提取码:qc0t

;