文章目录

  • 一、插件概念
  • 优点
  • 二、插件框架
  • 1. 插件框架要素
  • 2. 插件系统的构成
  • 主系统
  • 插件管理器
  • 插件
  • 程序流
  • 二、qt框架下的插件
  • 2.0 插件路径
  • 2.1 Qt提供了两个用于创建插件的API:
  • 2.2 通过插件使应用程序可扩展包括以下步骤:
  • 2.3 编写插件包括以下步骤:
  • 2.4 正确的插件框架系统
  • 一个最简单的完整的实例
  • 参考demo
  • 参考的博客



看了很多相关qt plugins的文章,现简单记录下

一、插件概念

插件(Plug-in,又称addin、add-in、addon或add-on,又译外挂)是一种遵循一定规范的应用程序接口编写出来的程序。其只能运行在程序规定的系统平台下(可能同时支持多个平台),而不能脱离指定的平台单独运行。因为插件需要调用原纯净系统提供的函数库或者数据。很多软件都有插件,插件有无数种。例如在IE中,安装相关的插件后,WEB浏览器能够直接调用插件程序,用于处理特定类型的文件。

优点

其实插件的优点也是常说的设计模式的设计原则;
比如 易扩展、低耦合、热更新、面向接口等。对于大型系统来说,可以多人同时开发,互不干扰等优点。

插件都是关于接口的,以插件为基础的系统,其基本概念是:系统可以加载插件,但它不知道任何东西,并且通过一组定义良好的接口和协议与它们进行通信。

二、插件框架

1. 插件框架要素

要实现一个插件框架,需要考虑以下要素:

  • 如何注册插件
  • 如何调用插件
  • 如何测试插件 :框架要支持自动化测试:包括单元测试,集成测试。
  • 插件的生命周期管理
    插件的生命周期由插件框架控制,需要考虑以下问题:
  1. 插件的生命周期如何转换?
  2. 一旦插件的生命周期发生转变,引用此插件的类是否能得到通知。
  • 插件的管理和维护
    -对于插件框架而言,这属于基础功能。主要包括:
  1. 为插件提供名称、版本、状态等信息,并可以获取插件列表,记录插件的处理日志等。
  2. 提供插件加载、启动、停止、卸载等功能。
  • 插件的组装(附加考评要素)
    插件的组装是指可以灵活的将多个插件组装为一条链,然后链式的调用。
  • 插件的出错处理
    当插件在处理过程中发生错误时,最理想的结果是插件的调用停止,并记录相关的日志,另外的插件对此情况做出纠错处理(注意:不能影响插件框架和其他插件的正常运转)。

2. 插件系统的构成

插件系统,可以分为三部分:

主系统

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

插件管理器

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

插件

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

实际上,很少能看到这样一个相对独立的分离,插件管理器通常与主系统紧密耦合,因为插件管理器需要最终提供(定制)某些类型的插件对象的实例。

程序流

框架的基本程序流,如下所示:

html5 qt 插件 qt插件开发框架_#include

二、qt框架下的插件

2.0 插件路径

Qt应用程序自动知道哪些插件可用,因为插件存储在标准插件子目录中。正因为如此,应用程序不需要任何代码来查找和加载插件,因为Qt会自动处理插件。

在开发过程中,插件的目录是QTDIR/plugins(其中QTDIR是安装Qt的目录),每种类型的插件都位于该类型的子目录中,例如样式。如果希望应用程序使用插件,而不希望使用标准插件路径,请让安装过程确定要用于插件的路径,并保存路径(例如,通过使用QSettings),以便应用程序在运行时读取。然后,应用程序可以使用此路径调用QCoreApplication::addLibraryPath(),应用程序将可以使用您的插件。请注意,路径的最后一部分(例如,样式)无法更改。

如果希望插件可以加载,那么一种方法是在应用程序下创建一个子目录,并将插件放在该目录中。如果分发Qt附带的任何插件(位于插件目录中的插件),则必须将插件所在的插件下的子目录复制到应用程序根文件夹(即,不包括插件目录)。

2.1 Qt提供了两个用于创建插件的API:

  • 一个用于为Qt本身编写扩展的高级API:自定义数据库驱动程序、图像格式、文本编解码器、自定义样式等。
  • 用于扩展Qt应用程序的低级API。

