A
Air官方
【Air 系列 · 鸿蒙 PC 适配全记录】33 / 56
■ 应用:AirPress(from Scribus)(AirPress)
■ 定位:Scribus 移植,桌面排版
■ 上游:Scribus 1.7 (Qt6) | 路线:Qt6 6.11.1+QPA | 版本:1.1.0
■ 开源: https://gitcode.com/Andy88/Harmony-Scribus
■ 市场状态:已上架 AGC 应用页: https://appgallery.huawei.com/app/6917615797714882057
■ 官网(免费下载): https://www.shunidc.cn/
————————————————————
开源鸿蒙 PC 社区: https://harmonypc.csdn.net/
PC 社区项目申请: https://atomgit.com/OpenHarmonyPCDeveloper
适配开源地址:https://gitcode.com/Andy88/Harmony-Scribus
环境搭建文章:https://blog.csdn.net/weixin_52908342/article/details/161343743
▍一、为什么要适配 AirPress(Scribus)
Scribus 是开源桌面排版(DTP)领域成果最重的程序:专业页面布局、CMYK 色彩管理、PDF 出版输出、文本链与样式系统、字体管理与复杂停靠面板,对 Qt 的使用深到私有类与原生对话框定制。它的上游主线的 Qt 6 版本对移植工程是极限测试——文字排版依赖 harfbuzz 与 ICU,导入链依赖 poppler,拼写检查依赖 hunspell,任何一环在 musl 沙箱上的缺口都会让编译或运行直接失败。
鸿蒙侧工程名为 AirPress,包名 com.airpress.app,版本 1.0.0,deviceTypes 覆盖 phone、tablet 与 2in1。移植路线为三件套:AirGis 的 Qt 6.11.1 预编译栈(74 个模块)、AirCad 的 ohos QPA(Qt6 版)复制改造,加上 Air 双层架构。PORTING.md 记载的验证设备为 MatePad Mini 悦读版、MateBook Pro 与 MatePad 11.5;M2 里程碑时期的 HAP 约 233MB,dlopen 拓扑 94 个库全部加载成功,主窗完整渲染且字体零缺字。
对移植系列而言,Scribus 的样本价值在于它是目前该系列中依赖面最宽的上游:Qt6 重型 C++20、外部 C 库矩阵、Python 运行时、插件体系与停靠框架同时在场,任何在它上面成立的方案,向下兼容整个系列。
▍二、先划清边界:不把编译通过当成适配完成
Scribus 的启动流程本身就是桌面向的:首启向导、新建文档对话框、崩溃恢复弹窗,全是模态 exec();文件对话由自研的 CFileDialog 收口并强制禁用原生对话框;Help 菜单挂满官网、Wiki、IRC 与检查更新。这些能力在无窗口管理器的单表面 QPA 上要么死等、要么不可达,工程对每一层都做了明确替换或移除。
层次 原项目实现(桌面 Scribus 1.7) 鸿蒙侧当前方案
应用入口 main_nix.cpp 的 main() 可执行程序 airpress_scribus_main 导出 + 本体改 SHARED 库(P2)
窗口承载 X11/Wayland + WM 管理多窗 XComponent + ohos QPA,对话框子窗化(§7.4.1 全链)
首启流程 FirstStartWizard/startUpDialog 模态向导 P9/P10 跳过 + airpressNewDefaultDoc 免对话框建文档
崩溃恢复 每次启动弹恢复对话框 P15 鸿蒙跳过,自动保存文件保留磁盘可找回
文件对话框 CFileDialog 内嵌 Qt 对话框(禁用原生) CFileDialog __OHOS__ 分支同步桥接系统 Picker
文本引擎 ICU C++ 类(BreakIterator/UnicodeString) scicu_compat.h C API 兼容层(ubrk_* 等,P6)
停靠布局 桌面宽屏 adsDockState 持久恢复 P16/P17 收窄 + 画布自适应重排,忽略旧 state
Help 与在线服务 官网/Wiki/IRC/检查更新/关于外链 P14 Help 菜单整体移除,F1 加速键封堵
这张表的共同点是"单点收口":入口在一个函数、文件对话在一个类、ICU 在一层头文件——找到收口点,替换就是机械的;找不到,就会像批3 之前那样在渲染层空转。
▍三、鸿蒙版的整体架构
工程沿用 Air 双层架构并把对话框升级为独立子窗体系:ArkTS 壳承载菜单栏、加载层、对话框宿主与真子窗管理;NAPI 层做输入分发与桥接;原生侧 Qt 宿主线程预载 QPA、注入 EGL surface 后调用 airpress_scribus_main。
text
EntryAbility(windowStage 持有、沉浸式、loadContent 后挂装饰管理)
├── Index.ets:ArkTS 菜单栏(官方 Menu 组件树)+ LoadingProgress 加载层
├── DialogHost / DialogPage / DialogTitleBar # 平板浮层与 2in1 真子窗双路径
├── SubWindowManager.ets # 自由多窗闩语义与子窗创建
└── components / utils(ImmersiveManager、FileManager)
▼ NAPI:napi_init.cpp(触摸/鼠标/键盘/滚轮/捏合分发、pick 队列、dialog ops)
└── libairpress_core.so(Qt 宿主线程)
├── 预载 libqohos(RTLD_GLOBAL,须在 QApplication 前)
├── QOhos_SetDisplayMetrics 注入 dpr → StrategyA EGL surface 注入
└── airpress_scribus_main → libscribus_gui.so(SHARED,P2)
├── ScribusMainWindow + ADS 停靠框架 + Canvas
├── CFileDialog → 系统 Picker 同步桥(reqId 路由 FIFO)
├── 菜单桥(QJsonDocument 快照 + serial 门控 + trigger 队列)
├── scribus-plugins/(50 插件)+ Python 3.11(56 扩展模块)
└── harfbuzz 8.3.0 / hunspell 1.7.2 / poppler-cpp 26.07
主要目录如下:
text
AirPress/
├── AppScope/app.json5 # 包名 com.airpress.app、版本 1.0.0
├── entry/src/main/
│ ├── ets/
│ │ ├── pages/Index.ets # 菜单栏、加载层、XComponent 宿主
│ │ ├── components/DialogHost.ets # 对话框浮层宿主(z 序、遮罩、取消)
│ │ └── utils/SubWindowManager.ets # 2in1 真子窗管理
│ └── cpp/
│ ├── airpress_core.cpp # Qt 宿主线程、surface 生命周期、pick 队列
│ ├── napi_init.cpp # NAPI 导出与 TSFN 桥
│ ├── qt_platform/ # ohos QPA(AirCad 蓝本改造,含对话框子窗化)
│ └── elf_edit.py / patch_libs.py # NEEDED 规整与 ELF 处理
├── native/
│ ├── scripts/env.sh # 交叉环境(NDK/粮仓路径)
│ ├── scripts/build_deps.sh # harfbuzz/hunspell/poppler-cpp 自编
│ ├── scripts/build_scribus.sh # libscribus_gui.so 交叉构建
│ ├── scripts/deploy.sh # 部署与 ELF 规整(qpa 模式除外)
│ └── sources/ # scribus、harfbuzz、hunspell 源码
└── evidence-queue/ # 验证证据队列
▍四、核心适配过程
### 1. GUI 转共享库与入口改造
P2 把 main_nix.cpp 的 main 改为 extern "C" __attribute__((visibility("default"))) 的 airpress_scribus_main,本体以 add_library SHARED 输出 scribus_gui。这里有两个实证过的坑:只加 visibility 属性不够,C++ 名字修饰会让 dlsym 按 _Z21... 找不到符号,必须 extern "C";Scribus 全局 -fvisibility=hidden 又要求导出点显式声明。本体对 core 的跨层调用一律走 dlsym,避免 NEEDED 链入 core 形成加载环。
### 2. 依赖粮仓与自编三件
Qt6 全家来自 AirGis 预编译栈;cairo/freetype/lcms2/fontconfig/libxml2/poppler 核心库来自 AirScape;Python 3.11.9 来自 AirCad。冲突面经 llvm-readelf 审计:Qt6.11 栈的 NEEDED 仅 libz/libzstd/libssl3/libcrypto3/libGLESv2,与 AirScape 的 C 库栈零符号冲突。本工程自编三项:harfbuzz 8.3.0(含 icu 与 subset 变体,Scribus REQUIRED 两项硬依赖)、hunspell 1.7.2、poppler-cpp(在 AirScape poppler 26.07 源码树上补编 cpp 绑定)。
### 3. ICU C API 兼容层与 include 劫持根治
OHOS 系统的 libicu 是纯 C 库(零个 C++ 符号、无 U_NAMESPACE),而 Scribus 的文本整形直接用 icu::BreakIterator 与 UnicodeString。P6 写了 scicu_compat.h,把 8 个文件的 C++ 用法替换为 ubrk_* C API;更棘手的是编译期劫持——AirScape 粮仓的 icu 头是五文件垫片,其顶层 include 路径经 fontconfig.pc/expat.pc/FindJPEG 多路注入抢在 sysroot 之前解析,导致 U_CAPI unknown 家族报错。根治三件套为:AirPress 自己的 include 转发树(sysroot 全部 unicode 头)、覆盖 fontconfig.pc/expat.pc 的 Cflags、根 CMakeLists 的 include_directories(BEFORE);且顶层 .h 必须真实拷贝——文本转发头会破坏 FindPNG 的 file(STRINGS) 版本解析。
### 4. 系统 Picker 桥与菜单桥
批3-S7 破除了一个普遍假设:QPA 的文件对话框 helper 在这里永远不被咨询,因为 Scribus 不走 Qt 原生对话框——CFileDialog 是全部文件对话的单一收口且强制 DontUseNativeDialog。拦截点因此放在 CFileDialog 本身:__OHOS__ 分支经 airpress_core_pick_request_sync(Qt 主线程 QEventLoop + 5 分钟安全阀 + reqId 路由 FIFO 队列)交 ArkTS 拉 DocumentViewPicker/DocumentSavePicker/FOLDER,沙箱中转后回传。模式映射、suggest 必须 basename、暂存名用 ASCII 等细节都有实证记录。菜单桥则从 mw->scrActions 反查规范 id(ScrAction 从不 setObjectName),QJsonDocument 快照加 serial 门控增量下发,批5 升级为官方 Menu 组件树渲染,支持级联、快捷键列、灰显与勾选态。
### 5. 对话框子窗化与多实例
批8 把 AirCast 的 §7.4.1 蓝图整体移植为 Qt 版全链:QPA 侧 Qt::Dialog/Sheet 分类与五类 ops、对话框专属 EGLSurface、flush 分离合成、QOhos_InjectDialogPointer 千分比换算注入;ArkTS 侧 DialogHost(平板浮层)与 SubWindowManager(2in1 真子窗)双路径,路由谓词为"自由多窗永续闩 ∪ 实时 FLOATING"。多实例的排队根因(模态 exec 期间 QTimer tick 不派发,触发滞留 73 秒)以 QMetaObject::invokeMethod 的 QueuedConnection 即时唤醒修复,实测打开 A 后点 B,194ms 内 B 建立专属 surface 双窗并存。
▍五、适配中遇到的几个难点
### 难点一:初始化路径上的模态 exec 是全链毒药
FirstStartWizard、startUpDialog、崩溃恢复对话框三个模态在无 WM 的 QPA 上死等或推迟一切后续桥接。批4e 的取证最具代表性:冷启动菜单栏空白的真因是"崩溃恢复"模态卡住 app.init(),而菜单桥恰在 init 之后才启动——用户看到的是"顶部没沉浸",实际是菜单没有内容。工程由此确立"鸿蒙上初始化路径禁模态"的原则(P9/P10/P15 三例同族),并引入"菜单桥首快照流动"作为 GUI 真就绪信号:scribus-main 这个 stage 名字暗示主窗就绪,实际在入口函数调用前置位,两者差出整个启动期。
### 难点二:画布白页的"渲染故障"假象
用户报"画布空白",探针证明 Canvas/ScribusView/ScribusDoc 根本未构造——P10 跳过新建对话框后没有任何文档被创建,灰色只是 ScribusView 调色板的空态背景。修复是 S12-lite:按偏好默认(A4/边距/单位/装订方向)走 doFileNew 免对话框路径,实机复现 A4 白页、红页框、边距线与标尺。此案沉淀的方法是"'渲染故障'先证伪有没有在画",探针必须埋在 paintEvent 早退判断之前。后续批4b 更进一步证明 NewDoc 对话框本身可正常渲染,批8 遂把"文件→新建"恢复为上游完整对话框体验。
### 难点三:停靠面板宽度的三层根因
"两边工具栏太宽"背后叠了三层:旧 adsDockState 恢复的是 dpr 1.0 宽屏时代的绝对像素;桌面默认 panelWidth 290 过宽;插件面板在右列外侧再叠独列。P16 分层修复后仍有下限钳制——configureDock 的 setMinimumWidth(minimumSizeHint) 让 cap 落空,批5 用探针实测各面板 minimumSizeHint(PropertiesPalette 357、PagePalette 343)后才对症:OHOS 跳过 setMinimumSize(sizeHint())、.ui 网格手术把 X/Y/W/H 双列改单列、ScrSpinBox 降 min。画布从 180 逻辑像素升到 542。教训被固化为"面板宽度问题先探针后动手,别盲试 cap"。
### 难点四:多实例下的单槽资源与可达性
pick 管线初版是单槽(g_pick_pending/g_pick_result),多实例并发时他人结果会误杀等待中的请求;插件动作的 QAction 挂在 ScripterCore 上、不在 scrActions 也不在 widget 树,菜单桥三路查找全落空,插件功能窗"从不可达"。修复为 FIFO 队列加 reqId 路由 map、菜单桥加全应用 QMenu 扫描兜底(含孤儿 QMenu 这类无 parent 顶层对象)。批9 又从 376MB hilog 里挖出无文档态的 SIGSEGV@0x6ac——反汇编显示 0x6ac 恰为 itemToolPrefs.polyCorners 成员偏移,GetPolyProps 裸解引用空 doc;P19 补守卫后复现率 0/5,且带文档的正路径弹窗正常。
### 难点五:数据管线与签名运维的静默失败
154MB 部署数据踩过一连串坑:fs.mkdirSync(dir, true) 对已存在目录抛 EEXIST 并炸掉整个 try 块,stamp 数月级静默失败;fs.writeSync 只收 ArrayBuffer,传 Uint8Array 视图伪报 ENOENT;逐文件 getRawFileContent 在数千文件规模挂死,终态改为 tar 分卷(44MB×4)+ getRawFileDescriptor fd 直传 + native 后台线程解包,147MB 部署进入 3 秒级。签名侧,devecocli run 默认 product 的 release 签名会被 hdc 以 9568322 拒装,工程增设 debug product;跨代 profile 换签名必须带 --uninstall。
▍六、编译、安装与启动
构建入口在 native/scripts/,顺序为:env.sh 装载 NDK 与粮仓路径;build_deps.sh 交叉编译 harfbuzz/hunspell/poppler-cpp;build_scribus.sh 以 ninja 编出 libscribus_gui.so 与 core;deploy.sh libs 部署并做 ELF 规整(注意 qpa 模式只拷贝不规整 NEEDED,混用会让 libqohos 被 musl 拒载、症状远隔一次重启)。产物为 233MB 级 HAP(M2 记载值),首启沙箱拷贝约 196MB。HAP 打包:
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.airpress.app
PORTING.md 记载的验证基准:MatePad Mini 上 M2/M3 达成主窗完整渲染(菜单/工具栏/停靠面板/状态栏全出、字体零 tofu)与触摸输入链打通;批7 启动加载层改造后温启动约 6 秒、品牌 splash 与白闪零帧;批8 多实例对话框在 Mini 与 MateBook Pro 双路径实测;MateBook Pro 全新安装首启 2/2 通过,加载层交接后直达完整中文主界面。
▍七、当前功能边界
按 PORTING.md §6 遗留清单与各批记录,已完成并经真机验证的能力:
- S1 ArkTS 菜单栏(官方 Menu 组件树、级联、快捷键列、灰显、勾选态、serial 门控);
- S2 全面汉化(scribus.zh_CN.qm + qt_zh_CN.qm 兜底,界面全中文);
- S3 双模输入(触摸+鼠标合成链、批5 物理键盘修饰键全链、Ctrl+O 触发 picker 实测);
- S6 光标桥实装、S7 系统 picker 全链路(35 组 filters 全量传递)、S8 文件夹授权镜像写入、S9 剪贴板双通道;
- S11 暗黑(ScribusProxyStyle Dark 实测)、S12 完整版新建文档对话框(模板缩略图/参数联动);
- S13 数据管线(tar 分卷 + fd 直传 + 后台解包,147MB 秒级)、S14 上游对齐(Ctrl+滚轮原生缩放语义);
- Python 脚本台复活(PYTHONHOME 前缀重组 + 56 扩展模块)、对话框子窗化多实例、P19 无文档态闪退修复;
- 启动加载层(LoadingProgress + 菜单桥快照门控 + 120s 兜底)与顶部/底部沉浸适配。
尚未覆盖或待验证的项同样登记在册:S4 手写笔与 S5 捏合缩放代码就位但需 M-Pencil 真笔与真机双指人工验证;S10 碰一碰需 NFC 硬件;markdown 导入因 Qt6.11 栈无 textmarkdown 支持而禁用(P7,接受现状);CustomFDialog 直连入口(图像导入等)的 picker 化为后续增强;文本直输与滚轮缩放的硬件复核留人工;S13 深度量化与 S14 完整对齐为部分状态;保存确认弹窗存在中英混排(.qm 覆盖缺口)。因此当前版本更准确的定位是"Scribus 1.7 鸿蒙 PC 可用排版基础版"——从启动、文档创建到编辑、导出、系统接驳的主链已在三类设备上闭环,剩余项集中在硬件依赖的验证与长尾对话框体验。
▍八、总结
AirPress 是这个移植系列里依赖面最宽的一役:Qt6 预编译栈、纯 C 的系统 ICU、沙箱禁 exec、无 WM 模态、Python 运行时、停靠框架,每个环节都要求给出可复现的工程答案。九批真机迭代给出的答案是成体系的——单点收口的替换、显式导出的桥接、探针先行的布局调优、门控流动的启动层——而不是零散的补丁堆叠。
后续工作在登记册里已经排好序:S4/S5 的硬件验证、CustomFDialog 的 picker 化、.qm 覆盖缺口补译,以及把批4e 以来的"初始化路径禁模态""面板先探针后动手"等经验继续沉淀回移植手册。对一个把桌面排版搬进鸿蒙的工程来说,这些纪律本身就是可迁移的产出。
■ 应用:AirPress(from Scribus)(AirPress)
■ 定位:Scribus 移植,桌面排版
■ 上游:Scribus 1.7 (Qt6) | 路线:Qt6 6.11.1+QPA | 版本:1.1.0
■ 开源: https://gitcode.com/Andy88/Harmony-Scribus
■ 市场状态:已上架 AGC 应用页: https://appgallery.huawei.com/app/6917615797714882057
■ 官网(免费下载): https://www.shunidc.cn/
————————————————————
开源鸿蒙 PC 社区: https://harmonypc.csdn.net/
PC 社区项目申请: https://atomgit.com/OpenHarmonyPCDeveloper
适配开源地址:https://gitcode.com/Andy88/Harmony-Scribus
环境搭建文章:https://blog.csdn.net/weixin_52908342/article/details/161343743
▍一、为什么要适配 AirPress(Scribus)
Scribus 是开源桌面排版(DTP)领域成果最重的程序:专业页面布局、CMYK 色彩管理、PDF 出版输出、文本链与样式系统、字体管理与复杂停靠面板,对 Qt 的使用深到私有类与原生对话框定制。它的上游主线的 Qt 6 版本对移植工程是极限测试——文字排版依赖 harfbuzz 与 ICU,导入链依赖 poppler,拼写检查依赖 hunspell,任何一环在 musl 沙箱上的缺口都会让编译或运行直接失败。
鸿蒙侧工程名为 AirPress,包名 com.airpress.app,版本 1.0.0,deviceTypes 覆盖 phone、tablet 与 2in1。移植路线为三件套:AirGis 的 Qt 6.11.1 预编译栈(74 个模块)、AirCad 的 ohos QPA(Qt6 版)复制改造,加上 Air 双层架构。PORTING.md 记载的验证设备为 MatePad Mini 悦读版、MateBook Pro 与 MatePad 11.5;M2 里程碑时期的 HAP 约 233MB,dlopen 拓扑 94 个库全部加载成功,主窗完整渲染且字体零缺字。
对移植系列而言,Scribus 的样本价值在于它是目前该系列中依赖面最宽的上游:Qt6 重型 C++20、外部 C 库矩阵、Python 运行时、插件体系与停靠框架同时在场,任何在它上面成立的方案,向下兼容整个系列。
▍二、先划清边界:不把编译通过当成适配完成
Scribus 的启动流程本身就是桌面向的:首启向导、新建文档对话框、崩溃恢复弹窗,全是模态 exec();文件对话由自研的 CFileDialog 收口并强制禁用原生对话框;Help 菜单挂满官网、Wiki、IRC 与检查更新。这些能力在无窗口管理器的单表面 QPA 上要么死等、要么不可达,工程对每一层都做了明确替换或移除。
层次 原项目实现(桌面 Scribus 1.7) 鸿蒙侧当前方案
应用入口 main_nix.cpp 的 main() 可执行程序 airpress_scribus_main 导出 + 本体改 SHARED 库(P2)
窗口承载 X11/Wayland + WM 管理多窗 XComponent + ohos QPA,对话框子窗化(§7.4.1 全链)
首启流程 FirstStartWizard/startUpDialog 模态向导 P9/P10 跳过 + airpressNewDefaultDoc 免对话框建文档
崩溃恢复 每次启动弹恢复对话框 P15 鸿蒙跳过,自动保存文件保留磁盘可找回
文件对话框 CFileDialog 内嵌 Qt 对话框(禁用原生) CFileDialog __OHOS__ 分支同步桥接系统 Picker
文本引擎 ICU C++ 类(BreakIterator/UnicodeString) scicu_compat.h C API 兼容层(ubrk_* 等,P6)
停靠布局 桌面宽屏 adsDockState 持久恢复 P16/P17 收窄 + 画布自适应重排,忽略旧 state
Help 与在线服务 官网/Wiki/IRC/检查更新/关于外链 P14 Help 菜单整体移除,F1 加速键封堵
这张表的共同点是"单点收口":入口在一个函数、文件对话在一个类、ICU 在一层头文件——找到收口点,替换就是机械的;找不到,就会像批3 之前那样在渲染层空转。
▍三、鸿蒙版的整体架构
工程沿用 Air 双层架构并把对话框升级为独立子窗体系:ArkTS 壳承载菜单栏、加载层、对话框宿主与真子窗管理;NAPI 层做输入分发与桥接;原生侧 Qt 宿主线程预载 QPA、注入 EGL surface 后调用 airpress_scribus_main。
text
EntryAbility(windowStage 持有、沉浸式、loadContent 后挂装饰管理)
├── Index.ets:ArkTS 菜单栏(官方 Menu 组件树)+ LoadingProgress 加载层
├── DialogHost / DialogPage / DialogTitleBar # 平板浮层与 2in1 真子窗双路径
├── SubWindowManager.ets # 自由多窗闩语义与子窗创建
└── components / utils(ImmersiveManager、FileManager)
▼ NAPI:napi_init.cpp(触摸/鼠标/键盘/滚轮/捏合分发、pick 队列、dialog ops)
└── libairpress_core.so(Qt 宿主线程)
├── 预载 libqohos(RTLD_GLOBAL,须在 QApplication 前)
├── QOhos_SetDisplayMetrics 注入 dpr → StrategyA EGL surface 注入
└── airpress_scribus_main → libscribus_gui.so(SHARED,P2)
├── ScribusMainWindow + ADS 停靠框架 + Canvas
├── CFileDialog → 系统 Picker 同步桥(reqId 路由 FIFO)
├── 菜单桥(QJsonDocument 快照 + serial 门控 + trigger 队列)
├── scribus-plugins/(50 插件)+ Python 3.11(56 扩展模块)
└── harfbuzz 8.3.0 / hunspell 1.7.2 / poppler-cpp 26.07
主要目录如下:
text
AirPress/
├── AppScope/app.json5 # 包名 com.airpress.app、版本 1.0.0
├── entry/src/main/
│ ├── ets/
│ │ ├── pages/Index.ets # 菜单栏、加载层、XComponent 宿主
│ │ ├── components/DialogHost.ets # 对话框浮层宿主(z 序、遮罩、取消)
│ │ └── utils/SubWindowManager.ets # 2in1 真子窗管理
│ └── cpp/
│ ├── airpress_core.cpp # Qt 宿主线程、surface 生命周期、pick 队列
│ ├── napi_init.cpp # NAPI 导出与 TSFN 桥
│ ├── qt_platform/ # ohos QPA(AirCad 蓝本改造,含对话框子窗化)
│ └── elf_edit.py / patch_libs.py # NEEDED 规整与 ELF 处理
├── native/
│ ├── scripts/env.sh # 交叉环境(NDK/粮仓路径)
│ ├── scripts/build_deps.sh # harfbuzz/hunspell/poppler-cpp 自编
│ ├── scripts/build_scribus.sh # libscribus_gui.so 交叉构建
│ ├── scripts/deploy.sh # 部署与 ELF 规整(qpa 模式除外)
│ └── sources/ # scribus、harfbuzz、hunspell 源码
└── evidence-queue/ # 验证证据队列
▍四、核心适配过程
### 1. GUI 转共享库与入口改造
P2 把 main_nix.cpp 的 main 改为 extern "C" __attribute__((visibility("default"))) 的 airpress_scribus_main,本体以 add_library SHARED 输出 scribus_gui。这里有两个实证过的坑:只加 visibility 属性不够,C++ 名字修饰会让 dlsym 按 _Z21... 找不到符号,必须 extern "C";Scribus 全局 -fvisibility=hidden 又要求导出点显式声明。本体对 core 的跨层调用一律走 dlsym,避免 NEEDED 链入 core 形成加载环。
### 2. 依赖粮仓与自编三件
Qt6 全家来自 AirGis 预编译栈;cairo/freetype/lcms2/fontconfig/libxml2/poppler 核心库来自 AirScape;Python 3.11.9 来自 AirCad。冲突面经 llvm-readelf 审计:Qt6.11 栈的 NEEDED 仅 libz/libzstd/libssl3/libcrypto3/libGLESv2,与 AirScape 的 C 库栈零符号冲突。本工程自编三项:harfbuzz 8.3.0(含 icu 与 subset 变体,Scribus REQUIRED 两项硬依赖)、hunspell 1.7.2、poppler-cpp(在 AirScape poppler 26.07 源码树上补编 cpp 绑定)。
### 3. ICU C API 兼容层与 include 劫持根治
OHOS 系统的 libicu 是纯 C 库(零个 C++ 符号、无 U_NAMESPACE),而 Scribus 的文本整形直接用 icu::BreakIterator 与 UnicodeString。P6 写了 scicu_compat.h,把 8 个文件的 C++ 用法替换为 ubrk_* C API;更棘手的是编译期劫持——AirScape 粮仓的 icu 头是五文件垫片,其顶层 include 路径经 fontconfig.pc/expat.pc/FindJPEG 多路注入抢在 sysroot 之前解析,导致 U_CAPI unknown 家族报错。根治三件套为:AirPress 自己的 include 转发树(sysroot 全部 unicode 头)、覆盖 fontconfig.pc/expat.pc 的 Cflags、根 CMakeLists 的 include_directories(BEFORE);且顶层 .h 必须真实拷贝——文本转发头会破坏 FindPNG 的 file(STRINGS) 版本解析。
### 4. 系统 Picker 桥与菜单桥
批3-S7 破除了一个普遍假设:QPA 的文件对话框 helper 在这里永远不被咨询,因为 Scribus 不走 Qt 原生对话框——CFileDialog 是全部文件对话的单一收口且强制 DontUseNativeDialog。拦截点因此放在 CFileDialog 本身:__OHOS__ 分支经 airpress_core_pick_request_sync(Qt 主线程 QEventLoop + 5 分钟安全阀 + reqId 路由 FIFO 队列)交 ArkTS 拉 DocumentViewPicker/DocumentSavePicker/FOLDER,沙箱中转后回传。模式映射、suggest 必须 basename、暂存名用 ASCII 等细节都有实证记录。菜单桥则从 mw->scrActions 反查规范 id(ScrAction 从不 setObjectName),QJsonDocument 快照加 serial 门控增量下发,批5 升级为官方 Menu 组件树渲染,支持级联、快捷键列、灰显与勾选态。
### 5. 对话框子窗化与多实例
批8 把 AirCast 的 §7.4.1 蓝图整体移植为 Qt 版全链:QPA 侧 Qt::Dialog/Sheet 分类与五类 ops、对话框专属 EGLSurface、flush 分离合成、QOhos_InjectDialogPointer 千分比换算注入;ArkTS 侧 DialogHost(平板浮层)与 SubWindowManager(2in1 真子窗)双路径,路由谓词为"自由多窗永续闩 ∪ 实时 FLOATING"。多实例的排队根因(模态 exec 期间 QTimer tick 不派发,触发滞留 73 秒)以 QMetaObject::invokeMethod 的 QueuedConnection 即时唤醒修复,实测打开 A 后点 B,194ms 内 B 建立专属 surface 双窗并存。
▍五、适配中遇到的几个难点
### 难点一:初始化路径上的模态 exec 是全链毒药
FirstStartWizard、startUpDialog、崩溃恢复对话框三个模态在无 WM 的 QPA 上死等或推迟一切后续桥接。批4e 的取证最具代表性:冷启动菜单栏空白的真因是"崩溃恢复"模态卡住 app.init(),而菜单桥恰在 init 之后才启动——用户看到的是"顶部没沉浸",实际是菜单没有内容。工程由此确立"鸿蒙上初始化路径禁模态"的原则(P9/P10/P15 三例同族),并引入"菜单桥首快照流动"作为 GUI 真就绪信号:scribus-main 这个 stage 名字暗示主窗就绪,实际在入口函数调用前置位,两者差出整个启动期。
### 难点二:画布白页的"渲染故障"假象
用户报"画布空白",探针证明 Canvas/ScribusView/ScribusDoc 根本未构造——P10 跳过新建对话框后没有任何文档被创建,灰色只是 ScribusView 调色板的空态背景。修复是 S12-lite:按偏好默认(A4/边距/单位/装订方向)走 doFileNew 免对话框路径,实机复现 A4 白页、红页框、边距线与标尺。此案沉淀的方法是"'渲染故障'先证伪有没有在画",探针必须埋在 paintEvent 早退判断之前。后续批4b 更进一步证明 NewDoc 对话框本身可正常渲染,批8 遂把"文件→新建"恢复为上游完整对话框体验。
### 难点三:停靠面板宽度的三层根因
"两边工具栏太宽"背后叠了三层:旧 adsDockState 恢复的是 dpr 1.0 宽屏时代的绝对像素;桌面默认 panelWidth 290 过宽;插件面板在右列外侧再叠独列。P16 分层修复后仍有下限钳制——configureDock 的 setMinimumWidth(minimumSizeHint) 让 cap 落空,批5 用探针实测各面板 minimumSizeHint(PropertiesPalette 357、PagePalette 343)后才对症:OHOS 跳过 setMinimumSize(sizeHint())、.ui 网格手术把 X/Y/W/H 双列改单列、ScrSpinBox 降 min。画布从 180 逻辑像素升到 542。教训被固化为"面板宽度问题先探针后动手,别盲试 cap"。
### 难点四:多实例下的单槽资源与可达性
pick 管线初版是单槽(g_pick_pending/g_pick_result),多实例并发时他人结果会误杀等待中的请求;插件动作的 QAction 挂在 ScripterCore 上、不在 scrActions 也不在 widget 树,菜单桥三路查找全落空,插件功能窗"从不可达"。修复为 FIFO 队列加 reqId 路由 map、菜单桥加全应用 QMenu 扫描兜底(含孤儿 QMenu 这类无 parent 顶层对象)。批9 又从 376MB hilog 里挖出无文档态的 SIGSEGV@0x6ac——反汇编显示 0x6ac 恰为 itemToolPrefs.polyCorners 成员偏移,GetPolyProps 裸解引用空 doc;P19 补守卫后复现率 0/5,且带文档的正路径弹窗正常。
### 难点五:数据管线与签名运维的静默失败
154MB 部署数据踩过一连串坑:fs.mkdirSync(dir, true) 对已存在目录抛 EEXIST 并炸掉整个 try 块,stamp 数月级静默失败;fs.writeSync 只收 ArrayBuffer,传 Uint8Array 视图伪报 ENOENT;逐文件 getRawFileContent 在数千文件规模挂死,终态改为 tar 分卷(44MB×4)+ getRawFileDescriptor fd 直传 + native 后台线程解包,147MB 部署进入 3 秒级。签名侧,devecocli run 默认 product 的 release 签名会被 hdc 以 9568322 拒装,工程增设 debug product;跨代 profile 换签名必须带 --uninstall。
▍六、编译、安装与启动
构建入口在 native/scripts/,顺序为:env.sh 装载 NDK 与粮仓路径;build_deps.sh 交叉编译 harfbuzz/hunspell/poppler-cpp;build_scribus.sh 以 ninja 编出 libscribus_gui.so 与 core;deploy.sh libs 部署并做 ELF 规整(注意 qpa 模式只拷贝不规整 NEEDED,混用会让 libqohos 被 musl 拒载、症状远隔一次重启)。产物为 233MB 级 HAP(M2 记载值),首启沙箱拷贝约 196MB。HAP 打包:
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.airpress.app
PORTING.md 记载的验证基准:MatePad Mini 上 M2/M3 达成主窗完整渲染(菜单/工具栏/停靠面板/状态栏全出、字体零 tofu)与触摸输入链打通;批7 启动加载层改造后温启动约 6 秒、品牌 splash 与白闪零帧;批8 多实例对话框在 Mini 与 MateBook Pro 双路径实测;MateBook Pro 全新安装首启 2/2 通过,加载层交接后直达完整中文主界面。
▍七、当前功能边界
按 PORTING.md §6 遗留清单与各批记录,已完成并经真机验证的能力:
- S1 ArkTS 菜单栏(官方 Menu 组件树、级联、快捷键列、灰显、勾选态、serial 门控);
- S2 全面汉化(scribus.zh_CN.qm + qt_zh_CN.qm 兜底,界面全中文);
- S3 双模输入(触摸+鼠标合成链、批5 物理键盘修饰键全链、Ctrl+O 触发 picker 实测);
- S6 光标桥实装、S7 系统 picker 全链路(35 组 filters 全量传递)、S8 文件夹授权镜像写入、S9 剪贴板双通道;
- S11 暗黑(ScribusProxyStyle Dark 实测)、S12 完整版新建文档对话框(模板缩略图/参数联动);
- S13 数据管线(tar 分卷 + fd 直传 + 后台解包,147MB 秒级)、S14 上游对齐(Ctrl+滚轮原生缩放语义);
- Python 脚本台复活(PYTHONHOME 前缀重组 + 56 扩展模块)、对话框子窗化多实例、P19 无文档态闪退修复;
- 启动加载层(LoadingProgress + 菜单桥快照门控 + 120s 兜底)与顶部/底部沉浸适配。
尚未覆盖或待验证的项同样登记在册:S4 手写笔与 S5 捏合缩放代码就位但需 M-Pencil 真笔与真机双指人工验证;S10 碰一碰需 NFC 硬件;markdown 导入因 Qt6.11 栈无 textmarkdown 支持而禁用(P7,接受现状);CustomFDialog 直连入口(图像导入等)的 picker 化为后续增强;文本直输与滚轮缩放的硬件复核留人工;S13 深度量化与 S14 完整对齐为部分状态;保存确认弹窗存在中英混排(.qm 覆盖缺口)。因此当前版本更准确的定位是"Scribus 1.7 鸿蒙 PC 可用排版基础版"——从启动、文档创建到编辑、导出、系统接驳的主链已在三类设备上闭环,剩余项集中在硬件依赖的验证与长尾对话框体验。
▍八、总结
AirPress 是这个移植系列里依赖面最宽的一役:Qt6 预编译栈、纯 C 的系统 ICU、沙箱禁 exec、无 WM 模态、Python 运行时、停靠框架,每个环节都要求给出可复现的工程答案。九批真机迭代给出的答案是成体系的——单点收口的替换、显式导出的桥接、探针先行的布局调优、门控流动的启动层——而不是零散的补丁堆叠。
后续工作在登记册里已经排好序:S4/S5 的硬件验证、CustomFDialog 的 picker 化、.qm 覆盖缺口补译,以及把批4e 以来的"初始化路径禁模态""面板先探针后动手"等经验继续沉淀回移植手册。对一个把桌面排版搬进鸿蒙的工程来说,这些纪律本身就是可迁移的产出。
楼主
8 天前