多读书多实践,勤思考善领悟

QT 插件(QtPlugin)教程

本文于1041天之前发表,文中内容可能已经过时。

教程源码:https://github.com/myhhub/qtPluginApp

初识QtPlugin

概述

为什么我们要学习插件化,其和 windows 导出 dll 有什么区别呢?

  1. 导出的动态库如果缺失,程序不能运行。但插件可以。
  2. 同一套代码,即可分别在 windows 下和 linux 下生成插件。

QT 本身提供两种插件支持,一种称为高级 API,一种称为低级 API。

  1. 高级API的作用是扩展 QT 程序本身,需要子类化 QT 提供的插件基类,例如现有的 QTSqlDriver,因此你可也以编写自己的 QTStyle 扩展 QT。
  2. 低级 API 的作用是扩展自己的程序,也就是动态库的形式,在windows下就是个dll。同时因为高级 API 基于低级 API 创建,因此掌握低级 API 用法,高级 API 的用法也不在话下。

QT 的插件加载需要用到类 QPluginloader,你可以编写支持任意功能的插件。如何创建一个插件和加载这个插件 QT Assist 中是这样描述的:

创建一个扩展应用程序的插件:

  1. 定义一组用于与插件对话的接口(仅具有纯虚拟函数的类)。(预留好接口,方便建立通信)。
  2. 接口内使用 Q_DECLARE_INTERFACE() 宏告诉qt的元对象系统有关接口的信息。
  3. 使用 QPluginLoader 加载插件。
  4. 使用 qobject_cast 检验插件是否实现了既定的接口(以防外部插件注入),转换成功即可得到插件实例。

插件编写具体步骤(代码编写):

  1. 声明插件类,并继承 QObject 和 实现上面提到的既定接口。
  2. 使用 Q_INTERFACES 告诉 QT 元对象系统有关这个接口的信息(虚方法)。
  3. 使用 Q_PLUGIN_METADATA 导出插件。(Q_PLUGIN_METADATA 是 QT5的宏,QT4 使用的是 Q_EXPORT_PLUGIN2)
  4. 编写合适的 .pro 文件。

实例

1、 新建子目录项目 PluginApp
2、 在 PluginApp 下 新建 QWidget 项目,名为 Main
3、 右键 PluginApp 新建子项目 pluginA
4、 编写接口,在 Main 下新建头文件 interfaceplugin.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#ifndef INTERFACEPLUGIN_H
#define INTERFACEPLUGIN_H

#include <QString>
#include <QtPlugin>

//定义接口
class InterfacePlugin
{
public:
virtual ~InterfacePlugin() {}
virtual QString output(const QString &message) = 0;
};

#define InterfacePlugin_iid "Test.Plugin.InterfacePlugin" // 唯一标识符

Q_DECLARE_INTERFACE(InterfacePlugin, InterfacePlugin_iid)

#endif

5、 加载插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

#include "widget.h"
#include <QApplication>
#include <QDir>
#include <QPluginLoader>
#include "interfaceplugin.h"
#include <QObject>
#include <QDebug>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
// Widget w;
// w.show();

//加载exe所在目录下 plugin文件夹的所有插件
QDir path = QDir(qApp->applicationDirPath());
path.cd("plugins");
foreach (QFileInfo info, path.entryInfoList(QDir::Files | QDir::NoDotAndDotDot))
{
QPluginLoader pluginLoader(info.absoluteFilePath());
QObject *plugin = pluginLoader.instance();
if (plugin)
{
InterfacePlugin *app = qobject_cast<InterfacePlugin*>(plugin);
if (app)
{
// 获取元数据(名称、版本、依赖)
QJsonObject json = pluginLoader.metaData().value("MetaData").toObject();
qDebug() << "********** MetaData **********";
qDebug() << json.value("author").toVariant();
qDebug() << json.value("date").toVariant();
qDebug() << json.value("name").toVariant();
qDebug() << json.value("version").toVariant();
qDebug() << json.value("dependencies").toArray().toVariantList();

// 访问感兴趣的接口
app->output("i am a plugin");
}
}
}
return a.exec();
}

6、 编写插件 .pro 文件、头文件、 cpp 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
TEMPLATE        = lib           #表示这个makefile是一个lib的makefile
CONFIG += plugin #应用程序是一个插件
TARGET = pluginA #生成插件的名称
win32 {
CONFIG(debug, debug|release) {
DESTDIR = ../Main/debug/plugins
} else {
DESTDIR = ../Main/release/plugins
}
}#生成插件的目录

