Air 系列应用交流/轻手写(from Xournal++)|AirInk · 鸿蒙 PC 适配全记录

轻手写(from Xournal++)|AirInk · 鸿蒙 PC 适配全记录

【Air 系列 · 鸿蒙 PC 适配全记录】17 / 56

■ 应用:轻手写(from Xournal++)(AirInk)
■ 定位:手写笔记与 PDF 批注(Xournal++)
■ 上游:Xournal++ | 路线:GTK3 gtk3-stack | 版本:1.0.0
■ 市场状态:开发中,即将上架
■ 官网(免费下载): https://www.shunidc.cn/

————————————————————
开源鸿蒙 PC 社区: https://harmonypc.csdn.net/

PC 社区项目申请: https://atomgit.com/OpenHarmonyPCDeveloper

适配开源地址:(AtomGit 仓库建立后回填)

环境搭建文章:https://blog.csdn.net/weixin_52908342/article/details/161343743

▍一、为什么要适配 轻手写

手写笔记是平板与二合一设备上最典型的生产力场景,Xournal++ 是这个领域最成熟的开源桌面应用:图层化笔记、多纸张模板、压感笔迹、PDF 批注、录音锚点,v1.3.8 版本的依赖与构建体系也相当规范。轻手写(工程名 AirInk)要做的,是把这套 GTK3 应用完整带到鸿蒙的平板与 PC 上,而不是用 ArkTS 重写一个形似的笔记应用。

选型阶段先回答了三个问题(PORTING.md 第 0 节"选型三问")。GUI 路线定为 GTK3 + 自研 OHOS GDK 后端——共享的 gtk3-stack 栈已内置该后端(15 个 gdk_ohos_* 导出符号实证),且关键发现是 Xournal++ 零 gtkmm 依赖,全树 grep 无 gtkmm 引用,纯 GTK3 C API,与 GIMP/darktable 同线。架构定为 Air 双层架构:ArkTS 壳 + 薄 NAPI 桥 libinkbridge.so(dlopen/dlsym)+ 重型 libairink_core.so(GTK 线程宿主 + libxournalpp-core.a 静态链)。XComponent 定为 TEXTURE——AirRT 的实证是 SURFACE 会遮挡 ArkUI 弹层,而后续 picker 与菜单桥需要 ArkUI 浮层可见。

工程包名 com.airink.app,应用名"轻手写",版本 1.0.0,deviceTypes 覆盖 tablet、2in1 与 phone。ArkTS 侧只有 3 个文件共 305 行,壳的职责被压缩到最小,这正是该路线的形态特征。主验证设备为 MatePad Mini 悦读版(2560x1600,density 2.38),M1 里程碑(Xournal++ 本体交叉编译 + 主窗渲染 + 中文 + 触摸链)已于 2026-09-25 真机达成。

▍二、先划清边界:不把编译通过当成适配完成

桌面上的 Xournal++ 默认有窗口管理器替它摆放窗口、有文件系统直读直写、有鼠标键盘手写笔三套输入。这些前提在鸿蒙沙箱里全部不成立。工程的做法是用里程碑(M0-M7)把"能编译"与"能交付"明确切开:M1 只承诺主窗渲染、中文化与触摸链贯通,输入深化、文件链路、菜单桥、手写笔压感全部排进后续里程碑,不提前宣称。

层次 桌面 Xournal++ 鸿蒙侧当前方案(M1)
应用入口 main() + GtkApplication EntryAbility + Index.ets 的 XComponent(TEXTURE)
窗口系统 GTK 窗口 + 窗口管理器 OHOS GDK 后端 + gtk_window_maximize(patch 0005)
输入 鼠标/键盘/手写笔 触摸与鼠标注入链已通,防双发未做;手写笔未接入(M5)
文件读写 文件系统直读直写 未接 picker,计划 M3 走 S7/S8 授权
菜单与工具栏 GTK menubar/toolbar 完整两行 GTK 原生渲染;toolbar.ini 缺失仅 4 图标(缺陷 D2)
本地化 gettext 全量 xournalpp.mo 已生效,gtk30.mo 未部署(S2 部分完成)
音频/插件 portaudio + Lua 插件 CMake 关闭(ENABLE_AUDIO/ENABLE_PLUGINS=OFF)

