0 插件开发教程
wingsummer edited this page 2022-10-18 10:13:43 +08:00
This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

内容贡献者: 寂静的羽夏

简述

  本篇重点介绍插件如何开发,插件的机制是什么。如果你是非开发人员,本篇 Wiki 对你的作用不大,请不要浪 自己的时间,本篇将会比较冗长。废话不多说下面开始:

知识要求

  熟练使用C++并掌握Qt开发的基本知识

开发环境

  本篇使用Qt 5.15.3,建议与主程序使用的Qt框架版本保持一致,否则可能会有插件加载失败的问题。

模型

  本程序提供一个插件接口,名为IWingToolPlg,所有的插件都必须继承并实现相应的功能。下面给一个图来 解本程序的插件模型(假设我要编写一个名为Plugin的插件):

  可以看出,该插件结构十分简单,具备了插件的基本功能,同时保留了必要的功能。下面开始逐步介绍:

插件信息

  插件信息提供了最基本的信息,包含插件名称、作者、备注等以及要注册的托盘菜单。如下是iwingtoolplg.h相关可以重写的函数:

// 指示 SDK 版本,目前采用不兼容模式
virtual int sdkVersion() = 0;
// 签名,目前固定为 WINGSUMMER 这个宏
virtual QString signature() = 0;
// 插件的名称,可以使用 tr 实现多语言
virtual QString pluginName() = 0;
// 插件作者
virtual QString pluginAuthor() = 0;
// 插件类别,目前还没啥特定的作用,以后发掘
virtual Catagorys pluginCatagory() = 0;
// 插件版本号,作者定
virtual uint pluginVersion() = 0;
// 插件网站,供插件作者宣传用
virtual QString pluginWebsite() = 0;
// 插件说明
virtual QString pluginComment() = 0;
// 插件图标,我建议必须有一个,否则后面不好识别
virtual QIcon pluginIcon() = 0;
// 插件服务类,必须继承 QObject ,自定义
virtual const QMetaObject *serviceMeta() = 0;
// 插件指针
virtual const QPointer<QObject> serviceHandler() = 0;
// 插件订阅,如果需要跟踪鼠标就需要订阅
virtual HookIndex getHookSubscribe() { return HookIndex::None; }
// 注册在程序右键托盘菜单,这个对于某些功能会十分方便
// 但非必要不要弄,因为这样的插件多了,反而麻烦了,一个插件仅有一项
// 类型仅支持 QMenu* 或者 QAction* 否则不载入
virtual QObject *trayRegisteredMenu() { return nullptr; }
// 插件的语言包文件名,如果空插件系统默认不加载
// 如果有需要还请手动加载
virtual QString translatorFile() { return QString(); }

  注释写的十分明白,我就在这里强调一下插件订阅。   插件订阅是为了防止接受频率过高的信号发送不必要的接收者,这有一定的资源浪费,目前里面只有鼠标跟踪相关的订阅。   订阅完毕后,还需要重写相关的函数,如下所示:

// 当鼠标任何一个键被按下就会触发该函数,如果想处理重载
virtual void buttonPress(Qt::MouseButton btn, int x, int y) {
    Q_UNUSED(btn);
    Q_UNUSED(x);
    Q_UNUSED(y);
}

// 当鼠标任何一个键从被按下的状态释放就会触发该函数,如果想处理重载
virtual void buttonRelease(Qt::MouseButton btn, int x, int y) {
    Q_UNUSED(btn);
    Q_UNUSED(x);
    Q_UNUSED(y);
}

// 当鼠标进行左键单击时会触发该函数,如果想处理重载
// 该函数也就是 buttonPress 的一种特殊情况
virtual void clicked(int x, int y) {
    Q_UNUSED(x);
    Q_UNUSED(y);
}

// 当鼠标双击时会触发该函数,如果想处理重载
// 注:当鼠标双击时,系统无法识别好第一个点击,会被识别
// 为单击,但第二个紧接的单击会被识别为双击
virtual void doubleClicked(int x, int y) {
    Q_UNUSED(x);
    Q_UNUSED(y);
}

  // 当鼠标滚轮滚动时会触发该函数,如果想处理重载
  virtual void mouseWheel(MouseWheelEvent direction) { Q_UNUSED(direction); }

