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.
内容贡献者: 寂静的羽夏
上一篇: 设置相关
- 简述
- 知识要求
- 开发环境
- 模型
- 插件信息
- WingPlugin::Reader
- WingPlugin::Controller
- 抢占机制
- 当前文档
- 其他函数
- 消息管道
- 插件加载流程
- 插件卸载
- 插件项目编写
- 日志输出
- 插件开发规范
简述
本篇重点介绍插件如何开发,插件的机制是什么。如果你是非开发人员,本篇 Wiki 对你的作用不大,请不要浪费自己的时间,本篇将会比较冗长。废话不多说下面开始:
知识要求
熟练使用C++
并掌握Qt
开发的基本知识
开发环境
本篇使用Qt 5.15.3
,建议与主程序使用的Qt
框架版本保持一致,否则可能会有插件加载失败的问题。
模型
本程序提供一个插件接口,名为IWingPlugin
,所有的插件都必须继承并实现相应的功能。下面给一个图来理解本程序的插件模型(假设我要编写一个名为Plugin
的插件):
可以看出,该插件结构十分简单,具备了插件的基本功能,同时保留了必要的功能。下面开始逐步介绍:
插件信息
插件信息提供了最基本的信息,包含插件名称、作者、备注以及要注册的 Dock 窗体、菜单。如下是iwingplugin.h
相关可以重写的函数:
virtual int sdkVersion() = 0;
virtual QString signature() = 0;
virtual QMenu *registerMenu() { return nullptr; }
virtual QToolButton *registerToolButton() { return nullptr; }
virtual void registerDockWidget(QMap<QDockWidget *, Qt::DockWidgetArea> &rdw) {
Q_UNUSED(rdw);
}
virtual QToolBar *registerToolBar() { return nullptr; }
virtual Qt::ToolBarArea registerToolBarArea() {
return Qt::ToolBarArea::TopToolBarArea;
}
virtual QString pluginName() = 0;
virtual QString pluginAuthor() = 0;
virtual uint pluginVersion() = 0;
virtual QString pluginComment() = 0;
virtual HookIndex getHookSubscribe() { return HookIndex::None; }
sdkVersion
:插件使用的 SDK 版本,该函数必须重写,且内容固定,之后的例子将会介绍。插件系统会通过该返回值检查是否兼容插件,所以不要随意更改。signature
:表示插件合法的签名,必须为字符串wingsummer
,否则会加载失败。SDK
提供了这个宏WINGSUMMER
,可以直接使用。registerMenu
:提供要加载的菜单实例指针。registerToolButton
:提供要加载的工具栏按钮指针,如果registerToolBar
有合法返回值,就不进行对其进行注册。registerDockWidget
:提供要加载的 Dock 窗体实例指针图表,如果不想添加就不必重写。registerToolBar
:提供要加载的工具条的指针。pluginName
:提供加载插件的名称,不能为空或者纯空格字符串,否则插件会加载失败。pluginAuthor
:提供插件作者,可以为空字符串。pluginVersion
:提供插件版本,仅供插件开发者使用。pluginComment
:提供插件备注信息,可以为空。getHookSubscribe
:提供该插件想要订阅那些消息,它是一个枚举,如下所示:
enum HookIndex {
None = 0,
OpenFileBegin = 1,
OpenFileEnd = 2,
OpenDriverBegin = 4,
OpenDriverEnd = 8,
CloseFileBegin = 16,
CloseFileEnd = 32,
NewFileBegin = 64,
NewFileEnd = 128
};
如果你想要订阅打开文件前和打开驱动器前的消息,需要这么写:
HookIndex Plugin::getHookSubscribe(){
return HookIndex(HookIndex::OpenFileBegin | HookIndex::OpenDriverBegin);
}
也就是说,消息订阅是基于位实现订阅的。
WingPlugin::Reader
这是一个纯信号类,提供与读取相关的函数信号接口。一旦插件被加载,就可以使用,如下是其相关函数:
int currentDoc();
QString currentDocFilename();
// document
bool isReadOnly();
bool isKeepSize();
bool isLocked();
quint64 documentLines();
quint64 documentBytes();
HexPosition currentPos();
HexPosition selectionPos();
quint64 currentRow();
quint64 currentColumn();
quint64 currentOffset();
quint64 selectlength();
bool stringVisible();
bool addressVisible();
bool headerVisible();
quint64 addressBase();
bool isModified();
bool isEmpty();
bool atEnd();
bool canUndo();
bool canRedo();
void copy(bool hex = false);
QByteArray read(qint64 offset, int len);
// extension
qint8 readInt8(qint64 offset);
qint16 readInt16(qint64 offset);
qint32 readInt32(qint64 offset);
qint64 readInt64(qint64 offset);
QString readString(qint64 offset, QString encoding = QString());
qint64 searchForward(qint64 begin, const QByteArray &ba);
qint64 searchBackward(qint64 begin, const QByteArray &ba);
void findAllBytes(qlonglong begin, qlonglong end, QByteArray b,
QList<quint64> &results, int maxCount = -1);
// render
quint64 documentLastLine();
int documentLastColumn();
// metadata
bool lineHasMetadata(quint64 line) const;
QList<HexMetadataAbsoluteItem> getMetadatas(qint64 offset);
HexLineMetadata getMetaLine(quint64 line) const;
// bookmark
bool lineHasBookMark(quint64 line);
QList<qint64> getsBookmarkPos(quint64 line);
BookMark bookMark(qint64 pos);
QString bookMarkComment(qint64 pos);
void getBookMarks(QList<BookMark> &bookmarks);
bool existBookMark(qint64 pos);
// extension
QList<QString> getOpenFiles();
QStringList getSupportedEncodings();
QString currentEncoding();
currentDoc
:获取当前文档索引,如果无效默认为-1
。currentDocFilename
:获取当前文档索引,如果无效默认为空字符串。isReadOnly
:获取当前文档是否只读,如果无效默认为true
。isKeepSize
:获取当前文档是否锁定文件大小,如果无效默认为true
。isLocked
:获取当前文档是否被锁定修改字节,如果无效默认为true
。documentLines
:获取当前文档行数,如果无效默认为0
。documentBytes
:获取当前文档字节数,如果无效默认为0
。currentPos
:获取当前文档光标偏移位置,如果无效默认为结构体内容全部为0
。selectionPos
:获取当前文档选区位置,如果无效默认为结构体内容全部为0
。currentRow
:获取当前文档光标所在行索引,如果无效默认为0
。currentColumn
:获取当前文档光标所在列索引,如果无效默认为0
。currentOffset
:获取当前文档光标所在偏移,如果无效默认为0
。selectlength
:获取当前文档选区长度,如果无效默认为0
。stringVisible
:获取当前文档解码字符串是否显示,如果无效默认为true
。addressVisible
:获取当前文档地址栏是否显示,如果无效默认为true
。headerVisible
:获取当前文档表头是否显示,如果无效默认为true
。addressBase
:获取当前文档文件基址,如果无效默认为0
。isModified
:获取当前文档是否被修改,如果无效默认为false
。isEmpty
:获取当前文档是否无内容,如果无效默认为true
。atEnd
:获取当前文档光标是否在末尾,如果无效默认为false
。canUndo
:获取当前文档是否可撤销,如果无效默认为false
。canRedo
:获取当前文档是否可恢复,如果无效默认为false
。copy
:执行对当前文档选区复制操作;hex
表示是否以十六进制字符串形式复制。read
:执行对当前文档读取操作;offset
为读取的偏移,len
为读取长度,返回值为含有读取字节的QByteArray
类。readInt8
:执行对当前文档读取一个8位整数操作;offset
为读取的偏移,返回值为一个8位整数。readInt16
:执行对当前文档读取一个16位整数操作;offset
为读取的偏移,返回值为一个16位整数。readInt32
:执行对当前文档读取一个32位整数操作;offset
为读取的偏移,返回值为一个32位整数。readInt64
:执行对当前文档读取一个16位整数操作;offset
为读取的偏移,返回值为一个64位整数。readString
:执行对当前文档读取字符串操作;offset
为读取的偏移,encoding
是指定编码名称,如果不填写是指当前文档编码,返回值为一个解码后的字符串。searchForward
:执行对当前文档向文件尾搜索字节操作;ba
为要搜索的字节,返回值为找到的第一个匹配所在的文件偏移,如果未找到返回-1
。searchBackward
:执行对当前文档向文件头搜索字节操作;ba
为要搜索的字节,返回值为找到的第一个匹配所在的文件偏移,如果未找到返回-1
。findAllBytes
:执行对当前文档全部搜索字节操作;begin
表示开始搜索的偏移,end
表示结束搜索的偏移,b
表示要匹配的字节,results
表示要保存结果的列表,maxcount
表示最大搜索数量;若begin = -1
表示文件开头,若end = -1
表示文件尾,maxcount = -1
表示不受最大数量限制。documentLastLine
:获取当前文档最后一行索引。documentLastColumn
:获取当前文档最后一列索引。lineHasMetadata
:获取当前文档某行是否有标注;line
表示行索引。getMetadatas
:获取当前文档指定偏移的所有标注;line
表示行索引。getMetaLine
:获取当前文档指定行标注;line
表示行索引。lineHasBookMark
:获取当前文档某行是否有书签;line
表示行索引。getsBookmarkPos
:获取当前文档某行的所有书签偏移;line
表示行索引。bookMark
:获取当前文档某偏移的书签;pos
表示文件偏移。bookMarkComment
:获取当前文档某偏移的书签评语;pos
表示文件偏移。getBookMarks
:获取当前文档所有书签;bookmarks
为存放所有书签的列表。existBookMark
:获取当前文档是否在某偏移有书签;pos
表示文件偏移。getOpenFiles
:获取所有当前打开文档,返回一个字符串列表。getSupportedEncodings
:获取该程序所有支持字符串解码的编码,返回一个字符串列表。currentEncoding
:获取当前文档的字符串解码的编码,返回字符串。
WingPlugin::Controller
这是一个纯信号类,提供与写操作相关的函数信号接口。该类不能直接使用,如果强制使用会处于无效状态,因为插件系统并没有与该类建立信号槽关系。如果想使用该类,请阅读 抢占机制 。
// document
bool switchDocument(int index, bool gui = false);
bool setLockedFile(bool b);
bool setKeepSize(bool b);
void setStringVisible(bool b);
void setAddressVisible(bool b);
void setHeaderVisible(bool b);
void setAddressBase(quint64 base);
void undo();
void redo();
bool cut(bool hex = false);
void paste(bool hex = false);
bool write(qint64 offset, uchar b);
bool write(qint64 offset, const QByteArray &data);
// extesion
bool writeInt8(qint64 offset, qint8 value);
bool writeInt16(qint64 offset, qint16 value);
bool writeInt32(qint64 offset, qint32 value);
bool writeInt64(qint64 offset, qint64 value);
bool writeString(qint64 offset, QString value, QString encoding = QString());
bool insert(qint64 offset, uchar b);
bool insert(qint64 offset, const QByteArray &data);
bool append(uchar b);
bool append(const QByteArray &data);
// extension
bool appendInt8(qint8 value);
bool appendInt16(qint16 value);
bool appendInt32(qint32 value);
bool appendInt64(qint64 value);
bool appendString(QString value, QString encoding = QString());
bool remove(qint64 offset, int len);
bool removeAll(qint64 offset); // extension
// cursor
void moveTo(const HexPosition &pos);
void moveTo(quint64 line, int column, int nibbleindex = 1);
void moveTo(qint64 offset);
void select(quint64 line, int column, int nibbleindex = 1);
void selectOffset(qint64 offset, int length);
void setInsertionMode(bool isinsert);
void enabledCursor(bool b);
void select(qint64 offset, int length);
// metadata
bool metadata(qint64 begin, qint64 end, const QColor &fgcolor,
const QColor &bgcolor, const QString &comment);
bool metadata(quint64 line, int start, int length, const QColor &fgcolor,
const QColor &bgcolor, const QString &comment);
bool removeMetadata(qint64 offset);
bool clearMeta();
bool color(quint64 line, int start, int length, const QColor &fgcolor,
const QColor &bgcolor);
bool foreground(quint64 line, int start, int length, const QColor &fgcolor);
bool background(quint64 line, int start, int length, const QColor &bgcolor);
bool comment(quint64 line, int start, int length, const QString &comment);
void applyMetas(QList<HexMetadataAbsoluteItem> metas);
bool setMetaVisible(bool b);
void setMetafgVisible(bool b);
void setMetabgVisible(bool b);
void setMetaCommentVisible(bool b);
// mainwindow
void newFile(bool bigfile = false);
ErrFile openFile(QString filename);
ErrFile openDriver(QString driver);
ErrFile closeFile(int index, bool force = false);
ErrFile saveFile(int index, bool ignoreMd5 = false);
ErrFile openRegionFile(QString filename, int *openedIndex = nullptr,
qint64 start = 0, qint64 length = 1024);
ErrFile exportFile(QString filename, int index, bool ignoreMd5 = false);
void exportFileGUI();
ErrFile saveasFile(QString filename, int index);
void saveasFileGUI();
ErrFile closeCurrentFile(bool force = false);
ErrFile saveCurrentFile(bool ignoreMd5 = false);
void openFileGUI();
void openDriverGUI();
void findGUI();
void gotoGUI();
void fillGUI();
void fillzeroGUI();
void fillnopGUI();
// bookmark
bool addBookMark(qint64 pos, QString comment);
bool modBookMark(qint64 pos, QString comment);
void applyBookMarks(QList<BookMark> books);
bool removeBookMark(qint64 pos);
bool clearBookMark();
// workspace
bool openWorkSpace(QString filename);
bool setCurrentEncoding(QString encoding);
switchDocument
:切换当前文档;index
是索引,gui
表示是否与编辑器同步,返回值指示是否切换成功。setLockedFile
:设置当前文档是否锁定文件字节写入;setKeepSize
:设置当前文档是否锁定文件字节大小;setStringVisible
:设置当前文档是否显示解码字符串。setAddressVisible
:设置当前文档是否显示基址栏。setHeaderVisible
:设置当前文档是否显示表头。setAddressBase
:设置当前文档文件基址。undo
:执行当前文档撤销操作。redo
:执行当前文档恢复操作。cut
:执行当前文档剪切操作;hex
表示是否以十六进制字符串形式复制。paste
:执行当前文档粘贴操作;hex
表示是否以十六进制字符串形式复制。insert
:执行当前文档插入字节操作。append
:执行当前文档追加字节操作。write
:执行当前文档替换字节操作。remove
:执行当前文档删除字节操作。moveTo
:执行当前文档将光标移动到某个位置。select
:执行当前文档选择某个区域。selectOffset
:执行当前文档在某偏移进行选区操作。setInsertionMode
:执行当前文档设置光标插入模式。setLineWidth
:设置当前文档字节行宽,默认16
。metadata
:添加当前文档标注。removeMetadatar
:删除当前文档标注。clearMeta
:清空当前文档标注。color
:添加当前文档颜色标注。foreground
:添加当前文档前景色标注。background
:添加当前文档背景色标注。comment
:添加当前文档评语标注。applyMetas
:覆盖当前文档所有标注。newFile
:新建文档;bigfile
表示是否使用超大文件,如果为真则使用。openFile
:打开文档。openRegionFile
:以局部的方式打开文档。openDriver
:打开驱动器。closeFile
:关闭文件。saveFile
:保存文件;ignoreMd5
只是是否忽略原文件在打开前被修改仍保存,仅对打开的局部文件有效。exportFile
:导出文件;ignoreMd5
只是是否忽略原文件在打开前被修改仍保存,仅对打开的局部文件有效。exportFileGUI
:调用宿主的导出文件功能。saveasFile
:另存为文件。saveasFileGUI
:调用宿主的另存为文件功能。closeCurrentFile
:关闭当前文档。saveCurrentFile
:保存当前文档;ignoreMd5
只是是否忽略原文件在打开前被修改仍保存,仅对打开的局部文件有效。openFileGUI
:调用宿主的打开文件功能。openDriverGUI
:调用宿主的打开驱动器功能。findGUI
:调用宿主的查找功能。gotoGUI
:调用宿主的跳转功能。fillGUI
:调用宿主的填充功能。fillzeroGUI
:调用宿主的填充零功能。fillnopGUI
:调用宿主的填充 nop 功能。setMetafgVisible
:设置当前文档前景色标注是否显示。setMetabgVisible
:设置当前文档背景色标注是否显示。setMetaCommentVisible
:设置当前文档评语标注是否显示。addBookMark
:添加当前文档书签。modBookMark
:修改当前文档书签。applyBookMarks
:覆盖当前文档全部书签。removeBookMark
:删除当前文档书签。clearBookMark
:清空当前文档书签。openWorkSpace
:打开工作区。setCurrentEncoding
:设置当前文档解码字符串编码。
抢占机制
在WingPlugin::Controller
这个类的时候我提了一句:如果强制使用会处于无效状态,因为插件系统并没有与该类建立信号槽关系。 那么如何建立关系呢?我们可以使用requestControl
函数来申请试图获取控制十六进制编辑器的权限。
为什么要构建这样的机制,把写操作功能直接开放给插件不好吗?如果多个插件对同一份文档进行写操作,会不会容易导致混乱,为了避免这种现象,于是乎我设计了抢占机制
,也就是说,最多只能有一个插件具备控制文档内容的权限。何为抢占,为了避免某些插件“占着茅坑不拉屎”,会有超时,如果在一秒内没有任何新的写操作,就会被后来申请的插件强制夺舍。被夺舍的插件会通过消息管道接收到ConnectTimeout
消息。这个消息在WingPluginMessage
枚举类当中有。
所以,当你想要申请控制文档内容的时候,需要判断是否抢占成功,如果成功,使用完毕后请立刻调用requestRelease
释放,这是一个好习惯。也不要恶意占用该权限,否则会对羽云编辑器其他插件使用带来麻烦。
对于抢占机制,还提供了一个函数hasControl
,这个函数告诉插件你是否当前还存在控制权。这通常用于防止超时被抢后判断是否自己是否还能用,但往往用的很少。
如果有插件占用了控制权限,我们可以在菜单设置
-插件
中看到相关信息(热键:Ctrl + Shift + P
),如果没有“占坑”的,就不会显示:
如果插件恶意占坑不放,你就会看到如下信息,即没及时调用requestRelease
函数信号接口:
所有的信息都会被一览无余的展示出来,找到对应的插件,关闭主程序,挪走该插件即可干掉不合规范的插件。
如果想要使用该类来操控羽云十六进制编辑器,为了更好的让大家理解,给一个流程图:
当前文档
在介绍WingPlugin::Reader
和WingPlugin::Controller
这两个类的函数功能介绍的时候,我一直在强调当前文档
。那么当前文档到底指什么?
如果没有任何插件加载的情况下,当前文档和编辑器显示的当前文档一致,如果有插件具有控制权限,那么当前文档是指插件所在的文档。这个可能说起来比较抽象,当你看到 WingPlugin::Controller 的switchDocument
就明白为什么了。
其他函数
还有一些插件接口都不在这些分类的,但它你可能经常使用,具体函数如下:
QWidget *getParentWindow();
void toast(QIcon icon, QString message);
QDialog *newAboutDialog(QPixmap img = QPixmap(),
QStringList searchPaths = QStringList(),
QString source = QString());
QDialog *newSponsorDialog(QPixmap qrcode = QPixmap(),
QString message = QString());
QDialog *newDDialog();
bool addContent(QDialog *ddialog, QWidget *widget, Qt::Alignment align = {});
bool addSpace(QDialog *ddialog, int space);
void moveToCenter(QDialog *ddialog);
getParentWindow
:获取主窗体指针,获取可以控制主窗体的行为。toast
:调用DTK
的DMessageManager
的sendMessage
函数,由于该函数比较实用,故提供该接口,直接在宿主主窗体显示该信息。newAboutDialog
:新建一个具有DTK
风格的关于窗体,返回QDialog
指针,这个是为了简化插件开发显示关于窗体。该窗体和“羽云十六进制编辑器”的关于软件一致。如果选项都为缺省值,则结果和“羽云十六进制编辑器”的关于保持一致。注意申请的指针必须手动释放。newSponsorDialog
:新建一个具有DTK
风格的赞助窗体,返回QDialog
指针,这个是为了简化插件开发显示赞助窗体。该窗体和“羽云十六进制编辑器”的赞助一致。如果选项都为缺省值,则结果和“羽云十六进制编辑器”的关于保持一致。注意申请的指针必须手动释放。newDDialog
:新建一个具有DTK
风格的窗体,返回QDialog
指针,这个是为了让不想添加DTK
依赖导出的一个函数。注意申请的指针必须手动释放。addContent
:向申请的DTK
风格的窗体添加组件,这个和DDialog
的addContent
函数本质一样。addSpace
:向申请的DTK
风格的窗体添加间隔,这个和DDialog
的addSpacing
函数本质一样。moveToCenter
:将申请的DTK
风格的窗体移动到屏幕中央。
消息管道
消息管道是插件获取宿主消息的唯一渠道,有关加载插件阶段信息以及消息都是通过消息管道传送的。该消息管道函数必须重写,如下所示:
void plugin2MessagePipe(WingPluginMessage type, QList<QVariant> msg) override;
WingPluginMessage
是消息类型,它是一个枚举类:
enum class WingPluginMessage {
PluginLoading,
PluginLoaded,
PluginUnLoading,
PluginUnLoaded,
ErrorMessage,
ConnectTimeout,
MessageResponse,
HookMessage
};
可以看出消息有有关插件加载的消息、获取控制相关消息、以及订阅消息三种类型。这些消息都是通过消息管道传输的,该函数的重要性可想而知。
插件加载流程
当插件一旦被插件系统选中并尝试加载时,首先是校验插件的合法性,相关代码:
auto p = qobject_cast<IWingPlugin *>(loader.instance());
if (p) {
lp = LP::signature;
if (p->signature() != WINGSUMMER) {
qCritical() << tr("ErrLoadPluginSign");
loader.unload();
return;
}
lp = LP::sdkVersion;
if (p->sdkVersion() != SDKVERSION) {
qCritical() << tr("ErrLoadPluginSDKVersion");
loader.unload();
return;
}
lp = LP::pluginName;
if (!p->pluginName().trimmed().length()) {
qCritical() << tr("ErrLoadPluginNoName");
loader.unload();
return;
}
lp = LP::puid;
auto puid = IWingPlugin::GetPUID(p);
if (puid != p->puid()) {
qCritical() << tr("ErrLoadPluginPUID");
loader.unload();
return;
}
if (loadedpuid.contains(puid)) {
qCritical() << tr("ErrLoadLoadedPlugin");
loader.unload();
return;
}
qobject_cast
会检查插件是否符合约定,然后后面开始检查插件的签名、名字合法性以及插件标识符,如果插件未被加载就开始进入初始化阶段:
emit p->plugin2MessagePipe(WingPluginMessage::PluginLoading,
emptyparam);
if (!p->init(loadedplginfos)) {
qCritical() << tr("ErrLoadInitPlugin");
loader.unload();
return;
}
WingPluginInfo info;
info.puid = p->puid();
info.pluginName = p->pluginName();
info.pluginAuthor = p->pluginAuthor();
info.pluginComment = p->pluginComment();
info.pluginVersion = p->pluginVersion();
loadedplginfos.push_back(info);
loadedplgs.push_back(p);
loadedpuid << puid;
qWarning() << tr("PluginWidgetRegister");
lp = LP::registerMenu;
auto menu = p->registerMenu();
if (menu) {
emit this->PluginMenuNeedAdd(menu);
}
lp = LP::registerTool;
auto tb = p->registerToolBar();
if (tb) {
emit this->PluginToolBarAdd(tb, p->registerToolBarArea());
} else {
auto tbtn = p->registerToolButton();
if (tbtn) {
emit this->PluginToolButtonAdd(tbtn);
}
}
lp = LP::registerDockWidget;
QMap<QDockWidget *, Qt::DockWidgetArea> dws;
p->registerDockWidget(dws);
if (dws.count()) {
emit this->PluginDockWidgetAdd(p->pluginName(), dws);
}
emit ConnectBase(p);
lp = LP::getHookSubscribe;
auto sub = p->getHookSubscribe();
#define INSERTSUBSCRIBE(HOOK) \
if (sub & HOOK) \
dispatcher[HOOK].push_back(p);
INSERTSUBSCRIBE(HookIndex::OpenFileBegin);
INSERTSUBSCRIBE(HookIndex::OpenFileEnd);
INSERTSUBSCRIBE(HookIndex::OpenDriverBegin);
INSERTSUBSCRIBE(HookIndex::OpenDriverEnd);
INSERTSUBSCRIBE(HookIndex::CloseFileBegin);
INSERTSUBSCRIBE(HookIndex::CloseFileEnd);
INSERTSUBSCRIBE(HookIndex::NewFileBegin);
INSERTSUBSCRIBE(HookIndex::NewFileEnd);
INSERTSUBSCRIBE(HookIndex::DocumentSwitched);
emit p->plugin2MessagePipe(WingPluginMessage::PluginLoaded, emptyparam);
在调用插件的初始化函数之前,插件系统会向插件发送WingPluginMessage::PluginLoading
消息表示开始加载了,然后调用初始化函数,然后记录一下加载临时信息供传参使用,让其它插件知道哪些插件在自己前面加载了。
继续注册完菜单和 Dock 窗体等组件之后,就开始启用WingPlugin::Reader
,实现相关函数是ConnectBase
。它负责建立WingPlugin::Reader
与宿主的信号槽接口。最后这些完毕后再发送WingPluginMessage::PluginLoaded
消息表示插件已可以正常使用。
插件卸载
插件如何合法,是不会被插件系统主动卸载的。插件会随着插件系统的销毁而被卸载:
PluginSystem::~PluginSystem() {
for (auto item : loadedplgs) {
item->plugin2MessagePipe(WingPluginMessage::PluginUnLoading, emptyparam);
item->controller.disconnect();
item->reader.disconnect();
item->unload();
item->plugin2MessagePipe(WingPluginMessage::PluginUnLoaded, emptyparam);
item->deleteLater();
}
}
在卸载插件前,会向插件发送WingPluginMessage::PluginUnLoading
消息,表示插件要被插件系统卸载了。然后注销插件与插件系统的读写关系,调用插件的卸载函数,再发送WingPluginMessage::PluginUnLoaded
表示插件已被插件系统卸载,最后插件被彻底卸载并释放内存。
这一个阶段我们可以用来对插件做一些收尾操作,比如保存插件信息等。
插件项目编写
首先我们需要创建C++库
项目,如下所示:
创建项目完毕后,需要在项目文件pro
中添加如下一行:
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
这行的作用就是添加基于QtWidgets
支持,否则将无法使用开发SDK
。
于此同时,你需要删除以下几行内容,删除不必要的操作:
DESTDIR = $$[QT_INSTALL_PLUGINS]/generic
unix {
target.path = /usr/lib
INSTALLS += target
}
然后将对应的头文件包含到项目中,包含以下头文件:
#include "iwingplugin.h"
#include <QList>
#include <QObject>
由于原来的类是继承QGenericPlugin
,我们需要修改继承的类为IWingPlugin
,并修改Q_PLUGIN_METADATA
的IID
为宏IWINGPLUGIN_INTERFACE_IID
。然后再添加一行:
Q_INTERFACES(IWingPlugin)
以上所有工作就是在声明我要使用羽云十六进制编辑器的插件模型接口,我们需要重载插件信息相关函数,如下所示:
TestPlugin(QObject *parent = nullptr);
bool init(QList<WingPluginInfo> loadedplugin) override;
~TestPlugin() override;
void unload() override;
int sdkVersion() override;
QMenu *registerMenu() override;
void registerDockWidget(QMap<QDockWidget *, Qt::DockWidgetArea> &rdw) override;
QString pluginName() override;
QString pluginAuthor() override;
uint pluginVersion() override;
QString signature() override;
QString pluginComment() override;
void plugin2MessagePipe(WingPluginMessage type, QList<QVariant> msg) override;
重载完这些函数,就可以生成,并将.so
后缀改为.wingplg
,就可以被加载到羽云十六进制编辑器当中了。当然,更强大的功能需要更细致的编写和大量的代码。编写一个插件是不是相当容易?
为了使开发更加简单,还提供了几个宏简化开发:
GETPLUGINQM
:获取插件语言包;name
表示语言包名称,包括扩展名。PluginDockWidgetInit
:初始化 Dock 窗体组件;dw
表示要初始化的QDockWidget
对象指针,widget
表示要放入QDockWidget
里面的组件对象指针,title
表示标题,objname
表示布局保存名词,如果不为空,主程序会一起保存插件界面布局。PluginMenuInitBegin
:初始化插件菜单开始宏,必须与PluginMenuInitEnd
配对使用;menu
表示要初始化的QMenu
对象指针,title
表示菜单名称。PluginMenuAddItemAction
:添加插件菜单项目宏,只能在PluginMenuInitBegin
和PluginMenuInitEnd
代码中间区域使用;menu
表示要初始化的QMenu
对象指针,title
表示菜单名称,slot
表示对应的槽函数。PluginMenuAddItemLamba
:添加插件菜单项目宏,只能在PluginMenuInitBegin
和PluginMenuInitEnd
代码中间区域使用;menu
表示要初始化的QMenu
对象指针,title
表示菜单名称,lamba
表示对应的lamba
槽函数。PluginMenuAddItemIconAction
:添加插件菜单项目宏,可带有图标,只能在PluginMenuInitBegin
和PluginMenuInitEnd
代码中间区域使用;menu
表示要初始化的QMenu
对象指针,title
表示菜单名称,icon
表示对应的QIcon
对象,slot
表示对应的槽函数。PluginMenuAddItemIconLamba
:添加插件菜单项目宏,可带有图标,只能在PluginMenuInitBegin
和PluginMenuInitEnd
代码中间区域使用;menu
表示要初始化的QMenu
对象指针,title
表示菜单名称,icon
表示对应的QIcon
对象,lamba
表示对应的lamba
槽函数。PluginMenuInitEnd
:初始化插件菜单结束宏,必须与PluginMenuInitBegin
配套使用。PluginToolButtonInit
:初始化工具栏按钮开发宏;tbtn
表示要初始化的QToolButton
对象指针,menu
表示要给tbtn
的展开菜单,icon
表示对应的QIcon
对象。PluginToolBarInitBegin
:初始化工具栏开始开发宏,与toolbar
表示要初始化的QToolBar
对象指针,title
表示改该具栏的名称,如果为空,我建议你调用toolbar->toggleViewAction()->setVisible(false);
隐藏右键选项,objname
表示布局保存名词,如果不为空,主程序会一起保存插件界面布局。PluginToolBarAddAction
:添加工具栏工具按钮开发宏;toolbar
表示要初始化的QToolBar
对象指针,icon
表示对应的QIcon
对象,slot
表示对应的槽函数,ToolTip
表示鼠标悬浮在工具按钮上的提示内容。PluginToolBarAddLamba
:添加工具栏工具按钮开发宏;toolbar
表示要初始化的QToolBar
对象指针,icon
表示对应的QIcon
对象,lamba
表示对应的lamba
槽函数,ToolTip
表示鼠标悬浮在工具按钮上的提示内容。PluginToolBarInitEnd
:初始化工具栏结束开发宏,必须与PluginToolBarInitBegin
成对使用。PluginMenuAddItemCheckAction
:添加带有多选框的插件菜单项目宏;menu
表示要初始化的QMenu
对象指针,title
表示菜单名称,checked
表示多选框的初始状态,slot
表示对应的槽函数。PluginMenuAddItemCheckLamba
:添加带有多选框的插件菜单项目宏;menu
表示要初始化的QMenu
对象指针,title
表示菜单名称,checked
表示多选框的初始状态,lamba
表示对应的lamba
槽函数。USINGCONTROL
:类似C#
的using(){}
和python
的with
语句,它是调用requestControl
申请权限并使用完控制权限后主动释放。设计该宏的目的就是避免老是调用requestControl
使用完控制权限忘记调用requestRelease
释放;Segment
表示使用控制权限的代码片段。
日志输出
该功能仅 1.5.2 版本及其以上的支持
如果你想将信息输出到宿主的日志窗口里面,是十分容易实现的,你只需调用QT
自带的qDebug
、qInfo
、qWarning
、qCritical
宏函数进行输出,每调用这四个函数的其中一个,都会向日志中追加一行日志,如下是效果图:
注:在调试插件的过程中,你可以通过日志窗口判断输出,因为该软件的所有调试信息都会重定向到该窗口。该窗口的内容是提交反馈的重要依据。
插件开发规范
如果您想开发插件,请遵守以下规范:
- 尽量使用提供的开发宏进行开发工作,这样可以提高你的开发效率和减少 Bug ,虽然这对于你不是
C/C++
熟练开发人员来说可能有点难受。 - 如果使用多语言本地化操作,如果语言包在文件外,请放到
plglang
文件夹下,并保持开头必须包含你的插件相关信息。比如我开发了一个插件liba.wingplg
,如果是中文和其它语言区分,请命名为a-zh.qm
或liba-zh.qm
形式,也就是插件名
+语言标识
的类型。 - 使用
Qt
开发插件的时候,它会默认在前面加lib
,建议保留。 - 插件文件名不建议使用中文名称。
- 不要随意修改
iwingplugin.h
文件,如果你不是项目开发维护者,这是很不明智的行为。它可能会使插件加载失败、想要调用函数A
结果调用B
,甚至宿主程序崩溃的情况。 - 开发插件强烈建议 开放源代码,因为插件接口一旦更改,将采用互不兼容的模式,如果你能紧跟我的发行版也是没问题的。
- 如果插件含有资源,请在根目录前缀修改为和插件名称一致。由于默认新建的资源为
/
,也就是根,这个必须修改,以防和其他插件甚至和宿主资源冲突。
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。