我想学习一下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*>` 验证并加进列表。
