Qt插件机制demo示例

我想学习一下Qt的插件机制,你可以给我写一个简单的demo吗


项目结构

cpp 复制代码
PluginTest/
├── PluginTest.pro                              # 顶层 subdirs 工程
├── common/
│   └── plugininterface.h                       # 共享接口(契约)
├── app/                                        # 宿主程序
│   ├── app.pro
│   ├── main.cpp
│   ├── mainwindow.h
│   └── mainwindow.cpp
└── plugins/
    └── helloworld/                             # 示例插件
        ├── helloworld.pro
        ├── helloworld.h
        ├── helloworld.cpp
        └── helloworld.json

架构解读

上面这张图把 Qt 插件机制拆成了 两条时间线 :

编译期 (灰线):宿主和插件都`#include` 同一份接口头文件,但各自独立编译。宿主完全不知道插件类名、源码、实现细节,它只认接口。这就是插件机制能做到"运行时才知道对方"的根本原因。

运行时 (紫色虚线):用户点击"加载插件"后, mainwindow.cpp 里的`QPluginLoader` 把磁盘上的`helloworld.dll` 加载进进程,调用`instance()` 实例化对象,再用`qobject_cast<PluginInterface*>` 验证它确实实现了这个接口。


效果示例:

5 个必须记住的关键点

宏 / 类 出现位置 作用
Q_DECLARE_INTERFACE plugininterface.h 给接口注册全局唯一 iid

plugininterface.h

cpp 复制代码
#ifndef PLUGININTERFACE_H
#define PLUGININTERFACE_H

#include <QString>

/**
 * 插件接口(纯虚类)
 *
 * 宿主程序和插件都引用这个头文件,但它们各自独立编译。
 * 宿主程序只看到接口,看不到插件的具体实现 ------ 这正是插件机制的核心。
 *
 * 关键宏说明:
 * - Q_DECLARE_INTERFACE:将该接口注册到 Qt 的元对象系统,
 *   关联一个全局唯一的字符串 iid(接口标识符)。
 *   运行时 QPluginLoader 通过 iid 找到插件中实现了该接口的对象。
 */
class PluginInterface
{
public:
    virtual ~PluginInterface() = default;

    // 插件名称(展示用)
    virtual QString name() const = 0;

    // 插件描述(展示用)
    virtual QString description() const = 0;

    // 插件要做的事(由插件实现具体行为)
    virtual void doSomething() = 0;
};

// 接口唯一标识符:组织域名 + 接口名 + 版本
#define PluginInterface_iid "org.example.PluginInterface/1.0"

Q_DECLARE_INTERFACE(PluginInterface, PluginInterface_iid)

#endif // PLUGININTERFACE_H

helloworld.h

|---------------------|-----------------|--------------------------------------------|
| Q_INTERFACES | helloworld.h | 声明本类实现了哪个接口(让 qobject_cast 能识别) |
| Q_PLUGIN_METADATA | helloworld.h:24 | 把iid + 元数据写入二进制(QPluginLoader 靠它实例化) |

cpp 复制代码
#ifndef HELLOWORLD_H
#define HELLOWORLD_H

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

/**
 * 一个具体的插件实现。
 *
 * 关键三件套(缺一不可):
 * 1. 同时继承 QObject 和 PluginInterface(多继承)。
 * 2. Q_INTERFACES(PluginInterface) ------ 告诉 Qt 元对象系统:
 *    "本类实现了 PluginInterface 这个接口",这样宿主用 qobject_cast 才能成功。
 * 3. Q_PLUGIN_METADATA ------ 在编译期把插件元数据写进二进制,
 *    QPluginLoader::instance() 时靠它实例化对象。IID 必须与接口的 iid 一致。
 */
class HelloWorld : public QObject, public PluginInterface
{
    Q_OBJECT
    Q_INTERFACES(PluginInterface)
    Q_PLUGIN_METADATA(IID PluginInterface_iid FILE "helloworld.json")

public:
    explicit HelloWorld(QObject *parent = nullptr);

    QString name() const override;
    QString description() const override;
    void doSomething() override;
};

#endif // HELLOWORLD_H