HEADERS += \
pluginA.h

SOURCES += \
pluginA.cpp

DISTFILES += \
programmer.json #插件描述文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#ifndef PLUGINA_H
#define PLUGINA_H

#include <QObject>
#include <QtPlugin>
#include "../Main/interfaceplugin.h"

class PluginA : public QObject, public InterfacePlugin
{
// programmer.json 插件的信息描述类
Q_OBJECT
Q_PLUGIN_METADATA(IID InterfacePlugin_iid FILE "programmer.json") // QT5.0 引入
Q_INTERFACES(InterfacePlugin)
public:
explicit PluginA(QObject *parent = 0);
virtual QString output(const QString &message) Q_DECL_OVERRIDE;
};
#endif // PLUGINA_H
1
2
3
4
5
6
7
8
9
10
11
12
13
#include "pluginA.h"
#include <QtDebug>

PluginA::PluginA(QObject *parent) :
QObject(parent)
{
}

QString PluginA::output(const QString &message)
{
qDebug() << message + "插件A加载成功";
return message;
}

“programmer.json” 为插件描述文件,会作为元数据被加载到插件中,可在需要的时候手动读取。其中包含了插件的基本信息,更重要的是包含了插件的依赖项,这些依赖项决定了插件的加载顺序,关于插件依赖解决后面再讲。

1
2
3
4
5
6
7
8
{
"author" : "myh",
"date" : "2021/04/22",
"name" : "pluginA",
"version" : "1.0.0",
"des" : "这是一个插件A,按此方法加载插件B、C等",
"dependencies" : []
}

注:
1、Main 项目选择 QWidget GUI项目是有原因的,下篇再说是为什么。
2、windows 下生成的插件为 dll 后缀,linux 下生成的即为 .so后缀(下篇出 linux 测试结果)。

元信息metaData

JSON 与Qt插件的元信息 MetaData

Qt插件的源码中,基本都能见到一个 xxx.json 的文件,这个文件中通常只包含一句:

1
2
3
{
"Keys": [ "yyy" ]
}123

我们可以猜到这个文件中的”Keys”应该是指定了与插件相关的关键字。那这个 .json 文件到底是如何起作用的?先来认识一下 JSON 。

JSON是一种存储结构化数据的格式,它有6中基本数据类型,分别是:

  • bool 布尔型,取值可以是 true 或 false
  • double 数字类型
  • string 字符串类型
  • array 数组类型
  • object 对象类型
  • null 空类型

具体可参见 Qt Assistant 中关于”JSON Support in Qt “的介绍。

A simple JSON document encoding a person, his/her age, address and phone numbers could look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1. {  
2. "FirstName": "John", # FirstName是变量(字段)的名称;John是变量的值
3. "LastName": "Doe",
4. "Age": 43,
5. "Address": {
6. "Street": "Downing Street 10",
7. "City": "London",
8. "Country": "Great Britain"
9. },
10. "Phone numbers": [
11. "+44 1234567",
12. "+44 2345678"
13. ]
14. }

值得一提的是,数组类型的字段在.json文件中赋值时应该用方括号 ‘[’ 和 ‘]’ 括起来,对象类型的字段在赋值时应用花括号 ‘{’ 和 ‘}’ 括起来,普通类型的数据则不需要括。每一个 .json 文件描述了一个 JSON对象,而一个JSON对象中的对象类型字段,又可以看做是一个子JSON对象(JSON对象的嵌套)。

.json在Qt插件中主要用于存储Qt插件的元信息(metaData),在Qt中,有一个专门的类 QJsonObject 来描述一个JSON。

Qt中的JSON相关类

QJsonArray Encapsulates a JSON array 封装JSON数组
QJsonDocument Way to read and write JSON documents 读取和写入JSON文本的方式
QJsonObject Encapsulates a JSON object 封装JSON对象
QJsonObject::iterator QJsonObject::iterator class provides an STL-style non-const iterator for QJsonObject JSON迭代器
QJsonObject::const_iterator QJsonObject::const_iterator class provides an STL-style const iterator for QJsonObject JSON const迭代器
QJsonParseError Used to report errors during JSON parsing 用于报告JSON解析期间的错误
QJsonValue Encapsulates a value in JSON 封装JSON中的值

接口间通信

插件接口(Interface)的作用,就是提供一个与其他系统交互的方法。其他系统无需(也无法)了解内部的具体细节,只能通过对外提供的接口来与进行通信。