最后两行说明当前产物的能力范围由依赖矩阵直接决定:音频录制回放与 Lua 插件系统在交叉编译期整体关闭,属于明确"不做"而非"没做完"。

▍三、鸿蒙版的整体架构

运行时的关键设计有三处。其一,libinkbridge.so 只链系统库,职责是 XComponent 回调注册、按依赖拓扑序 dlopen GTK3 与 Xournal++ 的全部依赖(RTLD_GLOBAL),再经 dlsym 取 Core API;其二,libairink_core.so 里的 core 线程按最严格的顺序初始化——env 注入先于一切 glib 调用,随后 trace 日志、SIGSEGV handler、数据就绪 condvar 门控、gtk_init、native window 预绑定;其三,运行模式分流:empty 模式渲染 M0 最小窗口(files/airink_mode.txt 切换),缺省 xopp 模式直接进入 XournalMain::run(GtkApplication NON_UNIQUE,GIMP3 在 OHOS 上的同款实证配置)。

text
ArkTS 壳(EntryAbility + Index.ets)
├── rawfile 载荷部署:xopp-share → files/share/xournalpp、xopp-locale → share/locale、
│ xopp-icons → share/icons(Adwaita + hicolor)+ 就绪戳跳过重复拷贝
└── XComponent('airink-main', TEXTURE, libraryname='inkbridge') + splash 轮询撤层
│ NAPI
libinkbridge.so(薄桥:XComponent 回调 + 40 库拓扑 dlopen + dlsym 表)
│
libairink_core.so(core 线程:env 最前置 → trace.txt → SIGSEGV handler →
datafiles condvar 门控 → gtk_init → native window 预绑定 → 模式分流)
├── empty 模式:M0 最小窗口(airink_mode.txt 切换)
└── xopp 模式:XournalMain::run(GtkApplication NON_UNIQUE)
│ GDK_BACKEND=ohos
gtk3-stack(共享栈:GTK 3.24.53 / glib 2.80.4,OHOS GDK 后端编入 libgdk-3.so)
+ poppler-glib 26.07(自 gtk4-stack 跨栈复用)+ librsvg + libzip 1.11.4(新编)


主要目录如下:

text
AirInk/
├── AppScope/app.json5 # 包名 com.airink.app
├── PORTING.md # 唯一权威入口:选型/依赖/补丁/里程碑
├── ai_log/ # 会话记录(立项与 M1 全程)
├── entry/src/main/
│ ├── module.json5 # tablet/2in1/phone
│ ├── ets/
│ │ ├── pages/Index.ets # XComponent(TEXTURE) + splash + 返回键桥
│ │ └── entryability/ # EntryAbility
│ ├── resources/rawfile/
│ │ ├── xopp-share/ # ui(glade + 4 套图标主题)/ resources / palettes
│ │ ├── xopp-locale/ # zh_CN xournalpp.mo(msgfmt 现编)
│ │ └── xopp-icons/ # Adwaita + hicolor(复用 AirTerm 资产)
│ └── cpp/
│ ├── napi_init.cpp # inkbridge:回调注册与拓扑 dlopen
│ └── libs/ # 闭包内依赖(arm64-v8a)
├── entry/libs/arm64-v8a/ # 模块级通道:二级传递依赖(liblcms2 等)
└── native/
├── patches/xournalpp/ # 0001/0002/0004/0005 四个补丁
├── scripts/ # env.sh / build_libzip / build_xournalpp / deploy_stack
├── cross/ohos-aarch64.toolchain.cmake # 标准模板派生的交叉工具链配置
├── install/ # 本工程安装前缀(libzip)
└── sources/ # xournalpp(symlink 至 Upstreams v1.3.8)与 libzip


