【大话QT之十六】使用ctkPluginFramework插件系统构建项目实战
"使用ctkPluginFramework插件系统构建项目实战",这篇文章是写博客以来最纠结的一篇文章。
倒不是由于技术都多么困难,而是想去描写叙述一个项目架构採用ctkPluginFramework来构建总是未尽其意,描写叙述的太少未免词不达意。描写叙述的太多又显得太啰嗦。
有些看过之前写的【大话QT之四】ctkPlugin插件系统实现项目插件式开发这篇文章的朋友也想了解一下究竟假设从零開始架构一个项目。
在写这篇文章的时候又回头总结了下我之前觉得已经懂了的东西,发现还是好多东西没有真正明确其原理是什么,在文章以下的描写叙述中,对构建项目中的关键流程给出实现过程。关于总体细节之处大家能够看样例中的代码。
一、准备阶段
由于本次想实现的项目架构就是基于ctkPluginFramework的。因此開始之前须要:
1) Windows下安装VS2010和QT环境。
2) CTK编译好的CTKCore.dll、CTKPluginFramework.dll以及相关的头文件。
3) 了解QT中基本插件的制作与载入方式。
二、实战阶段
2.1 项目架构分析
每个中型或大型的项目,在实际开发之前,其代码组织架构一定会经过细致的规划。比方:文件夹架构(头文件放在哪个文件夹;lib库放在哪个文件夹;开发源码放在哪个文件夹;生成的项目插件放在哪个文件夹;终于公布程序放在哪个文件夹)。如今,如果我们要为图书馆开发一个图书管理程序。称之为:LibraryProject,然后在该文件夹下有:includes,libs。plugins。bin。application等文件夹。其基本结构例如以下:
其基本用途例如以下:applications用于放置项目载入程序的源代码;bin下包括conf、plugins文件夹。bin下存放项目入口exe程序以及独立执行时依赖的dll文件。bin/conf下存放项目配置文件,bin/plugins下放置项目中全部使用到自己开发的插件;includes文件夹用于放置头文件,包括第三方头文件以及项目中自定义的接口文件等;libs用于放置项目中使用的第三方开源库的lib文件;plugins用于放置项目中全部的插件开发源代码;而上图中的cuc_base.pri是用来定义或载入项目中通用的内容,比方:INCLUDEPATH在该文件里定义则在其他插件子项目中仅仅要include一下这个文件。就能够使用includes中包括的全部头文件。
接下来说一下项目总体运作流程。首先,项目是基于插件开发。因而项目中全部的功能模块都是以dll插件的形式提供。插件大致分为两种类型。即界面插件和功能插件,界面插件主要用来完毕界面显示,以及界面上的操作流程,功能插件主要用来提供某一方面的功能。以Ftpclient工具为例,其操作界面就能够採用一个界面插件来实现,而其本地文件系统数据的提供、文件上传下载等功能的实现就能够使用功能插件来提供。其次,尽管项目全部界面显示以及功能实现都採用dll插件来提供。难道整个项目就是一堆dll吗?并不是如此,一个项目总归须要一个启动入口,即传统意义上的exe文件。它既能够复杂到实现全部的功能。也能够简单到仅仅负责载入一个逻辑插件执行就能够了,就好像上程序设计课时老师提及的一句话,main()函数是一定存在的。在本实现中即须要一个这种最简化的假设程序exe来通过某种方式调用到主逻辑插件,然后開始执行。
在实际动手前我们再总结一下前面提到的几个关键模块:项目启动程序、完毕插件注冊的插件、功能插件、主逻辑插件以及它们之间的相互关系:
从简单来说,整个项目的执行就如上如所看到的。读配置。注冊插件,载入插件;从复杂来说。在实现上远没有那么简答。总之在使用过程中会碰上各种各样的问题。仅仅有在重复的使用过程中才干不断了解,对出现故障时才干迅速地排查。
2.2 构建项目启动程序
以下,我们就来逐步构建这个项目。首先。项目启动程序终于生成的是exe文件,它是项目的主入口,其主要功能是:1) 通过QPluginLoader的方式来载入 “用于注冊插件的插件”。2) 利用 “用于注冊插件的插件”来执行主逻辑插件。
关键代码例如以下:
QSettings settings(strConfFile, QSettings::IniFormat); settings.setIniCodec(QTextCodec::codecForName("UTF-8")); QString strPluginLoader = settings.value(CUC_PLUGIN_LOADER).toString(); QString strPluginPortal = settings.value(CUC_PLUGIN_PORTAL).toString(); //!载入controller插件(用于载入其他插件) QString strPluginLoaderPath = QString(CUC_PLUGIN_PATH).arg(qApp->applicationDirPath()) + strPluginLoader + QString(".dll"); QString strPluginPortalPath = QString(CUC_PLUGIN_PATH).arg(qApp->applicationDirPath()) + strPluginPortal + QString(".dll"); if (!QFile(strPluginLoaderPath).exists()) { qDebug() << "[Error] Controller Plugin does not exists ..."; return CUC_FAILED; } QPluginLoader PluginLoader(strPluginLoaderPath); CUControllerInterface *Controller = qobject_cast<CUControllerInterface *>(PluginLoader.instance()); if (!Controller) { qCritical() << QObject::tr("The required module (%1) is invalid, the application will quit.(%2)").arg(strPluginLoader).arg(PluginLoader.errorString()); return CUC_FAILED; } else { CUCParameters Parameters; Parameters[CUC_KEY_CONF_FILE] = strConfFile; Parameters[CUC_KEY_PLUGIN_PATH] = strPluginPath; Parameters[CUC_KEY_MAIN_HANDLE] = qVariantFromValue((void *)&CUC); if (Controller->Init(Parameters) != CUC_SUCCESS) { return CUC_FAILED; } Controller->LoadAllPlugin(strPluginPath, settings.value(CUC_PLUGIN_EXCLUDE).toString()); if (Controller->ExecutePlugin(strPluginPortalPath, Parameters) != CUC_SUCCESS) { return CUC_FAILED; } }代码中,Init()、LoadAllPlugin()以及ExecutePlugin()函数均是在“用于注冊插件”的插件中提供的,详细实现请看2.3。
此外,在以上代码调试过程中碰到一个问题:使用QPluginLoader载入插件时。load()一直返回false。而errorstring()提示的为“找不到指定模块”,关于这个问题的解决方法请看第三部分。
2.3 编写第一个用于注冊插件的插件
本部分開始介绍。第一个用于“注冊插件的”插件。请注意,这个插件是传统意义上的插件,它还不算真正的ctkPlugin的插件。由于它不具有声明周期。
2.4会介绍一个真正的ctkPlugin的插件怎样定义。
有经验的开发者都会这样想。插件既然作为基于ctkPluginFramework构建的项目中的模块单元,应该会一直用到,那么全部的这些插件应该存在一些共性的东西,因此,我们应该定义一个统一的接口来约束这样的规范。
这里,我们实现的定义接口的规则为:首先定义一个全部插件必须实现的接口,当然能够在实现里不去做事情,这里如果我们的通用插件接口文件为:cuc_base_interface。其次。在每一个插件的实现中。我们定义插件的接口。插件中的接口类继承自通用插件接口。并在此基础上定义自己插件对外服务的功能。接下来会通过关键代码来展示应怎样定义这样一个插件:
1> 基础插件类,用于定义全部插件必须实现的接口。
/* * 定义基础插件的接口 */ class CUCBaseInterface { public: virtual int Init(const CUCParameters &Parameters) = 0; virtual int Uninit() = 0; virtual int CreateInstance(const CUCParameters &Parameters) = 0; virtual int DestoryInstance() = 0; virtual int Launch(const CUCParameters &Parameters) = 0; virtual int Close() = 0; //!插件更新接口 virtual void Upgrade() = 0; }; Q_DECLARE_INTERFACE(CUCBaseInterface, "com.cuc.base")
2> 插件接口类。继承自基础插件类,并定义自己对外的服务接口。
class CUControllerInterface: public CUCBaseInterface { public: /* * 接口说明:完毕全部插件的载入工作 * 參数说明:strPluginPath: 插件文件所在路径 * strFilter: 插件过滤条件 */ virtual void LoadAllPlugin(const QString &strPluginPath, const QString &strFilter) = 0; /* * 接口说明:安装插件 * 參数说明:strPlugin:插件文件绝对路径 */ virtual int InstallPlugin(const QString &strPlugin) = 0; /* * 接口说明:执行插件 * 參数说明:strPlugin:插件文件绝对路径 * Parameters: 插件载入相关參数说明 */ virtual int ExecutePlugin(const QString &strPlugin, const CUCParameters &Parameters) = 0; }; Q_DECLARE_INTERFACE(CUControllerInterface, "com.cuc.controller")
3> 插件类。继承自插件接口类,实现全部的接口。这里仅仅对关键实现进行说明。
1) Init()实现ctkPluginFramework的初始化。
int CUController::Init(const CUCParameters &Parameters) { if (m_bInit) { return CUC_SUCCESS; } m_Parameters = Parameters; //!初始化CTKPluginFramework系统 m_PluginFramework = m_FrameworkFactory.getFramework(); try { m_PluginFramework->init(); m_PluginFramework->start(); qDebug() << "[Info] ctkPluginFramework start ..."; } catch(const ctkPluginException &Exception) { qCritical()<<QObject::tr("Failed to initialize the plug-in framework: ")<<Exception.what(); return CUC_FAILED; } m_bInit = true; return CUC_SUCCESS; }
2) InstallPlugin()实现插件载入。
int CUController::InstallPlugin(const QString &strPlugin) { QString strPluginName = GetPluginNameWithVersion(strPlugin); //!假设插件已经载入则直接返回 if (m_mapPlugins.contains(strPluginName)) { return CUC_SUCCESS; } try { QSharedPointer<ctkPlugin> Plugin = m_PluginFramework->getPluginContext()->installPlugin(QUrl::fromLocalFile(strPlugin)); Plugin->start(ctkPlugin::START_TRANSIENT); m_strPluginLog += QObject::tr("%1 (%2) is loaded.\r\n").arg(Plugin->getSymbolicName()).arg(Plugin->getVersion().toString()); } catch(const ctkPluginException &Exc) { m_strPluginLog += QObject::tr("Failed to load %1: ctkPluginException(%2).\r\n").arg(strPlugin).arg(Exc.what()); qDebug() << m_strPluginLog; return CUC_FAILED; } catch(const std::exception &E) { m_strPluginLog += QObject::tr("Failed to load %1: std::exception(%2).\r\n").arg(strPlugin).arg(E.what()); qDebug() << m_strPluginLog; return CUC_FAILED; } catch(...) { m_strPluginLog += QObject::tr("Failed to load %1: Unknown error.\r\n").arg(strPlugin); qDebug() << m_strPluginLog; return CUC_UNKNOW; } return CUC_SUCCESS; }
3) ExecutePlugin()实现主插件执行。
int CUController::ExecutePlugin(const QString &strPlugin, const CUCParameters &Parameters) { if (!m_bInit) { return CUC_NOT_INIT; } QString strPluginName = GetPluginNameWithVersion(strPlugin); CUCBaseInterface *Base = 0 ; if (!m_mapPlugins.contains(strPluginName)) { QPluginLoader PluginLoader(strPlugin); PluginLoader.load(); Base = qobject_cast<CUCBaseInterface *>(PluginLoader.instance()); } else { Base = qobject_cast<CUCBaseInterface *>(m_mapPlugins.value(strPluginName)); } if (Base) { m_Parameters[CUC_KEY_CONTROLLER_HANDLE] = qVariantFromValue((void *)this); m_Parameters[CUC_KEY_PLGUIN_CONTEXT] = qVariantFromValue((void *)m_PluginFramework->getPluginContext()); Base->Init(m_Parameters); Base->CreateInstance(m_Parameters); if (Base->Launch(m_Parameters) != CUC_SUCCESS) { qDebug() << QObject::tr("Failed to launch (%1).").arg(strPlugin); return CUC_FAILED; } } else { qDebug() << QObject::tr("The portal (%1) is invalid.").arg(strPlugin); return CUC_FAILED; } }
2.4 编写第一个功能插件
将要实现的这个插件是一个真正的ctkPluginFramework的插件。结合【大话QT之四】ctkPlugin插件系统实现项目插件式开发 以及例如以下的图形象地展示了一个插件的生命周期,由start開始,向系统注冊能够提供服务;由stop结束。终止对外提供服务;由uninstall终止,在ctkPluginFramework系统中移除。
因此,在2.3介绍的插件实现的基础上(即也遵循每一个插件有自己的插件接口类。它继承自通用插件接口,并由插件类继承实现)。每一个ctk的插件都应该通过定义Plugin插件类来提供start和stop接口,实现自身插件的声明周期,以下来看一下插件的Plugin类是怎样定义实现的:
1> 插件类头文件,例如以下所看到的,该类继承自ctkPluginActivator,用于实现自己的声明周期。
#include <ctkPluginActivator.h> class CUHelloWorld; class CUHelloWorldPlugin : public QObject, public ctkPluginActivator { Q_OBJECT Q_INTERFACES(ctkPluginActivator) public: void start(ctkPluginContext *Context); void stop(ctkPluginContext *Context); private: CUHelloWorld *m_HelloWorld; };
2> 插件类的实现,例如以下所看到的。在start()阶段通过:registerService()向ctkPluginFramework系统进行注冊;在stop阶段删除指针。
#include "cuc_helloworld_plugin.h" #include <QtPlugin> #include "cuc_helloworld.h" void CUHelloWorldPlugin::start(ctkPluginContext *Context) { m_HelloWorld = new CUHelloWorld; Context->registerService(QStringList("CUHelloWorldInterface"), m_HelloWorld); } void CUHelloWorldPlugin::stop(ctkPluginContext *Context) { if (m_HelloWorld) { delete m_HelloWorld; m_HelloWorld = 0; } } Q_EXPORT_PLUGIN2(com_cuc_helloworld, CUHelloWorldPlugin)这个插件介绍完之后。相信关于ctk的插件怎样定义就能有一个大概的了解,仅仅有真正去写一个插件才干了解它是怎样实现的。其他的插件都大同小异,仅仅是自己实现的接口不通。
鉴于上面插件注冊的实现,这里关于插件怎样引用只是多说了,关键代码例如以下:
3> 使用ctkPluginFramework中的插件
int CULibraryManager::LoadUsedPlugins() { try { //!初始化HelloWorld插件 ctkServiceReference refHelloWorld = m_PluginContext->getServiceReference("CUHelloWorldInterface"); m_HelloWorld = (qobject_cast<CUHelloWorldInterface *>(m_PluginContext->getService(refHelloWorld))); if (!m_HelloWorld || m_HelloWorld->Init(m_Parameters) != CUC_SUCCESS) { qDebug()<<QObject::tr("Module %1 is invalid").arg("com.cuc.helloworld"); return CUC_FAILED; } } catch(...) { return CUC_FAILED; } return CUC_SUCCESS; }通过getService获取到插件指针,就行使用它提供的服务了。
三、解决的问题
1. QPluginLoader在load插件com.cuc.controller插件的时候一直无法成功,返回false。
可能出现的问题。见相关链接:http://stackoverflow.com/questions/17920303/strange-error-error-loading-plugin-the-specified-module-could-not-be-found 它提出了几点可能会出现的问题,逐个排查就可以。
假设你确定你写的插件格式没有问题。那么非常有可能是插件存在其他依赖。依赖的dll没有放进来,我就是这个问题。利用小工具:Dependency Walker能够查看一个dll依赖的其他dll文件。
2. 为插件文件加入插件具体信息。
一般,dll插件文件都有自己的具体信息,即在右键属性具体信息里面。里面会对插件的一些具体信息进行说明。要实现这样必须定义一个资源文件,相关參考连接例如以下,这里就不具体描写叙述了:http://msdn.microsoft.com/en-us/library/aa381058%28v=VS.85%29.aspx
四、总结
这篇文章拖了好久。一是由于写论文占了好大部分时间。二是在自己写样例的过程中又遇到了非常多问题。花费了非常多的时间。这里我想突出的重点是怎样从一个项目的角度来设计它的实现,诚然会写ctk的插件是前提,但不是重点。站在项目的角度会有一览众山小的感觉。不要拘泥于某个详细的东西。我这里有实现源代码。假设感兴趣的能够在以下留下邮箱。我会发送到你的邮箱中。或者发站内信也能够。
千里之行始于足下,不要好高骛远!。加油。