纯虚函数(包括槽)很容易理解,那么信号呢?

1
在 Qt 中,定义一个纯虚信号有效吗?

的确,这个话题非常有意思。。。通常,我们会定义一些纯虚的槽函数,但关于纯虚信号这个话题讨论的比较少!那么,信号可不可以是纯虚的呢?

一些尝试

关于信号和纯虚,我们知道:

  • 信号永远不会有实现(也就是说,在 .h 中定义了信号,在 .cpp 中不需要实现)
  • 声明一个纯虚函数的主要目的,是强制继承的类提供一个实现。

信号没有实现,如果将其声明为纯虚的,需要继承的类来提供一个实现,这与“信号没有实现”直接冲突。就好比让一个人同时出现在两个地方,这是不可能的。因此,似乎声明一个纯虚信号是一个错误。

在编写完一个接口时,为了能使用 Qt 的信号槽特性,很多人可能会写出类似下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#ifndef PLUGININTERFACE_H
#define PLUGININTERFACE_H
#include <QObject>
#include <QStringList>
#include <QtWidgets/qwidget.h>
class PluginInterface : public QObject
{
Q_OBJECT
public:
virtual ~PluginInterface() {}

public:
virtual void setInitData(QStringList &strlist) = 0;
virtual void getResultData(QStringList &strlist) = 0;

signals:
virtual void information(const QString&) = 0;
};
#define PluginInterface_iid "QtPluginsTest.QtPluginsManager.PluginInterface"

Q_DECLARE_INTERFACE(PluginInterface, PluginInterface_iid)

#endif // PLUGININTERFACE_H

很遗憾,Qt 发出了警告:

warning: Signals cannot be declared virtual

那么,如何解决这个问题呢?

解决方案

下面,列出三种解决方案:

  • 派生自 QObject,信号不使用 virtual
  • 将“信号”定义为一个纯虚函数
  • 在接口中定义信号槽的连接方式

派生自 QObject,信号不使用 virtual

不让用 virtual,好吧,那就不用了,这算是最简单的解决方式!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#ifndef PLUGININTERFACE_H
#define PLUGININTERFACE_H
#include <QObject>
#include <QStringList>
#include <QtWidgets/qwidget.h>
class PluginInterface : public QObject
{
Q_OBJECT
public:
virtual ~PluginInterface() {}

public:
virtual void setInitData(QStringList &strlist) = 0;
virtual void getResultData(QStringList &strlist) = 0;

signals:
void information(const QString&) = 0;
};
#define PluginInterface_iid "QtPluginsTest.QtPluginsManager.PluginInterface"

Q_DECLARE_INTERFACE(PluginInterface, PluginInterface_iid)

#endif // PLUGININTERFACE_H

具体的实现也与信号无关了,只需要实现其他接口即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#ifndef GENERICPLUGIN1_H
#define GENERICPLUGIN1_H

#include <QObject>
#include <QtDebug>
#include <../pluginmanager/plugininterface.h>

class GenericPlugin1 : public PluginInterface
{
Q_OBJECT

public:
GenericPlugin1() {}

void setInitData(QStringList &strlist) Q_DECL_OVERRIDE {
qDebug() << "setInitDat..."<<strlist;
}
void getResultData(QStringList &strlist) Q_DECL_OVERRIDE {
qDebug() << "getResultData..."<<strlist;
}
};

#endif // GENERICPLUGIN1_H

但是这种方式并不算理想,因为一般来说,接口是一个简单的 C++ 类,不需要派生自 QObject

将“信号”定义为一个纯虚函数

将“信号”定义为一个纯虚函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#ifndef PLUGININTERFACE_H
#define PLUGININTERFACE_H
#include <QStringList>
#include <QtWidgets/qwidget.h>
class PluginInterface
{
public:
virtual ~PluginInterface() {}

public:
virtual void setInitData(QStringList &strlist) = 0;
virtual void getResultData(QStringList &strlist) = 0;

// 不是一个信号(但可以当做信号来用),因为 PluginInterface 不是 QObject
virtual information(const QString&) = 0;
};
#define PluginInterface_iid "QtPluginsTest.QtPluginsManager.PluginInterface"

Q_DECLARE_INTERFACE(PluginInterface, PluginInterface_iid)

#endif // PLUGININTERFACE_H

