为什么使用Qt插件
根据官方文档介绍,Qt插件开发允许你通过动态加载和使用插件来扩展应用程序的功能。在应用程序中,我们可以事先定义好接口,把接口的具体实现交给插件去完成。这样做的好处是应用程序和插件可以分别开发,降低代码耦合度,实现代码模块化设计。当接口的具体实现需要修改时,我们不必重写编译应用程序,只要把修改编译后的插件dll文件拷贝到应用程序指定目录,即可完成功能升级。
如何创建Qt插件
QGenericPlugin是Qt提供的用于创建插件的基类。它具有一个create函数,用于根据插件名称创建接口实例。这个类允许你创建插件框架,并在其中实现插件用户逻辑。QGenericPlugin官方文档详见https://doc.qt.io/qt-6.2/qgenericplugin.html。
如何加载Qt插件
加载Qt插件的方法之一,是使用QPluginLoader,它提供了插件加载机制,允许应用程序在运行时动态加载插件。QPluginLoader官方文档详见https://doc.qt.io/qt-6.2/qpluginloader.html。
使用示例
接下来,我们Step by step演示如何使用QGenericPlugin和QPluginLoader创建和加载插件。示例代码测试环境为Windows 10,Qt Creator 11.0.2, Qt 6.2.4, MinGw 11.2.0 64-bit。
1. 创建工作目录
使用Windows资源管理器在“文档”下,创建一个MyApp文件夹,我们所有的代码测试均在该目录下编辑、调试和运行。
2. 打开Qt Creator 创建子目录项目
这里我使用了子目录项目,把主应用程序和插件两个分项目包含到一个.pro文件里,这样方便打开整个工程项目。
- 选择“其它项目”,模板为子目录项目。
- 创建路径选择MyApp文件夹,项目名称Application。
- 选择构建套件,我使用的是Desktop Qt 6.2.4 MinGW 64-bit。
- 完成&添加子项目
3. 新建主应用程序项目App。
完成子目录创建后,自动弹出菜单,让你添加第一个子项目。这里我们开始创建主应用程序项目,我选择Qt Widgets应用模板。
- 设置主应用程序的名称和路径。
- 这里我使用的是qmake。
- 选择QWidget基类,类名AppWidget,其它默认。
- 一路“下一步”,直到“汇总”界面,注意看一下是否作为子项目添加到Application.pro,然后“完成”。
到此为止,我们创建了Application子目录项目和App主应用程序项目,Qt Creator给出了默认模板的代码。
4. 创建插件项目Plugin。
右键Application,点击新子目录…
- 插件项目模板选择C++库。
- 插件项目命名Plugin。
- 同样使用qmake
- 库的类型选择Qt Plugin,其它默认。
- 一路“下一步”,直到汇总界面,注意看一下是否作为子项目添加到Application.pro项目中。
- 同样,完成后Qt Creator给出了默认的模板代码。
到此为止,我们已经创建了主程序和插件的框架,并把它们放到一个目录下统一管理。
5. 理解GenericPlugin类
一顿操作后,让我们稍微休息一下,喝点什么。
在开始编写代码之前,有必要先熟悉一下插件模板代码。查阅网上资料,我给出关键部分的注释。
#ifndef GENERICPLUGIN_H
#define GENERICPLUGIN_H
/* 这是包含 Qt 的 QGenericPlugin 类
* 的头文件,它是 Qt 插件机制的一部分,
* 提供了一些必要的基础设施来创建插件。
* */
#include <QGenericPlugin>
/* 这个插件类继承自 QGenericPlugin,
* 它是Qt插件机制的一部分。
* */
class GenericPlugin : public QGenericPlugin
{
/* 这个宏用于启用 Qt 的元对象系统,
* 它通常出现在所有继承自QObject
* 的类中,以支持 Qt 的信号和槽机制
* 以及其他元对象相关功能。
* */
Q_OBJECT
/* 这个宏用于定义插件的元数据,
* 包括插件的 IID(Interface ID)
* 和其他信息。在这里,IID 是
* QGenericPluginFactoryInterface_iid,
* 这是 Qt 插件工厂接口的 IID。
* FILE "Plugin.json" 部分指定了
* 包含插件元数据的 JSON 文件的名称。
* */
Q_PLUGIN_METADATA(IID QGenericPluginFactoryInterface_iid FILE "Plugin.json")
public:
explicit GenericPlugin(QObject *parent = nullptr);
private:
/* 这是一个纯虚函数,必须在子类中实现。
* 在插件加载时,Qt插件系统会调用这个
* 函数来创建插件的实例。name 参数通常
* 是插件的名称,spec 参数是规格说明,
* 用于进一步区分不同的插件。
* */
QObject *create(const QString &name, const QString &spec) override;
};
#endif // GENERICPLUGIN_H
整个代码的目的是定义一个插件类,它可以通过插件工厂接口来创建。当你加载这个插件后,Qt 插件系统允许你在主应用程序中调用 create 函数,创建一个具体的插件实例,并返回给调用者。这允许你动态加载和使用插件,而无需在应用程序代码中硬编码每个插件的创建。
6. 创建接口
接下来我们要创建一个接口文件。接口在应用程序和插件之间的通信和集成中确实起着重要作用。通过定义清晰的接口,应用程序可以与插件进行交互,而不需要了解插件的内部实现细节。这有助于实现模块化、可扩展和可维护的应用程序设计。
- 右键App,添加新文件,选择C++头文件模板,文件名Interface,添加到App.pro项目。
以下是Interface.h代码,关键部分给出注释。
#ifndef INTERFACE_H
#define INTERFACE_H
#include <QGenericPlugin>
class Interface
{
public:
virtual ~Interface() = default;
virtual void doSomething(const QString ¶m) = 0;
};
QT_BEGIN_NAMESPACE
Q_DECLARE_INTERFACE(Interface, QGenericPluginFactoryInterface_iid)
QT_END_NAMESPACE
#endif // INTERFACE_H
这段代码定义了一个纯虚接口(Interface),它是一个纯虚类,没有实际的实现,只有一组虚函数声明。接口文件的目的是为了定义一个插件必须遵循的接口规范,以便插件和应用程序之间能够互操作。
#include <QGenericPlugin>
这里包含了 Qt 的 QGenericPlugin 类的头文件,因为我们将使用该类的接口来创建插件。
virtual void doSomething(const QString ¶m) = 0;
接口声明了一个纯虚函数 doSomething,该函数需要一个QString参数,并且没有实际的实现。
Q_DECLARE_INTERFACE(Interface, QGenericPluginFactoryInterface_iid)
这个宏告诉 Qt 插件系统 Interface 类是一个插件接口,并将其关联到 QGenericPluginFactoryInterface_iid,这是插件工厂接口的 IID。这样,Qt 插件系统就知道 Interface 是一个插件接口,可以被用来创建插件。
7. 接口的实现
接下来,我们在插件项目中实现class Interface。既然GenericPlugin类是通过create函数给应用程序返回一个接口实例对象,那么我们就顺势而为,在插件项目里头创建一个MyPlugin类,并且让它继承QObject和Interface。
MyPlugin类的头文件myplugin.h里,记得#include “Interface.h”,并且使用宏Q_INTERFACES(Interface),向Qt插件系统声明MyPlugin类实现了Interface接口,以便主应用程序App可以在运行时识别和使用这个接口。在这个示例中,我们仅用qDebug()输出一行文本,表示插件已被正确加载并且运行正常。
Plugin.pro文件这里补充两行代码,以便MyPlugin类include到App项目里的"Interface.h",同时将编译后生成的插件dll文件输出到一个独立的目录Plugins下。
INCLUDEPATH += ../App
DESTDIR = ../App/Plugins
myplugin.h
#ifndef MYPLUGIN_H
#define MYPLUGIN_H
#include <QObject>
#include "Interface.h"
class MyPlugin : public QObject, public Interface
{
Q_OBJECT
Q_INTERFACES(Interface)
public:
explicit MyPlugin(QObject *parent = nullptr);
void doSomething(const QString ¶m) override;
signals:
};
#endif // MYPLUGIN_H
myplugin.cpp
#include "myplugin.h"
MyPlugin::MyPlugin(QObject *parent)
: QObject{parent}
{
}
void MyPlugin::doSomething(const QString ¶m)
{
qDebug() << "传入参数:" << param << ",插件功能执行完毕!";
}
接下来修改genericplugin.cpp,实现create函数。这里我们从简,忽略了name和spec参数,直接返回一个MyPlugin对象。
#include "genericplugin.h"
#include "myplugin.h"
GenericPlugin::GenericPlugin(QObject *parent)
: QGenericPlugin(parent)
{
}
QObject *GenericPlugin::create(const QString &name, const QString &spec)
{
//static_assert(false, "You need to implement this function");
Q_UNUSED(name)
Q_UNUSED(spec)
return new MyPlugin();
}
至此,我们基本完成插件项目代码编写。
8. 加载和运行插件
在AppWidget类里添加一个私有成员函数pluginLoader,用来演示加载和验证插件功能是否正确实现。
pluginLoader()
void AppWidget::pluginLoader()
{
//使用QPluginLoader加载插件
//pluginLoader的参数要使用绝对路径文件名,否则会找不到Plugin.dll
QPluginLoader pluginLoader("C:/Users/Simon/Documents/MyApp/build-Application-Desktop_Qt_6_2_4_MinGW_64_bit-Debug/App/Plugins/Plugin.dll");
//将插件实例pluginLoader.instance()转换为QGenericPlugin类型
QGenericPlugin *genericPlugin = qobject_cast<QGenericPlugin *>(pluginLoader.instance());
if (genericPlugin) {
// 插件加载成功,可以使用插件
// 使用 QGenericPlugin 的 create 函数来创建插件对象
QObject *pluginInstance = genericPlugin->create("", "");
if (pluginInstance) {
// 创建成功,可以使用插件实例
// 尝试转换插件对象为MyPlugin类型
Interface *myPlugin = qobject_cast<Interface *>(pluginInstance);
if (myPlugin) {
// 使用插件的方法
myPlugin->doSomething("COM3");
} else {
// 无法将插件实例转换为你的插件类
}
} else {
// 插件创建失败
}
} else {
// 插件加载失败
}
}
下面给出class AppWidget完整代码
appwidget.h
#ifndef APPWIDGET_H
#define APPWIDGET_H
#include <QWidget>
#include <QGenericPlugin>
#include <QPluginLoader>
#include <QDir>
QT_BEGIN_NAMESPACE
namespace Ui { class AppWidget; }
QT_END_NAMESPACE
class AppWidget : public QWidget
{
Q_OBJECT
public:
AppWidget(QWidget *parent = nullptr);
~AppWidget();
private:
Ui::AppWidget *ui;
void pluginLoader();
};
#endif // APPWIDGET_H
appwidget.cpp
#include "appwidget.h"
#include "ui_appwidget.h"
#include "Interface.h"
AppWidget::AppWidget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::AppWidget)
{
ui->setupUi(this);
pluginLoader();
}
AppWidget::~AppWidget()
{
delete ui;
}
void AppWidget::pluginLoader()
{
//使用QPluginLoader加载插件
//pluginLoader的参数要使用绝对路径文件名,否则会找不到Plugin.dll
//实际调试时,记得换成你自己的路径
QPluginLoader pluginLoader("C:/Users/Simon/Documents/MyApp/build-Application-Desktop_Qt_6_2_4_MinGW_64_bit-Debug/App/Plugins/Plugin.dll");
//将插件实例pluginLoader.instance()转换为QGenericPlugin类型
QGenericPlugin *genericPlugin = qobject_cast<QGenericPlugin *>(pluginLoader.instance());
if (genericPlugin) {
// 插件加载成功,可以使用插件
// 使用 QGenericPlugin 的 create 函数来创建插件对象
QObject *pluginInstance = genericPlugin->create("", "");
if (pluginInstance) {
// 创建成功,可以使用插件实例
// 尝试转换插件对象为MyPlugin类型
Interface *myPlugin = qobject_cast<Interface *>(pluginInstance);
if (myPlugin) {
// 使用插件的方法
myPlugin->doSomething("COM3");
} else {
// 无法将插件实例转换为你的插件类
}
} else {
// 插件创建失败
}
} else {
// 插件加载失败
}
}
9. 测试结果
在Qt Creator中运行Application项目,结果弹出一个空白窗口,并在“应用程序输出窗口”输出“传入参数: “COM3” ,插件功能执行完毕!”