Qt自带插件框架的使用
简述
不仅Qt本身可以通过插件进行扩展,而且Qt应用程序也可以通过插件来扩展,这需要应用程序使用 QPluginLoader
检测和加载插件。在这种情况下,插件可能提供任意功能,不限于数据库驱动程序、图像格式、文本编解码器、样式以及扩展 Qt 功能的其他类型的插件。
扩展Qt应用程序
编写扩展Qt应用程序的插件,涉及以下步骤:
- 声明一个继承自
QObject
和插件想要提供的接口的插件类 - 使用
Q_INTERFACES()
宏来告诉 Qt 元对象系统有关接口的情况 - 使用
Q_PLUGIN_METADATA()
宏导出插件 - 使用合适的
.pro
文件构建插件
通过插件使应用程序可扩展,涉及以下步骤:
- 定义一组用于与插件通信的接口(只有纯虚函数的类)
- 使用
Q_DECLARE_INTERFACE()
宏来告诉 Qt 的元对象系统关于该接口 - 在应用程序中使用
QPluginLoader
加载插件 - 使用
qobject_cast()
来测试插件是否实现了指定的接口
编写插件
我们需要构建一个共享库,所以在.pro
文件中将TEMPLATE
设置为lib,同时还必须将 CONFIG
设置为plugin。
plugin.pro
内容如下:
QT += core
QT -= gui
TEMPLATE = lib
CONFIG += plugin
TARGET = peoplePlugin
HEADERS += \
people.h \
programmer.h
SOURCES += programmer.cpp
OTHER_FILES += programmer.json
win32 {
CONFIG(debug, release|debug):DESTDIR = ../debug/plugins/
CONFIG(release, release|debug):DESTDIR = ../release/plugins/
} else {
DESTDIR = ../plugins/
}
声明了一个接口类 - IPeople
,定义插件将提供的功能。
注意: 接口是一个由纯虚函数组成的类,如果在类中存在非虚函数,那么在moc文件中会出现编译错误。
people.h
内容如下:
#ifndef PEOPLE_H
#define PEOPLE_H
#include <QtPlugin>
#include <QString>
class IPeople
{
public:
virtual ~IPeople() {}
virtual QString name() = 0; // 名字
virtual void eat() = 0; // 吃东西
virtual void sleep() = 0; // 睡觉
virtual void doSomething() = 0; // 做其他事
};
#define IPeople_iid "org.qt-project.Qt.Examples.IPeople"
Q_DECLARE_INTERFACE(IPeople, IPeople_iid)
#endif // PEOPLE_H
使用 Q_DECLARE_INTERFACE
宏让Qt元对象系统感知到该接口,这样在运行时可以识别到实现接口的插件。其中第二个参数(IPeople_iid
)是一个标识接口的字符串且必须唯一。
定义一个 Programmer
类,继承自 QObject
和 IPeople
,使这个类成为一个插件。
programmer.h
内容如下:
#ifndef PROGRAMMER_H
#define PROGRAMMER_H
#include "people.h"
#include <QObject>
class Programmer : public QObject, IPeople
{
Q_OBJECT
Q_PLUGIN_METADATA(IID IPeople_iid FILE "programmer.json")
Q_INTERFACES(IPeople)
public:
virtual QString name() Q_DECL_OVERRIDE;
virtual void eat() Q_DECL_OVERRIDE;
virtual void sleep() Q_DECL_OVERRIDE;
virtual void doSomething() Q_DECL_OVERRIDE;
};
#endif // PROGRAMMER_H
Q_INTERFACES
宏用于告诉Qt该类实现的接口。Q_PLUGIN_METADATA
宏包含插件的 IID
,并指向一个包含插件元数据的Json文件。该Json文件被编译到插件中,不需要安装。
programmer.cpp
内容如下:
#include "programmer.h"
#include <QtDebug>
QString Programmer::name()
{
return "peter";
}
void Programmer::eat()
{
qDebug() << "water";
}
void Programmer::sleep()
{
qDebug() << "7h";
}
void Programmer::doSomething()
{
qDebug() << "play...";
}
到这里插件已经完成,但是还缺元数据为插件提供一些相关的信息。
之前提到的Json文件可以包含一些元数据,例如:作者、日期、插件名、版本号、依赖关系等…
programmer.json
内容如下:
{
"author" : "test",
"Date" : "2018/09/01",
"name" : "peoplePlugin",
"version" : "1.0.0",
"dependencies" : []
}
由于现在只有一个插件,并没有对于其他插件的依赖,所以暂时没有列出依赖关系(要实现一个插件系统,依赖关系必不可少)。
注意: 如果不想为插件提供信息,只要提供一个空的Json文件就行。
加载插件
实现一个控制台应用程序来动态加载插件。
由于是一个简单的控制台应用程序,所以,除了将TARGET
设置为 app 之外,还需要将 CONFIG
设置为 console。
app.pro
内容如下:
QT += core
QT -= gui
CONFIG += c++11
TARGET = app
CONFIG += console
TEMPLATE = app
INCLUDEPATH += $$PWD/..
SOURCES += main.cpp
win32 {
debug:DESTDIR = ../debug/
release:DESTDIR = ../release/
} else {
DESTDIR = ../
}
为了能在程序中使用CTK相关的接口,需要使用 INCLUDEPATH
将插件的相关路径包含进来。
注意: 对于使用插件的应用程序,Qt插件只是一个QObject
,该 QObject
使用多重继承来实现插件接口。
main.cpp
内容如下:
#include <QCoreApplication>
#include <QPluginLoader>
#include <QDir>
#include <QtDebug>
#include <QJsonObject>
#include <QJsonArray>
#include <plugin/people.h>
void loadPlugin()
{
// 进入插件目录
QDir pluginsDir(qApp->applicationDirPath());
pluginsDir.cd("plugins");
// 查找目录中的所有插件
foreach (QString fileName, pluginsDir.entryList(QDir::Files)) {
QPluginLoader loader(pluginsDir.absoluteFilePath(fileName));
// 返回插件的根组件对象
QObject *pPlugin = loader.instance();
if (pPlugin != Q_NULLPTR) {
// 获取元数据(名称、版本、依赖)
QJsonObject json = loader.metaData().value("MetaData").toObject();
qDebug() << "********** MetaData **********";
qDebug() << json.value("author").toVariant();
qDebug() << json.value("date").toVariant();
qDebug() << json.value("name").toVariant();
qDebug() << json.value("version").toVariant();
qDebug() << json.value("dependencies").toArray().toVariantList();
// 访问感兴趣的接口
IPeople *pPeople = qobject_cast<IPeople *>(pPlugin);
if (pPerson != Q_NULLPTR) {
qDebug() << "********** IPeople **********";
qDebug() << pPeople->name();
pPeople->eat();
pPeople->sleep();
pPeople->doSomething();
} else {
qWarning() << "qobject_cast falied";
}
}
}
}
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
loadPlugin();
return a.exec();
}
初始化 pluginsDir
成员变量,以引用 plugins
子目录。然后,使用 QDir::entryList()
获取该目录中所有文件列表,并通过 foreach
迭代结果,尝试使用 QPluginLoader
加载插件。
插件提供的 QObject
可以通过 QPluginLoader::instance()
访问。如果动态库不是 Qt 插件,或者是由不兼容版本的 Qt 库进行编译,QPluginLoader::instance()
将返回一个空指针。
对于每个插件,可以根据 QPluginLoader::metaData()
来获取其元数据(定义在 Json 文件中)。要检查插件实现的接口,需要使用 qobject_cast()
,从而进一步访问接口中的定义。
定位插件
Qt应用程序自动知道哪些插件可用,因为插件存储在标准插件子目录中。由于这个应用程序不需要任何代码来查找和加载插件,因为Qt会自动处理它们。
在开发期间,插件的目录是QTDIR/plugins(其中QTDIR是安装Qt的目录),每种类型的插件都在该类型的子目录中,例如样式。如果您希望应用程序使用插件而您不想使用标准插件路径,请让安装过程确定要用于插件的路径,并保存路径,例如,使用QSettings,应用程序在运行时读取。然后,应用程序可以使用此路径调用QCoreApplication::addLibraryPath()
,并且您的插件将可供应用程序使用。请注意,无法更改路径的最后部分(例如,styles)。
如果您希望插件可加载,那么一种方法是在应用程序下创建一个子目录,并将插件放在该目录中。如果您分发Qt附带的任何插件(位于plugins目录中的插件),则必须将插件所在的插件下的子目录复制到应用程序根文件夹(即,不包括插件目录)。
有关部署的更多信息,请参阅部署Qt应用程序和部署插件文档。
静态插件
将插件与应用程序一起打包最灵活常规的方法是将其编译为动态库,该库单独提供,并在运行时检测和加载。
插件可以静态链接到您的应用程序中。 如果您构建Qt的静态版本,这是包含Qt预定义插件的唯一选项。 使用静态插件使部署不易出错,但缺点是如果没有完全重建和重新分发应用程序,就无法添加插件功能。
要静态链接插件,您需要使用QTPLUGIN
将所需的插件添加到您的构建中。
在应用程序的.pro文件中,您需要以下条目:
QTPLUGIN += qjpeg \
qgif \
qkrcodecs123
qmake自动将插件添加到QTPLUGIN
中,这些插件通常是所使用的Qt模块所需的(参见QT),而需要手动添加更专业的插件。 每种类型都可以覆盖自动添加的插件的默认列表。 例如,要链接最小插件而不是默认的Qt平台适配插件,请使用:
QTPLUGIN.platforms = qminimal1
如果您既不想要自动链接默认插件,也不想要最小QPA插件,请使用:
QTPLUGIN.platforms =-1
默认设置被调整为最佳的开箱即用体验,但可能会不必要地使应用程序变大。 建议检查qmake构建的链接器命令行,并消除不必要的插件。
链接静态插件的详细信息
要使静态插件实际链接和实例化,应用程序代码中也需要Q_IMPORT_PLUGIN()
宏,但这些宏由qmake自动生成并添加到您的应用程序项目中。
如果您不希望自动链接添加到QTPLUGIN
的所有插件,请从CONFIG
变量中删除import_plugins:
CONFIG -= import_plugins 1
创建静态插件
也可以按照以下步骤创建自己的静态插件:
1.将CONFIG + = static添加到插件的.pro文件中。
2.在应用程序中使用Q_IMPORT_PLUGIN()
宏。
3.如果插件附带qrc文件,请在应用程序中使用Q_INIT_RESOURCE()
宏。
4.使用.pro文件中的LIBS将应用程序与插件库链接。
有关如何执行此操作的详细信息,请参阅即插即用示例和关联的基本工具插件。
注意:如果您不使用qmake构建插件,则需要确保定义了QT_STATICPLUGIN
预处理器宏。
部署和调试插件
部署插件文档介绍了使用应用程序部署插件并在出现问题时对其进行调试的过程。