對於一個大型系統,如何保證可擴展性和可維護性是十分重要的。Qt爲我們提供了一套插件系統,能夠較好的解決擴展性的問題。但是在將插件系統與信號槽機制相結合的過程中,也遇到了一些問題。經過一番探索之後總算成功了,這裏寫一個小小的教程,供有需要的同學查閱。
一、概述
Qt的插件系統分爲High-Level API接口和Low-Level API接口。
所謂High-Level API 是指通過繼承Qt爲我們提供的特定的插件基類,然後實現一些虛函數、添加需要的宏即可。該種插件開發方式主要是用來擴展Qt庫本身的功能,比如自定義數據庫驅動、圖片格式、文本編碼、自定義樣式等。而我們爲自己的應用程序編寫插件來擴展其功能時主要使用第二種方式,即Low-Level API 的方式,該方式不僅能擴展我們自己的應用程序,同樣也能像High-Level API 那樣用來擴展Qt本身的功能。
在本文中,我們使用Low-Level API接口進行插件的編寫。有關High-Level相關內容,參閱Qt官方說明文檔。
除此之外,插件還有靜態和動態之分。靜態插件顧名思義,就是編譯出一個lib作爲插件,在程序中靜態編譯。動態插件則是一個dll(windows下),方便擴展和維護。因此我們使用動態插件作爲說明。
總的來說,插件擴展並使用信號槽機制需要以下步驟:
- 定義一個插件接口類,作爲主程序與插件之間通信的橋樑,定義普通成員函數(純虛函數)、槽函數(純虛函數)、信號(不能爲虛)。在接口類中使用 Q_DECLARE_INTERFACE() 通知 Qt 元對象系統這裏有這麼個接口。這個接口需要繼承QObject;
- 繼承接口類,實現其中的虛函數。並:a) 使用 Q_PLUGIN_METADATA()宏導出插件;b) 使用Q_INTERFACES()通知元對象系統此插件使用了哪些接口類;
- 在主程序中利用QPluginLoader加載插件,使用QPluginLoader::instance()方法實例化插件,使用qobject_cast強制轉換爲接口類指針,connect有關信號和槽;
- 開始愉快的使用;
注意:主程序、接口、插件應分別作爲3個子工程。主程序和插件需要調用接口生成的lib文件,否則會出現未定義的外部符號錯誤。https://stackoverflow.com/questions/50516359/declaring-signals-in-interface-class-using-qt-plugin-system-with-new-signal-slot
二、實現
2.1 工程(Qt creator)
2.1.1 新建項目
新建一個子項目目錄,這個項目將包含主工程、接口和插件三個子工程。這裏我們起名爲PluginTest
2.1.2 新建主程序工程
主程序。起名叫App。右鍵PluginTest,選新建子項目,選Qt Widgets Application
2.1.3 新建接口工程
起名叫PluginInterface。右鍵PluginTest,選新建子項目, 在“項目”框中選library,右邊只有一個項目可選
選共享庫。
一路下一步,自動生成四個文件。
2.1.4 新建插件工程
起名叫myPlugin。右鍵PluginTest,選新建子項目,選Empty qmake Project
會提示個這,不用管它,因爲這個工程裏沒有任何文件。
到此爲止,插件的框架就搭好了。總結一下,我們用了三個子工程:
- 主程序:Qt Widgets Application,用來生成一個exe調用插件
- 接口:c++共享庫,用來生成一個lib向編譯器提供接口信息
- 插件:空qmake工程,後面手動對其進行填寫,生成dll作爲主程序的插件
另外,c++庫中的qt plugin選項是生成High-Level API的,不用在這裏。
2.2 接口類
工程是PluginInterface。plugininterface_global.h是自動生成的,這裏我們無需修改它。
plugininterface.h:
//plugininterface.h
#ifndef PLUGININTERFACE_H
#define PLUGININTERFACE_H
#include "plugininterface_global.h"
#include <QObject>
//接口類
class PLUGININTERFACESHARED_EXPORT PluginInterface : public QObject
{
Q_OBJECT
public:
virtual ~PluginInterface() {} //虛析構函數,c++多態
public slots:
virtual void SayHello(QWidget *parent) = 0;//虛槽函數,而且是純的
signals:
void doSomething();//信號不能爲虛
};
#define InterfaceIID "interface"
Q_DECLARE_INTERFACE(PluginInterface, InterfaceIID)
#endif // PLUGININTERFACE_H
因爲不需要對其進行實現,所以plugininterface.cpp就可以刪掉了
注意到我們將槽函數聲明成爲一個純虛函數,使得接口類成爲了一個抽象類。增強了代碼的健壯性。
又注意到信號是一個普通成員函數的聲明,信號不能是虛的,否則連接不過。(moc相關,就不展開講了)
我們在pro文件中指定生成的目錄,方便其他工程對其進行引用:PluginInterface.pro:
#PluginInterface.pro
QT -= gui
TARGET = PluginInterface
TEMPLATE = lib
DEFINES += PLUGININTERFACE_LIBRARY
# The following define makes your compiler emit warnings if you use
# any feature of Qt which has been marked as deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS
# You can also make your code fail to compile if you use deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
SOURCES +=
HEADERS += \
plugininterface.h \
plugininterface_global.h
unix {
target.path = /usr/lib
INSTALLS += target
}
###修改生成目錄,到pro文件目錄/lib中
DESTDIR = $$PWD/lib
2.3 插件類
首先向其添加一個類:右鍵myPlugin,添加新文件,c++class,起名爲Plugin,繼承PluginInterface
然後修改pro文件:
#myPlugin.pro
HEADERS += \
plugin.h
SOURCES += \
plugin.cpp
QT += widgets
TARGET = Plugin#類型是plugin
TEMPLATE = lib#模板是lib
INCLUDEPATH += $$PWD/../PluginInterface#指定了包含目錄
DEPENDPATH += $$PWD/../PluginInterface#指定了附加依賴項目錄
LIBS += -L$$PWD/../PluginInterface/lib/ -lPluginInterface#要添加接口生成的庫給編譯器
DESTDIR = ../app/debug#生成的dll直接扔到app的目錄下
plugin.h:
//plugin.h
#ifndef PLUGIN_H
#define PLUGIN_H
#include "plugininterface.h"
#include <QWidget>
class Plugin : public PluginInterface
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "my.test.plugin.interface")//導出plugin
Q_INTERFACES(PluginInterface)
public:
void SayHello(QWidget *parent) Q_DECL_OVERRIDE;//聲明是重寫虛函數
};
#endif // PLUGIN_H
注意到需要一個Q_OBJECT宏。
又注意到Q_PLUGIN_METADATA用來導出插件,Q_INTERFACES聲明使用的接口。
還注意到虛函數聲明後邊加了一個Q_DECL_OVERRIDE,用來向編譯器說明這是個重寫的虛函數。
plugin.cpp:
//plugin.cpp
#include "plugin.h"
#include "QMessageBox"
void Plugin::SayHello(QWidget *parent)
{
emit doSomething();
QMessageBox::information(parent, "123", "12345");
}
注意到,在plugin中直接emit了doSomething信號。
2.4 主程序
修改pro 文件:
#App.pro
QT += core gui
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
TARGET = App
TEMPLATE = app
# The following define makes your compiler emit warnings if you use
# any feature of Qt which has been marked as deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS
# You can also make your code fail to compile if you use deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
SOURCES += \
main.cpp \
widget.cpp
HEADERS += \
widget.h
FORMS += \
widget.ui
LIBS += -L$$PWD/../PluginInterface/lib/ -lPluginInterface
INCLUDEPATH += $$PWD/../PluginInterface
DEPENDPATH += $$PWD/../PluginInterface
跟插件類類似,添加了接口類的lib、包含目錄和附加依賴項。
在ui中添加兩個按鈕:
然後是widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QPluginLoader>
#include "plugininterface.h"
namespace Ui {
class Widget;
}
class Widget : public QWidget
{
Q_OBJECT
public:
explicit Widget(QWidget *parent = 0);
~Widget();
private slots:
void on_pushButton_clicked();
void on_pushButton_2_clicked();
void onDoSomething();
signals:
void saySomething(QWidget *);
private:
Ui::Widget *ui;
PluginInterface *interface = nullptr;
QPluginLoader pluginLoader;
};
#endif // WIDGET_H
widget.cpp:
#include "widget.h"
#include "ui_widget.h"
#include <QMessageBox>
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);
}
Widget::~Widget()
{
delete ui;
}
void Widget::on_pushButton_clicked()
{
if(!interface)
{
pluginLoader.setFileName("Plugin.dll");
QObject *plugin = pluginLoader.instance();
if(plugin)
{
interface = qobject_cast<PluginInterface*>(plugin);//使用多態,將基類指針強制轉換成派生類指針,檢查能否轉換
if(interface)
{
connect(this, &Widget::saySomething, interface, &PluginInterface::SayHello);
connect(interface, &PluginInterface::doSomething, this, &Widget::onDoSomething);
}
else
{
QMessageBox::critical(this, "err", "this is not a proper plugin");
return;
}
}
else
{
QMessageBox::critical(this, "err", "could not find any plugin");
return;
}
}
emit saySomething(this);
}
void Widget::on_pushButton_2_clicked()
{
if(interface)
{
if (pluginLoader.unload())
{
interface = nullptr;
QMessageBox::information(this,"info","unloaded");
}
else
QMessageBox::critical(this,"err", "unload failed");
}
}
void Widget::onDoSomething()
{
QMessageBox::information(this,"info","you want to do something");
}