Qt外掛使用的學習筆記
之前做了一個電壓箱控制介面程式,這個介面程式需要用作控制兩個不同型號的電壓箱,之前的做法是寫出兩個裝置類,然後每次讓使用者選擇生成某一個裝置的物件,然後進行操作。這次的做法是把兩個裝置寫成外掛形式,在程式執行開始的時候使用者選擇不同的外掛載入,再執行控制程式。這種做法是住程式碼可以不做過多修改,如果控制邏輯不變的話,如果再多出一個裝置,我可以把新的裝置寫成外掛,然後放程序序中讓使用者來進行選擇。目前有兩種方法來實現裝置類的外掛,現在我主要記錄一下我實現的第一種做法。寫的比較亂,只求自己能看懂。
無視我類或者檔案的命名,此專案只是練習。再這個專案裡,我把兩個裝置做成兩個不同的專案裡,然後生成連個不同的lib。user_interface部分是我以後不需要修改的介面及其控制邏輯。
devices.pro檔案如下
TEMPLATE = subdirs
SUBDIRS = precharge \
floodcontrol \
floolcontrol.pro檔案如下:
TEMPLATE設定為lib,CONFIG設定為plugin,DEFINES引數在我的專案中沒什麼用,自動生成留下的,我也沒刪除,INCLUDEPATH的設定是因為我把介面(interface)放在了user_interface的專案中,DESTDIR的設定是因為我想把生成的庫集中放在一個地址然後呼叫庫的程式找起來比較方便。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
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
這個類相當於一個dll庫檔案的一個外部的視窗,是外部程式可以訪問的,裡面都是純虛擬函式,而且用Qt工具巨集做了如下定義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這個中間層,兩個裝置子類的外掛函式都是外部可見的。還沒有實現,有機會用這種方式做一下。