|-----------------|----------------|------------------------|
| QPluginLoader | mainwindow.cpp | 运行时加载 dll/so 并创建对象 |
| qobject_cast | mainwindow.cpp | 安全类型转换,验证对象实现了接口 |

cpp 复制代码
    // 遍历目录下每个文件,尝试用 QPluginLoader 加载
    for (const QFileInfo &info : pluginsDir.entryInfoList(QDir::Files)) {
        QString filePath = info.absoluteFilePath();

        QPluginLoader loader(filePath);
        QObject *obj = loader.instance();   // 实例化插件对象(若已实例则返回同一指针)
        if (!obj) {
            qWarning() << "加载失败:" << filePath << " -> " << loader.errorString();
            continue;
        }

        // 用 qobject_cast 验证对象是否实现了 PluginInterface
        PluginInterface *plugin = qobject_cast<PluginInterface*>(obj);
        if (!plugin) {
            delete obj;   // 不是我们要的接口类型,释放掉
            continue;
        }

        m_plugins.append(plugin);
        m_listWidget->addItem(plugin->name() + "  ------  " + plugin->description());
        ++count;
    }

该程序的第一版是有问题的:

bash 复制代码
加载失败: 
"D:/MyCode/PluginTest/build_app/app/debug/plugins/libhelloworld.a"  
->  
"Failed to extract plugin meta data from 
'D:/MyCode/PluginTest/build_app/app/debug/plugins/libhelloworld.a'"

原因 :MinGW 构建共享库时,会同时生成两个文件 ------`helloworld.dll` (真正的插件,能加载)和`libhelloworld.a` (导入库,只供链接期使用)。

原代码用`entryInfoList(QDir::Files)` 无差别扫描所有文件,把`.a` 也喂给`QPluginLoader` ,自然提取不出插件元数据。

修法 :加上`setNameFilters` ,按平台只挑真正的插件扩展名(Windows 走`.dll` 、macOS 走`.dylib` 、Linux 走`.so`)。这是 Qt 官方插件示例的标准写法。




cpp 复制代码
    PluginInterface *plugin = m_plugins.at(row);
    plugin->doSomething();   // 多态调用 ------ 实际执行的是插件里实现的方法

点击调用插件时,实际执行的逻辑。


再加一个插件吧

完成。新增的`sysinfo` 插件结构与`helloworld` 完全对称:

plugins/sysinfo/

├── sysinfo.pro # 与 helloworld.pro 同模板,TARGET=sysinfo

├── sysinfo.h # SysInfo 类,同样继承 QObject + PluginInterface

├── sysinfo.cpp # doSomething() 显示时间 / Qt 版本 / 操作系统

└── sysinfo.json # 元数据

关键点:

  • 接口不变 :`sysinfo.h` 用的还是`common/plugininterface.h` 那份契约,只是实现完全不同。

  • 宿主零改动 : mainwindow.cpp 一行都不用改 ------ 它扫到`plugins/sysinfo.dll` 后会自动通过`qobject_cast<PluginInterface*>` 验证并加进列表。

相关推荐
Quz4 小时前
QML Flickable:内容滚动与边界行为
qt
Quz4 小时前
QML Flipable:卡片翻转与登录设置
qt
阿钱真强道12 小时前
24 嵌入式操作系统 | 项目启动与环境准备(PyQt5 / Flask 两个“最小可跑“)
开发语言·qt·flask·架构设计·协议契约
物极必反102415 小时前
QT使用FFMpeg编译安卓程序.md
qt·ffmpeg·安卓
励志不掉头发的内向程序员19 小时前
【从零写一个CAD 03】三个 double 值得单独一个类吗:把视图变换抽成 View
开发语言·c++·qt·学习·系统架构
sycmancia1 天前
Qml——第一个qml的程序
qt
实心儿儿1 天前
Qt — Qt 多线程
开发语言·qt
沫璃染墨2 天前
《Qt从零入门系列(十一):Qt事件机制详解——从QEvent到鼠标、键盘与定时器事件》
c++·qt·ui·硬件工程·交互·个人开发·qt5
实心儿儿2 天前
Qt — Qt 窗口
开发语言·qt