Qt 插件机制使用及原理
目录
1.引言
2.插件原理
3.插件实现
3.1.定义一个接口集(只有纯虚函数的类)
3.2.实现接口
4.插件的加载
4.1.静态插件
4.1.1.静态插件实现方式
4.1.2.静态插件加载的过程
4.1.3.示例
4.2.动态插件
4.2.1.动态插件的加载过程
5.定位插件
6.插件开发的优势
7.总结
1.引言
在设计大型软件时,插件式开发都会被考虑到。无论在普通的桌面软件还是大型的游戏软件,都可以看到插件的身影。例如著名的Qt Creator 系统开发软件都用插件架构。插件系统最大的功能是在一定程度内提高了软件的灵活度和可扩展性。一个设计精良插件系统甚至可以在宿主软件不退出的情况下加入新的插件,实现热插拔的功能。这一点在我的博客C++架构设计中也有涉及:
C++架构设计-CSDN博客
那么,Qt的插件系统是怎么实现的呢?Qt 提供了两种API用于创建插件:一种是高阶 API,用于扩展 Qt 本身的功能,如自定义数据库驱动,图像格式,文本编码,自定义样式等;一种是低阶 API,用于扩展 Qt 应用程序。本文主要是通过低阶 API 来创建 Qt 插件,并通过静态、动态两种方式来调用插件。
2.插件原理
在C++ 中,插件一般以动态库的显示加载方式提供。利用C++多态的原理,在程序中首先声明一个插件的interface。该interface 只需要实现构造和析构函数,所有用到的功能函数都先定义为虚函数,然后在插件中实现该interface 的具体接口。那么当插件创建的时候,把插件中的子类对象赋值到宿主程序中的基类对象interface。即可实现不同的插件实现不同的功能。
3.插件实现
3.1.定义一个接口集(只有纯虚函数的类)
interfaces.h
#ifndef INTERFACES_H #define INTERFACES_H #include QT_BEGIN_NAMESPACE class QImage; class QPainter; class QWidget; class QPainterPath; class QPoint; class QRect; class QString; class QStringList; QT_END_NAMESPACE //! [0] class BrushInterface { public: virtual ~BrushInterface() {} virtual QStringList brushes() const = 0; virtual QRect mousePress(const QString &brush, QPainter &painter, const QPoint &pos) = 0; virtual QRect mouseMove(const QString &brush, QPainter &painter, const QPoint &oldPos, const QPoint &newPos) = 0; virtual QRect mouseRelease(const QString &brush, QPainter &painter, const QPoint &pos) = 0; }; //! [0] //! [1] class ShapeInterface { public: virtual ~ShapeInterface() {} virtual QStringList shapes() const = 0; virtual QPainterPath generateShape(const QString &shape, QWidget *parent) = 0; }; //! [1] //! [2] class FilterInterface { public: virtual ~FilterInterface() {} virtual QStringList filters() const = 0; virtual QImage filterImage(const QString &filter, const QImage &image, QWidget *parent) = 0; }; //! [2] QT_BEGIN_NAMESPACE //! [3] //! [4] #define BrushInterface_iid "org.qt-project.Qt.Examples.PlugAndPaint.BrushInterface/1.0" Q_DECLARE_INTERFACE(BrushInterface, BrushInterface_iid) //! [3] #define ShapeInterface_iid "org.qt-project.Qt.Examples.PlugAndPaint.ShapeInterface/1.0" Q_DECLARE_INTERFACE(ShapeInterface, ShapeInterface_iid) //! [5] #define FilterInterface_iid "org.qt-project.Qt.Examples.PlugAndPaint.FilterInterface/1.0" Q_DECLARE_INTERFACE(FilterInterface, FilterInterface_iid) //! [4] //! [5] QT_END_NAMESPACE #endif
创建一个BrushInterface,ShapeInterface,FilterInterface基类,直接可以把它定义为纯虚接口。和普通的多态不同的是,在interfece 中我们需要定义 iid。iid 可以理解为插件的一个标识或者ID。在加载插件的过程中会对IID 进行判断,如果插件中的IID 和 interface 中的IID 不匹配,那么该插件就不会被加载。Q_DECLARE_INTERFACE把IID 和类名进行绑定,这也是必须的。作用是导出一些可以通过 定义了接口ID查找函数和几个QObject到接口的转换函数。
宏Q_DECLARE_INTERFACE导入名为PluginName的插件,它与Q_PLUGIN_METADATA()为插件声明元数据的类的名称相对应。
3.2.实现接口
basictoolsplugin.h
#ifndef BASICTOOLSPLUGIN_H #define BASICTOOLSPLUGIN_H //! [0] #include #include #include #include #include #include #include //! [1] class BasicToolsPlugin : public QObject, public BrushInterface, public ShapeInterface, public FilterInterface { Q_OBJECT //! [4] Q_PLUGIN_METADATA(IID "org.qt-project.Qt.Examples.PlugAndPaint.BrushInterface" FILE "basictools.json") //! [4] Q_INTERFACES(BrushInterface ShapeInterface FilterInterface) //! [0] //! [2] public: //! [1] // BrushInterface QStringList brushes() const override; QRect mousePress(const QString &brush, QPainter &painter, const QPoint &pos) override; QRect mouseMove(const QString &brush, QPainter &painter, const QPoint &oldPos, const QPoint &newPos) override; QRect mouseRelease(const QString &brush, QPainter &painter, const QPoint &pos) override; // ShapeInterface QStringList shapes() const override; QPainterPath generateShape(const QString &shape, QWidget *parent) override; // FilterInterface QStringList filters() const override; QImage filterImage(const QString &filter, const QImage &image, QWidget *parent) override; //! [3] }; //! [2] //! [3] #endif
实现interface 也是和多态一样,只是多了两个宏 Q_PLUGIN_METADATA和Q_INTERFACES。前面讲到了,因为宿主程序是不知道插件的名字的,所以如何让插件的子类在宿主程序中实例化是插件系统的关键,在Qt 中 该功能由Q_PLUGIN_METADATA 完成。
跟踪源码发现Q_PLUGIN_METADATA的定义如下:
#define Q_PLUGIN_METADATA(x) QT_ANNOTATE_CLASS(qt_plugin_metadata, x) # ifndef Q_COMPILER_VARIADIC_MACROS # define QT_ANNOTATE_CLASS(type, x) # else # define QT_ANNOTATE_CLASS(type, ...) # endif #endif
发现根本没干什么,那么该宏应该在编译的时候由元对象系统Moc解析。在moc 解析的时候将 Q_PLUGIN_METADATA 转化为QT_MOC_EXPORT_PLUGIN 宏并插入到代码中。我们在编译后生成的" moc_basictoolsplugin.cpp" 文件中发现了此宏:
翻看Qt的源码(5.12.12版本)找到了该宏返回了插件的元对象数据以及 qt_plugin_instance 函数的实现,该函数的函数体Q_PLUGIN_INSTANCE 定义,函数中的 _instance = new IMPLEMENTATION; 也就是插件的对象,然后通过QObject 指针 _instance 的形式返回(代码如下)。上层通过该方式(见插件的加载过程),获取插件的子类对象赋值给基类,实现多态的调用。
下面是插件的具体实现代码:basictoolsplugin.cpp
#include "basictoolsplugin.h" #include #include #include //! [0] QStringList BasicToolsPlugin::brushes() const { return {tr("Pencil"), tr("Air Brush"), tr("Random Letters")}; } //! [0] //! [1] QRect BasicToolsPlugin::mousePress(const QString &brush, QPainter &painter, const QPoint &pos) { return mouseMove(brush, painter, pos, pos); } //! [1] //! [2] QRect BasicToolsPlugin::mouseMove(const QString &brush, QPainter &painter, const QPoint &oldPos, const QPoint &newPos) { painter.save(); int rad = painter.pen().width() / 2; QRect boundingRect = QRect(oldPos, newPos).normalized() .adjusted(-rad, -rad, +rad, +rad); QColor color = painter.pen().color(); int thickness = painter.pen().width(); QColor transparentColor(color.red(), color.green(), color.blue(), 0); //! [2] //! [3] if (brush == tr("Pencil")) { painter.drawLine(oldPos, newPos); } else if (brush == tr("Air Brush")) { int numSteps = 2 + (newPos - oldPos).manhattanLength() / 2; painter.setBrush(QBrush(color, Qt::Dense6Pattern)); painter.setPen(Qt::NoPen); for (int i = 0; i bounded('A', 'Z' + 1)); QFont biggerFont = painter.font(); biggerFont.setBold(true); biggerFont.setPointSize(biggerFont.pointSize() + thickness); painter.setFont(biggerFont); painter.drawText(newPos, QString(ch)); QFontMetrics metrics(painter.font()); boundingRect = metrics.boundingRect(ch); boundingRect.translate(newPos); boundingRect.adjust(-10, -10, +10, +10); } painter.restore(); return boundingRect; } //! [3] //! [4] QRect BasicToolsPlugin::mouseRelease(const QString & /* brush */, QPainter & /* painter */, const QPoint & /* pos */) { return QRect(0, 0, 0, 0); } //! [4] //! [5] QStringList BasicToolsPlugin::shapes() const { return {tr("Circle"), tr("Star"), tr("Text...")}; } //! [5] //! [6] QPainterPath BasicToolsPlugin::generateShape(const QString &shape, QWidget *parent) { QPainterPath path; if (shape == tr("Circle")) { path.addEllipse(0, 0, 50, 50); } else if (shape == tr("Star")) { path.moveTo(90, 50); for (int i = 1; i到此为止,一个插件的实现就已经完成了。下面来看一看插件加载的过程。
4.插件的加载
4.1.静态插件
4.1.1.静态插件实现方式
静态插件可以把下面这个宏插入到插件应用程序的源代码中:
Q_IMPORT_PLUGIN(qjpeg)或在pro文件中配置项目为静态插件:
TEMPLATE = lib CONFIG += plugin static在构建应用程序时,静态插件也必须包含在链接器中。对于Qt预定义的插件,您可以使用QTPLUGIN来添加所需的插件到您的构建中。例如:
TEMPLATE = app QTPLUGIN += qjpeg qgif # image formats静态插件的加载比较简单,直接使用 QPluginLoader::staticInstances() 加载,代码如下:
void PluginDialog::findPlugins(const QString &path, const QStringList &fileNames) { label->setText(tr("Plug & Paint found the following plugins\n" "(looked in %1):") .arg(QDir::toNativeSeparators(path))); const QDir dir(path); const auto staticInstances = QPluginLoader::staticInstances(); for (QObject *plugin : staticInstances) populateTreeWidget(plugin, tr("%1 (Static Plugin)") .arg(plugin->metaObject()->className())); for (const QString &fileName : fileNames) { QPluginLoader loader(dir.absoluteFilePath(fileName)); QObject *plugin = loader.instance(); if (plugin) populateTreeWidget(plugin, fileName); } }4.1.2.静态插件加载的过程
QPluginLoader::staticInstances()为什么就可以加载所有插件呢?翻看QPluginLoader的源码:
/*! Returns a list of static plugin instances (root components) held by the plugin loader. \sa staticPlugins() */ QObjectList QPluginLoader::staticInstances() { QObjectList instances; const StaticPluginList *plugins = staticPluginList(); if (plugins) { const int numPlugins = plugins->size(); instances.reserve(numPlugins); for (int i = 0; i at(i).instance(); } return instances; } /*! Returns a list of QStaticPlugins held by the plugin loader. The function is similar to \l staticInstances() with the addition that a QStaticPlugin also contains meta data information. \sa staticInstances() */ QVector QPluginLoader::staticPlugins() { StaticPluginList *plugins = staticPluginList(); if (plugins) return *plugins; return QVector(); } typedef QVector StaticPluginList; Q_GLOBAL_STATIC(StaticPluginList, staticPluginList)从以上代码可以看出:
1)首先定义了全局的向量集合StaticPluginList来存储所有的插件信息
2)然后,插件静态加载的过程中,需要要把自己的插件信息注册上来,这个也是最常见的设计手法,于是Qt库提供了注册的函数:
/*! \relates QPluginLoader \since 5.0 Registers the \a plugin specified with the plugin loader, and is used by Q_IMPORT_PLUGIN(). */ void Q_CORE_EXPORT qRegisterStaticPluginFunction(QStaticPlugin plugin) { staticPluginList()->append(plugin); }3)看到注册函数,估计你要恍然大悟了吧,为什么在4.1.1节中要使用Q_IMPORT_PLUGIN来声明静态插件,肯定是要调用qRegisterStaticPluginFunction注册自己:
#define Q_IMPORT_PLUGIN(PLUGIN) \ extern const QT_PREPEND_NAMESPACE(QStaticPlugin) qt_static_plugin_##PLUGIN(); \ class Static##PLUGIN##PluginInstance{ \ public: \ Static##PLUGIN##PluginInstance() { \ qRegisterStaticPluginFunction(qt_static_plugin_##PLUGIN()); \ } \ }; \ static Static##PLUGIN##PluginInstance static##PLUGIN##Instance;果然,定义全局变量static##PLUGIN##Instance,在构造函数中调用了注册函数,注册的对象正是宏QT_MOC_EXPORT_PLUGIN定义的QStaticPlugin,下面看一下QT_MOC_EXPORT_PLUGIN
4)QT_MOC_EXPORT_PLUGIN定义
#define Q_PLUGIN_INSTANCE(IMPLEMENTATION) \ { \ static QT_PREPEND_NAMESPACE(QPointer) _instance; \ if (!_instance) { \ QT_PLUGIN_RESOURCE_INIT \ _instance = new IMPLEMENTATION; \ } \ return _instance; \ } # define QT_MOC_EXPORT_PLUGIN(PLUGINCLASS, PLUGINCLASSNAME) \ static QT_PREPEND_NAMESPACE(QObject) *qt_plugin_instance_##PLUGINCLASSNAME() \ Q_PLUGIN_INSTANCE(PLUGINCLASS) \ static const char *qt_plugin_query_metadata_##PLUGINCLASSNAME() { return reinterpret_cast(qt_pluginMetaData); } \ const QT_PREPEND_NAMESPACE(QStaticPlugin) qt_static_plugin_##PLUGINCLASSNAME() { \ QT_PREPEND_NAMESPACE(QStaticPlugin) plugin = { qt_plugin_instance_##PLUGINCLASSNAME, qt_plugin_query_metadata_##PLUGINCLASSNAME}; \ return plugin; \ }定义了生成插件对象的全局函数和QtPluginMetaDataFunction,并返回了QStaticPlugin:
struct Q_CORE_EXPORT QStaticPlugin { //... // Since qdoc gets confused by the use of function // pointers, we add these dummes for it to parse instead: QObject *instance(); const char *rawMetaData(); //... };4.1.3.示例
运行上面的实例,显示出全部的插件信息,如下:
4.2.动态插件
动态插件一般以动态库的方式来加载的。
4.2.1.动态插件的加载过程
在Qt 中加载这些插件的流程比较复杂的,我们自己写的时候就简单多了。流程大体相似,首先定义QPluginLoader 对象。QPluginLoader 的构造函数加载插件目录下所有插件,然后调用 instance 创建插件对象,最后就可以调用插件功能了。
QString loadPlugin(QString pluginPath) { QPluginLoader loader(pluginPath); if(! loader.load()) { qDebug()isPlugin()) return false; did_load = true; return d->loadPlugin(); } bool QLibraryPrivate::loadPlugin() { if (instance) { libraryUnloadCount.ref(); return true; } if (pluginState == IsNotAPlugin) return false; if (load()) { instance = (QtPluginInstanceFunction)resolve("qt_plugin_instance"); return instance; } if (qt_debug_component()) qWarning()