例如,如果您想编写一个定制的QStyle子类,并让Qt应用程序动态加载它,那么可以使用更高级的API。

由于更高级别的API是在较低级别的API之上构建的,因此一些问题对两者都是常见的。

  • High level plugin

用来扩展qt本身

  • Low Level plugin

用来扩展你的appliction

详细内容可参考: https://doc.qt.io/qt-5/plugins-howto.html

2.2 通过插件使应用程序可扩展包括以下步骤:

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

2.3 编写插件包括以下步骤:

  1. 声明一个插件类,该类继承自QObject和该插件想要提供的接口。
  2. 使用Q_INTERFACES()宏告诉Qt的元对象系统有关接口的信息。
  3. 使用Q_plugin_METADATA()宏导出插件。

Q_PLUGIN_METADATA(IID IPerson_iid FILE "programmer.json") 用该宏导出插件,programmer.json文件描述插件的属性

{
    "author" : "wzx",
    "date" : "2019/11/28",
    "name" : "personPlugin",
    "version" : "1.0.0",
    "dependencies" : []
}
  1. 使用合适的 .pro 文件构建插件
TEMPLATE = lib
CONFIG += plugin

例如,以下是接口类的定义:

class FilterInterface
  {
  public:
      virtual ~FilterInterface() {}

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

下面是实现该接口的插件类的定义:

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

  #include <plugandpaint/interfaces.h>

  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;
      QImage filterImage(const QString &filter, const QImage &image,
                         QWidget *parent);
  };

2.4 正确的插件框架系统

推荐的插件系统应该是下面的工程结构

TEMPLATE = subdirs

SUBDIRS += \
    MainApp \
    plugin1 \
    plugin2 \
    plugin3 \
    ...

一个最简单的完整的实例

官方实例:C:\Qt\Qt5.14.2\Examples\Qt-5.14.2\widgets\tools\echoplugin

整体结构如图:

html5 qt 插件 qt插件开发框架_plugins_02


接口类的定义 echointerface.h

#ifndef ECHOINTERFACE_H
#define ECHOINTERFACE_H

#include <QObject>
#include <QString>

//! [0]
class EchoInterface
{
public:
    virtual ~EchoInterface() = default;
    virtual QString echo(const QString &message) = 0;
};


QT_BEGIN_NAMESPACE

#define EchoInterface_iid "org.qt-project.Qt.Examples.EchoInterface"

Q_DECLARE_INTERFACE(EchoInterface, EchoInterface_iid)
QT_END_NAMESPACE

//! [0]
#endif

plugin.pro

#! [0]
TEMPLATE        = lib
CONFIG         += plugin
QT             += widgets
INCLUDEPATH    += ../echowindow
HEADERS         = echoplugin.h
SOURCES         = echoplugin.cpp
TARGET          = $$qtLibraryTarget(echoplugin)
DESTDIR         = ../plugins
#! [0]

EXAMPLE_FILES = echoplugin.json

# install
target.path = $$[QT_INSTALL_EXAMPLES]/widgets/tools/echoplugin/plugins
INSTALLS += target

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

echoplugin.h

#ifndef ECHOPLUGIN_H
#define ECHOPLUGIN_H

#include <QObject>
#include <QtPlugin>
#include "echointerface.h"

//! [0]
class EchoPlugin : public QObject, EchoInterface
{
    Q_OBJECT
    Q_PLUGIN_METADATA(IID "org.qt-project.Qt.Examples.EchoInterface" FILE "echoplugin.json")
    Q_INTERFACES(EchoInterface)

public:
    QString echo(const QString &message) override;
};
//! [0]

#endif

echoplugin.cpp

#include "echoplugin.h"

//! [0]
QString EchoPlugin::echo(const QString &message)
{
    return message;
}
//! [0]

主窗口 echowindow.h

#ifndef ECHODIALOG_H
#define ECHODIALOG_H

#include <QWidget>

#include "echointerface.h"

QT_BEGIN_NAMESPACE
class QString;
class QLineEdit;
class QLabel;
class QPushButton;
class QGridLayout;
QT_END_NAMESPACE

