FEATURED · 精选文章

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

发布时间 / 2026/9/6 3:23:07
来源 / 创域科博编辑部
栏目 / 资讯中心
Qt C++插件化编写项目(1) 插件化编程的特点插件化工业数据采集监控平台非常普遍使用主要有宿主程序main.cpp MainWindow负责加载插件、管理UI、提供深色工业风界面。插件系统所有业务功能均以动态库.dll形式存在通过 PluginInterface 标准接口被宿主发现和管理。消息总线以 MessageBusPlugin 插件的形式提供通信能力内部实现优先级队列、通配符订阅、背压保护、统计监控等工业级特性。为什么这种架构在工业领域是主流工业现场环境复杂需求多变这种架构能很好地应对挑战应对不确定性实现“热插拔”工业现场设备种类繁多通信协议各异如Modbus、OPC UA、MQTT等。插件化架构允许软件像搭积木一样通过加载不同的“协议插件”或“设备驱动插件”来适配新设备而无需改动核心系统。这种热插拔能力对于需7x24小时不间断运行的工业系统至关重要。实现系统解耦提升稳定性工业监控系统功能繁多数据采集、报警、历史存储、可视化等。消息总线模式将这些功能模块彻底解耦。一个模块的故障如某个第三方插件崩溃理论上不会拖垮整个系统大大提升了系统的健壮性。促进团队协作加速开发在大型工业软件项目中不同团队可并行开发各自的插件。例如A团队开发Modbus驱动插件B团队开发报警插件他们只需遵守统一的插件接口和消息格式互不干扰显著提升开发效率。行业巨头的共同选择大厂都采用了类似的设计思想。所以对插件化项目编程非常有必要进行整体的理解小demo助理解这里先来一个小demo为的是对最简化插件化编程工作流程的理解。这个demo只实现了一个插件节目上有两个按钮当点击开始时计数从1开始每秒递增计数再点击一下停止递增点击清除按钮计数再从零开始。插件的节目设计如图所示在主界面放置一个加载插件按钮mainwindow.h#ifndefMAINWINDOW_H#defineMAINWINDOW_H#includeQMainWindow#includeQList#includeQPluginLoader#includePluginInterface.hQT_BEGIN_NAMESPACEnamespaceUi{classMainWindow;}QT_END_NAMESPACEclassMainWindow:publicQMainWindow{Q_OBJECTpublic:MainWindow(QWidget*parentnullptr);~MainWindow();privateslots:voidonLoadPlugin();private:Ui::MainWindow*ui;QListQPluginLoader*m_loaders;QListPluginInterface*m_plugins;QListQWidget*m_widgets;};#endif// MAINWINDOW_H这段代码是宿主程序应用程序框架的主窗口头文件它扮演着“插件容器”和“UI管家”的角色。如果说插件是“可插拔的零件”那么这个 MainWindow 就是承载这些零件的“机箱”。 QPluginLoader是 Qt 提供的动态加载器类。宿主就是靠它在程序运行时去读取硬盘上的动态库文件并把插件“拽”进内存的。 “PluginInterface.h”这是宿主和插件之间的契约。宿主不关心插件内部的具体实现只关心它是不是实现了 createWidget、initialize 等方法。 核心成员变量QListQPluginLoader*m_loaders;QListPluginInterface*m_plugins;QListQWidget*m_widgets;m_loaders加载器账册存储 QPluginLoader* 指针。当点击“加载”选择了一个 .dll 后就会 new 一个 Loader 并存入这里。作用当程序关闭或卸载插件时必须调用 loader-unload()并且 delete loader以防止内存泄漏和句柄占用。m_plugins插件接口账册存储 PluginInterface* 指针。通过 qobject_castPluginInterface*(loader-instance()) 转型成功后存入。作用当需要释放插件时可以遍历调用 iface-release()让插件有机会释放自己内部占用的资源。m_widgets界面窗口账册存储 QWidget* 指针。插件调用 createWidget(this) 返回的界面会被存入这里。作用当宿主退出时可以统一删除这些窗口虽然 Qt 父子机制会自动删但显式管理可以避免野指针风险。如果把整个系统比作一台电脑主机MainWindow 就是主板提供了电源事件循环和插槽QStackedWidget。m_loaders 是PCIe 接口的驱动程序负责识别新插入的硬件。m_plugins 是设备管理器记录当前插了哪些显卡、网卡。m_widgets 是显示器负责把硬件的性能UI界面展示给用户看。这段代码完全没有提及任何具体的业务逻辑比如报警表格、数据模拟。它只定义了“如何加载”和“如何管理”至于加载进来的是计算器还是工业监控它完全不关心。这就是“面向接口编程”的典型体现。mainwindow.cpp#includemainwindow.h#includeui_mainwindow.h#includeQFileDialog#includeQMessageBox#includeQDebugMainWindow::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 fileQFileDialog::getOpenFileName(this,选择插件,./bin/plugins,*.dll);if(file.isEmpty())return;QPluginLoader*loadernewQPluginLoader(file,this);QObject*instloader-instance();if(!inst){QMessageBox::warning(this,错误,loader-errorString());deleteloader;return;}PluginInterface*ifaceqobject_castPluginInterface*(inst);if(!iface){QMessageBox::warning(this,错误,不是有效的插件);deleteloader;return;}if(!iface-initialize()){QMessageBox::warning(this,错误,初始化失败);deleteloader;return;}QWidget*wiface-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()内部调用了操作系统的动态链接 APIWindows 上是 LoadLibraryLinux 上是 dlopen。它将 .dll 加载进进程内存并执行其中的工厂函数由 Q_PLUGIN_METADATA 宏生成的代码。qobject_cast 的工作机制它利用 Qt 的元对象系统MOC检查 inst 对应的 QMetaObject。只有当插件类在声明时使用了 Q_INTERFACES(PluginInterface) 宏且编译时生成的元数据中包含了 PLUGIN_INTERFACE_IIDqobject_cast 才会返回正确的 PluginInterface* 指针。PluginInterface.h#ifndefPLUGININTERFACE_H#definePLUGININTERFACE_H#includeQObject#includeQString#includeQWidgetstructPluginInfo{QString name;QString version;};classPluginInterface{public:virtual~PluginInterface(){}virtualPluginInfopluginInfo()const0;virtualQWidget*createWidget(QWidget*parentnullptr)0;virtualboolinitialize()0;virtualvoidrelease()0;};#definePLUGIN_INTERFACE_IIDcom.demo.PluginInterface/1.0Q_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_castPluginInterface*(inst) 时它并不是去比较 C 的类地址而是去比较 inst 背后携带的 IID 字符串和当前上下文中 PluginInterface 的 IID 字符串是否完全一致。一致 → 转换成功返回指针。不一致比如插件用的旧版接口 /0.9宿主用的是新版 /1.0→ 转换失败返回 nullptr程序安全退出。 结论没有这个宏qobject_cast 就退化成普通的 C 风格强转失去跨 DLL 类型安全保障。它是必须的且绝对不能省略。counterPlugin.h#ifndefCOUNTERPLUGIN_H#defineCOUNTERPLUGIN_H#includeQObject#includePluginInterface.hclassCounterPlugin: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{}};#endifcounterPlugin.cpp#includecounterplugin.h#includecounterwidget.hQWidget*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_castPluginInterface*(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#includeQWidget#includeQTimernamespaceUi{classCounterWidget;}classCounterWidget:publicQWidget{Q_OBJECTpublic:explicitCounterWidget(QWidget*parentnullptr);~CounterWidget();privateslots:voidonStartClicked();voidonClearClicked();voidonTimeout();private:Ui::CounterWidget*ui;QTimer m_timer;intm_count0;};#endifcounterwidget.cpp#includecounterwidget.h#includeui_counterwidget.hCounterWidget::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_count0;ui-lcdNumber-display(0);}voidCounterWidget::onTimeout(){m_count;ui-lcdNumber-display(QString::number(m_count));}这就是具体的业务类了插件的实现就在这里
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