▍四、核心适配过程

### 1. 依赖矩阵实测与跨栈复用

PORTING.md 第 2 节按 v1.3.8 的 CMakeLists 逐项实测依赖来源:GTK3/glib/gio 全部复用 gtk3-stack 3.24.53(glib 两栈同为 2.80.4,混链安全经实证);libxml2、librsvg、zlib 直接复用;poppler-glib 要求 ≥0.41,gtk3-stack 没有,从 gtk4-stack 取 26.07 跨栈复用(NEEDED 已归一、链 libc++_shared);唯一全栈皆无的 libzip 新编 1.11.4,仅 zlib 依赖、加密后端全关。明确关闭的有 portaudio/sndfile(音频)、Lua(插件)、gtksourceview(TeX 高亮)、qpdf、X11(CMAKE_DISABLE_FIND_PACKAGE_X11=ON)。宿主侧的 glib-compile-resources、glib-mkenums、msgfmt 由 brew 提供,缺 help2man 则 -DWITH_MAN=OFF。

### 2. 四个源码补丁

上游树只经补丁修改(铁律 19),四个补丁各有明确根因。0001-utf8-view-clang15-cxx20-compat 处理 utf8 视图哨兵 operator== 在 Sentinel=Iterator 代换下触发 clang15 的改写候选歧义(P2468R2 之前的语义);0002-viewiteratorbase-typename-and-symmetric-eq 补 dependent type 的 typename 并把成员 operator== 改为对称双参,绕开 equality_comparable 概念链在 clang15 下对成员形式的误判;0004-pathutil-ohos-env-overrides 让 __OHOS__ 下的数据路径/locale 路径/install UI 路径尊重 XOURNALPP_SHARE_DIR 与 XOURNALPP_LOCALE_DIR 环境变量——/proc/self/exe 在 OHOS 指向系统路径,安装前缀推导不可用;0005-ohos-mainwindow-maximize 在主窗 show 之后调用 gtk_window_maximize,因为无窗口管理器时默认 1600x1200 的窗口在 2560x1600 表面四周留黑边。补丁由 build_xournalpp.sh 开头幂等应用(git apply --check 与 --check --reverse 双检测)。

### 3. 双层架构与 40 库拓扑 dlopen

napi_init.cpp 的注释明确了薄桥的三项职责:注册 XComponent 回调(surface 生命周期加触摸/鼠标/键盘/滚轮)、按依赖拓扑序以 RTLD_GLOBAL dlopen 全部依赖、导出七个 NAPI 函数(isLoaded/notifyDatafilesReady/setDataPath/setDisplayDensity/resetKeyModifiers/handleBackPress/getVersion)。加载顺序在 LoadCoreLibraries 里按 40 多个库的拓扑排列,libc++_shared 必须在 poppler 之前。core 库内,环境变量注入(HOME/XDG 指向沙箱、GDK_BACKEND=ohos、GDK_OHOS_DIALOG_SUBWINDOW=1 预留、DBUS 禁用、LANGUAGE=zh_CN)发生在任何 glib 调用之前——这是 glib 行为确定性的前提。日志走文件优先(沙箱 airink_trace.txt),仅 ERROR 上 hilog。

### 4. Day-1 打包五件套与 hvigor 闭包一层实证

部署纪律固化在 PORTING.md 第 4 节:strip:false 在 entry 的 build-profile 双置;依赖分双通道——闭包内进 entry/src/main/cpp/libs/arm64-v8a,闭包外与二级传递依赖进模块级 entry/libs/arm64-v8a;deploy_stack.sh 对部署副本做 ELF 原位编辑(strip-version、去 libc.so、对齐修复);dlopen 拓扑清单兜底加载序;验收用 HAP unzip 字节级核对。本工程的新实证是 hvigor 闭包只收一层:libpoppler 的二级 NEEDED(libopenjp2、liblcms2)即使拷进 cpp/libs 也会被静默丢弃,必须走模块级通道。另有一条重链陷阱:改了静态库后 hvigor 不重链,需删三处 intermediates 的 libairink_core.so 强制。