注意: 对于 PluginInterface来说,information() 只是一个纯虚函数,并不是一个“信号”(但可以当做信号来用),因为 PluginInterface 不是 QObject

由于需要信号支持,所以具体实现需要派生自 QObject。此外,还需要将 information() 定义为 signals

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#ifndef GENERICPLUGIN1_H
#define GENERICPLUGIN1_H

#include <QObject>
#include <QtPlugin>
#include <QtDebug>
#include <../pluginmanager/plugininterface.h>

class GenericPlugin1 : public QObject, public PluginInterface
{
Q_OBJECT

public:
GenericPlugin1() {}

void setInitData(QStringList &strlist) Q_DECL_OVERRIDE {
qDebug() << "setInitDat..."<<strlist;
}
void getResultData(QStringList &strlist) Q_DECL_OVERRIDE {
qDebug() << "getResultData..."<<strlist;
}

signals:
// 实现由 moc 来完成
void information(const QString&) Q_DECL_OVERRIDE;
};


#endif // GENERICPLUGIN1_H

这时,information() 的具体的实现在内部是由 moc 来完成的。

为了测试是否有效,再定义一个类 - monitor.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#ifndef MONITOR_H
#define MONITOR_H

#include <QObject>
#include <QtDebug>
#include <../pluginmanager/plugininterface.h>

class Monitor : public QObject
{
Q_OBJECT

private slots:
void onInformation(const QString& info) {
qDebug() <<"Information..."<< info;
}

public:
void monitorInformation(PluginInterface *plugin) {
QObject *Object = dynamic_cast<QObject *>(plugin);
if (Object) {
connect(Object, SIGNAL(information(const QString&)), this, SLOT(onInformation(const QString&)));
} else {
qWarning() << "cannot monitor Information";
}
}
};

#endif // MONITOR_H

注意: 连接信号时,需要将其转换为 QObject

main.cpp 中进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <QCoreApplication>
#include "genericplugin1.h"
#include "monitor.h"

int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);

GenericPlugin1 plugin;
Monitor monitor;
monitor.monitorInformation(&plugin);
emit plugin.information("ddddddddd");

return a.exec();
}

在接口中定义信号槽的连接方式

除了上述方式之外,还可以在接口中定义信号槽的连接方式。

首先,定义一个连接信号槽的接口 - connectToInformation()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#ifndef PLUGININTERFACE_H
#define PLUGININTERFACE_H
#include <QStringList>
#include <QtWidgets/qwidget.h>
class PluginInterface
{
public:
virtual ~PluginInterface() {}

public:
virtual void setInitData(QStringList &strlist) = 0;
virtual void getResultData(QStringList &strlist) = 0;

//connect to signals
virtual bool connectToInformation(QObject *receiver, const char* pszSlot, bool isConnect = true) const = 0;
};
#define PluginInterface_iid "QtPluginsTest.QtPluginsManager.PluginInterface"

Q_DECLARE_INTERFACE(PluginInterface, PluginInterface_iid)

#endif // PLUGININTERFACE_H

然后,在派生类中实现信号槽的具体连接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#ifndef GENERICPLUGIN1_H
#define GENERICPLUGIN1_H

#include <QObject>
#include <QtPlugin>
#include <../pluginmanager/plugininterface.h>

class GenericPlugin1 : public QObject, public PluginInterface
{
// programmer.json 插件的信息描述类
Q_OBJECT
Q_PLUGIN_METADATA(IID PluginInterface_iid FILE "programmer.json") // QT5.0 引入
Q_INTERFACES(PluginInterface)
public:
GenericPlugin1() {}

void setInitData(QStringList &strlist) Q_DECL_OVERRIDE {
qDebug() << "setInitDat..." << strlist;
//发射信号
emit information(strlist.at(0));
}

void getResultData(QStringList &strlist) Q_DECL_OVERRIDE {
qDebug() << "getResultData..." << strlist;
}

bool connectToInformation(QObject *receiver, const char *pszSlot, bool isConnect) const Q_DECL_OVERRIDE {
if(isConnect)
return connect(this, SIGNAL(information(const QString&)), receiver, pszSlot);
else
return disconnect(this, SIGNAL(information(const QString&)), receiver, pszSlot);
}


signals:
void information(const QString&);

};

#endif // GENERICPLUGIN1_H

这样以来,连接方式就变得简单多了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#ifndef MONITOR_H
#define MONITOR_H

#include <QObject>
#include <QtDebug>
#include <../pluginmanager/plugininterface.h>

