如何在Qt项目中创建插件
一、插件概念
插件(Plug-in,又称addin、add-in、addon或add-on,又译外挂)是一种遵循一定规范的应用程序接口编写出来的程序。其只能运行在程序规定的系统平台下(可能同时支持多个平台),而不能脱离指定的平台单独运行。因为插件需要调用原纯净系统提供的函数库或者数据。很多软件都有插件,插件有无数种。例如在IE中,安装相关的插件后,WEB浏览器能够直接调用插件程序,用于处理特定类型的文件。
插件都是关于接口的,以插件为基础的系统,其基本概念是:系统可以加载插件,但它不知道任何东西,并且通过一组定义良好的接口和协议与它们进行通信。
优点
其实插件的优点也是常说的设计模式的设计原则,比如易扩展、低耦合、热更新、面向接口等。
一个大型项目的开发离不开插件化,可以让整个框架结构更加清晰和容易理解,比如说一个该项目经常会针对不同客户做功能定制,或者对于软件使用的不同场景,功能有所区别,那这时候插件就变得非常有用了,主工程中包含所有功能模块的调用,但是如果某些功能如果不需要,那最终程序打包只要不把插件的dll打包进去就OK了,程序依然可以正常运行,只是该插件的功能无法使用而已。
这样,对于多功能模块的情况下,如果不同版本仅需要其中几项功能,就可以不用像动态链接库那样,全部dll都包含进去,从而也节省了安装包的空间。
二、插件框架
插件框架要素
要实现一个插件框架,需要考虑以下要素:
- 如何注册插件
- 如何调用插件
- 如何测试插件 :框架要支持自动化测试:包括单元测试,集成测试。
- 插件的生命周期管理
插件的生命周期由插件框架控制,需要考虑以下问题:
- 插件的生命周期如何转换?
- 一旦插件的生命周期发生转变,引用此插件的类是否能得到通知。
- 插件的管理和维护
对于插件框架而言,这属于基础功能。主要包括:
- 为插件提供名称、版本、状态等信息,并可以获取插件列表,记录插件的处理日志等。
- 提供插件加载、启动、停止、卸载等功能。
- 插件的组装(附加考评要素) 插件的组装是指可以灵活的将多个插件组装为一条链,然后链式的调用。
- 插件的出错处理 当插件在处理过程中发生错误时,最理想的结果是插件的调用停止,并记录相关的日志,另外的插件对此情况做出纠错处理(注意:不能影响插件框架和其他插件的正常运转)。
插件系统的构成
插件系统,可以分为三部分:
主系统
通过插件管理器加载插件,并创建插件对象。一旦插件对象被创建,主系统就会获得相应的指针/引用,它可以像任何其他对象一样使用。
插件管理器
用于管理插件的生命周期,并将其暴露给主系统。它负责查找并加载插件,初始化它们,并且能够进行卸载。它还应该让主系统迭代加载的插件或注册的插件对象。
插件
插件本身应符合插件管理器协议,并提供符合主系统期望的对象。
实际上,很少能看到这样一个相对独立的分离,插件管理器通常与主系统紧密耦合,因为插件管理器需要最终提供(定制)某些类型的插件对象的实例。
程序流
框架的基本程序流,如下所示:
三、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})
- 声明一个插件类,该类继承自QObject和该插件想要提供的接口。
- 使用Q_INTERFACES()宏告诉Qt的元对象系统有关接口的信息。
- 使用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" : []
}
- 使用合适的 .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);
};