### 5. 数据载荷与首启防御

Xournal++ 的运行时资产随 HAP 携带:rawfile/xopp-share 装 ui(glade 与四套图标主题)、resources、palettes;xopp-locale 装 msgfmt 现编的 zh_CN xournalpp.mo;xopp-icons 复用 AirTerm 的 Adwaita 与 hicolor。部署带就绪戳,二次启动跳过拷贝。首启最大的坑是图标主题实为硬依赖:主题缺失时 'image-missing' 兜底图标也不存在,gtkiconhelper 直接断言 SIGABRT——这是首装实测换来的教训,也解释了为什么图标主题要随包而非按需下载。

▍五、适配中遇到的几个难点

### 难点1:clang 15 与 C++20 的模板边界

Xournal++ 全树在 clang 15(NDK 同系工具链,musl 环境)下编译,两个模板边界问题接连出现:utf8 视图的哨兵比较器在 Sentinel=Iterator 代换下触发改写候选歧义,iterator 基类缺 typename 且成员 operator== 被 equality_comparable 概念链误判。两处都以最小补丁收敛——约束原友元于非同型、改对称双参形式——并把自测断言 #if 0(libc++15 尚有 constexpr char8_t 比较缺口)。这类问题的共性是代码完全合法却依赖尚未实现的提案语义,只能改写形式不能改语义。

### 难点2:安装前缀推导在沙箱里失效

上游经 /proc/self/exe 反推安装前缀,再拼出 ui/locale 路径;OHOS 上该链接指向系统路径,推导结果指向不存在的目录,首启弹出"Missing ui/about.glade"错误框。修复是 patch 0004 让路径函数尊重环境变量,由 core 线程在 env 注入阶段把 XOURNALPP_SHARE_DIR/XOURNALPP_LOCALE_DIR 指到 rawfile 部署后的沙箱目录。缺陷登记里 D5/D6 保留了一条方法论教训:该补丁最初写于静态库编译之后且未重编,问题"复发"实为旧产物——改补丁必须重编 libxournalpp-core.a。

### 难点3:无窗口管理器的首屏几何

桌面 GTK 应用的窗口尺寸由用户与 WM 协商,OHOS 上没有这个角色。默认 1600x1200 的主窗落在 2560x1600 的表面上,四周黑边,观感如同半屏应用。patch 0005 在 on_startup 的 show 之后补 gtk_window_maximize,与 AirRaw 的 ui_last/maximized 对策同源。这是 GTK 路线上最小的补丁,却是首屏观感决定性的一处。

### 难点4:hvigor 依赖闭包只收一层

部署验证时发现 liblcms2 "拷了却没进 HAP"。排查确认 hvigor 打包闭包实测只收一层 NEEDED:libpoppler 自身(一层)在 cpp/libs 通道能进包,但它的二级依赖 libopenjp2、liblcms2 会被静默丢弃。对策是把二级传递依赖统一改走模块级通道(entry/libs/arm64-v8a),并把这一发现登记为风险 R5——此后每轮部署都执行 HAP unzip 清单验证,不再依赖"拷了就在"的假设。

### 难点5:图标主题是隐性的硬依赖

首装实测 GTK 应用直接 SIGABRT。排查链:应用请求的图标不存在时 GTK 会取 'image-missing' 兜底图标,而兜底图标本身属于主题——Adwaita/hicolor 未部署时兜底也不存在,gtkiconhelper 断言崩溃。这个案例把"资源完整性"从事项问题提升为启动安全问题的等价物:rawfile 载荷部署(含图标主题)因此被设计为 core 线程的 condvar 门控条件,数据未就绪 GTK 不启动。