class Monitor : public QObject
{
Q_OBJECT

public slots:
void onInformation(const QString& info) {
qDebug() << "Information..." << info;
}

public:
void monitorInformation(PluginInterface *plugin) {
bool isConnect = plugin->connectToInformation(this, SLOT(const onInformation()), true);
if (isConnect) {
qWarning() << "connectToInformation";
} else {
qWarning() << "cannot monitor Information";
}
}
};

#endif // MONITOR_H

除了信号槽之外,Qt 还可以通过事件机制(sendEvent()postEvent())来实现接口之间的通信。正如上所示,只要通过接口来获得 QObject 即可。

插件管理器

Qt 本身提供了插件相关的技术,但并没有提供一个通用的插件框架!倘若要开发一个较大的 GUI 应用程序,并希望使其可扩展,那么拥有这样一个插件框架无疑会带来很大的好处。

插件系统构成

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

  • 主系统
    通过插件管理器加载插件,并创建插件对象。一旦插件对象被创建,主系统就会获得相应的指针/引用,它可以像任何其他对象一样使用。
  • 插件管理器
    用于管理插件的生命周期,并将其暴露给主系统。它负责查找并加载插件,初始化它们,并且能够进行卸载。它还应该让主系统迭代加载的插件或注册的插件对象。
  • 插件
    插件本身应符合插件管理器协议,并提供符合主系统期望的对象。

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

程序流

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

img

插件管理器

上面提到,插件管理器有一个职责 - 加载插件。那么,是不是所有的插件都需要加载呢?当然不是!只有符合我们约定的插件,才会被认为是标准的、有效的插件,外来插件一律认定为无效。

创建空项目pluginmanager

.pro 文件为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
QT       -= gui

TARGET = qtpluginmanager
TEMPLATE = lib

DEFINES += QTPLUGINSMANAGER_LIBRARY

win32 {
CONFIG(debug, debug|release) {
DESTDIR = ../Main/debug
} else {
DESTDIR = ../Main/release
}
}

DEFINES += QT_DEPRECATED_WARNINGS

SOURCES += \
qtpluginmanager.cpp \
qtpluginsmanagerprivate.cpp

HEADERS += plugininterface.h \
pluginmetadata.h \
qtpluginmanager.h \
qtpluginsmanager_global.h \
qtpluginsmanagerprivate.h

为了解决这个问题,可以为插件设定一个“标识(Interface)” - PluginInterface.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#ifndef PLUGININTERFACE_H
#define PLUGININTERFACE_H
#include "pluginmetadata.h"
class PluginInterface
{
public:
virtual ~PluginInterface() {}

public:

virtual void setInitData(QMap<QString,QVariant> &data) = 0;

virtual QMap<QString,QVariant>& getResultData() = 0;

virtual void recMsgFromManager(PluginMetaData &msg) = 0;

//connect to signals sendMsgToManager
//virtual void sendMsgToManager(PluginMetaData &msg) = 0;
virtual bool connectTosendMsgToManager(QObject *receiver, const char* pszSlot, bool isConnect = true) const = 0;
};
#define PluginInterface_iid "QtPlugins.QtPluginsManager.PluginInterface"

Q_DECLARE_INTERFACE(PluginInterface, PluginInterface_iid)

#endif // PLUGININTERFACE_H

后期实现的所有插件,都必须继承自 PluginInterface,这样才会被认定是自己的插件,以防外部插件注入。

注意:使用 Q_DECLARE_INTERFACE 宏,将 PluginInterface接口与标识符一起公开。

插件的基本约束有了,插件的具体实现插件管理器并不关心,它所要做的工作是加载插件、卸载插件、检测插件的依赖、以及扫描插件的元数据(Json 文件中的内容)。。。为了便于操作,将其实现为一个单例。

qtpluginsmanager_global.h内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
#ifndef QTPLUGINSMANAGER_GLOBAL_H
#define QTPLUGINSMANAGER_GLOBAL_H

#include <QtCore/qglobal.h>

#if defined(QTPLUGINSMANAGER_LIBRARY)
# define QTPLUGINSMANAGERSHARED_EXPORT Q_DECL_EXPORT
#else
# define QTPLUGINSMANAGERSHARED_EXPORT Q_DECL_IMPORT
#endif

#endif // QTPLUGINSMANAGER_GLOBAL_H

qtpluginmanager.h内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#ifndef QTPLUGINSMANAGER_H
#define QTPLUGINSMANAGER_H

