文章目录
- 一、插件概念
- 优点
- 二、插件框架
- 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. 插件框架要素
要实现一个插件框架,需要考虑以下要素:
- 如何注册插件
- 如何调用插件
- 如何测试插件 :框架要支持自动化测试:包括单元测试,集成测试。
- 插件的生命周期管理
插件的生命周期由插件框架控制,需要考虑以下问题:
- 插件的生命周期如何转换?
- 一旦插件的生命周期发生转变,引用此插件的类是否能得到通知。
- 插件的管理和维护
-对于插件框架而言,这属于基础功能。主要包括:
- 为插件提供名称、版本、状态等信息,并可以获取插件列表,记录插件的处理日志等。
- 提供插件加载、启动、停止、卸载等功能。
- 插件的组装(附加考评要素)
插件的组装是指可以灵活的将多个插件组装为一条链,然后链式的调用。 - 插件的出错处理
当插件在处理过程中发生错误时,最理想的结果是插件的调用停止,并记录相关的日志,另外的插件对此情况做出纠错处理(注意:不能影响插件框架和其他插件的正常运转)。
2. 插件系统的构成
插件系统,可以分为三部分:
主系统
通过插件管理器加载插件,并创建插件对象。一旦插件对象被创建,主系统就会获得相应的指针/引用,它可以像任何其他对象一样使用。
插件管理器
用于管理插件的生命周期,并将其暴露给主系统。它负责查找并加载插件,初始化它们,并且能够进行卸载。它还应该让主系统迭代加载的插件或注册的插件对象。
插件
插件本身应符合插件管理器协议,并提供符合主系统期望的对象。
实际上,很少能看到这样一个相对独立的分离,插件管理器通常与主系统紧密耦合,因为插件管理器需要最终提供(定制)某些类型的插件对象的实例。
程序流
框架的基本程序流,如下所示:
二、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 通过插件使应用程序可扩展包括以下步骤:
- 定义一组用于与插件对话的接口(只有纯虚拟函数的类)。
- 使用Q_DECLARE_INTERFACE()宏告诉Qt的元对象系统有关该接口的信息。
- 在应用程序中使用QPluginLoader加载插件。
- 使用qobject_cast()测试插件是否实现了给定的接口。
2.3 编写插件包括以下步骤:
- 声明一个插件类,该类继承自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);
};
2.4 正确的插件框架系统
推荐的插件系统应该是下面的工程结构
TEMPLATE = subdirs
SUBDIRS += \
MainApp \
plugin1 \
plugin2 \
plugin3 \
...
一个最简单的完整的实例
官方实例:C:\Qt\Qt5.14.2\Examples\Qt-5.14.2\widgets\tools\echoplugin
整体结构如图:
接口类的定义 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]
效果:
参考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);//转发给对应插件
}
}
}