news 2026/9/6 3:22:30

Qt C++插件化编写项目(1)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Qt C++插件化编写项目(1)

插件化编程的特点

  1. 插件化工业数据采集监控平台非常普遍使用,主要有
  • 宿主程序(main.cpp + MainWindow)负责加载插件、管理UI、提供深色工业风界面。

  • 插件系统:所有业务功能均以动态库(.dll)形式存在,通过 PluginInterface 标准接口被宿主发现和管理。

  • 消息总线:以 MessageBusPlugin 插件的形式提供通信能力,内部实现优先级队列、通配符订阅、背压保护、统计监控等工业级特性。
    为什么这种架构在工业领域是主流?

  1. 工业现场环境复杂,需求多变,这种架构能很好地应对挑战:
  • 应对不确定性,实现“热插拔”:工业现场设备种类繁多,通信协议各异(如Modbus、OPC UA、MQTT等)。插件化架构允许软件像搭积木一样,通过加载不同的“协议插件”或“设备驱动插件”来适配新设备,而无需改动核心系统。这种热插拔能力,对于需7x24小时不间断运行的工业系统至关重要。

  • 实现系统解耦,提升稳定性:工业监控系统功能繁多(数据采集、报警、历史存储、可视化等)。消息总线模式将这些功能模块彻底解耦。一个模块的故障(如某个第三方插件崩溃)理论上不会拖垮整个系统,大大提升了系统的健壮性。

  • 促进团队协作,加速开发:在大型工业软件项目中,不同团队可并行开发各自的插件。例如,A团队开发Modbus驱动插件,B团队开发报警插件,他们只需遵守统一的插件接口和消息格式,互不干扰,显著提升开发效率。

  • 行业巨头的共同选择:大厂都采用了类似的设计思想。

所以对插件化项目编程非常有必要进行整体的理解

小demo助理解


这里先来一个小demo,为的是对最简化插件化编程工作流程的理解。这个demo只实现了一个插件,节目上有两个按钮,当点击开始时计数从1开始每秒递增计数,再点击一下停止递增,点击清除按钮计数再从零开始。插件的节目设计如图所示

在主界面放置一个加载插件按钮

mainwindow.h

#ifndefMAINWINDOW_H#defineMAINWINDOW_H#include<QMainWindow>#include<QList>#include<QPluginLoader>#include"PluginInterface.h"QT_BEGIN_NAMESPACEnamespaceUi{classMainWindow;}QT_END_NAMESPACEclassMainWindow:publicQMainWindow{Q_OBJECTpublic:MainWindow(QWidget*parent=nullptr);~MainWindow();privateslots:voidonLoadPlugin();private:Ui::MainWindow*ui;QList<QPluginLoader*>m_loaders;QList<PluginInterface*>m_plugins;QList<QWidget*>m_widgets;};#endif// MAINWINDOW_H

这段代码是宿主程序(应用程序框架)的主窗口头文件,它扮演着“插件容器”和“UI管家”的角色。如果说插件是“可插拔的零件”,那么这个 MainWindow 就是承载这些零件的“机箱”。
< QPluginLoader:是 Qt 提供的动态加载器类。宿主就是靠它,在程序运行时去读取硬盘上的动态库文件,并把插件“拽”进内存的。
< “PluginInterface.h”:这是宿主和插件之间的契约。宿主不关心插件内部的具体实现,只关心它是不是实现了 createWidget、initialize 等方法。
< 核心成员变量

QList<QPluginLoader*>m_loaders;QList<PluginInterface*>m_plugins;QList<QWidget*>m_widgets;

m_loaders(加载器账册):存储 QPluginLoader* 指针。
当点击“加载”选择了一个 .dll 后,就会 new 一个 Loader 并存入这里。作用:当程序关闭或卸载插件时,必须调用 loader->unload(),并且 delete loader,以防止内存泄漏和句柄占用。

m_plugins(插件接口账册):存储 PluginInterface* 指针。
通过 qobject_cast<PluginInterface*>(loader->instance()) 转型成功后存入。作用:当需要释放插件时,可以遍历调用 iface->release(),让插件有机会释放自己内部占用的资源。

m_widgets(界面窗口账册):存储 QWidget* 指针。
插件调用 createWidget(this) 返回的界面,会被存入这里。作用:当宿主退出时,可以统一删除这些窗口(虽然 Qt 父子机制会自动删,但显式管理可以避免野指针风险)。

