Qt中开发一个插件式程序的简单探索

在前一篇博客c++中的反射机制与插件式编程中实现了c++中反射机制的“简化版本”,现在更进一步地尝试在Qt中做一个插件系统的demo,也算是自己下一步的一个试探。

1.生成反射机制的运行库

按照c++中的反射机制与插件式编程博客中的方法可以实现c++中的反射机制,为了更方便使用,将其编译成动态链接库。

  1. 在QT Creator中新建项目,选择c++库项目
    在这里插入图片描述
  2. 设置项目名称为Reflex
    在这里插入图片描述
  3. 接下来全部默认,生成如下文件结构。其中reflex_global.h为Qt生成c++库项目默认带的一个头文件,里面进行一些条件编译等,不必管它。
    在这里插入图片描述
  4. 代码大体与之前博客的一样,稍作如下更改:
//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类,因为前者在使用宏CONSTRUCTORREGISTER_REFLEX时会用到,不导出的话会出现链接错误。像静态变量m_classInfoMap没有导出,在外部是不可直接使用该变量的。
  • Factory类添加了GetClassNames静态方法,这样可以获取所有注册了反射机制的类的名称
  • 相应的源文件reflex.cpp没有改动(可能需要去掉#include "stdafx.h"指令,如果是使用VS编写的话,该文件是VS中默认带的但是Qt中不需要)

2.编写插件基类

这里以一个简单的案例来说明——比如对两个数执行某种操作得到结果(输入数与结果都是double型),执行的操作就是未来的插件需要实现的,所以这里如下定义接口:

  • 插件有一个getDescription函数,用于获取该插件信息
  • 插件有一个run函数,执行插件的功能
  • 插件有一个getResult函数,获取执行操作后的结果
  1. 上一节一样,新建一个插件的c++库,
  2. 右击项目,选择添加库,链接外部库,选择上一步生成的Reflex.lib,下一步,完成(添加外部链接库与VS中添加项目的依赖项是对应的)。
    在这里插入图片描述
    链接外部库在这里插入图片描述
  3. 添加了外部链接库可以在.pro项目文件中看到自动进行了一些配置,其中CONFIG的那几行配置相当于指定库文件(即.lib.dll的路径与文件名);INCLUDEPATH为包含目录添加(对应VS中的添加包含目录),这个要确认其包含目录的正确性,可能自动添加的目录会有错误;DEPENDPATH相当于VS中的库目录,这一句可以去掉无妨。不进行这一步的配置也可以,那样需要把上一步生成的libdll,与.h文件复制到当前目录下也是可以的。
    在这里插入图片描述
  4. 添加了外部库之后需要重新构建项目以及重新执行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,过程简单,不再细述,设计如下界面:
在这里插入图片描述
第一个文本框用于输入第一个参数,第二个输入第二个参数,第三个用于输出结果,分别命名为parameter1parameter2result;下拉框用于选择进行的操作(下拉框上显示的是插件名);等号按钮点击时执行操作输出结果。

  • 在界面类中定义私有变量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++库项目,添加ReflexIPlugin的外部链接库及其头文件,这里只需要头文件及相应的库来定义自己的插件;不需要主程序源代码,也不需要重新编译主程序,完全就是即插即用式。
  • 然后更改头文件与源文件如下:
    注意以下头文件两个宏的使用,第一个是生成一致格式的构造函数(这个是其他文件生成该类对象的关键),这个宏最后放在底端或者publicprivateprotected等关键字上面,因为这个宏自带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;
}
  • 编译插件,将生成的dlllib文件放在./plugins目录下,甚至都不要头文件,运行主程序即可发现插件“挂”上去了,功能也实现了。效果如下:
    在这里插入图片描述

5.小结

这个示例麻雀虽小,五脏俱全,基本的思路方法都涉及到了。通过插件式编程,主程序只要设计良好的扩展接口,剩下的更多的功能可以交由插件去实现;实现插件时只需专注自己的任务,不需管主程序,无需重新编译主程序,设计好了插件直接把它放到指定文件夹下即可。而且在更新插件功能时只需把插件文件夹下的动态链接库更新即可,可以做到程序的部分更新。总之,我认为插件式的编程是一个比较好的开发范式。
补充项目源码
链接:百度网盘下载 提取码:9fht
项目目录结构:

  • PluginTest
    • Reflex——反射机制
    • PluginDll——插件接口
    • UsePlugins——使用插件的主程序
      • plugins——放置插件目录,主程序为debug版则放置debug版dll;否则应放置release版
    • AddPlugin——加法插件
    • MinusPlugin——减法插件
    • MultiPlugin——乘法插件
    • DividePlugin——除法插件
发布了28 篇原创文章 · 获赞 14 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/liyunxin_c_language/article/details/97004437