构建自己的Qt插件系统

简介: 简述Qt 本身提供了插件相关的技术,但并没有提供一个通用的插件框架!倘若要开发一个较大的 GUI 应用程序,并希望使其可扩展,那么拥有这样一个插件框架无疑会带来很大的好处。

简述

Qt 本身提供了插件相关的技术,但并没有提供一个通用的插件框架!倘若要开发一个较大的 GUI 应用程序,并希望使其可扩展,那么拥有这样一个插件框架无疑会带来很大的好处。

根据 深入理解插件系统 一文,对插件系统有了一定的了解之后,我们可以很快的构建一个属于自己的 Qt 插件系统。

版权所有:一去丶二三里,转载请注明出处:http://blog.csdn.net/liang19890820

插件系统的构成

插件系统,可以分为三部分:

  • 主系统
    通过插件管理器加载插件,并创建插件对象。一旦插件对象被创建,主系统就会获得相应的指针/引用,它可以像任何其他对象一样使用。

  • 插件管理器
    用于管理插件的生命周期,并将其暴露给主系统。它负责查找并加载插件,初始化它们,并且能够进行卸载。它还应该让主系统迭代加载的插件或注册的插件对象。

  • 插件
    插件本身应符合插件管理器协议,并提供符合主系统期望的对象。

实际上,很少能看到这样一个相对独立的分离,插件管理器通常与主系统紧密耦合,因为插件管理器需要最终提供(定制)某些类型的插件对象的实例。

程序流

框架的基本程序流,如下所示:

这里写图片描述

插件管理器

上面提到,插件管理器有一个职责 - 加载插件。那么,是不是所有的插件都需要加载呢?当然不是!只有符合我们约定的插件,才会被认为是标准的、有效的插件,外来插件一律认定为无效。

为了解决这个问题,可以为插件设定一个“标识” - plugin.h

#pragma once

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

class PLUGIN_EXPORT Plugin : public QObject
{
    Q_OBJECT

public:
    Plugin() {}
    virtual ~Plugin() {}
};

#define PluginInterface_iid "com.github.Waleon.PluginInterface"
Q_DECLARE_INTERFACE(Plugin, PluginInterface_iid)

后期实现的所有插件,都必须继承自 Plugin,这样才会被认定是自己的插件,以防外部插件注入。

注意: 使用 Q_DECLARE_INTERFACE 宏,将 Plugin 接口与标识符一起公开。

插件的基本约束有了,插件的具体实现插件管理器并不关心,它所要做的工作是加载插件、卸载插件、检测插件的依赖、以及扫描插件的元数据(Json 文件中的内容)。。。为了便于操作,将其实现为一个单例。

plugin_manager.h 内容如下:

#pragma once

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

class QPluginLoader;
class PluginManagerPrivate;

class PLUGIN_EXPORT PluginManager : public QObject
{
    Q_OBJECT

public:
    static PluginManager *instance();

    // 加载所有插件
    void loadAll();

    // 扫描 JSON 文件中的插件元数据
    void scan(const QString& path);

    // 加载插件
    void load(const QString& path);

    // 卸载所有插件
    void unloadAll();

    // 卸载插件
    void unload(const QString& path);

    // 获取所有插件
    QList<QPluginLoader *> plugins();

private:
    PluginManager();
    ~PluginManager();

private:
    static PluginManager *m_instance;
    PluginManagerPrivate *d;
};

可以看到,插件管理器中有一个 d 指针,它包含了插件元数据的哈希表。此外,由于其拥有所有插件的元数据,所以还为其赋予了另外一个职能 - 检测插件的依赖关系:

/********** PluginManagerPrivate **********/

class PluginManagerPrivate
{
public:
    bool check(const QString& path);

public:
    QHash<QString, QVariant> m_names;  // 插件路径 - 名称
    QHash<QString, QVariant> m_versions;  // 插件路径 - 版本
    QHash<QString, QVariantList> m_dependencies;  // 插件路径 - 其额外依赖的插件
    QHash<QString, QPluginLoader *> m_loaders;  // 插件路径 - QPluginLoader 实例
};