#include "qtpluginsmanager_global.h"
#include "pluginmetadata.h"
#include <QObject>
#include <QPluginLoader>
#include <QVariant>

class QtPluginsManagerPrivate;

class QTPLUGINSMANAGERSHARED_EXPORT QtPluginsManager : public QObject
{
Q_OBJECT
public:

QtPluginsManager();
~QtPluginsManager();

static QtPluginsManager *instance(){
if(m_instance==nullptr)
m_instance=new QtPluginsManager();
return m_instance;
}

//加载所有插件
void loadAllPlugins();

//扫描JSON文件中的插件元数据
void scanMetaData(const QString &filepath);
//加载其中某个插件
void loadPlugin(const QString &filepath);
//卸载所有插件
void unloadAllPlugins();
//卸载某个插件
void unloadPlugin(const QString &filepath);
//获取所有插件
QList<QPluginLoader *> allPlugins();
//获取某个插件
QPluginLoader* getPlugin(const QString &name);
//获取所有插件名称
QList<QVariant> allPluginsName();
//获取某个插件名称
QVariant getPluginName(QPluginLoader *loader);

private:
static QtPluginsManager *m_instance;
QtPluginsManagerPrivate *d;

private slots:
void recMsgFromManager(PluginMetaData&);

};

#endif

可以看到,插件管理器中有一个 d 指针,它包含了插件元数据的哈希表。此外,由于其拥有所有插件的元数据,所以还为其赋予了另外一个职能 - 检测插件的依赖关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#ifndef QTPLUGINSMANAGERPRIVATE_H
#define QTPLUGINSMANAGERPRIVATE_H

#include <QHash>
#include <QVariant>
#include <QPluginLoader>

class QtPluginsManagerPrivate
{
public:
//插件依赖检测
bool check(const QString &filepath);

QHash<QString, QVariant> m_names; //插件路径--插件名称
QHash<QString, QVariant> m_versions; //插件路径--插件版本
QHash<QString, QVariantList>m_dependencies; //插件路径--插件额外依赖的其他插件
QHash<QString, QPluginLoader *>m_loaders; //插件路径--QPluginLoader实例
};

#endif // QTPLUGINSMANAGERPRIVATE_H

注意: 这里的 check() 是一个递归调用,因为很有可能存在“插件A”依赖于“插件B”,而“插件B”又依赖于“插件C”的连续依赖情况。

QtPluginsManagerPrivate中的哈希表在初始化插件管理器时被填充:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void QtPluginsManager::loadAllPlugins()
{
QDir pluginsdir = QDir(qApp->applicationDirPath());
pluginsdir.cd("plugins");

QFileInfoList pluginsInfo = pluginsdir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot);
//初始化插件中的元数据
for(QFileInfo fileinfo : pluginsInfo)
scanMetaData(fileinfo.absoluteFilePath());

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

元数据的具体扫描由 scan() 负责:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void QtPluginsManager::scanMetaData(const QString &filepath)
{
//判断是否为库(后缀有效性)
if(!QLibrary::isLibrary(filepath))
return ;
//获取元数据
QPluginLoader *loader = new QPluginLoader(filepath);
QJsonObject json = loader->metaData().value("MetaData").toObject();

QVariant var = json.value("name").toVariant();
d->m_names.insert(filepath, json.value("name").toVariant());
d->m_versions.insert(filepath, json.value("version").toVariant());
d->m_dependencies.insert(filepath, json.value("dependencies").toArray().toVariantList());

delete loader;
loader = nullptr;
}

一旦所有元数据被扫描,便可以检查是否能够加载插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void QtPluginsManager::loadPlugin(const QString &filepath)
{
if(!QLibrary::isLibrary(filepath))
return;

//检测依赖
if(!d->check(filepath))
return;

//加载插件
QPluginLoader *loader = new QPluginLoader(filepath);
if(loader->load())
{
// 如果继承自 Plugin,则认为是自己的插件(防止外部插件注入)。
PluginInterface *plugin = qobject_cast<PluginInterface *>(loader->instance());
if(plugin)
{
d->m_loaders.insert(filepath, loader);
plugin->connectTosendMsgToManager(this, SLOT(recMsgFromManager(PluginMetaData&)), true);
}
else
{
delete loader;
loader = nullptr;
}
}
}

注意: 这里用到了前面提到的标识 - PluginInterface,只有 qobject_cast 转换成功,才会加载到主系统中,这可以算作是真正意义上的第一道防线。

