在Visual Studio 2022中实现Qt插件开发
习惯在VS开发,不习惯在QCreator上开发,所以摸索出在vs2022里开发Qt插件的方法,还是挺方便的。核心的内容其实是一样的,只有几点要注意。
1.在VS上创建QT主程序
这一步在VS上正常创建Qt项目,或者说解决方案。如果你已经拥有现成的Qt项目,只想为它开发Qt插件的话,那也不用做任何配置和代码修改,这也是Qt插件机制的好处。
我现在创建了一个最简单的Qt主项目,我的需求是,我在主项目下点击PushButton按钮,按钮可以调用我待会要写的Qt插件,并输出这个插件的函数------》即在控制台上打印Hello Plugin。就这么简单的需求,能够代表这种插件机制的作用了。
2.在同一个解决方案下创建一个新的Qt项目,作为Qt插件
这里先说清楚Qt插件的基本组成,其实Qt插件本质上跟平时用的第三方库一样,或者说跟开发DLL动态链接库一样。我们需要提供些什么内容给主程序呢?
答案是两部分,部分1:h头文件 (接口)和部分2:dll文件(隐藏的具体实现)
那其实主要逻辑跟DLL动态链接库的开发类似,我们目的也是使这个新的Qt项目能够生成一个DLL。然后为主程序提供我们定义的接口文件,和生成的DLL文件。
但是我们不创建DLL项目,我们直接创建一个Qt项目,这样可以免去我们花时间去为项目配置Qt。
创建一个Qt Widget Application项目。自动生成的初始模板文件其实我们都可以删掉,因为不影响,我们目标明确,就是要写好刚刚说的两个部分。
既然我要最后编译要生成DLL文件,那么我们应该去项目配置里,将配置类型从(.exe)改为(.dll) 。
2.1 部分1:h头文件(接口文件)
我们要写一个接口文件,代表我们要提供给主程序调用的功能。
要求是一个纯虚基类,
那我们就写一个纯虚基类的头文件作为接口:Interface.h
csharp
#pragma once
#include <QWidget>
#include <QtCore/qplugin.h>
#define InterfaceIID "com.QGL.Interface" //****关注点1*****
class Interface
{
public:
virtual ~Interface() = default;
virtual void print() = 0; //我要提供给主程序调用的函数
};
Q_DECLARE_INTERFACE(Interface, InterfaceIID); //****关注点2*****
与平时的基类写法相比,多了两个地方,就是我在注释里留下的关注点1 和关注点2
-
关注点1:
#define InterfaceIID "com.QGL.Interface"
我将"com.QGL.Interface"这个字符串定义为InterfaceIID。这个字符串作为这个接口的唯一标识符。如果你记得住这个字符串,且愿意修改这个字符串后,不厌其烦地在每个用到它的地方都修改一遍的话,那可以不写这个define。
-
关注点2:
Q_DECLARE_INTERFACE(Interface, InterfaceIID);
如果说关注点1是为方便,那关注点2这一行则是必须。这里可以理解为将你写的这个接口类,与接口标识符绑定起来,并告诉Qt,这个接口类是个Qt插件。
那我们最基本的一个Hello World级别的接口文件就写好了。
2.2 部分2:dll内容部分
dll里面放的是具体的实现内容,并隐藏起来,因为主程序只需要调用接口,不需要知道具体怎么实现的。
那现在我们要为接口类写好具体的实现,首先就是创建一个继承类(派生类),我命名这个继承类为InterfaceImp。
我创建一个继承自Qt Widget的类,作为InterfaceImp,那还得实现我的刚刚写好的接口:
cpp
class InterfaceImp : public QWidget, public Interface
这都没问题,后面自然就是跟着Q_OBJECT这个宏,还有就是具体的函数实现。
InterfaceImp.h:
cpp
#pragma once
#include <QWidget>
#include "Interface.h"
#include "ui_InterfaceImp .h"
//下面三行都是自动创建的东西
QT_BEGIN_NAMESPACE
namespace Ui { class InterfaceImpClass; };
QT_END_NAMESPACE
//下面的内容才是要关注的点
class InterfaceImp : public QWidget, public Interface
{
Q_OBJECT
Q_PLUGIN_METADATA(IID InterfaceIID) //****关注点1*****
Q_INTERFACES(Interface) //****关注点2*****
public:
InterfaceImp (QWidget *parent = nullptr);
~InterfaceImp ();
//接口里面print函数的具体实现
void print();
private:
Ui::InterfaceImpClass *ui;
};
相比普通一个继承QWidget的类,这个代码的注释里又出现了两个的关注点,这两个是构建Qt插件的重要步骤之一。
-
关注点1:Q_PLUGIN_METADATA宏将插件的信息嵌入到生成的dll中,类似dll开发时的导出声明。
Q_PLUGIN_METADATA(IID "插件接口标识符" FILE "元数据文件.json")
这个宏时这样的,FILE我们暂时不关注。我们要知道的是,得有这一行,这个派生类才会导出生成dll文件。
-
关注点2:Q_INTERFACES宏用于声明一个类实现了哪些接口,填上刚刚写好的接口类的类名。我要明确指出,这个类是实现了Interface这个接口,这样当插件被加载时,才能知道这个类时Interface接口类的具体实现。
至于cpp文件,其实只需要实现print函数的就可以了,我就简单列出来吧
InterfaceImp.cpp:
cpp
#include "InterfaceImp.h"
#include <QDebug>
InterfaceImp::InterfaceImp(QWidget *parent) : QWidget(parent)
, ui(new Ui::InterfaceImpClass())
{
ui->setupUi(this);
}
InterfaceImp::~InterfaceImp()
{
delete ui;
}
//以上代码自动生成,以下时print函数的具体实现
void InterfaceImp::print()
{
qDebug() << "Hello Plugin";
}
3.编译生成该插件项目
其实看完第2步骤,我们整个Qt插件项目只需要三个代码文件,其他代码文件无关痛痒,删掉也没事。对项目编译成功生成后,在生成目录里找,能够找到一个生成的该插件的.dll文件(假设名字就是Interface.dll)。
至此,我们两部分都有了,部分1接口文件有了:interface.h,部分2接口文件也有了:interface.dll。那么我们的插件就完成了。
接下来考虑怎么用了。
关于这两部分怎么存放,其实存放哪里都不是问题,关键是能够然主程序识别到就行。
我们该插件其实就是第三方库,我们可以像第三方库那样规范化存放,创建一个Plugin文件夹,然后里面创建bin文件夹和include文件夹。(因为不是静态链接库,所以不需要Lib文件夹)。我们把bin文件夹路径加入到系统环境变量path下,include文件夹路径加入到主项目的配置中,加到附加包含目录那一项里面(跟平时用第三方库是一样的配置方法)
总之,想方法让主程序能够找到它们。
4.在主程序里调用Qt插件
这里再次重复我的需求:我在主项目下界面点击PushButton按钮,按钮可以调用我待会要写的Qt插件,并输出这个插件的函数------》即在控制台上打印Hello Plugin。
关于QPushButton的添加,槽函数的声明这些,不再赘述。看着我的需求,我就是要在按钮与clicked信号对应的槽函数下去调用刚刚写好的插件。那这一步骤的关键就是,怎么调用?
Qt本身就提供一个插件加载器,QPluginLoader,我们将用它来加载。
我就放按钮的槽函数:
cpp
void MainWindow::buttonSlot()
{
//创建QPluginLoader
QPluginLoader loader;
//创建QDir,用于文件路径操作,我们通过QDir来获取Interface.dll的准确路径
//因为QPluginLoader是根据路径来加载dll文件,所以利用QDir来方便获得它的路径
//此处qApp->applicationDirPath()代表dll文件所在地
QDir dir(qApp->applicationDirPath());
loader.setFileName(dir.filePath("Interface.dll"));
if (!loader.load())
{
QMessageBox::critical(this, "", loader.errorString());
return;
}
//成功加载后,将加载的instance类型转换为Interface,这里意味着要包含Interface.h接口的头文件。
Interface* interface = qobject_cast<Interface*>(loader.instance());
if (interface)
{
//然后正常调用接口提供的方法。
interface->print();
}
}
7. 怎么修改内容和怎么更新
Qt插件写好后,后续要维护接口,例如要增加一些新的接口,那么应该怎么去更新呢?
还是正常在接口类里面写新的虚函数,然后在实现类里面去实现函数。做完后就对改Qt插件项目再次编译生成得到结果dll文件,替换使用此次最新版本的接口头文件和dll文件就完成了更新了。
在VS里面想要方便的话,可以给主程序这个项目添加引用,引用插件项目。这样添加引用后,等到插件项目做出修改了,即使不手动去编译生成,直接去编译运行主项目,VS也会识别到插件项目被修改了,从而先去编译生成插件项目,再去编译运行主程序项目,这样会方便一点。
6. Qt插件与dll动态链接库的区别
看上面加载插件和调用函数的代码你应该能察觉得到不同之处。Qt插件多了个运行时加载的这个步骤,在点击按钮之后才去加载这个插件,即使找不到dll文件,也不会影响主程序的正常运行。而dll动态链接库是拿来直接用,如果dll文件找不到了,主程序就会报错了。