在前一篇博客c++中的反射机制与插件式编程中实现了c++中反射机制的“简化版本”,现在更进一步地尝试在Qt中做一个插件系统的demo,也算是自己下一步的一个试探。
1.生成反射机制的运行库
按照c++中的反射机制与插件式编程博客中的方法可以实现c++中的反射机制,为了更方便使用,将其编译成动态链接库。
- 在QT Creator中新建项目,选择c++库项目
- 设置项目名称为Reflex
- 接下来全部默认,生成如下文件结构。其中
reflex_global.h
为Qt生成c++库项目默认带的一个头文件,里面进行一些条件编译等,不必管它。
- 代码大体与之前博客的一样,稍作如下更改:
//reflex.h
#ifndef REFLEX_H
#define REFLEX_H
#include "reflex_global.h"
#include <string>
#include <map>
#include <vector>
#define CONSTRUCTOR(class_name)\
public:\
static ReflexBase* CreateObject()\
{\
return new class_name;\
}\
protected:\
static ClassInfo m_classInfo;
#define REGISTER_REFLEX(class_name)\
ClassInfo class_name::m_classInfo(#class_name,class_name::CreateObject);
class REFLEXSHARED_EXPORT ClassInfo;
class REFLEXSHARED_EXPORT ReflexBase;
static std::map<std::string, ClassInfo*> *m_classInfoMap;
class REFLEXSHARED_EXPORT Factory
{
public:
Factory() {}
virtual ~Factory() {}
static bool Register(ClassInfo *pCInfo);
static std::vector<std::string>* GetClassNames();
static ReflexBase* CreateObject(std::string className);
static ClassInfo* GetClassInfo(std::string className);
};
typedef ReflexBase* (*objConstructorFun)();
class ClassInfo
{
public:
ClassInfo(const std::string className, objConstructorFun classConstructor) :
m_className(className), m_objConstructor(classConstructor)
{
Factory::Register(this);
}
virtual ~ClassInfo() {}
ReflexBase* CreateObject()const { return m_objConstructor ? (*m_objConstructor)() : NULL; }
bool IsDynamic()const { return NULL != m_objConstructor; }
const std::string GetClassName()const { return this->m_className; }
objConstructorFun GetConstructor()const { return this->m_objConstructor; }
private:
std::string m_className;
objConstructorFun m_objConstructor;
};
class ReflexBase
{
public:
ReflexBase() {}
virtual ~ReflexBase() {}
};
#endif // REFLEX_H
改动主要如下:
- 主要是在一些类前面加上了宏
REFLEXSHARED_EXPORT
,这个宏的作用是将文件中的类、函数导出,这样导出的类、函数对将来使用该项目生成的dll文件的人来说才可见,否则是无法使用的。比如这里除了导出了Factory
类还导出了ClassInfo
类与ReflexBase
类,因为前者在使用宏CONSTRUCTOR
与REGISTER_REFLEX
时会用到,不导出的话会出现链接错误。像静态变量m_classInfoMap
没有导出,在外部是不可直接使用该变量的。 - 给
Factory
类添加了GetClassNames
静态方法,这样可以获取所有注册了反射机制的类的名称 - 相应的源文件
reflex.cpp
没有改动(可能需要去掉#include "stdafx.h"
指令,如果是使用VS编写的话,该文件是VS中默认带的但是Qt中不需要)
2.编写插件基类
这里以一个简单的案例来说明——比如对两个数执行某种操作得到结果(输入数与结果都是double
型),执行的操作就是未来的插件需要实现的,所以这里如下定义接口:
- 插件有一个
getDescription
函数,用于获取该插件信息 - 插件有一个
run
函数,执行插件的功能 - 插件有一个
getResult
函数,获取执行操作后的结果
- 像上一节一样,新建一个插件的c++库,
- 右击项目,选择添加库,链接外部库,选择上一步生成的
Reflex.lib
,下一步,完成(添加外部链接库与VS中添加项目的依赖项是对应的)。
- 添加了外部链接库可以在
.pro
项目文件中看到自动进行了一些配置,其中CONFIG
的那几行配置相当于指定库文件(即.lib
与.dll
的路径与文件名);INCLUDEPATH
为包含目录添加(对应VS中的添加包含目录),这个要确认其包含目录的正确性,可能自动添加的目录会有错误;DEPENDPATH
相当于VS中的库目录,这一句可以去掉无妨。不进行这一步的配置也可以,那样需要把上一步生成的lib
,dll
,与.h
文件复制到当前目录下也是可以的。
- 添加了外部库之后需要重新构建项目以及重新执行qmake;在头文件中添加
reflex.h
头文件的包含等,如下定义IPlugin
接口类:
class PLUGINDLLSHARED_EXPORT IPlugin:public ReflexBase
{
public:
IPlugin();
virtual ~IPlugin();
virtual QString getDescription();
virtual void run(double a,double b);
virtual double getResult();
};
注意:
- 将要要被插件覆盖的方法要使用
virtual
关键字声明成虚函数,这是运行时多态的关键,这个很明显。(当然更进一步声明成纯虚函数也可以) - 继承自
ReflexBase
类,实现反射机制。这里基类接口IPlugin
没有注册反射,因为这个类一般与主程序一起写成,一起编译运行即可,不需要通过反射机制来在运行时根据类名字符串构造实例。
3.编写主程序
这里先不编写插件,就像真实情况下一样。这里先编写主程序,在Qt中新建一个Qt Widget Application,过程简单,不再细述,设计如下界面:
第一个文本框用于输入第一个参数,第二个输入第二个参数,第三个用于输出结果,分别命名为parameter1
,parameter2
,result
;下拉框用于选择进行的操作(下拉框上显示的是插件名);等号按钮点击时执行操作输出结果。
- 在界面类中定义私有变量
std::map<std::string,IPlugin*>* plugins;
以保存加载的插件生成的实例。(这里用到了IPlugin
以及后面会用到Factory
类的方法,所以要添加相应头文件与外部链接库,方法与上面的大同小异) - 在界面类初始化的时候,读取当前工作空间下
plugins
文件下所有dll
文件并加载,这些dll
即插件,以后会生成。界面构造函数如下:
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
plugins=new std::map<std::string,IPlugin*>();
loadPlugins();
qDebug()<<QString("共有%1个类").arg(plugins->size());
}
加载插件的函数,可以看出这里完全不知道这些用户定义的插件会是叫什么,只知道其继承自IPlugin
,动态库dll文件放在./plugins
文件夹下。
void MainWindow::loadPlugins()
{
QString path="./plugins";
QDir dir(path);
QStringList nameFilters;
nameFilters<<"*.dll";
QStringList files=dir.entryList(nameFilters,QDir::Files|QDir::Readable,QDir::Name);
for(auto f_iter=files.begin();f_iter!=files.end();++f_iter){
QLibrary *lib=new QLibrary("./plugins/"+(*f_iter));
lib->load();
qDebug()<<*f_iter;
delete lib;
}
std::vector<std::string> *cls=Factory::GetClassNames();
for(auto c_iter=cls->begin();c_iter!=cls->end();++c_iter){
IPlugin *plg=(IPlugin*)(Factory::CreateObject(*c_iter));
plugins->insert(std::map<std::string,IPlugin*>::value_type(*c_iter,plg));
ui->pluginIndex->addItem(QString::fromStdString(*c_iter));
}
}
- 写回调函数,下拉框选项变了执行的操作,以及等号按钮单击执行的操作:
//按钮点击
void MainWindow::on_calculate_clicked()
{
double a=ui->parameter1->text().toDouble();
double b=ui->parameter2->text().toDouble();
IPlugin *current=(*plugins)[ui->pluginIndex->currentText().toStdString()];
current->run(a,b);
ui->result->setText(QString::number(current->getResult(),10,5));
}
//下拉框选项变化
void MainWindow::on_pluginIndex_currentIndexChanged(int index)
{
qDebug()<<ui->pluginIndex->currentText()<<" has been selected...";
}
到此主界面大功告成,这一步完成之后主程序完全可以运行,只是现在没有“挂上”插件,所以没什么功能,下拉框拉没有选项。
4.制作插件
前面的主程序取了一个比较简单的例子,没有定义插件就没啥功能,这个开发者太懒了就开发了个框架,所有插件都等着用户自己定义实现。
现在这里定义四个插件,分别执行加减乘除操作。定义插件的方式也很简单:
- 首先,一样的是新建c++库项目,添加
Reflex
与IPlugin
的外部链接库及其头文件,这里只需要头文件及相应的库来定义自己的插件;不需要主程序源代码,也不需要重新编译主程序,完全就是即插即用式。 - 然后更改头文件与源文件如下:
注意以下头文件两个宏的使用,第一个是生成一致格式的构造函数(这个是其他文件生成该类对象的关键),这个宏最后放在底端或者public
,private
,protected
等关键字上面,因为这个宏自带protected
关键字,免得把紧跟这个宏的下一行变量/方法也变成了protected
属性了;第二个宏是用于注册该类的反射信息。
#ifndef ADDPLUGIN_H
#define ADDPLUGIN_H
#include "addplugin_global.h"
#include <plugindll.h>
#include <reflex.h>
class ADDPLUGINSHARED_EXPORT AddPlugin:public IPlugin
{
public:
AddPlugin();
~AddPlugin();
QString getDescription();
void run(double a,double b);
double getResult();
CONSTRUCTOR(AddPlugin)
private:
double m_result;
};
REGISTER_REFLEX(AddPlugin)
#endif // ADDPLUGIN_H
源文件执行相应的操作,其他几个插件类似,无非是把加法运算变成减法、乘法、除法,改一些输出信息,不再赘述了。
#include "addplugin.h"
#include <qdebug.h>
AddPlugin::AddPlugin()
{
qDebug("add plugin constructed");
}
AddPlugin::~AddPlugin()
{
qDebug("add plugin destroyed");
}
QString AddPlugin::getDescription()
{
return QString("add plugin");
}
void AddPlugin::run(double a, double b)
{
this->m_result=a+b;
}
double AddPlugin::getResult()
{
return this->m_result;
}
- 编译插件,将生成的
dll
与lib
文件放在./plugins
目录下,甚至都不要头文件,运行主程序即可发现插件“挂”上去了,功能也实现了。效果如下:
5.小结
这个示例麻雀虽小,五脏俱全,基本的思路方法都涉及到了。通过插件式编程,主程序只要设计良好的扩展接口,剩下的更多的功能可以交由插件去实现;实现插件时只需专注自己的任务,不需管主程序,无需重新编译主程序,设计好了插件直接把它放到指定文件夹下即可。而且在更新插件功能时只需把插件文件夹下的动态链接库更新即可,可以做到程序的部分更新。总之,我认为插件式的编程是一个比较好的开发范式。
补充项目源码
链接:百度网盘下载 提取码:9fht
项目目录结构:
- PluginTest
- Reflex——反射机制
- PluginDll——插件接口
- UsePlugins——使用插件的主程序
- plugins——放置插件目录,主程序为debug版则放置debug版dll;否则应放置release版
- AddPlugin——加法插件
- MinusPlugin——减法插件
- MultiPlugin——乘法插件
- DividePlugin——除法插件