▍六、编译、安装与启动

构建入口按 PORTING.md 第 1 节记载的顺序执行:native/scripts/env.sh 初始化交叉环境(工具链 ~/Library/AirChat-SDK24/native,clang 15 / lld / musl),随后 build_libzip.sh、build_xournalpp.sh 交叉编译,deploy_stack.sh 部署依赖与 ELF 修正。上游树变动后补丁由构建脚本幂等重放。之后走标准 hvigor 构建:

bash
export JAVA_HOME=/Applications/DevEco-Studio.app/Contents/jbr/Contents/Home
export DEVECO_SDK_HOME=/Applications/DevEco-Studio.app/Contents/sdk

hvigorw --mode module -p module=entry assembleHap --no-daemon


签名产物位于 entry/build/default/outputs/default/entry-default-signed.hap。连接设备后安装并启动:

bash
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b com.airink.app


工程文档同时记录了部署循环细节:签名用 devecocli signature generate 一次性生成;替换 .so 后必须 force-stop 或 --uninstall 再部署,否则旧库残留。

▍七、当前功能边界

按里程碑(M1,2026-09-25 达成)与缺陷登记,当前版本已覆盖:

- Xournal++ 1.3.8 本体交叉编译进 libairink_core.so,主窗在 MatePad Mini 真机渲染;
- 中文界面(xournalpp.mo + LANGUAGE=zh_CN 环境注入链生效);
- 触摸与鼠标输入注入链贯通(XComponent 回调 → NAPI → core 线程投递);
- rawfile 三类数据载荷(share/locale/icons)自动部署与就绪戳跳过;
- splash 低频轮询撤层(就绪后停止,稳态零开销);
- 返回键经 handleBackPress 桥接 GTK 层裁决;
- empty 模式最小窗口作为渲染管线的备用验证通道。

尚未覆盖的部分(均对应明确里程碑或缺陷登记):触摸绘制、滚轮、捏合缩放、手势返回与 IME(M2);picker 文件链与目录授权,.xopp 打开保存(M3,S7/S8);菜单 ArkTS 化与对话框桥(M4,S1);手写笔压感与防误触(M5,S4,手写应用的硬性标准);剪贴板、分享、暗黑模式、模板弹窗(M6);性能优化与上游对齐(M7)。已知缺陷 D1-D4 未修:纸张与缩略图边框渲染为红色、工具栏因 toolbar.ini 缺失仅 4 图标、状态栏空白、侧栏一个图标缺失。S1-S14 强制移植标准中仅 S2、S3、S13 为部分完成。

因此,当前版本更准确的定位是"Xournal++ 在鸿蒙上的 M1 渲染验证版":GTK3 路线的可行性与 Day-1 工程纪律已经真机证实,距离可日常使用还隔着输入深化、文件链与手写笔三个硬里程碑。

▍八、总结

轻手写的价值集中在两件事上。第一是路线证明:GTK3 应用经自研 OHOS GDK 后端在鸿蒙上直跑成立,Xournal++ 零 gtkmm 依赖的实证让 GIMP 系应用在鸿蒙上的迁移成本有了明确参照;四补丁、一依赖矩阵、一打包纪律的组合可以直接复制到同线工程。第二是工程方法:立项即落 PORTING.md 唯一权威入口、S1-S14 强制标准、风险登记册与里程碑门控,使"编译通过"与"可交付"从第一天起就是两个概念。

后续工作按里程碑推进,优先级最高的三件是 M2 输入深化、M3 文件链与 M5 手写笔压感——对一款手写应用,压感与防误触是硬性标准(AirDraw 金标准待移植)。已知缺陷 D1-D4 会在对应里程碑内收敛,每轮修复仍以真机验证与 HAP unzip 字节级核对收口。
楼主 8 天前
想参与讨论?登录 或 30 秒注册。