如何在Qt项目中创建插件

一、插件概念

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

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

优点

​ 其实插件的优点也是常说的设计模式的设计原则,比如易扩展、低耦合、热更新、面向接口等。

​ 一个大型项目的开发离不开插件化,可以让整个框架结构更加清晰和容易理解,比如说一个该项目经常会针对不同客户做功能定制,或者对于软件使用的不同场景,功能有所区别,那这时候插件就变得非常有用了,主工程中包含所有功能模块的调用,但是如果某些功能如果不需要,那最终程序打包只要不把插件的dll打包进去就OK了,程序依然可以正常运行,只是该插件的功能无法使用而已。

​ 这样,对于多功能模块的情况下,如果不同版本仅需要其中几项功能,就可以不用像动态链接库那样,全部dll都包含进去,从而也节省了安装包的空间。

二、插件框架

插件框架要素

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

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

插件系统的构成

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

主系统

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

插件管理器

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

插件

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

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

程序流

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

插件系统基本程序流.png

三、qt框架下的插件

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

下面的内容中,我们要讨论的是 Low Level plugin 的创建方法

插件路径

​ 插件本质上是一个拥有一组特定接口的动态库,但是,它与动态库还是有一些差别的。通常来说,即使在主程序运行时,插件不存在,主程序仍然可以正常运行,只是相应的插件功能无法正常使用;如果使用的是动态库,在程序运行时动态库不存在,则程序无法正常启动。

​ Qt 提供了 QPluginLoader 类让我们可以很轻松的加载插件,它只需要我们提供动态库的绝对路径即可将其加载。通常来讲,我们可以将插件放置在特定目录下,如 processDir/plugins(其中processDir是程序所在的目录)。如果不希望使用标准插件路径,我们可以将插件路径保存在设置中(例如,通过使用QSettings),以便应用程序在运行时读取。

编写插件的步骤

要想使用插件来扩展应用程序,那么首先在主程序中的步骤如下:

  • 定义一组用于与插件通信的接口(只有纯虚函数的类)
    • 使用 Q_DECLARE_INTERFACE() 宏来告诉 Qt 元对象系统有关接口的情况
  • 在应用程序中使用 QPluginLoader 加载插件
  • 使用 qobject_cast() 来测试插件是否实现了指定的接口

编写扩展 Qt 应用程序的插件,步骤如下:

  • 声明一个继承自 QObject 和插件想要提供的接口的插件类
  • 使用 Q_INTERFACES() 宏来告诉 Qt 元对象系统有关接口的情况
  • 使用 Q_PLUGIN_METADATA() 宏导出插件

然后,使用合适的构建工具(如cmake)构建你的项目

以下是一个简单的插件框架

  • 声明一个接口类
#include <QObject>
#include <QtPlugin>

class myPluginInterface
{
public:
    virtual ~myPluginInterface() = default;

    //初始化函数,在插件被加载时会调用
    virtual bool initialize(const QStringList &arguments, QString *errorString) = 0;
};

QT_BEGIN_NAMESPACE

#define myPluginInterface_iid "com.pcer.myPluginInterface"
// 该宏将标识符(字符串)与名为 ClassName 的接口类相关联。标识符必须是唯一的
Q_DECLARE_INTERFACE(myPluginInterface, myPluginInterface_iid)
QT_END_NAMESPACE
  • 实现该接口类的插件类的定义
#include <QObject>
#include <QtPlugin>
#include "../../src/extension/pcerplugininterface.h"

class pluginTest : public QObject, PCerPluginInterface
{
    Q_OBJECT
    // Q_INTERFACES 宏用于告诉 Qt 该类实现的接口
    Q_INTERFACES(PCerPluginInterface)
    // Q_PLUGIN_METADATA宏用于描述插件元数据
    Q_PLUGIN_METADATA(IID PcerPluginInterface_iid)

public:
    pluginTest(QObject *parent = nullptr);
    // 实现虚函数
    virtual bool initialize(const QStringList &arguments, QString *errorString) override;
};
  • 在主程序中测试插件
int MainWindow::loadPlugins()
{
    int count = 0;
    QDir PluginsDir(qApp->applicationDirPath());
    if(!PluginsDir.cd("plugins")) return -1;
    const QStringList entries = PluginsDir.entryList(QDir::Files);
    foreach(QString fileName, entries) {
        QPluginLoader pluginLoader(PluginsDir.absoluteFilePath(fileName));
        QObject* plugin = pluginLoader.instance();
        if(plugin) {
            auto interface = qobject_cast<myPluginInterface*>(plugin);
            if(interface) {
                ++count;
                qDebug() << "plugin loaded!";
            }
        }
    }
    return count;
}
  • 在CMakeLists中将插件作为子项目编译
# 在CMakeLists总文件下,加入子项目
add_subdirectory(plugins/pluginTest)

# 将插件源文件编译为动态库
set(LIB_HEAD_FILES plugintest.h)
set(LIB_SOURCE_FILES plugintest.cpp)
add_library(plugintest SHARED ${LIB_HEAD_FILES} ${LIB_SOURCE_FILES})

target_link_libraries(plugintest Qt::Core)
# 下面是可选项,可对动态库的版本号进行设置
set_target_properties(plugintest PROPERTIES VERSION 1.0 SOVERSION 1)
# 使用make install命令时,会将头文件与动态库文件放到目标路径下
install(FILES ${LIB_HEAD_FILES} DESTINATION ${PCER_INCLUDE_DIRECTORY})
install(TARGETS plugintest LIBRARY DESTINATION ${PCER_PLUGIN_DIRECTORY})
  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);
  };