//! [0]
class EchoWindow : public QWidget
{
    Q_OBJECT

public:
    EchoWindow();

private slots:
    void sendEcho();

private:
    void createGUI();
    bool loadPlugin();

    EchoInterface *echoInterface;
    QLineEdit *lineEdit;
    QLabel *label;
    QPushButton *button;
    QGridLayout *layout;
};
//! [0]

echowindow.cpp

#include "echowindow.h"

#include <QCoreApplication>
#include <QDir>
#include <QLabel>
#include <QLayout>
#include <QLineEdit>
#include <QMessageBox>
#include <QPluginLoader>
#include <QPushButton>

//! [0]
EchoWindow::EchoWindow()
{
    createGUI();
    setLayout(layout);
    setWindowTitle("Echo Plugin Example");

    if (!loadPlugin()) {
        QMessageBox::information(this, "Error", "Could not load the plugin");
        lineEdit->setEnabled(false);
        button->setEnabled(false);
    }
}
//! [0]

//! [1]
void EchoWindow::sendEcho()
{
    QString text = echoInterface->echo(lineEdit->text());
    label->setText(text);
}
//! [1]

//! [2]
void EchoWindow::createGUI()
{
    lineEdit = new QLineEdit;
    label = new QLabel;
    label->setFrameStyle(QFrame::Box | QFrame::Plain);
    button = new QPushButton(tr("Send Message"));

    connect(lineEdit, &QLineEdit::editingFinished,
            this, &EchoWindow::sendEcho);
    connect(button, &QPushButton::clicked,
            this, &EchoWindow::sendEcho);

    layout = new QGridLayout;
    layout->addWidget(new QLabel(tr("Message:")), 0, 0);
    layout->addWidget(lineEdit, 0, 1);
    layout->addWidget(new QLabel(tr("Answer:")), 1, 0);
    layout->addWidget(label, 1, 1);
    layout->addWidget(button, 2, 1, Qt::AlignRight);
    layout->setSizeConstraint(QLayout::SetFixedSize);
}
//! [2]

//! [3]
bool EchoWindow::loadPlugin()
{
    QDir pluginsDir(QCoreApplication::applicationDirPath());
#if defined(Q_OS_WIN)
    if (pluginsDir.dirName().toLower() == "debug" || pluginsDir.dirName().toLower() == "release")
        pluginsDir.cdUp();
#elif defined(Q_OS_MAC)
    if (pluginsDir.dirName() == "MacOS") {
        pluginsDir.cdUp();
        pluginsDir.cdUp();
        pluginsDir.cdUp();
    }
#endif
    pluginsDir.cd("plugins");
    const QStringList entries = pluginsDir.entryList(QDir::Files);
    for (const QString &fileName : entries) {
        QPluginLoader pluginLoader(pluginsDir.absoluteFilePath(fileName));
        QObject *plugin = pluginLoader.instance();
        if (plugin) {
            echoInterface = qobject_cast<EchoInterface *>(plugin);
            if (echoInterface)
                return true;
            pluginLoader.unload();
        }
    }

    return false;
}
//! [3]

main.cpp

#include <QApplication>

#include "echowindow.h"
#include "echointerface.h"

//! [0]
int main(int argv, char *args[])
{
    QApplication app(argv, args);

    EchoWindow window;
    window.show();

    return app.exec();
}
//! [0]

效果:

html5 qt 插件 qt插件开发框架_html5 qt 插件_03

参考demo

参考1 :良好结构的插件系统:

参考2:定义了插件间的通信结构

下载地址:

#ifndef PLUGINMANAGER_H
#define PLUGINMANAGER_H

#include <QObject>
#include <QHash>
#include "PluginInterface.h"

class QPluginLoader;

class PluginManager : public QObject
{
    Q_OBJECT

public:
    static PluginManager *instance()
    {
        if(m_instance == nullptr)
            m_instance = new PluginManager();
        return m_instance;
    }

    void loadAllPlugins();
    void loadPlugin(const QString &filepath);
    void unloadPlugin(const QString &filepath);
    void unloadAllPlugins();
    QPluginLoader* getPlugin(const QString &name);
    QVariant getPluginName(QPluginLoader *loader);

public slots:
    void recMsgfromPlugin(PluginMetaData metadata);

private:
    explicit PluginManager(QObject *parent = nullptr);
    ~PluginManager();

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

