插件机制是一种框架,允许开发人员简单地在应用程序中添加或扩展功能。它使广泛使用,因为它可以作为模块被重复使用,并使它们更易于维护和扩展,因此它们在应用程序中非常有用。插件机制允许管理员在需要时轻松安装和卸载插件,而无需对基础应用程序做出更改。
NDD介绍
这里再介绍推荐下优秀的国产软件开源项目 NDD(notepad--)。一个支持windows/linux/mac的文本编辑器,目标是要国产替换同类软件。对比其它竞品Notepad类软件而言,优势是可以跨平台,支持linux mac操作系统。期待国人参与开源,贡献更多有意思的插件。
gitee仓库地址:https://gitee.com/cxasm/notepad--
插件的优势
基于插件的扩展性,进而实现业务模块儿的独立和解耦,增加可维护性和可扩展性。插件使得第三方开发人员可以为系统做增值和拓展工作,也可以使其他开发人员协同开发相互配合,增加新的功能而不破坏现有的核心功能。插件化还能够促进将关注点分开,保证隐藏实现细节,且可以将测试独立开来,并最具有实践意义。
比如强大的Eclipse的平台实际上就是一个所有功能都由插件提供的骨架。Eclipse IDE自身(包括UI和Java开发环境)仅仅是一系列挂在核心框架上的插件。
NDD的插件化实现,是一种很好的范例,让我们看到插件化机制的好处,可以灵活的对软件进行功能拓展,以下对NDD的插件化实现原理做下分析。
NDD插件机制分析
用C++实现插件机制的基本思路是:
一、应用程序(框架)提供出插件接口。
二、由用户或第三方实现这些接口,并编译出相应的动态库(即插件);
三、将所有插件放到某个特定目录,应用程序(框架)运行时会自动搜索该目录,并动态加载目录中的插件。
按照以上思路,分析下NDD源码中的插件机制实现。
插件接口
NDD源码中提供出来的插件接口有两个,接口声明如下:
#define NDD_EXPORT __declspec(dllexport) #ifdef __cplusplus extern "C" { #endif NDD_EXPORT bool NDD_PROC_IDENTIFY(NDD_PROC_DATA* pProcData); NDD_EXPORT int NDD_PROC_MAIN(QWidget* pNotepad, const QString& strFileName, std::function<QsciScintilla* ()>getCurEdit, NDD_PROC_DATA* procData); #ifdef __cplusplus } #endif
需要注意,插件接口必须要用extern "C"包含,因为C++的编译器会对程序中符号进行修饰,这个过程在编译器中叫符号修饰(Name Decoration)或者符号改编(Name Mangling)。如果不改为c的方式,那么动态库resolve这种查找入口方式,会找不到句柄handle入口。
以上两个接口,一个是插件的相关说明信息,一个是插件的核心功能实现。
插件实现
NDD_PROC_IDENTIFY接口最简单,就是用来让插件开发者填充插件信息用的。传进来的参数有以下信息:
struct ndd_proc_data { QString m_strPlugName; //插件名称 必选 QString m_strFilePath; //lib 插件的全局路径。必选。插件内部不用管,主程序传递下来 QString m_strComment; //插件说明 QString m_version; //版本号码。可选 QString m_auther;//作者名称。可选 int m_menuType;//菜单类型。0:不使用二级菜单 1:创建二级菜单 QMenu* m_rootMenu;//如果m_menuType = 1,给出二级根菜单的地址。其他值nullptr ndd_proc_data(): m_rootMenu(nullptr), m_menuType(0) { } }; typedef struct ndd_proc_data NDD_PROC_DATA;
bool NDD_PROC_IDENTIFY(NDD_PROC_DATA* pProcData) { if(pProcData == NULL) { return false; } pProcData->m_strPlugName = QObject::tr("Hello World Plug"); pProcData->m_strComment = QObject::tr("char to Upper."); pProcData->m_version = QString("v1.0"); pProcData->m_auther = QString("yangqq.xyz"); pProcData->m_menuType = 1; return true; }
另外一个接口是NDD_PROC_MAIN
这个是插件功能的具体实现接口,插件开发者可在此接口中实现插件的主要功能。
//插件的入口点接口实现 //则点击菜单栏按钮时,会自动调用到该插件的入口点函数接口。 //pNotepad:就是CCNotepad的主界面指针 //strFileName:当前插件DLL的全路径,如果不关心,则可以不使用 //getCurEdit:从NDD主程序传递过来的仿函数,通过该函数获取当前编辑框操作对象QsciScintilla int NDD_PROC_MAIN(QWidget* pNotepad, const QString &strFileName, std::function<QsciScintilla*()>getCurEdit, NDD_PROC_DATA* pProcData) { //对于不需要创建二级菜单的例子,pProcData总是nullptr。 //该函数每次点击插件菜单时,都会被执行。 QsciScintilla* pEdit = getCurEdit(); if (pEdit == nullptr) { return -1; } //务必拷贝一份pProcData,在外面会释放。 if (pProcData != nullptr) { s_procData = *pProcData; } s_pMainNotepad = pNotepad; s_getCurEdit = getCurEdit; //做一个简单的转大写的操作 QtTestClass* p = new QtTestClass(pNotepad,pEdit); //主窗口关闭时,子窗口也关闭。避免空指针操作 p->setWindowFlag(Qt::Window); p->show(); return 0; }
完成了以上这两个接口,编译成动态dll库,其实插件开发就完成啦。如果编译器和使用的QT库同NDD发行版一致,则直接把dll库放入plugin目录即可。接下来看下NDD应用程序是如何加载和使用插件的。
NDD插件加载过程
从ndd应用程序启动到插件加载。过程大致如下:
int main(int argc, char *argv[]) { //可以防止某些屏幕下的字体拥挤重叠问题 QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); #ifdef Q_OS_MAC MyApplication a(argc, argv); #else QApplication a(argc, argv); #endif //...... CCNotePad *pMainNotepad = new CCNotePad(true); pMainNotepad->setAttribute(Qt::WA_DeleteOnClose); pMainNotepad->setShareMem(&shared); pMainNotepad->quickshow(); a.exec(); } // //先快速让窗口展示处理,后续再去做复杂的初始化 void CCNotePad::quickshow() { //...... init_toolsMenu(); } // void CCNotePad::init_toolsMenu() { slot_dynamicLoadToolMenu(); //connect(ui.menuTools,&QMenu::aboutToShow,this,&CCNotePad::slot_dynamicLoadToolMenu); } //动态加载工具菜单项 void CCNotePad::slot_dynamicLoadToolMenu() { //...... #ifdef NO_PLUGIN //动态加载插件 m_pluginList.clear(); loadPluginLib(); #endif }
插件的加载过程在loadPluginLib()函数中,进入到plugin目录中加载插件。
#ifdef NO_PLUGIN void CCNotePad::loadPluginLib() { QString strDir = qApp->applicationDirPath(); QDir dir(strDir); if (dir.cd("./plugin")) { strDir = dir.absolutePath(); loadPluginProcs(strDir,ui.menuPlugin); } }
foundCallback回调函数接口,找到插件信息后 在onPlugFound函数中处理,完成与界面菜单的绑定。
void CCNotePad::loadPluginProcs(QString strLibDir, QMenu* pMenu) { std::function<void(NDD_PROC_DATA&, QMenu*)> foundCallBack = std::bind(&CCNotePad::onPlugFound, this, std::placeholders::_1, std::placeholders::_2); int nRet = loadProc(strLibDir, foundCallBack, pMenu); if (nRet > 0) { ui.statusBar->showMessage(tr("load plugin in dir %1 success, plugin num %2").arg(strLibDir).arg(nRet)); } }
在点击菜单后触发执行onPlugWork,如果设置的有启用二级菜单,则初始化设置二级菜单。
void CCNotePad::onPlugFound(NDD_PROC_DATA& procData, QMenu* pUserData) { QMenu* pMenu = pUserData; if (pMenu == NULL) { return; } //创建action if (procData.m_menuType == 0) { QAction* pAction = new QAction(procData.m_strPlugName, pMenu); pMenu->addAction(pAction); pAction->setText(procData.m_strPlugName); pAction->setData(procData.m_strFilePath); connect(pAction, &QAction::triggered, this, &CCNotePad::onPlugWork); } else if (procData.m_menuType == 1) { //创建二级菜单 QMenu* pluginMenu = new QMenu(procData.m_strPlugName, pMenu); pMenu->addMenu(pluginMenu); //菜单句柄通过procData传递到插件中 procData.m_rootMenu = pluginMenu; sendParaToPlugin(procData); } else { return; } // 暂存加载到的插件信息 m_pluginList.append(procData); }
//把插件需要的参数,传递到插件中去 void CCNotePad::sendParaToPlugin(NDD_PROC_DATA& procData) { QString plugPath = procData.m_strFilePath; QLibrary* pLib = new QLibrary(plugPath); NDD_PROC_MAIN_CALLBACK pMainCallBack; pMainCallBack = (NDD_PROC_MAIN_CALLBACK)pLib->resolve("NDD_PROC_MAIN"); if (pMainCallBack != NULL) { std::function<QsciScintilla* ()> foundCallBack = std::bind(&CCNotePad::getCurEditView, this); pMainCallBack(this, plugPath, foundCallBack, &procData); } else { ui.statusBar->showMessage(tr("plugin %1 load failed !").arg(plugPath), 10000); } }
//真正执行插件的工作 void CCNotePad::onPlugWork(bool check) { QAction* pAct = dynamic_cast<QAction*>(sender()); if (pAct != nullptr) { QString plugPath = pAct->data().toString(); QLibrary* pLib = new QLibrary(plugPath); NDD_PROC_MAIN_CALLBACK pMainCallBack; pMainCallBack = (NDD_PROC_MAIN_CALLBACK)pLib->resolve("NDD_PROC_MAIN"); if (pMainCallBack != NULL) { std::function<QsciScintilla* ()> foundCallBack = std::bind(&CCNotePad::getCurEditView, this); pMainCallBack(this, plugPath, foundCallBack, nullptr); } else { ui.statusBar->showMessage(tr("plugin %1 load failed !").arg(plugPath), 10000); } } }
虽然以上过程看似复杂一点儿,其实关键调用就是拿到函数指针,然后根据需要做些处理。插件信息存储在QList<NDD_PROC_DATA> m_pluginList。有个界面对这个信息进行展示。
void CCNotePad::slot_pluginMgr() { #ifdef NO_PLUGIN PluginMgr* pWin = new PluginMgr(this, m_pluginList); pWin->setAttribute(Qt::WA_DeleteOnClose); pWin->show(); #else QMessageBox::warning(this, "info", u8"便携版本不支持插件,请下载插件版!"); #endif }
为防止中文乱码,支持中文的方法是文件编码保存为utf-8格式。 输入汉字如上写法,u8"中文字符"。编译脚本指定如下:
# win下需要开启UNICODE进行支持TCHAR if(CMAKE_HOST_WIN32) add_definitions(-D_UNICODE -DUNICODE) endif()
plugin机制的关键,既定义函数指针,拿到函数指针,使用函数指针。
typedef bool (*NDD_PROC_IDENTIFY_CALLBACK)(NDD_PROC_DATA* pProcData); typedef void (*NDD_PROC_FOUND_CALLBACK)(NDD_PROC_DATA* pProcData, void* pUserData);
#include "plugin.h" #include <QLibrary> #include <QDir> #include <QMenu> #include <QAction> bool loadApplication(const QString& strFileName, NDD_PROC_DATA* pProcData) { QLibrary lib(strFileName); NDD_PROC_IDENTIFY_CALLBACK procCallBack; procCallBack = (NDD_PROC_IDENTIFY_CALLBACK)lib.resolve("NDD_PROC_IDENTIFY"); if (procCallBack == NULL) { return false; } if (!procCallBack(pProcData)) { return false; } pProcData->m_strFilePath = strFileName; return true; } int loadProc(const QString& strDirOut, std::function<void(NDD_PROC_DATA&, QMenu*)> funcallback, QMenu* pUserData) { int nReturn = 0; QStringList list; QDir dir; dir.setPath(strDirOut); QString strDir, strName; QStringList strFilter; strDir = dir.absolutePath(); strDir += QDir::separator(); #if defined(Q_OS_WIN) strFilter << "*.dll"; #else strFilter << "lib*.so"; #endif list = dir.entryList(strFilter, QDir::Files | QDir::Readable, QDir::Name); QStringList::Iterator it = list.begin(); for (; it != list.end(); ++it) { NDD_PROC_DATA procData; strName = *it; strName = strDir + strName; if (!loadApplication(strName, &procData)) { continue; } funcallback(procData, pUserData); nReturn++; } return nReturn; }