// 检测插件依赖
bool PluginManagerPrivate::check(const QString& path)
{
    bool status = true;

    foreach (QVariant item, m_dependencies.value(path)) {
        QVariantMap map = item.toMap();
        // 依赖的插件名称、版本、路径
        QVariant name = map.value("name");
        QVariant version = map.value("version");
        QString path = m_names.key(name);

        /********** 检测插件是否依赖于其他插件 **********/
        // 先检测插件名称
        if (!m_names.values().contains(name)) {
            qDebug() << Q_FUNC_INFO << "  Missing dependency:" << name.toString() << "for plugin" << path;
            status = false;
            continue;
        }

        // 再检测插件版本
        if (m_versions.value(path) != version) {
            qDebug() << Q_FUNC_INFO << "    Version mismatch:" << name.toString() << "version"
                     << m_versions.value(m_names.key(name)).toString() << "but" << version.toString() << "required for plugin" << path;
            status = false;
            continue;
        }

        // 然后,检测被依赖的插件是否还依赖于另外的插件
        if (!check(path)) {
            qDebug() << Q_FUNC_INFO << "Corrupted dependency:" << name.toString() << "for plugin" << path;
            status = false;
            continue;
        }
    }

    return status;
}

注意: 这里的 check() 是一个递归调用,因为很有可能存在“插件A”依赖于“插件B”,而“插件B”又依赖于“插件C”的连续依赖情况。

PluginManagerPrivate 中的哈希表在初始化插件管理器时被填充:

// 加载所有插件
void PluginManager::loadAll()
{
    // 进入插件目录
    QDir path = QDir(qApp->applicationDirPath());
    path.cd("plugins");

    // 初始化插件中的元数据
    foreach (QFileInfo info, path.entryInfoList(QDir::Files | QDir::NoDotAndDotDot))
        scan(info.absoluteFilePath());

    // 加载插件
    foreach (QFileInfo info, path.entryInfoList(QDir::Files | QDir::NoDotAndDotDot))
        load(info.absoluteFilePath());
}

元数据的具体扫描由 scan() 负责:

// 扫描 JSON 文件中的插件元数据
void PluginManager::scan(const QString& path)
{
    /*****
     *  判断是否是库(后缀有效性)
     * Windows:.dll、.DLL
     * Unix/Linux:.so
   *****/

    if (!QLibrary::isLibrary(path))
        return;

    // 获取元数据(名称、版本、依赖)
    QPluginLoader *loader = new QPluginLoader(path);
    QJsonObject json = loader->metaData().value("MetaData").toObject();

    d->m_names.insert(path, json.value("name").toVariant());
    d->m_versions.insert(path, json.value("version").toVariant());
    d->m_dependencies.insert(path, json.value("dependencies").toArray().toVariantList());

    delete loader;
    loader = Q_NULLPTR;
}

一旦所有元数据被扫描,便可以检查是否能够加载插件:

// 加载插件
void PluginManager::load(const QString& path)
{
    // 判断是否是库
    if (!QLibrary::isLibrary(path))
        return;

    // 检测插件依赖
    if (!d->check(path))
        return;

    // 加载插件
    QPluginLoader *loader = new QPluginLoader(path);
    if (loader->load()) {
        // 如果继承自 Plugin,则认为是自己的插件(防止外部插件注入)。
        Plugin *plugin = qobject_cast<Plugin *>(loader->instance());
        if (plugin) {
            d->m_loaders.insert(path, loader);
        } else {
            delete loader;
            loader = Q_NULLPTR;
        }
    }
}

注意: 这里用到了前面提到的标识 - Plugin,只有 qobject_cast 转换成功,才会加载到主系统中,这可以算作是真正意义上的第一道防线。

实际上,在内部检查是通过调用 PluginManagerPrivate::check() 递归地查询依赖元数据来完成的。