如果把整个系统比作一台电脑主机:
MainWindow 就是主板,提供了电源(事件循环)和插槽(QStackedWidget)。
m_loaders 是PCIe 接口的驱动程序,负责识别新插入的硬件。
m_plugins 是设备管理器,记录当前插了哪些显卡、网卡。
m_widgets 是显示器,负责把硬件的性能(UI界面)展示给用户看。
这段代码完全没有提及任何具体的业务逻辑(比如报警表格、数据模拟)。它只定义了“如何加载”和“如何管理”,至于加载进来的是计算器还是工业监控,它完全不关心。这就是“面向接口编程”的典型体现。
mainwindow.cpp

#include"mainwindow.h"#include"ui_mainwindow.h"#include<QFileDialog>#include<QMessageBox>#include<QDebug>MainWindow::MainWindow(QWidget*parent):QMainWindow(parent),ui(newUi::MainWindow){ui->setupUi(this);// 连接加载按钮connect(ui->pushButtonLoad,&QPushButton::clicked,this,&MainWindow::onLoadPlugin);}MainWindow::~MainWindow(){for(autoloader:m_loaders){loader->unload();deleteloader;}deleteui;}voidMainWindow::onLoadPlugin(){QString file=QFileDialog::getOpenFileName(this,"选择插件","./bin/plugins","*.dll");if(file.isEmpty())return;QPluginLoader*loader=newQPluginLoader(file,this);QObject*inst=loader->instance();if(!inst){QMessageBox::warning(this,"错误",loader->errorString());deleteloader;return;}PluginInterface*iface=qobject_cast<PluginInterface*>(inst);if(!iface){QMessageBox::warning(this,"错误","不是有效的插件");deleteloader;return;}if(!iface->initialize()){QMessageBox::warning(this,"错误","初始化失败");deleteloader;return;}QWidget*w=iface->createWidget(this);if(w){w->setAttribute(Qt::WA_DeleteOnClose);w->show();m_widgets.append(w);}m_loaders.append(loader);m_plugins.append(iface);qDebug()<<"Loaded:"<<iface->pluginInfo().name;}
loader->instance()

内部调用了操作系统的动态链接 API(Windows 上是 LoadLibrary,Linux 上是 dlopen)。它将 .dll 加载进进程内存,并执行其中的工厂函数(由 Q_PLUGIN_METADATA 宏生成的代码)。

qobject_cast 的工作机制:

它利用 Qt 的元对象系统(MOC)检查 inst 对应的 QMetaObject。只有当插件类在声明时使用了 Q_INTERFACES(PluginInterface) 宏,且编译时生成的元数据中包含了 PLUGIN_INTERFACE_IID,qobject_cast 才会返回正确的 PluginInterface* 指针。
PluginInterface.h

#ifndefPLUGININTERFACE_H#definePLUGININTERFACE_H#include<QObject>#include<QString>#include<QWidget>structPluginInfo{QString name;QString version;};classPluginInterface{public:virtual~PluginInterface(){}virtualPluginInfopluginInfo()const=0;virtualQWidget*createWidget(QWidget*parent=nullptr)=0;virtualboolinitialize()=0;virtualvoidrelease()=0;};#definePLUGIN_INTERFACE_IID"com.demo.PluginInterface/1.0"Q_DECLARE_INTERFACE(PluginInterface,PLUGIN_INTERFACE_IID)#endif

#define PLUGIN_INTERFACE_IID "com.demo.PluginInterface/1.0" Q_DECLARE_INTERFACE(PluginInterface, PLUGIN_INTERFACE_IID)
Q_DECLARE_INTERFACE(PluginInterface, PLUGIN_INTERFACE_IID) 告诉 Qt 的元对象编译器(MOC):

“请把这个类(PluginInterface)和这个字符串(“com.demo.PluginInterface/1.0”)绑定在一起,并写入插件的元数据中。”

运行时校验:当宿主执行 qobject_cast<PluginInterface*>(inst) 时,它并不是去比较 C++ 的类地址,而是去比较 inst 背后携带的 IID 字符串和当前上下文中 PluginInterface 的 IID 字符串是否完全一致。

一致 → 转换成功,返回指针。

不一致(比如插件用的旧版接口 /0.9,宿主用的是新版 /1.0)→ 转换失败,返回 nullptr,程序安全退出。

💡 结论:没有这个宏,qobject_cast 就退化成普通的 C 风格强转,失去跨 DLL 类型安全保障。它是必须的,且绝对不能省略。

counterPlugin.h

#ifndefCOUNTERPLUGIN_H#defineCOUNTERPLUGIN_H#include<QObject>#include"PluginInterface.h"classCounterPlugin:publicQObject,publicPluginInterface{Q_OBJECTQ_INTERFACES(PluginInterface)Q_PLUGIN_METADATA(IID PLUGIN_INTERFACE_IID)public:PluginInfopluginInfo()constoverride{return{"计数器插件","1.0"};}QWidget*createWidget(QWidget*parent)override;boolinitialize()override{returntrue;}voidrelease()override{}};#endif