    static PluginManager *m_instance;
    class GarbageCollector
    {
        ~GarbageCollector()
        {
            if (PluginManager::instance())
            {
                delete PluginManager::instance();
                PluginManager::m_instance = nullptr;
            }
        }
    };
    static GarbageCollector gc;
};

#endif // PLUGINMANAGER_H
struct PluginMetaData
{
    QString from;//消息来源
    QString dest;//消息目的地
    QString msg;

    QObject *object = nullptr;
    QJsonObject info = QJsonObject();
};
Q_DECLARE_METATYPE(PluginMetaData);//确保类型可以通过信号槽传递

class PluginInterface
{
public:
    virtual ~PluginInterface() {}
    virtual QString get_name() const = 0;
    virtual QString show_text() const = 0;
    virtual void recMsgfromManager(PluginMetaData) = 0;//接收到来自创建管理器的消息
    virtual void sendMsg2Manager(PluginMetaData)   = 0;//给插件管理器发消息
};

Q_DECLARE_INTERFACE(PluginInterface,"org.galaxyworld.plugins.PluginInterface/1.0")

插件管理器代码

#include "pluginmanager.h"
#include <QPluginLoader>
#include <QDir>
#include <QDebug>

PluginManager* PluginManager::m_instance;
PluginManager::PluginManager(QObject *parent) : QObject(parent)
{

}

PluginManager::~PluginManager()
{
    unloadAllPlugins();
}

//加载所有插件
void PluginManager::loadAllPlugins()
{
    QDir pluginsdir(QDir::currentPath());
    pluginsdir.cd("debug");//打开文件夹
    pluginsdir.cd("plugins");

    QFileInfoList pluginsInfo = pluginsdir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot);

    //加载插件
    for(QFileInfo fileinfo : pluginsInfo)
    {
        qDebug()<<fileinfo.absoluteFilePath();
        loadPlugin(fileinfo.absoluteFilePath());
    }
}

//加载其中某个插件
void PluginManager::loadPlugin(const QString &filepath)
{
    if(!QLibrary::isLibrary(filepath))
        return;

    //加载插件
    QPluginLoader *loader = new QPluginLoader(filepath);
    QString plugin_name;
    if(loader->load())
    {
        PluginInterface *plugin = qobject_cast<PluginInterface *>(loader->instance());
        if(plugin)
        {
            plugin_name = plugin->get_name();
            m_loaders.insert(filepath, loader);
            m_names.insert(filepath,plugin_name);
            qDebug()<<"插件名称:"<<plugin->get_name()<<"插件信息:"<<plugin->show_text();

            connect(loader->instance(),SIGNAL(sendMsg2Manager(PluginMetaData)),this,SLOT(recMsgfromPlugin(PluginMetaData)));
        }
        else
        {
            delete loader;
            loader = nullptr;
        }
    }
    else
    {
        qDebug()<<"loadPlugin:"<<filepath<<loader->errorString();
    }
}

//卸载所有插件
void PluginManager::unloadAllPlugins()
{
    for(QString filepath : m_loaders.keys())
        unloadPlugin(filepath);
}

void PluginManager::unloadPlugin(const QString &filepath)
{
    QPluginLoader *loader = m_loaders.value(filepath);
    //卸载插件,并从内部数据结构中移除
    if(loader->unload())
    {
        m_loaders.remove(filepath);
        delete loader;
        loader = nullptr;
    }
}

//获取某个插件名称
QVariant PluginManager::getPluginName(QPluginLoader *loader)
{
    if(loader)
        return m_names.value(m_loaders.key(loader));
    else
        return "";
}

//根据名称获得插件
QPluginLoader *PluginManager::getPlugin(const QString &name)
{
    return m_loaders.value(m_names.key(name));
}

void PluginManager::recMsgfromPlugin(PluginMetaData metadata)
{
    auto loader = getPlugin(metadata.dest);//目标插件
    if(loader)
    {
        auto interface = qobject_cast<PluginInterface*>(loader->instance());;
        if(interface)
        {
            interface->recMsgfromManager(metadata);//转发给对应插件
        }
    }
}