实际上,在内部检查是通过调用 QtPluginManagerPrivate::check() 递归地查询依赖元数据来完成的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
bool QtPluginsManagerPrivate::check(const QString &filepath)
{
for(QVariant item : m_dependencies.value(filepath))
{
QVariantMap map = item.toMap();
//依赖的插件名称、版本、路径
QVariant name = map.value("name");
QVariant version = map.value("version");
QString path = m_names.key(name);

/********** 检测插件是否依赖于其他插件 **********/
// 先检测插件名称
if(!m_names.values().contains(name))
{
QString strcons = "Missing dependency: "+ name.toString()+" for plugin "+path;
qDebug()<<Q_FUNC_INFO<<strcons;
//QMessageBox::warning(nullptr, ("Plugins Loader Error"), strcons, QMessageBox::Ok);
return false;
}
//再检测插件版本
if(m_versions.value(path) != version)
{
QString strcons = "Version mismatch: " + name.toString() +" version "+m_versions.value(m_names.key(name)).toString()+
" but " + version.toString() + " required for plugin "+path;
qDebug()<<Q_FUNC_INFO<<strcons;
//QMessageBox::warning(nullptr, "Plugins Loader Error", strcons, QMessageBox::Ok);
return false;
}
//最后检测被依赖的插件是否还依赖其他的插件
if(!check(path))
{
QString strcons = "Corrupted dependency: "+name.toString()+" for plugin "+path;
qDebug()<<Q_FUNC_INFO<<strcons;
//QMessageBox::warning(nullptr, "Plugins Loader Error", strcons, QMessageBox::Ok);
return false;
}
}

return true;
}

插件卸载的过程正好相反:

1
2
3
4
5
void QtPluginsManager::unloadAllPlugins()
{
for(QString filepath : d->m_loaders.keys())
unloadPlugin(filepath);
}

而具体的卸载由 unloadPlugin() 来完成:

1
2
3
4
5
6
7
8
9
10
11
void QtPluginsManager::unloadPlugin(const QString &filepath)
{
QPluginLoader *loader = d->m_loaders.value(filepath);
//卸载插件,并从内部数据结构中移除
if(loader->unload())
{
d->m_loaders.remove(filepath);
delete loader;
loader = nullptr;
}
}

万事俱备,然后返回所有的插件,以便主系统访问:

1
2
3
4
QList<QPluginLoader *> QtPluginsManager::allPlugins()
{
return d->m_loaders.values();
}

槽recMsgFromManager:

1
2
3
4
5
6
7
8
9
10
11
12
13
void QtPluginsManager::recMsgFromManager(PluginMetaData &msg)
{
qDebug() <<"QtPluginsManager::recMsgFromManager..."<< msg.dest;
auto loader = getPlugin(msg.dest);
if(loader)
{
auto plugin = qobject_cast<PluginInterface*>(loader->instance());;
if(plugin)
{
plugin->recMsgFromManager(msg);
}
}
}

这样,整个插件管理的机制已经建立起来了,剩下的事基本就比较简单了!插件的编写、插件之间的交互。

编写插件

插件1:GenericPlugin1,.pro、.h文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
TEMPLATE        = lib           #表示这个makefile是一个lib的makefile
CONFIG += plugin #应用程序是一个插件
TARGET = GenericPlugin1 #生成插件的名称
win32 {
CONFIG(debug, debug|release) {
DESTDIR = ../Main/debug/plugins
} else {
DESTDIR = ../Main/release/plugins
}
}#生成插件的目录

HEADERS += \
genericplugin1.h


DISTFILES += \
programmer.json #插件描述文件