// 当鼠标移动时会触发该函数,如果想处理重载
virtual void mouseMove(int x, int y) {
    Q_UNUSED(x);
    Q_UNUSED(y);
}

// 当鼠标进行拖拽操作时触发该函数,如果想处理重载
virtual void mouseDrag(int x, int y) {
    Q_UNUSED(x);
    Q_UNUSED(y);
}

API

  本插件的所有API(即开发接口)都是以信号提供的,如下是支持的函数:

// 注册热键,如果被占用则返回空表示失败(通常是重复),
// 大于等于 0 则表示成功,返回句柄
QUuid registerHotkey(QKeySequence keyseq);

// 修改热键状态,其中 id 为注册热键句柄enable 为热键的新状态
bool enableHotKey(const QUuid id, bool enabled = true);

// 修改热键
bool editHotkey(const QUuid id, QKeySequence seq);

// 注销热键,其中 id 为注册热键句柄
bool unregisterHotkey(const QUuid id);

// 跨插件函数远程调用,其中 puid 为插件的唯一标识,
// callback 为回调函数名称, params 为远程调用的参数
QVariant remoteCall(const QString provider, const QString callback,
                    QVector<QVariant> params, RemoteCallError &err);

// 向某个插件发送一个消息
// 注:不要用它发送数值小于 0 的消息,会发送失败滴,别瞎搞
QVariant sendRemoteMessage(const QString provider, int id,
                            QList<QVariant> params, RemoteCallError &err);

// 查询某个插件是否存在
bool isProviderExists(const QString provider);

// 查询某个插件服务是否含有所述服务
bool isServiceExists(const QString provider, const QString callback);

// 查询某个插件服务是否接口
bool isInterfaceExists(const QString provider, const QString callback);

// 获取服务的参数类型
QVector<QList<int>> getServiceParamTypes(const QString provider,
                                         const QString callback);

// 获取接口的参数类型
QVector<QList<int>> getInterfaceParamTypes(const QString provider,
                                            const QString callback);

// 获取全局按下的修饰键序列
Qt::KeyboardModifiers getPressedKeyModifiers();

// 获取全局按下的鼠标按键序列
Qt::MouseButtons getPressedMouseButtons();

// 获取所有插件提供者名称
QStringList getPluginProviders();

// 获取插件信息
WingPluginInfo getPluginInfo(const QString provider);

// 获取插件的所有服务名isTr 指示是否使用本地化的
QStringList getPluginServices(const QString provider, bool isTr = false);

// 获取所有插件的所有接口名(注:无去重,可能有重复项)
QStringList getPluginInterfaces(const QString provider);

  同理,这里不赘述。

提供服务

  了解了这些函数,你还需要学习如何提供服务。而我们的服务,是通过一个类来实现的。相关的接口:

// 插件服务类,必须继承 QObject ,自定义
virtual const QMetaObject *serviceMeta() = 0;
// 插件指针
virtual const QPointer<QObject> serviceHandler() = 0;

  从接口可以判断出,我们的服务是通过 Qt 的反射机制实现的,这个是为了考虑功能复杂的插件开发难度更改的。我们看看TestPlugin是如何提供的,如下是相关代码:

// 如下是提供服务
class TestService : public QObject {
  Q_OBJECT
public:
  explicit TestService() {}
  explicit TestService(const TestService &) : QObject(nullptr) {}
  explicit TestService(QTextBrowser *browser, QDialog *d)
      : b(browser), dialog(d) {}
  virtual ~TestService() {}

public slots:
  PLUGINSRV void func1(int v) {
    b->append(QString("[func1 call] : %1").arg(v));
  }
  PLUGINSRV void func2(QString v) { b->append(QString("[func2 call] : ") + v); }
  PLUGINSRV void func3() { dialog->setVisible(!dialog->isVisible()); }

private:
  void trans() {
    tr("func1");
    tr("func2");
    tr("func3");
  }

private:
  QTextBrowser *b;
  QDialog *dialog;
};

// 初始化相关
bool TestPlugin::preInit() {
  //... 略
  services = new TestService(tbinfo, dialog);
  //... 略
  return true;
}

// 如下是提供服务说明
const QPointer<QObject> TestPlugin::serviceHandler() {
  return QPointer<QObject>(services);
}

