在Visual Studio 2022中实现Qt插件开发

在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文件找不到了,主程序就会报错了。

相关推荐
科技小花4 小时前
数据治理平台架构演进观察:AI原生设计如何重构企业数据管理范式
数据库·重构·架构·数据治理·ai-native·ai原生
一江寒逸4 小时前
零基础从入门到精通MySQL(中篇):进阶篇——吃透多表查询、事务核心与高级特性,搞定复杂业务SQL
数据库·sql·mysql
D4c-lovetrain4 小时前
linux个人心得22 (mysql)
数据库·mysql
阿里小阿希5 小时前
CentOS7 PostgreSQL 9.2 升级到 15 完整教程
数据库·postgresql
荒川之神5 小时前
Oracle 数据仓库雪花模型设计(完整实战方案)
数据库·数据仓库·oracle
做个文艺程序员5 小时前
MySQL安全加固十大硬核操作
数据库·mysql·安全
不吃香菜学java5 小时前
Redis简单应用
数据库·spring boot·tomcat·maven
计算机安禾5 小时前
【数据结构与算法】第36篇:排序大总结:稳定性、时间复杂度与适用场景
c语言·数据结构·c++·算法·链表·线性回归·visual studio
一个天蝎座 白勺 程序猿5 小时前
Apache IoTDB(15):IoTDB查询写回(INTO子句)深度解析——从语法到实战的ETL全链路指南
数据库·apache·etl·iotdb
无限进步_6 小时前
【C++】电话号码的字母组合:从有限处理到通用解法
开发语言·c++·ide·windows·git·github·visual studio