msvc {
#QMAKE_CFLAGS += /utf-8
QMAKE_CXXFLAGS += /utf-8
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#ifndef GENERICPLUGIN1_H
#define GENERICPLUGIN1_H

#include <QObject>
#include <QtPlugin>
#include "../qtpluginmanager/pluginmetadata.h"
#include "../qtpluginmanager/plugininterface.h"

class GenericPlugin1 : public QObject, public PluginInterface
{
// programmer.json 插件的信息描述类
Q_OBJECT
Q_PLUGIN_METADATA(IID PluginInterface_iid FILE "programmer.json") // QT5.0 引入
Q_INTERFACES(PluginInterface)
public:
GenericPlugin1() {}
~GenericPlugin1() {}

void setInitData(QMap<QString,QVariant> &data) Q_DECL_OVERRIDE {
qDebug() << "GenericPlugin1 setInitDat..." << data;
map=&data;

#测试插件1发送信息给插件2
PluginMetaData metadata;
metadata.from = "GenericPlugin1";
metadata.dest = "GenericPlugin2";
metadata.type = 1;
emit sendMsgToManager(metadata);
}

QMap<QString,QVariant>& getResultData() Q_DECL_OVERRIDE {
qDebug() << "GenericPlugin1 getResultData...";
return *map;
}


void recMsgFromManager(PluginMetaData &msg) Q_DECL_OVERRIDE {
qDebug() << "GenericPlugin1 recMsgFromManager..." << msg.from;
}

bool connectTosendMsgToManager(QObject *receiver, const char *pszSlot, bool isConnect) const Q_DECL_OVERRIDE {
if(isConnect)
return connect(this, SIGNAL(sendMsgToManager(PluginMetaData&)), receiver, pszSlot);
else
return disconnect(this, SIGNAL(sendMsgToManager(PluginMetaData&)), receiver, pszSlot);
}

private:
QMap<QString,QVariant> *map;

signals:
void sendMsgToManager(PluginMetaData&);

};

#endif // GENERICPLUGIN1_H

插件2:GenericPlugin1,.pro、.h文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
TEMPLATE        = lib           #表示这个makefile是一个lib的makefile
CONFIG += plugin #应用程序是一个插件
TARGET = GenericPlugin2 #生成插件的名称
win32 {
CONFIG(debug, debug|release) {
DESTDIR = ../Main/debug/plugins
} else {
DESTDIR = ../Main/release/plugins
}
}#生成插件的目录

HEADERS += \
genericplugin2.h


DISTFILES += \
programmer.json #插件描述文件

msvc {
#QMAKE_CFLAGS += /utf-8
QMAKE_CXXFLAGS += /utf-8
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#ifndef GENERICPLUGIN2_H
#define GENERICPLUGIN2_H

#include <QObject>
#include <QtPlugin>
#include "../qtpluginmanager/pluginmetadata.h"
#include "../qtpluginmanager/plugininterface.h"

class GenericPlugin2 : public QObject, public PluginInterface
{
// programmer.json 插件的信息描述类
Q_OBJECT
Q_PLUGIN_METADATA(IID PluginInterface_iid FILE "programmer.json") // QT5.0 引入
Q_INTERFACES(PluginInterface)
public:
GenericPlugin2() {}
~GenericPlugin2() {}

void setInitData(QMap<QString,QVariant> &data) Q_DECL_OVERRIDE {
qDebug() << "GenericPlugin2 setInitDat..." << data;
map=&data;
}

QMap<QString,QVariant>& getResultData() Q_DECL_OVERRIDE {
qDebug() << "GenericPlugin2 getResultData...";
return *map;
}


void recMsgFromManager(PluginMetaData &msg) Q_DECL_OVERRIDE {
qDebug() << "GenericPlugin2 recMsgFromManager..." << msg.from;
}

bool connectTosendMsgToManager(QObject *receiver, const char *pszSlot, bool isConnect) const Q_DECL_OVERRIDE {
if(isConnect)
return connect(this, SIGNAL(sendMsgToManager(PluginMetaData&)), receiver, pszSlot);
else
return disconnect(this, SIGNAL(sendMsgToManager(PluginMetaData&)), receiver, pszSlot);
}

private:
QMap<QString,QVariant> *map;

signals:
void sendMsgToManager(PluginMetaData&);

};

#endif // GENERICPLUGIN2_H

测试插件通讯

测试插件1发送信息给插件2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
QtPluginsManager::instance()->loadAllPlugins();//插件管理器 加载所有插件
auto plugins=QtPluginsManager::instance()->allPlugins();
foreach(auto plugin,plugins)
{
PluginInterface *app = qobject_cast<PluginInterface*>(plugin->instance());
if (app)
{
QMap<QString,QVariant> data;
data.insert("0001",10000);
app->setInitData(data);
}
}

QtPluginsManager::instance()->unloadAllPlugins();

打印出如下信息,说明成功。

1
2
3
4
GenericPlugin2 setInitDat... QMap(("0001", QVariant(int, 10000)))
GenericPlugin1 setInitDat... QMap(("0001", QVariant(int, 10000)))
QtPluginsManager::recMsgFromManager... "GenericPlugin2"
GenericPlugin2 recMsgFromManager... "GenericPlugin1"