counterPlugin.cpp

#include"counterplugin.h"#include"counterwidget.h"QWidget*CounterPlugin::createWidget(QWidget*parent){returnnewCounterWidget(parent);}

为什么要多重继承?
插件必须是 QObject 的子类(因为需要元对象信息、信号槽和 qobject_cast 支持),同时又必须是 PluginInterface 的子类(以满足宿主的业务契约)。C++ 的多重继承在这里完美地将“Qt 基础设施”和“业务接口”结合起来。

Q_OBJECTQ_INTERFACES(PluginInterface)Q_PLUGIN_METADATA(IID PLUGIN_INTERFACE_IID)

这是插件类区别于普通类的最关键三行:

Q_OBJECT:激活 Qt 元对象系统。它让 MOC(元对象编译器)为这个类生成 metaObject()、className() 等关键方法。如果没有这个宏,qobject_cast 将完全失效。

Q_INTERFACES(PluginInterface):向元对象系统显式声明:“我实现了 PluginInterface 接口”。

宿主执行 qobject_cast<PluginInterface*>(inst) 时,会检查 inst 的元对象中是否注册了 PLUGIN_INTERFACE_IID。这个宏就是完成注册的关键步骤。

Q_PLUGIN_METADATA(IID PLUGIN_INTERFACE_IID):这是真正的“导出工厂”。

它在编译时生成一个全局唯一的工厂函数,这个函数会被 QPluginLoader 调用。

它将 PLUGIN_INTERFACE_IID(接口 ID)写入 DLL 的元数据段。宿主在不加载整个 DLL 的情况下,就能读取这个 IID 来判断是否匹配。

关键点:正是因为有这个宏,我们才完全不需要手动写 __declspec(dllexport),也不需要那个烦人的 _global.h 文件。

counterwidget.h

#ifndefCOUNTERWIDGET_H#defineCOUNTERWIDGET_H#include<QWidget>#include<QTimer>namespaceUi{classCounterWidget;}classCounterWidget:publicQWidget{Q_OBJECTpublic:explicitCounterWidget(QWidget*parent=nullptr);~CounterWidget();privateslots:voidonStartClicked();voidonClearClicked();voidonTimeout();private:Ui::CounterWidget*ui;QTimer m_timer;intm_count=0;};#endif

counterwidget.cpp

#include"counterwidget.h"#include"ui_counterwidget.h"CounterWidget::CounterWidget(QWidget*parent):QWidget(parent),ui(newUi::CounterWidget){ui->setupUi(this);connect(ui->btnStart,&QPushButton::clicked,this,&CounterWidget::onStartClicked);connect(ui->btnClear,&QPushButton::clicked,this,&CounterWidget::onClearClicked);connect(&m_timer,&QTimer::timeout,this,&CounterWidget::onTimeout);m_timer.setInterval(1000);ui->lcdNumber->display("0");}CounterWidget::~CounterWidget(){deleteui;}voidCounterWidget::onStartClicked(){if(m_timer.isActive()){m_timer.stop();ui->btnStart->setText("继续");}else{m_timer.start();ui->btnStart->setText("暂停");}}voidCounterWidget::onClearClicked(){m_count=0;ui->lcdNumber->display("0");}voidCounterWidget::onTimeout(){m_count++;ui->lcdNumber->display(QString::number(m_count));}

这就是具体的业务类了,插件的实现就在这里

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/6 3:18:24

Python 自动化办公实战:用代码提升日常工作效率

前言在日常工作中&#xff0c;我们经常会遇到一些重复性任务&#xff0c;例如批量重命名文件、整理文件夹、处理 Excel 表格、生成统计报表、发送通知邮件等。这些工作虽然难度不高&#xff0c;却非常耗费时间。如果每天都依靠手工操作&#xff0c;不仅效率较低&#xff0c;还容…

作者头像 李华
网站建设 2026/9/6 3:17:49

iPhone XS Max OLED屏幕烧屏修复指南:软件校准与电池优化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/6 3:13:48

河北昂纳建材携手一网推geo 关键词优化赋能业绩长效增长

建材家装行业流量竞争日趋白热化,精准锁定目标客群、挖掘长尾搜索流量,成为建材企业突破获客瓶颈的关键。河北昂纳建材有限公司主营矿棉板、高晶板、烤漆龙骨、玻纤板等建材产品,依托【一网推 geo 河北本地服务中心】的定制化 GEO 优化运营服务,搭配一网推总部招财兔 GEO 工具箱…

作者头像 李华
网站建设 2026/9/6 3:13:03

SpringBoot3 + Vue3 失物招领系统:从表设计到部署的完整工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华