bool PluginManagerPrivate::check(const QString& path)
{
    bool status = true;

    foreach (QVariant item, m_dependencies.value(path)) {
        QVariantMap map = item.toMap();
        // 依赖的插件名称、版本、路径
        QVariant name = map.value("name");
        QVariant version = map.value("version");
        QString path = m_names.key(name);

        /********** 检测插件是否依赖于其他插件 **********/
        // 先检测插件名称
        if (!m_names.values().contains(name)) {
            qDebug() << Q_FUNC_INFO << "  Missing dependency:" << name.toString() << "for plugin" << path;
            status = false;
            continue;
        }

        // 再检测插件版本
        if (m_versions.value(path) != version) {
            qDebug() << Q_FUNC_INFO << "    Version mismatch:" << name.toString() << "version"
                     << m_versions.value(m_names.key(name)).toString() << "but" << version.toString() << "required for plugin" << path;
            status = false;
            continue;
        }

        // 然后,检测被依赖的插件是否还依赖于另外的插件
        if (!check(path)) {
            qDebug() << Q_FUNC_INFO << "Corrupted dependency:" << name.toString() << "for plugin" << path;
            status = false;
            continue;
        }
    }

    return status;
}

插件卸载的过程正好相反:

// 卸载所有插件
void PluginManager::unloadAll()
{
    foreach (const QString &path, d->m_loaders.keys())
        unload(path);
}

而具体的卸载由 unload() 来完成:

// 卸载插件
void PluginManager::unload(const QString& path)
{
    QPluginLoader *loader = d->m_loaders.value(path);

    // 卸载插件,并从内部数据结构中移除
    if (loader->unload()) {
        d->m_loaders.remove(path);
        delete loader;
        loader = Q_NULLPTR;
    }
}

万事俱备,然后返回所有的插件,以便主系统访问:

QList<QPluginLoader *> PluginManager::plugins()
{
    return d->m_loaders.values();
}

这样,整个插件管理的机制已经建立起来了,万里长征第一步。。。那剩下的事基本就比较简单了!插件的编写、插件之间的交互。。。

未完,待续。。。

目录
相关文章
|
6月前
|
开发框架 Linux API
Qt:构建高效且用户友好的跨平台应用
Qt:构建高效且用户友好的跨平台应用
|
1月前
|
存储 Windows
(13) Qt事件系统(two)
文章详细介绍了Qt事件系统,包括事件分发、自定义事件、事件传播机制、事件过滤以及事件与信号的区别。
71 3
(13) Qt事件系统(two)
|
1月前
|
编解码 程序员
(12)Qt事件系统(one)
本文详细介绍了Qt事件系统,包括各种系统事件、鼠标事件、键盘事件、定时器等的处理方法和示例代码。
66 0
(12)Qt事件系统(one)
|
2月前
|
计算机视觉
基于QT的opencv插件框架qtCvFrameLearn实战
这篇文章详细介绍了如何基于Qt框架开发一个名为qtCvFrameLearn的OpenCV插件,包括项目配置、插件加载、Qt与OpenCV图像转换,以及通过各个插件学习OpenCV函数的使用,如仿射变换、卡通效果、腐蚀、旋转和锐化等。
44 10
|
2月前
|
C语言 C++ Windows
QT多插件通信框架CTK编译记录
本文记录了编译QT多插件通信框架CTK的过程,包括编译结果截图、部署配置、Log4Qt编译配置、参考链接和拓展资料。文中提供了详细的编译步骤和配置文件示例,以及相关的资源链接。
QT多插件通信框架CTK编译记录
|
3月前
|
存储 C++
【C++】C++ 基于QT实现散列表学生管理系统(源码+数据+课程论文)【独一无二】
【C++】C++ 基于QT实现散列表学生管理系统(源码+数据+课程论文)【独一无二】
【C++】C++ 基于QT实现散列表学生管理系统(源码+数据+课程论文)【独一无二】
|
3月前
|
API
Qt绘图之Paint系统
Qt绘图之Paint系统
58 2
|
6月前
|
开发框架 自然语言处理 Linux
Qt:构建强大跨平台应用程序的框架
Qt:构建强大跨平台应用程序的框架
|
3月前
|
开发者
【Qt 学习笔记】Qt系统相关 | Qt事件 | 事件的介绍及基本概念
【Qt 学习笔记】Qt系统相关 | Qt事件 | 事件的介绍及基本概念
199 4
|
3月前
从源码角度分析Qt元对象系统2
从源码角度分析Qt元对象系统
54 0