const QMetaObject *TestPlugin::serviceMeta() { return services->metaObject(); }

  我们先看看如何写一个服务类。   服务类需要创建继承于QObject的类,带着Q_OBJECT这个宏。初始化和析构类相关的和正常的类没有任何区别,只是你要提供的所有的服务需要定义成槽函数,这个的目的是让Qtmoc系统知道这个函数,以便提供反射相关的服务,这个也是最简单的方式。   你可以看到,所有的服务函数前面都带有PLUGINSRV,这个宏表示这个就是服务函数,只需加上这个插件系统就会将其注册为服务。   还有一个类似服务的存在的接口,上一篇我也介绍过,它对应的宏是PLUGININT,使用方式和注册服务函数是一样的。   仅仅这样声明函数,我们显示的服务名将和服务函数名一致,这样对使用用户极不友好,我们需要本地化,所以上面声明了一个trans函数。这个函数名随便,只要符合C++的函数定义要求即可,里面的内容就是一串的tr函数的调用,让Qt语言家能够识别到。   声明完毕后,初始化需要在preInit函数,也就是预初始化函数进行初始化服务,语言本地化是在preInit函数之前做好的,如果不手动加载语言包的话,之后处理起来会比较麻烦。   有关提供服务这块,我们就介绍这么多。

消息管道

  消息管道是插件获取消息的唯一渠道,有关加载插件阶段信息以及消息都是通过消息管道传送的。该消息管 函数必须重写,如下所示:

void plugin2MessagePipe(WingPluginMessage type, QList<QVariant> msg) override;

插件之间也是可以互相发送消息号大于等于0的消息的小于0的消息被保留。

插件加载流程

  这里就简单介绍插件加载流程,具体可以翻阅源代码。 当插件一旦被插件系统选中并尝试加载时首先是校验插件的合法性由于需要预初始化所以先只检查标识、SDK 版本和插件名的合法性。下一波开始加载插件的语言包文件,如果存在,开始预初始化操作。   如果预初始化成功,就开始检查服务的合法性。检查通过之后,就发送开始初始化消息,正式初始化函数。初始化通过之后,如果有托盘菜单就注册,然后建立 API 调用通信,最后发送插件已加载完的消息,此时,所有的API已经完全可用。

插件卸载

  插件如果合法,是不会被插件系统主动卸载的。插件会随着插件系统的销毁而被卸载:

void PluginSystem::UnloadPlugin() {
  for (auto item : m_plgs) {
    item->unload();
    item->deleteLater();
  }
}

插件开发规范

  为了让用户拥有更好的使用体验,避免已知 Bug ,请遵守以下规范:

  1. 如果使用多语言本地化操作,请放到plglang文件夹下,并保持开头必须包含你的插件相关信息。比如我开发了一个插件liba.wingplg,请命名为a.qmliba.qm形式。
  2. 使用Qt开发插件的时候,它会默认在前面加lib,建议保留。
  3. 插件文件名不建议使用中文名称。
  4. 不要随意修改iwingtoolplg.h文件,如果你不是项目开发维护者,这是很不明智的行为。它可能会使插件加载失败、想要用函数A结果调用B,甚至宿主程序崩溃的情况。
  5. 开发插件强烈建议 开放源代码,因为插件接口一旦更改,将采用互不兼容的模式,如果你能紧跟我的发行版也是没问题。
  6. 如果插件含有资源,请在根目录前缀修改为和插件名称一致。由于默认新建的资源为/,也就是根,这个必须修改,以防和他插件甚至和宿主资源冲突。
  7. 不要在插件加载完毕之前调用 API ,因为没用。
  8. 对于服务,声明函数 不要有缺省参数,同函数名不同参数! 因为这样会导致同一个服务名显示多个,这个插件的开发是不合格的。如果有这样的需求请自己将其定义为接口,然后通过其他方式解决,而不是服务!
  9. 服务参数中不得含有无法从字符串转化的参数类型,比如 QList 、QVector 等等。否则这也是插件不合格的一个体现。还请将其设计为 接口
  10. 不要忽略每一个警告,除非这警告不是因为你的代码而起。
  11. 插件内容不得含有违反国家法律法规和社会道德的内容,如果想打广告,只能含有插件功能相关的广告,并且只能在插件中心中体现,不能主动弹出。插件内部不得还有干扰用户使用的相关恶意代码。
  12. 插件中心的实现最起码要有个弹窗,不要啥反应都没有。

知识共享许可
议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。