之前做了一个电压箱控制界面程序,这个界面程序需要用作控制两个不同型号的电压箱,之前的做法是写出两个设备类,然后每次让用户选择生成某一个设备的对象,然后进行操作。这次的做法是把两个设备写成插件形式,在程序运行开始的时候用户选择不同的插件加载,再运行控制程序。这种做法是住代码可以不做过多修改,如果控制逻辑不变的话,如果再多出一个设备,我可以把新的设备写成插件,然后放进程序中让用户来进行选择。目前有两种方法来实现设备类的插件,现在我主要记录一下我实现的第一种做法。写的比较乱,只求自己能看懂。
无视我类或者文件的命名,此项目只是练习。再这个项目里,我把两个设备做成两个不同的项目里,然后生成连个不同的lib。user_interface部分是我以后不需要修改的界面及其控制逻辑。
devices.pro文件如下
TEMPLATE = subdirs
SUBDIRS = precharge \
floodcontrol \
floolcontrol.pro文件如下:
QT -= gui
QT +=core serialport
TARGET = floodcontrol
TEMPLATE = lib
CONFIG +=plugin
DEFINES += FLOODCONTROL_LIBRARY
SOURCES += floodcontrol.cpp \
device.cpp \
floodcontrolplugin.cpp
HEADERS += floodcontrol.h\
device.h \
INCLUDEPATH += ../../user_interface
DESTDIR = ../../plugins
TEMPLATE设置为lib,CONFIG设置为plugin,DEFINES参数在我的项目中没什么用,自动生成留下的,我也没删除,INCLUDEPATH的设置是因为我把接口(interface)放在了user_interface的项目中,DESTDIR的设置是因为我想把生成的库集中放在一个地址然后调用库的程序找起来比较方便。
device文件是定义基类ABSTRACTDEVICE用的,对于第一种不用插件的做法,代码没有做任何修改。floodcontrol文件是定义ABSTRACTDEVICE的子类的文件,抽象设备子类即FLOODCONTROL类是其中一个电压箱设备,FLOODCONTROL是设备型号名字,请无视。代码和没有用插件的做法是一样的,也没有做任何修改。关键部分在下面的floodcontrol.cpp里:
#include "device_interface.h"
#include "floodcontrol.h"
class Interface : public QObject,public DeviceInterface
{
Q_OBJECT
Q_INTERFACES( DeviceInterface )
Q_PLUGIN_METADATA(IID "deviceinterface")
public:
~Interface(){}
AbstractDevice *currentDevice(QObject *obj) //设置parent用的
{
return new FloodControl(obj);
}
};
#include "floodcontrolplugin.moc"
可以看到,这个Interface类继承了QObject类和我定义的DeviceInterface类。因为我要用Qt里自带的方便的宏,可以看到我声明了Q_OBJECT,Q_INTERFACES(DeviceInterface) 以及 Q_PLUGIN_METADATA(IID "deviceinterface"),具体用法可以去看Qt的帮助文档,这个METADATA里面的IID可以定义生随便什么内容,“1”也可以,这个是作为调用库的程序来查找插件的一个比较方便的东西,当然一些别的METADATA也可以定义在.json文件里。
为了方便,我把DeviceInterface类的代码贴在这里,这个DeviceInterface类我是放在user_interface的头文件里面的,其实放在哪里都无所谓,我是看到Qt自带例子,paint&plugin放在这里我才放在这里的。
但是在这里,我有的实现有一个内存泄漏问题,因为我的这句 return new FloodControl(obj)生成了一个对象,返回一个指针,但是一般原则我在哪里new,我就要解决在哪里delete,这种情况下我似乎没法回收这个对象的内存。。。。。。,我会尝试这在这个类里加一个成员,看看可行与否
device_interface.h:
#ifndef DEVICE_INTERFACE
#define DEVICE_INTERFACE
#include <QtPlugin>
#include "global.h"
#include "device.h"
class DeviceInterface{
public:
virtual ~DeviceInterface(){}
virtual AbstractDevice* currentDevice(QObject *obj) = 0;
};
Q_DECLARE_INTERFACE(DeviceInterface, "deviceinterface")
#endif // DEVICE_INTERFACE
Q_DECLARE_INTERFACE(DeviceInterface, "deviceinterface")
关于dll动态链接库如何定义一个“外部可访问的接口”问题可以查看之前博客,作为新手我不理解背后工作的原理,但是描述了我所知道的大概的步骤。
从上面代码可以看到我的Interface类实现了这些虚函数。
这个设计方式是dbzhang800老师教我的方法,很有意思的地方是,我这个动态库的“外部可访问接口”其实就是Interface类,也就是QObject与DeviceInterface的子类,而我的抽象设备类,FLOODCONTROL类在外部程序看来是隐藏的,或者说不能直接访问的,此接口,返回了一个当前设备的指针,相当于做了一个中间层的作用。为什么我要这么做呢?可以参照Qt自带的plug&paint的例子,他的接口设计也是一大堆纯虚函数,然后在DLL项目中实现这一大堆纯虚函数,他的INTERFACE类和我的一样不是任何一个东西的子类。那为什么我不把DEVICE里面的函数都拿出来,写成纯虚函数来做成接口呢?因为我要用QObject给我带来的一个便利的东西,connect还有其他一些事件的东西,SIGNAL和SLOT我不知道如何把他变成纯虚函数啊。。。。这些东西都会出现在moc_某某.cpp里,我还未能掌握如何运用这个文件。所以这种做法,我可以在调用库的主程序里得到一个从DLL文件里返回来的设备对象,然后其中的信号和槽我都可以用。
precharge这个设备的做法和上面基本上是一模一样,主要是设备类的事件有变,接口的设计完全一样,都是返回一个指针。
如何制作这个项目的插件大概讲完了,下面记录一下如何使用这两个插件,由上面看到,我们生成了两个DLL文件,每个DLL文件代表了一个设备插件,接下来要设计一种方式来让用户选择调用哪个插件,根据plugin&paint的例子稍作修改,我们可以做一个对话框,上面可以显示查找到的插件,然后让用户在对话框上进行选择,选择完毕后,主程序加载被选择的插件。我的整个设计过程实际上围绕了plugin&paint的例子,当然肯定有更加聪明的设计方式。
主要做法如下:
在mainwindow.cpp的mainwindow类的构造函数里:
getPluginDir();
getPluginFileNames();
QTimer::singleShot(500, this, SLOT(popPluginDialog()));
我做了如下工作,第一个我需要得到我所存插件的路径,记得上面我设置DESTDIR让生成的DLL文件在某个文件夹下,getPluginDir()函数就是我找这个文件夹的函数。
getPluginFileNames()函数是我如何找到我制作的插件文件的,也就是生成的那两个DLL,查找方式是根据IID或者其他METADATA(),宏里设置的或者json文件里设置的。在这个简单的例子里我只需要通过查找IID为“deviceinterface"关键字的文件。然后我有个信号槽让我查找后,弹出一个选择插件的窗口,来让用户进行选择。
void MainWindow::popPluginDialog()
{
PluginDialog *dialog=new PluginDialog (pluginsDir.path(), pluginFileNames, this);
connect(dialog,SIGNAL(pluginInfo(QString )),this,SLOT(loadPlugins(QString)));
dialog->exec();
}
void MainWindow::getPluginFileNames()
{
foreach(QString fileName, pluginsDir.entryList(QDir::Files)){
QPluginLoader loader(pluginsDir.absoluteFilePath(fileName));
qDebug()<<pluginsDir.absoluteFilePath(fileName);
qDebug()<<loader.metaData();
if(loader.metaData().value("IID").toString()==QString("deviceinterface"))
{
pluginFileNames+=fileName;
}
}
}
void MainWindow::getPluginDir()
{
pluginsDir = QDir(qApp->applicationDirPath());
#if defined(Q_OS_WIN)
if (pluginsDir.dirName().toLower() == "debug" || pluginsDir.dirName().toLower() == "release")
pluginsDir.cdUp();
pluginsDir.cdUp();
#endif
pluginsDir.cd("plugins");
}
我用qDebug()来看看我的路径是否正确,以及看看METADATA里都存了什么东西,DEBUG后可以看到METADATA里是有内容的,例如{"IID" "deviceinterface"}等等其他信息。我用了一个判断来查找包含这个信息的文件,然后我把文件名存在一个QSTRINGLIST里留作之后选择窗口的使用。
popPluginDialog()我基本照抄plugin&paint的例子做了一些修改。
下面看看弹出窗口我简单的实现方式。
plugindialog.h:
#ifndef PLUGINDIALOG
#define PLUGINDIALOG
#include <QDialog>
#include <QIcon>
#include <QLabel>
#include <QListWidget>
#include <QPushButton>
class PluginDialog : public QDialog
{
Q_OBJECT
public:
PluginDialog(const QString &path, const QStringList &fileNames,
QWidget *parent = 0);
signals:
void pluginInfo(QString );
private slots:
void choosePlugin();
private:
void findPlugins(const QString &path, const QStringList &fileNames);
void populateListWidget(QObject *plugin, const QString &text);
QLabel *label;
QListWidget *list;
QPushButton *okButton;
};
#endif // PLUGINDIALOG
一个QDialog子类,没什么说的,label随便显示点什么信息,例子里是显示插件路径,listwidget显示插件文件名,按键让用户进行选择。
plugindialog.cpp:
#include "device_interface.h"
#include "plugindialog.h"
#include <QPluginLoader>
#include <QStringList>
#include <QDir>
#include <QLabel>
#include <QGridLayout>
#include <QPushButton>
#include <QTreeWidget>
#include <QTreeWidgetItem>
#include <QHeaderView>
PluginDialog::PluginDialog(const QString &path, const QStringList &fileNames,
QWidget *parent) :
QDialog(parent),
label(new QLabel),
list(new QListWidget),
okButton(new QPushButton(tr("OK")))
{
okButton->setDefault(true);
connect(okButton,SIGNAL(clicked()),this,SLOT(choosePlugin()));
connect(okButton, SIGNAL(clicked()), this, SLOT(close()));
QGridLayout *mainLayout = new QGridLayout;
mainLayout->setColumnStretch(0, 1);
mainLayout->setColumnStretch(2, 1);
mainLayout->addWidget(label, 0, 0, 1, 3);
mainLayout->addWidget(list, 1, 0, 1, 3);
mainLayout->addWidget(okButton, 2, 1);
setLayout(mainLayout);
setWindowTitle(tr("Plugin Information"));
findPlugins(path, fileNames);
}
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)));
foreach (QString fileName, fileNames) {
list->addItem(fileName.left(fileName.size()-4));
}
}
void PluginDialog::choosePlugin()
{
if(list->count()!=0){
emit pluginInfo(list->currentItem()->text());
}
else
return;
}
从mainwindow里得到了所有需要插件名字的文件名,我把他们加载到listwidget里面,然后当用户点击确定按钮的时候,窗口关闭,并且传给主窗口一个QString的信号。这个信号其实也就是文件名,然后mainwindow里面就可以根据用户选择文件名,来加载插件了。
我的pluginInfo(QString)信号链接了一个主窗口loadplugin(QString)槽
void MainWindow::loadPlugins(QString info)
{
if(info=="floodcontrol")
{
pluginFileName=info+".dll";
}
else if(info=="precharge")
{
pluginFileName=info+".dll";
}
else
{
return;
}
}
这个槽我其实就是把DLL后缀加回来让它变成一个完整的文件名。
在我需要使用这个插件的时候,mainwindow.cpp里面的代码如下:
QPluginLoader loader(pluginsDir.absoluteFilePath(pluginFileName));
QObject *plugin = loader.instance();
interface=qobject_cast<DeviceInterface*>(plugin);
create_device=interface->currentDevice(this);
用QPluginLoader来构造一个根据完整文件名(路径+文件名)的loader对象。用loader.instance()函数来返回一个指针,可以看到这个指针其实就是一个DeviceInterface,虽然一开始返回一个QObject指针但是可以把他cast成DeviceInterface。我在user_interface里做了如下边角料工作。
INCLUDEPATH += ../devices/floodcontrol
这样我可以包含device头文件,然后定义一个 AbstractDevice *create_device 指针,我这个指针就可以指向interface返回过来的设备的对象了。
简单测试,插件可以工作,程序还在修改中,有机会用另一种方式实现一下插件试试。
这里简单谈一下第二种方式实现插件的思路。这个思路是我把AbstractDevice这个基类,做成一个动态链接库,但是不是插件,按照前几篇博客的方法,这个库有自己的头文件,我在工程文件里设置连接器,在mainwindow里包含头文件,这个库可以作为头文件的实现。这样在mainwindow里面我可以有一个AbstractDevice的类。然后我做插件的时候,也用包含头文件,设置连接器的方法来把AbstractDevice的子类做成插件,这样来看就是mainwindow运行时我需要链AbstractDevice的库,插件运行时也需要连AbstractDevice.dll这个库。我load插件之后,我应该可以直接把instance() cast成AbstractDevice的指针。
这种方式我没实验,但是具体区别就是我没有interface这个中间层,两个设备子类的插件函数都是外部可见的。还没有实现,有机会用这种方式做一下。