天宫鹤

Qt应用程序接口和插件的创建详细过程

对于一个大型软件系统来说,实现plugin是一件很美妙的事情,一个成功的plugin系统可以使软件增色不少。Plugin最大的功能是在一定程度内提高了软件的灵活度和可扩展性。一个设计精良的server软件plugin系统甚至在server程序不退出的情况下可以调用新加入的plugin,实现不间断服务的升级。那么,Qt是怎样实现它的plugin系统呢?

使用Qt创建plugin和在程序中调用plugin是很简单的事情,Qt提供了很多helper class供大家使用。总体来说,Qt的plugin分为2种,按照Qt文档的说法,一种是高等级的plugin。其实说白了就是已经确定interface的Qt本身的plugin。(大家可能都知道,Qt的很多功能,像数据库驱动、图片格式支持、文字内码等都是通过plugin实现的)举个例子来说,Qt可能本身没有西班牙语(希望这次西班牙夺冠:-)的文字内码,但是程序员可以通过按照codec interface写出西班牙语的codec plugin从而使Qt支持西班牙语。

另一种是低等级的plugin。就是该plugin的interface也需要程序员自己编写。所以如果你知道怎么写一个低等级的plugin并使用它之后,高等级的plugin也就完全掌握了。下面我就重点说说低等级的plugin在Qt里实现的一些要点。

从编程的角度,重点还是OOP。所谓的plugin,其实就是一些按照特定interface写成的子类。该Interface必须是虚基类,且所有函数(除了析构)都是虚函数。而所谓的plugin就是继承该虚基类和QObject的子类。当程序调用该plugin的某个函数时,是通过该plugin的虚基类在运行时动态绑定至子类的vtable执行的。所以Qt实现plugin的基础还是OOP的继承和多态。

举个大家都知道的例子来说明,PhotoShop(可能它并不是这么实现的)的所有滤镜有个统一的interface虚基类,该类提供了一个虚函数doSomeWork用于实现滤镜效果。当用户选择某个特殊滤镜时,程序会调用plugin中该滤镜class的doSomeWork实现函数来执行该操作,从而实现特定的滤镜功能。

那么为什么该plugin类不但要继承interface类,还需要继承QObject类呢?原因是调用plugin时需要该plugin类QObject那部分的meta信息。如果大家看过例子代码,会发现,用QPluginLoader调用plugin的文件后,关键的一步是确定该plugin是什么类型的。简单的另人惊讶,一句qobject_cast就搞定了。刚看到这句我百思不得其解,好在Qt有源代码可看,看了源代码发现qobject_cast类似于标准C++的dynamic_cast,且无需RTTI支持并能跨DLL。在代码中,qobject_cast是通过QObject的metaobject的cast函数来实现的。那么该函数是怎么写的呢?

