目錄
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 <QtPlugin>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 <interfaces.h>#include <QRect>
#include <QObject>
#include <QtPlugin>
#include <QStringList>
#include <QPainterPath>
#include <QImage>//! [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]// BrushInterfaceQStringList 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;// ShapeInterfaceQStringList shapes() const override;QPainterPath generateShape(const QString &shape, QWidget *parent) override;// FilterInterfaceQStringList 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 <QtMath>
#include <QtWidgets>#include <stdlib.h>//! [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 < numSteps; ++i) {int x = oldPos.x() + i * (newPos.x() - oldPos.x()) / (numSteps - 1);int y = oldPos.y() + i * (newPos.y() - oldPos.y()) / (numSteps - 1);painter.drawEllipse(x - (thickness / 2), y - (thickness / 2),thickness, thickness);}} else if (brush == tr("Random Letters")) {QChar ch(QRandomGenerator::global()->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 < 5; ++i) {path.lineTo(50 + 40 * std::cos(0.8 * i * M_PI),50 + 40 * std::sin(0.8 * i * M_PI));}path.closeSubpath();} else if (shape == tr("Text...")) {QString text = QInputDialog::getText(parent, tr("Text Shape"),tr("Enter text:"),QLineEdit::Normal, tr("Qt"));if (!text.isEmpty()) {QFont timesFont("Times", 50);timesFont.setStyleStrategy(QFont::ForceOutline);path.addText(0, 0, timesFont, text);}}return path;
}
//! [6]//! [7]
QStringList BasicToolsPlugin::filters() const
{return {tr("Invert Pixels"), tr("Swap RGB"), tr("Grayscale")};
}
//! [7]//! [8]
QImage BasicToolsPlugin::filterImage(const QString &filter, const QImage &image,QWidget * /* parent */)
{QImage result = image.convertToFormat(QImage::Format_RGB32);if (filter == tr("Invert Pixels")) {result.invertPixels();} else if (filter == tr("Swap RGB")) {result = result.rgbSwapped();} else if (filter == tr("Grayscale")) {for (int y = 0; y < result.height(); ++y) {for (int x = 0; x < result.width(); ++x) {QRgb pixel = result.pixel(x, y);int gray = qGray(pixel);int alpha = qAlpha(pixel);result.setPixel(x, y, qRgba(gray, gray, gray, alpha));}}}return result;
}
//! [8]
到此為止,一個插件的實現就已經完成了。下面來看一看插件加載的過程。
4.插件的加載
4.1.靜態插件
4.1.1.靜態插件實現方式
? ? ? ??靜態插件可以把下面這個宏插入到插件應用程序的源代碼中:
Q_IMPORT_PLUGIN(qjpeg)
? ? ? ? 或在pro文件中配置項目為靜態插件:
TEMPLATE = libCONFIG += plugin static
????????在構建應用程序時,靜態插件也必須包含在鏈接器中。對于Qt預定義的插件,您可以使用QTPLUGIN來添加所需的插件到您的構建中。例如:
TEMPLATE = appQTPLUGIN += 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) heldby 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 < numPlugins; ++i)instances += plugins->at(i).instance();}return instances;
}/*!Returns a list of QStaticPlugins held by the pluginloader. The function is similar to \l staticInstances()with the addition that a QStaticPlugin also containsmeta data information.\sa staticInstances()
*/
QVector<QStaticPlugin> QPluginLoader::staticPlugins()
{StaticPluginList *plugins = staticPluginList();if (plugins)return *plugins;return QVector<QStaticPlugin>();
}typedef QVector<QStaticPlugin> StaticPluginList;
Q_GLOBAL_STATIC(StaticPluginList, staticPluginList)
從以上代碼可以看出:
1)首先定義了全局的向量集合StaticPluginList來存儲所有的插件信息
2)然后,插件靜態加載的過程中,需要要把自己的插件信息注冊上來,這個也是最常見的設計手法,于是Qt庫提供了注冊的函數:
/*!\relates QPluginLoader\since 5.0Registers the \a plugin specified with the plugin loader, and is usedby 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)<QT_PREPEND_NAMESPACE(QObject)> _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<const char *>(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()<<"load failed ";}QObject* plugin = loader.instance();if(plugin) {BrushInterface* interface = qobject_cast<BrushInterface*>(plugin);if(interface) {//...}}else {qDebug()<<"loader.instance failed!";return QString();}
}
QPluginLoader的load過程如下:
bool QPluginLoader::load()
{if (!d || d->fileName.isEmpty())return false;if (did_load)return d->pHnd && d->instance;if (!d->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() << "QLibraryPrivate::loadPlugin failed on" << fileName << ":" << errorString;pluginState = IsNotAPlugin;return false;
}
上述代碼的核心在:
1) load() 為加載動態庫,window和linux實現方式不同,分別調用各自系統的API實現,windows一般調用LoadLibrary實現,linux一般調用dlopen實現,很具體的請自行查閱資料。
2)找到函數qt_plugin_instance的地址,并保存此地址,這個就是實例化插件對象的函數。
那么qt_plugin_instance是在那里定義的呢?自然會聯想到QT_MOC_EXPORT_PLUGIN:
#define Q_PLUGIN_INSTANCE(IMPLEMENTATION) \{ \static QT_PREPEND_NAMESPACE(QPointer)<QT_PREPEND_NAMESPACE(QObject)> _instance; \if (!_instance) { \QT_PLUGIN_RESOURCE_INIT \_instance = new IMPLEMENTATION; \} \return _instance; \}# define QT_MOC_EXPORT_PLUGIN(PLUGINCLASS, PLUGINCLASSNAME) \Q_EXTERN_C Q_DECL_EXPORT \const char *qt_plugin_query_metadata() \{ return reinterpret_cast<const char *>(qt_pluginMetaData); } \Q_EXTERN_C Q_DECL_EXPORT QT_PREPEND_NAMESPACE(QObject) *qt_plugin_instance() \Q_PLUGIN_INSTANCE(PLUGINCLASS)
在QT_MOC_EXPORT_PLUGIN里面就有qt_plugin_instance函數。
5.定位插件
????????Qt 應用程序將會自動感知可用的插件,因為插件都被存儲在標準的子目錄當中。因此應用程序不需要任何查找或者加載插件的代碼。
????????在開發過程中,插件的目錄是 QTDIR/plugins(QTDIR 是 Qt 的安裝目錄),每個類型的插件放在相應類型的目錄下面。如果想要應用程序使用插件,但不想用標準的插件存放路徑,可以在應用程序的安裝過程中指定要使用的插件的路徑,可以使用 QSettings,保存插件路徑,在應用程序運行時讀取配置文件。應用程序可以通過QCoreApplication::addLibraryPath()
函數將指定的插件路徑加載到應用程序中。
????????使插件可加載的一種方法是在應用程序所在目錄創建一個子目錄,用于存放插件。如果要發布和 Qt 一起發布的插件(存放在 plugins 目錄)中的任何插件,必須拷貝 plugins 目錄下的插件子目錄到應用程序的根目錄下。?
6.插件開發的優勢
- 方便功能擴展:通過插件,可以輕松地擴展應用程序的功能,而不需要修改應用程序本身。
- 更新量小:當底層接口不變時,只需要更新插件即可,而不需要重新發布整個應用程序。
- 降低模塊間依賴:插件與主程序之間通過接口交互,降低了模塊間的依賴關系,支持并行開發。
- 面向未來:通過插件,可以進一步演化API的功能,使API在長時間內保持可用性和適用性。
7.總結
????????Qt插件機制是一種強大且靈活的功能擴展方式,它允許開發者通過創建和加載插件來增強Qt應用程序的功能。通過遵循一定的規范,開發者可以輕松地創建和使用插件,從而滿足各種復雜和多變的需求。