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

cpp 复制代码
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QList>
#include <QPluginLoader>
#include "PluginInterface.h"

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private slots:
    void onLoadPlugin();
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 等方法。

< 核心成员变量

cpp 复制代码
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

cpp 复制代码
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QFileDialog>
#include <QMessageBox>
#include <QDebug>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent), ui(new Ui::MainWindow) {
    ui->setupUi(this);

    // 连接加载按钮
    connect(ui->pushButtonLoad, &QPushButton::clicked, this, &MainWindow::onLoadPlugin);
}

MainWindow::~MainWindow() {
    for (auto loader : m_loaders) {
        loader->unload();
        delete loader;
    }
    delete ui;
}

void MainWindow::onLoadPlugin() {
    QString file = QFileDialog::getOpenFileName(this, "选择插件", "./bin/plugins", "*.dll");
    if (file.isEmpty()) return;

    QPluginLoader* loader = new QPluginLoader(file, this);
    QObject* inst = loader->instance();
    if (!inst) {
        QMessageBox::warning(this, "错误", loader->errorString());
        delete loader;
        return;
    }

    PluginInterface* iface = qobject_cast<PluginInterface*>(inst);
    if (!iface) {
        QMessageBox::warning(this, "错误", "不是有效的插件");
        delete loader;
        return;
    }

    if (!iface->initialize()) {
        QMessageBox::warning(this, "错误", "初始化失败");
        delete loader;
        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;
}
cpp 复制代码
loader->instance() 

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

cpp 复制代码
qobject_cast 的工作机制:

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

PluginInterface.h

cpp 复制代码
#ifndef PLUGININTERFACE_H
#define PLUGININTERFACE_H

#include <QObject>
#include <QString>
#include <QWidget>

struct PluginInfo {
    QString name;
    QString version;
};

class PluginInterface {
public:
    virtual ~PluginInterface() {}
    virtual PluginInfo pluginInfo() const = 0;
    virtual QWidget* createWidget(QWidget* parent = nullptr) = 0;
    virtual bool initialize() = 0;
    virtual void release() = 0;
};

#define PLUGIN_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

cpp 复制代码
#ifndef COUNTERPLUGIN_H
#define COUNTERPLUGIN_H

#include <QObject>
#include "PluginInterface.h"

class CounterPlugin : public QObject, public PluginInterface {
    Q_OBJECT
    Q_INTERFACES(PluginInterface)
    Q_PLUGIN_METADATA(IID PLUGIN_INTERFACE_IID)

public:
    PluginInfo pluginInfo() const override {
        return {"计数器插件", "1.0"};
    }
    QWidget* createWidget(QWidget* parent) override;
    bool initialize() override { return true; }
    void release() override {}
};

#endif

counterPlugin.cpp

cpp 复制代码
#include "counterplugin.h"
#include "counterwidget.h"

QWidget* CounterPlugin::createWidget(QWidget* parent) {
    return new CounterWidget(parent);
}

为什么要多重继承?

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

cpp 复制代码
 Q_OBJECT
 Q_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

cpp 复制代码
#ifndef COUNTERWIDGET_H
#define COUNTERWIDGET_H

#include <QWidget>
#include <QTimer>

namespace Ui {
class CounterWidget;
}

class CounterWidget : public QWidget {
    Q_OBJECT
public:
    explicit CounterWidget(QWidget *parent = nullptr);
    ~CounterWidget();

private slots:
    void onStartClicked();
    void onClearClicked();
    void onTimeout();

private:
    Ui::CounterWidget *ui;
    QTimer m_timer;
    int m_count = 0;
};
#endif

counterwidget.cpp

cpp 复制代码
#include "counterwidget.h"
#include "ui_counterwidget.h"

CounterWidget::CounterWidget(QWidget *parent)
    : QWidget(parent), ui(new Ui::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() {
    delete ui;
}

void CounterWidget::onStartClicked() {
    if (m_timer.isActive()) {
        m_timer.stop();
        ui->btnStart->setText("继续");
    } else {
        m_timer.start();
        ui->btnStart->setText("暂停");
    }
}

void CounterWidget::onClearClicked() {
    m_count = 0;
    ui->lcdNumber->display("0");
}

void CounterWidget::onTimeout() {
    m_count++;
    ui->lcdNumber->display(QString::number(m_count));
}

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

相关推荐
见叶之秋1 小时前
【C++】C++ 核心进阶(一):泛型编程基石 —— 模板初阶与 STL 体系开篇
开发语言·c++
Escalating_xu1 小时前
【Makefile 进阶】从自动发现源文件、模式规则到目录分离与多模块递归构建
linux·开发语言
艾莉丝努力练剑1 小时前
【AI大模型接入SDK】Gemini模型接入知识体系
java·开发语言·网络·人工智能·网络协议·学习·http
传奇开心果编程1 小时前
【Rust入门知识点学与练】第13课:枚举 Enum
开发语言·学习·rust
一木 之林1 小时前
四、STL 容器与数据结构(进阶)(二)
数据结构·c++·哈希算法
zhanghaha13141 小时前
Python进阶教程:28_queue 队列模块 零基础超详细教程
java·开发语言·python
司小豆1 小时前
第七课:DeepSeek Harness 服务与依赖注入
java·服务器·开发语言·github·ai编程
会周易的程序员1 小时前
5Draft使用说明书
服务器·c++·分布式·raft·共识
weixin199701080162 小时前
[特殊字符]《闲鱼 + 淘宝 + 1688 三平台库存同源:二手ERP主数据治理与超卖防御》(附Python源码)
开发语言·python