plugin 是一个实现了一个或多个接口的DLL。(原文链接:https://blog.csdn.net/vample/article/details/78860005)

 

Qt应用程序接口:包含类定义的头文件(*.h),该类定义中一般只包含纯虚函数的声明。

Qt应用程序插件:继承自指定类和接口的C++类,该类实现了接口中定义的纯虚函数。

 1.定义接口(类)

 2.创建插件(类),pro 文件中添加“CONFIG += plugin”。

 3.使用/加载插件

插件是一种遵循一定规范的应用程序接口编写出来的程序,定位于开发实现应用软件平台不具备的功能的程序。

插件这个东西,唯一的作用就是“扩展程序的功能,丰富程序的应用”。

Qt提供了两种API用于创建插件:

一种是高阶API,用于扩展Qt本身的功能,如自定义数据库驱动,图像格式,文本编码,自定义样式等;

官方参考示例 Style Plugin Example 。

一种是低阶API,用于扩展Qt应用程序,Qt应用程序低阶插件(low-level)就是继承自QObject、实现了接口(类)的自定义C++类。

官方 参考示例 Echo Plugin Example 。

接口类头文件(*.h)模板源代码:

---------------Start

#ifndef ECHOINTERFACE_H
#define ECHOINTERFACE_H

#include <QObject>
#include <QString>

//! [0]
class EchoInterface
{
public:
    //! 虚析构函数
    //! virtual ~EchoInterface() {}
    virtual ~EchoInterface() = default;
    virtual QString echo(const QString &message) = 0;
};


QT_BEGIN_NAMESPACE

#define EchoInterface_iid "org.qt-project.Qt.Examples.EchoInterface"
//! 该宏就是向 Qt 元对象系统注册一下这个接口类
//! 该宏的第一个参数是接口类的名字,
//! 第二个参数是 iid(相当于一个身份证信息,可以随便写个字符串,但保证唯一)。
//! 第二个参数是 iid命名规则建议:类名_iid。
Q_DECLARE_INTERFACE(EchoInterface, EchoInterface_iid)
QT_END_NAMESPACE

//! [0]
#endif
---------------End
插件之头文件(*.h)模板源代码:
---------------Start
#ifndef ECHOPLUGIN_H
#define ECHOPLUGIN_H

#include <QObject>
#include <QtPlugin>
#include "echointerface.h"

//! [0]
//! 继承接口和 QObject,继承 QObject 是为了使用 Qt 的元对象系统
class EchoPlugin : public QObject, EchoInterface
{
    Q_OBJECT
    //! 用 Q_PLUGIN_METADATA() 宏导出插件
    //Q_PLUGIN_METADATA(IID "org.qt-project.Qt.Examples.EchoInterface" FILE "echoplugin.json")
    //Q_PLUGIN_METADATA(IID EchoInterface_iid FILE "echoplugin.json")
    Q_PLUGIN_METADATA(IID EchoInterface_iid)
    //! 用 Q_INTERFACES() 宏告诉元对象系统“我要用这个接口(类)”
    Q_INTERFACES(EchoInterface)

public:
    QString echo(const QString &message) override;
};
//! [0]

#endif
---------------End
插件之实现文件(*.cpp)模板源代码:
---------------Start
#include "echoplugin.h"

//! [0]
QString EchoPlugin::echo(const QString &message)
{
    return message;
}
//! [0]
---------------End

运行时怎么加载插件?

app 在运行时加载插件这个动作是这样的:

  1. QPluginLoader 加载插件名;
  2. 获取 QObject 示例;
  3. 转化成接口指针,然后判断不为空即为正常加载。
---------------Start
bool EchoWindow::loadPlugin()
{
    QDir pluginsDir(QCoreApplication::applicationDirPath());
#if defined(Q_OS_WIN)
    if (pluginsDir.dirName().toLower() == "debug" || pluginsDir.dirName().toLower() == "release")
        pluginsDir.cdUp();
#elif defined(Q_OS_MAC)
    if (pluginsDir.dirName() == "MacOS") {
        pluginsDir.cdUp();
        pluginsDir.cdUp();
        pluginsDir.cdUp();
    }
#endif
    pluginsDir.cd("plugins");
    const QStringList entries = pluginsDir.entryList(QDir::Files);
    for (const QString &fileName : entries) {
        QPluginLoader pluginLoader(pluginsDir.absoluteFilePath(fileName));
        QObject *plugin = pluginLoader.instance();
        if (plugin) {
            echoInterface = qobject_cast<EchoInterface *>(plugin);
            if (echoInterface)
                return true;
            pluginLoader.unload();
        }
    }

    return false;
}

或者使用重载函数:
bool EchoWindow::loadPlugin(const QString &pluginPath) {

    //! 首先判断要加载的插件-动态链接库(.dll)是否存在?

    QFile file(pluginPath);

    if (!file.exists()) {

        QMessageBox::warning(this, tr("错误信息"), tr("-*^*-找不到 \"%1\" 文件!!!-*^*-").arg(pluginPath));

        return false;

    }

    //! 加载插件-动态链接库(.dll)

    QPluginLoader pluginLoader(pluginPath, this);

    QObject *plugin = pluginLoader.instance();

    if (plugin) {

        //! 若加载成功,转换为具体的自定义接口

        //! qobject_cast 也可以替换为 dynamic_cast

        echoInterface = qobject_cast<EchoInterface *>(plugin);

        if (echoInterface)

            return true;

        pluginLoader.unload();

    }

    return false;

}

---------------End

参考:

0.Qt 插件机制使用及原理

    https://zhuanlan.zhihu.com/p/561901237

 

1.Qt插件框架:

    https://zhuanlan.zhihu.com/p/94178398

2.怎样用 Qt 写个插件?

    https://zhuanlan.zhihu.com/p/49943870

-----------------待完善!---------------------

posted on   GoGrid  阅读(976)  评论(0编辑  收藏  举报

相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

导航

统计

点击右上角即可分享
微信分享提示