一、Qt插件机制
代码在Qt5.12.11和Qt6.1.1的linux版下测试ok。
1、简介
插件是一种遵循一定规范的应用程序接口编写出来的程序,定位于开发实现/扩展应用软件平台不具备的功能的程序。
具体的体现形式:
linux: .so文件(动态库),.a文件(静态库)
windows: *.dll文件(动态库), *.lib文件(静态库)
2、Qt插件API
- 一种是高阶API,用于扩展Qt本身的功能,如自定义数据库驱动,图像格式,文本编码,自定义样式等;
- 一种是低阶API,用于扩展Qt应用程序。
3、插件调用
A、定义一个接口集(只有纯虚函数的类),用来与插件交流。
B、用宏Q_DECLARE_INTERFACE()将该接口告诉Qt元对象系统。
C、应用程序中用QPluginLoader来加载插件。
D、用宏qobject_cast()来判断一个插件是否实现了接口。
E、调用插件的功能。
4、创建插件
创建一个插件的步骤如下:
A、声明插件类,插件类继承自QObject和插件实现的接口。
B、用宏Q_INTERFACES()将插件接口告诉Qt元对象系统。
C、用宏Q_EXPORT_PLUGIN2()导出插件类。
D、用适当的.pro文件构建插件。
在加载插件前, QCoreApplication对象必须被初始化。
二、实操
1、创建工程
使用QtCreator创建子目录工程(Subdirs Project),命名为PluginDemoAndTest
;
其PluginDemoAndTest.pro文件如下:
TEMPLATE = subdirs
SUBDIRS += \
PluginDemo \
PluginTest
然后分别添加插件子工程,应用子工程。
2、插件子工程
在PluginDemoAndTest
上右键选择New Subproject
,选择创建为空的Qt工程,取名PluginDemo
,其PluginDemo.pro文件如下:
QT += core
TEMPLATE = lib
TARGET = $$qtLibraryTarget(PluginDemo)
CONFIG += c++11 plugin
TARGET = PluginDemo
DESTDIR = ../plugins
# You can make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
SOURCES += \
plugindemo.cpp
# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target
HEADERS += \
plugindemo.h \
plugininterface.h
创建接口文件plugininterface.h如下:
#ifndef PLUGININTERFACE_H
#define PLUGININTERFACE_H
#include <QString>
#include <QtPlugin>
class PluginInterface
{
public:
virtual ~PluginInterface() {} //避免编译器抱怨
virtual QStringList interfaceList() = 0;
virtual QString action(QString& interface) = 0;
};
QT_BEGIN_NAMESPACE
#define PluginInterface_iid "org.csm.embed.plugin.pluginInterface.1.0"
Q_DECLARE_INTERFACE(PluginInterface, PluginInterface_iid)
QT_END_NAMESPACE
#endif // PLUGININTERFACE_H
包含了提供接口查询的interfaceList()方法,以及根据调用接口实现功能的action()方法,使用纯虚方法,而析构函数的声明是为了满足一些编译器的要求。
然后创建PluginInterface的实现:
//plugindemo.h
#ifndef PLUGINDEMO_H
#define PLUGINDEMO_H
#include <QObject>
#include "plugininterface.h"
class PluginDemo : public QObject, PluginInterface
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "org.csm.embed.plugin.pluginInterface.1.0" FILE "plugindemo.json")
Q_INTERFACES(PluginInterface)
public:
explicit PluginDemo();
QStringList interfaceList();
QString action(QString& interface);
private:
QStringList m_ifList;
};
#endif // PLUGINDEMO_H
这里是一个PluginInterface的具体实现,其采用多继承方式,切第一个父类必须为QObject类,然后是接口。(如果其接口已经是QObject的子类,这段话作废)
//plugindemo.cpp
#include "plugindemo.h"
#include <QDebug>
PluginDemo::PluginDemo()
{
m_ifList.append("interface1");
m_ifList.append("interface2");
}
QStringList PluginDemo::interfaceList()
{
return m_ifList;
}
QString PluginDemo::action(QString &interface)
{
QString actionName;
if(m_ifList.contains(interface)) {
actionName = interface + " done...";
} else {
actionName = interface + " is not exists";
}
qDebug() << actionName;
return actionName;
}
通过interfaceList()返回接口列表,而具体的action则根据interface来动作,即返回/打印字符串。
Plugin工程至此结束。
3、应用子工程
接下来创建使用/测试PluginDemo的应用工程, 其PluginDemo.pro文件如下,其关键点是需要引入plugininterface.h 头文件
QT += core gui
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++11
# You can make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
SOURCES += \
main.cpp \
widget.cpp
HEADERS += \
../PluginDemo/plugininterface.h \
widget.h
# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target
main.cpp创建widget显示,并进入Qt事件循环机制:
//main.cpp
#include "widget.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Widget w;
w.show();
return a.exec();
}
widget类实现,负责具体的调用实现了PluginInterface接口的PluginDemo类。
//widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QStringList>
class QLineEdit;
class QLabel;
class PluginInterface;
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
void loadPlugin();
public slots:
void queryInterface();
void doAction();
private:
QLabel* ifListInfo; //显示接口列表
QLineEdit* actionName ; //输入接口名称
PluginInterface* interfacePtr; //保存接口指针
QStringList interfaceList; //保存运行结果
};
#endif // WIDGET_H
具体的widget实现了,关键点queryInterface
槽函数中的loadPlugin()
,以及doAction
槽函数中对接口的调用
//widget.cpp
#include "widget.h"
#include <QGridLayout>
#include <QPushButton>
#include <QLabel>
#include <QLineEdit>
#include <QDebug>
#include <QDir>
#include <QtGlobal>
#include <QApplication>
#include <QPluginLoader>
#include "../PluginDemo/plugininterface.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
{
this->setMinimumSize(600, 400);
QGridLayout* layout = new QGridLayout(this);
QLabel* ifListName = new QLabel("list: ");
ifListInfo = new QLabel("");
QPushButton* queryButton = new QPushButton("inqury");
connect(queryButton, &QPushButton::clicked,
this, &Widget::queryInterface);
layout->addWidget(ifListName, 0, 0);
layout->addWidget(ifListInfo, 0, 1);
layout->addWidget(queryButton, 0, 2);
//QPushButton
QLabel* actionLabel = new QLabel("interface:");
actionName = new QLineEdit();
actionName->setPlaceholderText("input one interface of list above");
QPushButton* actionButton = new QPushButton("action");
connect(actionButton, &QPushButton::clicked,
this, &Widget::doAction);
layout->addWidget(actionLabel, 1, 0);
layout->addWidget(actionName, 1, 1);
layout->addWidget(actionButton, 1, 2);
doneResult = new QLabel("result");
layout->addWidget(doneResult, 2, 0, 1, 3, Qt::AlignCenter);
interfacePtr = Q_NULLPTR;
}
Widget::~Widget()
{
}
void Widget::loadPlugin()
{
bool bRet = true;
//路径根据系统差异来具体实现
QDir pluginDir(qApp->applicationDirPath());
#if defined (Q_OS_WIN)
if (pluginDir.dirName().toLower() == "debug" || pluginDir.dirName().toLower() == "release") {
pluginDir.cdUp();
pluginDir.cdUp();
}
#elif defined Q_OS_LINUX //Q_OS_WIN
pluginDir.cdUp();
#endif //Q_OS_LINUX
pluginDir.cd("plugins");
foreach(QString fileName, pluginDir.entryList(QDir::Files)) {
//获取插件的具体路径
QString file = pluginDir.absoluteFilePath(fileName);
//实例化QPluginLoader类
QPluginLoader pluginLoader(file);
//返回插件实例
QObject* plugin = pluginLoader.instance();
if (plugin) {
//注意此处是插件类名,而不是接口类名称哦
QString plugName = plugin->metaObject()->className();
if (plugName == "PluginDemo") {
interfacePtr = qobject_cast<PluginInterface*>(plugin);
interfaceList = interfacePtr->interfaceList();
break;
}
} else {
qDebug() << pluginLoader.errorString();
}
}
}
void Widget::queryInterface()
{
//qDebug() << "query";
loadPlugin();
QString interfaceList;
if (interfacePtr != Q_NULLPTR) {
foreach(auto interfaceName, interfacePtr->interfaceList()) {
interfaceList.append(interfaceName);
interfaceList.append(", ");
}
if (!interfaceList.isEmpty())
interfaceList.remove(interfaceList.length() - 2, 2);
}
ifListInfo->setText(interfaceList);
}
void Widget::doAction()
{
//qDebug() << "doAction";
QString action = actionName->text();
QString doneAction = interfacePtr->action(action);
doneResult->setText(doneAction);
}
实际运行结果:
三、定位插件(此段照搬《Qt高级——Qt插件开发》)
Qt应用程序将会自动感知可用的插件,因为插件都被存储在标准的子目录当中。因此应用程序不需要任何查找或者加载插件的代码。
在开发过程中,插件的目录是QTDIR/plugins(QTDIR是Qt的安装目录),每个类型的插件放在相应类型的目录下面。如果想要应用程序使用插件,但不想用标准的插件存放路径,可以在应用程序的安装过程中指定要使用的插件的路径,可以使用QSettings,保存插件路径,在应用程序运行时读取配置文件。应用程序可以通过QCoreApplication::addLibraryPath()函数将指定的插件路径加载到应用程序中。
使插件可加载的一种方法是在应用程序所在目录创建一个子目录,用于存放插件。如果要发布和Qt一起发布的插件(存放在plugins目录)中的任何插件,必须拷贝plugins目录下的插件子目录到应用程序的根目录下。