Xournal++ 插件开发完整指南:用 30 分钟为手写笔记软件定制你的专属功能
【免费下载链接】xournalppXournal++ is a handwriting notetaking software with PDF annotation support. Written in C++ with GTK3, supporting Linux (e.g. Ubuntu, Debian, Arch, SUSE), macOS and Windows 10. Supports pen input from devices such as Wacom Tablets.项目地址: https://gitcode.com/gh_mirrors/xo/xournalpp
你是不是也经历过这样的场景:用 Xournal++ 在 PDF 上批改作业时,改完红笔换蓝笔,要鼠标划过三个菜单;导出 PDF 时,又要在文件菜单里翻找半天。每天重复几十次这种机械操作,手都快酸了。其实 Xournal++ 早就给你留了一扇门——插件系统。作为一个以手写笔记和 PDF 标注为核心的开源笔记软件,Xournal++ 允许你用 Lua 脚本直接操纵内部的菜单、工具栏、图层乃至导出流程。今天我就带你从"别人写的插件"一直走到"自己写的插件",看看怎么把你最烦的重复操作,压缩成一次按键。
别人家的插件到底长什么样?先拆开一个看看
在动手写代码之前,我们不妨先当一回"拆机党"。克隆仓库到本地后(仓库地址:https://gitcode.com/gh_mirrors/xo/xournalpp),直接进到plugins/目录,你会看到十来个现成插件,比如ColorCycle(颜色循环)、Export(一键导出)、LayerActions(图层批量操作)。
随便打开plugins/ColorCycle/,你会发现每个插件目录其实只有两个文件:一个plugin.ini负责"登记身份",一个main.lua负责"干实事"。为什么是这套双文件结构?原因很朴素:配置和逻辑分离。plugin.ini是给 C++ 的解析器读的(loadIni函数在src/core/plugin/Plugin.cpp第 209 行),声明作者、描述、版本、入口文件名;而main.lua才是你的业务逻辑。更妙的是,Lua 不需要编译,改完存盘、重启 Xournal++ 就能生效,开发调试的成本几乎为零。
Xournal++ 插件是怎么把自己的菜单项"塞"进主窗口的?
看代码之前,先回答一个关键问题:插件和主程序之间靠什么通信?答案是一个叫app的全局 Lua 表。Xournal++ 在启动插件时,会把内置的 Lua 库(注意看Plugin.cpp第 39 行的constexpr std::array loadedlibs{luaL_Reg{"app", luaopen_app}})注册进你的脚本环境,于是你的 Lua 代码里凭空多了一个无所不能的app对象。
而所有插件都必须实现一个约定俗成的入口函数initUi()。主程序在registerToolbar()(Plugin.cpp第 58 行)里通过lua_getglobal找到这个名字并调用它。在这个函数里,你最常用的一行代码就是:
function initUi() app.registerUi({["menu"] = "Cycle through color list", ["callback"] = "cycle", ["accelerator"] = "<Alt>c"}); end这就是 ColorCycle 插件的全部注册逻辑。registerUi接受一个表,四个字段各有分工:menu是显示在"插件"菜单里的文字,callback是点击后要调用的 Lua 函数名,accelerator是快捷键(这里Alt+C),toolbarID和iconName则是可选的工具栏按钮配置。完整字段说明在plugins/luapi_application.def.lua第 97 行附近有详细的注释。
从 Lua 到 C++,一次点击背后的调用链有多长?
你可能会好奇:菜单项明明注册的是字符串形式的函数名,主程序怎么知道去哪儿找它?这就要顺着registerUi的调用链往深处走了。
app.registerUi最终会落到Plugin::registerMenu(Plugin.cpp第 176 行),它把菜单项存进menuEntries向量;等到主窗口构建菜单时,populateMenuSection(第 79 行)会为每个菜单项创建一个 GTK 的GSimpleAction,并把回调接到executeMenuEntry上;而你点击菜单的那一刻,executeMenuEntry会调用callFunction(entry->callback, entry->mode),callFunction(第 338 行)做的事情用一句话概括就是:按名字去 Lua 虚拟机里查函数,再执行它。
这条链路就是整个 Xournal++ 插件系统的骨架:Lua 注册 → C++ 存表 → GTK 菜单 → 点击回查 Lua。如果你不按这个套路来,比如在initUi里写了一个不存在的 callback 函数名,点击菜单时lua_pcall会返回错误码,callFunction里第 349 行的错误检查就会弹出一个报错对话框——好在错误信息足够友好,会直接告诉你插件名和出错原因。
现场实操:写一个"一键循环换色"的插件
理论说完了,来点真格的。下面这个插件能让你在当前工具的颜色表里循环切换——把ColorCycle改造成带进度反馈和状态保护的版本:
-- 定义颜色表,{名字, 颜色值} local colorList = { {"black", 0x000000}, {"red", 0xff0000}, {"blue", 0x3333cc}, {"green", 0x008000}, {"orange", 0xff8000}, {"magenta", 0xff00ff} } local currentColor = 0 -- 主程序启动时调用,注册菜单项和快捷键 function initUi() app.registerUi({["menu"] = "Cycle Pen Color", ["callback"] = "cycleColor", ["accelerator"] = "<Alt>c"}) end -- 点击菜单或按 Alt+C 时触发 function cycleColor() currentColor = currentColor % #colorList + 1 app.changeToolColor({["color"] = colorList[currentColor][2], ["selection"] = true}) end逐行拆解一下关键部分:
- 第 1 行到第 6 行:
colorList是局部变量,只对本插件可见,不会污染其他插件的命名空间——这是 Lua 模块化的基本素养。 currentColor = currentColor % #colorList + 1:用取模运算实现环形切换,比if/else判断更简洁,也永远不会越界。app.changeToolColor({["color"] = ..., ["selection"] = true}):这才是核心动作,第一个参数是十六进制颜色值,第二个参数selection设为true表示连选中元素一起改色。
把这段代码存成plugins/MyColorCycle/main.lua,再配上plugin.ini:
[about] author=Your Name description=Cycle pen color with Alt+C version=1.0 [plugin] mainfile=main.lua插件装好了却不生效?三步排查法
写完了不等于能用。插件要真正跑起来,必须经过"安装目录正确 + 插件已启用 + 语法零错误"三重关卡,这是新手最容易栽跟头的地方。
第一关:放对目录。PluginController.cpp第 107 行到第 109 行给出了两个搜索路径:程序自带目录下的../plugins,以及用户配置目录下的plugins文件夹。放在前者需要系统权限,放后者更省事。Windows 上一般是%APPDATA%\xournalpp\plugins,Linux 上则是~/.config/xournalpp/plugins。
第二关:在插件管理器里启用。主窗口菜单"插件 → 管理插件"打开对话框,勾选你的插件。这个开关对应PluginController.cpp第 121 行的逻辑:插件启用状态保存在设置里,下次启动时setEnabled(true)后才会执行loadScript()。
第三关:检查日志。启用后如果菜单里没出现你的插件,多半是loadScript(Plugin.cpp第 282 行)报错了。注意看这段代码的细节:它先检查mainfile里有没有..路径穿越(第 288 行),再luaL_loadfile加载脚本(第 307 行),任何一步失败都会通过XojMsgBox::showPluginMessage弹窗告诉你具体错误。所以,优先保证 Lua 语法正确、函数名和 callback 完全一致,这两个是最常见的翻车原因。
进阶玩法:不满足于菜单,把插件按钮钉在工具栏上
菜单只能满足"鼠标点一下"的需求,如果你希望某个功能像笔盒一样常驻在眼前,registerUi的toolbarID和iconName字段就该出场了。注册完按钮后,打开"视图 → 工具栏 → 自定义"(对应上图的工具栏定制界面),在插件分类下找到你的按钮拖到任意位置即可。注意一个小坑:Plugin.cpp第 202 行会为你的 toolbarID 自动加上Plugin::前缀,所以你在toolbar.ini里手动配置时,必须写Plugin::你的ID,否则匹配不上。
回到开头那个痛点——改色、导出、翻页,这些动作现在都能被插件收编成一次按键。把ColorCycle改造成你自己的版本,给Export插件配一组顺手的快捷键,再翻翻plugins/LayerActions/main.lua里那种批量操作多个页面的写法,你会发现 Xournal++ 的插件 API 远比你想象的宽。下一步,不妨把luapi_application.def.lua里那 1218 行 API 注释当成你的词典,翻一翻app.export、app.getDocumentStructure、app.activateAction这些接口——它们就是你通往"任意自定义"的钥匙。写完第一个插件,你的笔记软件就已经和别人的不一样了。
【免费下载链接】xournalppXournal++ is a handwriting notetaking software with PDF annotation support. Written in C++ with GTK3, supporting Linux (e.g. Ubuntu, Debian, Arch, SUSE), macOS and Windows 10. Supports pen input from devices such as Wacom Tablets.项目地址: https://gitcode.com/gh_mirrors/xo/xournalpp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考