把 Flameshot 编译进鸿蒙 PC:一次 Qt C++ 截图工具的源码级移植实战

把 Flameshot 编译进鸿蒙 PC:一次 Qt C++ 截图工具的源码级移植实战

欢迎加入开源鸿蒙 PC 社区:https://harmonypc.csdn.net/

欢迎在 PC 社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper

适配开源地址:https://atomgit.com/OpenHarmonyPCDeveloper/ohos_flameshot

本文记录将开源截图标注工具 Flameshot 移植到 HarmonyOS PC 的完整过程:放弃 ArkTS 重写、选择源码级交叉编译、解决 Qt 头文件版本匹配、设计文件系统截屏桥、排查真机三连崩。约 90 个上游 C++ 文件原样编译,只新写 3 个平台适配文件。

图 0:DevEco Studio 中的工程与构建

一、缘起:截图工具是刚需空白

截图标注工具是桌面生产力的刚需:写文档、报 bug、做评审,都离不开"框一块屏幕、画个箭头、加行字"。鸿蒙 PC 起步阶段,系统截图能力有了,但独立的截图标注工具一直是空白。

Flameshot 是这个领域的标杆开源项目:区域截图、全屏截图、二十种标注工具(铅笔、箭头、矩形、圆、文字、高亮、像素化、计数圆......)、配置面板、上传分享,桌面 Linux 用户几乎人手一个。技术栈是 Qt 5 + C++------这意味着它天然接近"可移植"的那一类桌面应用,前提是目标平台有 Qt 运行时。

而 OpenHarmony 生态恰好有官方的 Qt-for-OpenHarmony 移植:一套 libqohos.so QPA(Qt Platform Abstraction)插件,把 Qt 5.12.12 的 Core/Gui/Widgets/Network 跑在鸿蒙的应用框架上。这条路此前已经在 Spyder(Python IDE)上验证过。

于是问题变成了:能不能把真实的 Flameshot C++ 源码编译进鸿蒙 PC?

二、先回答一个灵魂问题:重写还是移植?

这个项目其实有两个版本。第一版是 ArkTS 重写 :用 ArkTS + Canvas2D 照着 Flameshot 的功能清单,画了一套自己的截图标注界面。那一版跑是能跑的,但每写一行都于心不安------它没有编译任何一行 Flameshot 上游代码,只是照抄了需求清单。上游修一个 bug、加一个工具,重写版都无福消受;上游演进三年,重写版就落后三年。

"移植"和"重写"是两个完全不同性质的工程:

重写 移植
代码血缘 上游源码原样编译
跟进上游 不可能 定期同步
工作量重心 把功能重新实现一遍 把平台差异抹平
验收标准 "功能像不像" "是不是它本体"

公平地说,重写版也不是全无价值------它验证了"截图标注在鸿蒙上技术上可行",也摸清了权限申请、覆盖层渲染这些平台特性。但它每次演示能证明的只是"我可以做一个像它的东西";而移植版证明的是"Flameshot 在这里"。前者是仿品,后者是本体。社区要的是生态里的 Flameshot,不是某位开发者的个人作品。

所以第二版推倒重来,定下一条铁律:除平台底层(进程入口、截屏后端、系统通知)外,不改任何一行上游逻辑。这个决定塑造了后面所有的技术选择。

三、技术路线:Qt-for-OpenHarmony + 交叉编译

整体架构是四层叠加:

选 v0.10.2 有讲究:这是 Flameshot 最后一个 Qt5 版本。上游 master 已经在向 Qt6 迁移,而 OHOS 上的 Qt 运行时是 5.12.12------锁 v0.10.2 才能和运行时版本对齐。

上游源码放进 native/vendor/flameshot/,连同它自带的三个 bundled 依赖(singleapplication、spdlog、Qt-Color-Widgets)一起,一个字节都不动。版本管理的原则是:vendor 目录是上游的领地,我们的代码只住在隔壁。

四、最关键的一个决策:头文件必须精确匹配到 5.12.12

这是整个项目里最值得单独成章的一节,因为它直接决定移植的成败。

前车之鉴来自 Spyder 项目:HAP 内的 Qt runtime 是 5.12.12 ,而本机交叉编译时最顺手用的是 Homebrew 的 qt@55.15.19 )头文件。用 5.15 头文件编译、链接 5.12 运行库,会撞上一个非常阴险的坑:QString::arg() 在 5.15 里走 QtPrivate::argToQString(QStringView, ...) 模板快路径,这个符号在 5.12.12 的 libQt5Core.so 里根本不存在------编译链接全部成功,运行时 dlopen 直接 SIGABRT

Spyder 当时的对策是业务代码里禁用 .arg()。但这招在 Flameshot 上行不通:.arg() 在上游代码里用得遍地都是(ControllerConfigHandlerFileNameHandler......),逐个改掉等于篡改上游逻辑,违背了第二节定下的铁律。

解法是让编译环境回到 5.12.12 时代 :用 aqtinstall 下载官方真实的 Qt 5.12.12,从 macOS framework bundle 里把 QtCore/QtGui/QtWidgets/QtNetwork 的 Headers 和配套的 moc/rcc/uic 三个工具抽出来,入库到 native/qt5.12.12-headers/native/qt5.12.12-tools/(合计 19MB,纯头文件,不含库)。

bash 复制代码
pip install aqtinstall
aqt install-qt mac desktop 5.12.12 clang_64 -O /tmp/qt5122
# 注意:framework 里的 Headers 是符号链接,复制必须用 cp -RL 解引用,
# 否则拷进仓库的只是一堆空壳链接

用这份版本精确匹配的头文件编译,上游的 .arg() 调用自然走 5.12 时代的老符号------这些符号在 5.12.12 的 .so 里都存在。一行上游代码都不用改

这个决策的本质是:与其修改代码去迁就环境,不如把环境复原成代码出生时的样子。 后者才是"移植"的正统做法。

图 1:工程配置细节

五、只新写三个文件:替换矩阵

全项目 OHOS 侧新写的代码只有三个文件,每一个都对应一项 OHOS 没有的桌面系统能力:

上游文件 替换为 为什么
src/main.cpp(约 500 行) ohos/main_ohos.cpp 除 45 行 GUI 启动分支外,剩下 400+ 行是 CLI 参数解析,且每条命令都通过 D-Bus 给已运行实例发消息。OHOS 没有 D-Bus,HAP 拉起时 argv 恒为空,CLI 分支永远走不到。保留的 GUI 分支逻辑照抄,只去掉 DBus 服务注册
src/utils/screengrabber.cpp ohos/screengrabber_ohos.cpp 上游按平台调 X11/GDI/macOS 截屏,Wayland 下走 portal 的 DBus + 临时文件 模式。OHOS 都没有,新实现复刻同一种"平台 IPC + 临时文件"结构(见下节)。类声明 screengrabber.h 完全未改
src/utils/systemnotification.cpp ohos/systemnotification_ohos.cpp 上游走 D-Bus 通知服务。改为 qInfo() 日志 + 上游本就存在的安全空操作路径

另有几组文件直接不编译 ,且每一处都有充分理由:flameshotdbusadapterdbusutilsrequest(DBus proxy,只服务于已替换的 CLI 路径)、整个 src/cli/ 目录(同上)、external/QHotkey(上游 CMake 自己就用 IF(APPLE) 门控,非 macOS 平台从来不编它)。

没改动任何一行 的部分:capturewidget.cpp(标注画布主逻辑)、全部 20 个标注工具类、src/config/* 全部配置面板、controller.cppconfighandler.cppQt-Color-Widgets 的色轮、singleapplication------约 90 个文件原样通过交叉编译。

这个"替换矩阵"的价值在于:它让"改了什么、没改什么"变成一份可审计的清单,而不是一笔糊涂账。

六、截屏桥:不发明轮子,复刻上游的 IPC 模式

Flameshot 的核心入口是"抓取全屏 → 进入标注"。C++ 业务代码跑在 Qt 事件循环里,而 OHOS 的截屏能力 @ohos.screenshot 是 ArkTS-only API,跑在 QAbility 的 JS 线程------两边的世界互相够不着

上游 Wayland 分支的处理方式给了蓝本:DBus 请求 → 等 Response 信号 → 临时文件落盘 → QPixmap(uri) 读回。OHOS 版把 IPC 手段换成纯文件系统,协议逐步骤对应:

  1. C++ 侧 ScreenGrabber::grabEntireDesktop() 写请求文件 flameshot_bridge/req_<timestamp>.json$HOMEmain_ohos.cppsetenv 指向沙箱 filesDir);
  2. ArkTS 侧 FlameshotScreenshotBridge.ets 以 60ms 间隔轮询,发现请求后:检查 CUSTOM_SCREEN_CAPTURE 权限(user_grant,弹窗授权)→ 调 screenshot.capture()ImagePacker 编码 PNG → 写 res_<id>.png + .done 标记;
  3. C++ 侧用 QEventLoop 每 15ms 轮询等 .done(不阻塞 Qt 事件循环),QPixmap(path) 原生加载,删临时文件;
  4. 8 秒超时或 .err 标记 → 返回空 QPixmap,走上游原有的失败分支。

选纯文件系统而不是 NAPI,是刻意的权衡。NAPI 桥接理论上更快、更"正统",但它引入 C++/ArkTS 双向类型声明、线程安全和编译耦合,排查问题时还得靠日志反推。文件 IPC 零依赖,且天然可观测hdc shell 进沙箱目录,req_*.jsonres_*.png.done.err 四种文件肉眼可查------截图链路哪一步断了,ls 一下就知道。桥的两侧代码各不到两百行,剩下的全是上游逻辑。

顺带一提轮询参数的选择:ArkTS 侧 60ms 是权限弹窗和编码耗时容忍下的从容值,C++ 侧 15ms 配合 QEventLoop 是为了在等待期间仍能处理 Qt 事件(覆盖层的动画、光标更新不能停)。两边的间隔都不是拍脑袋,是"响应够快又不空转"的平衡点。

七、交叉编译与 ABI 护栏

构建脚本 build_ohos.sh 用 OHOS NDK 的 clang 交叉编译 ARM64 产物,CMakeLists.txt 里除了常规配置,还埋了一道护栏 :链接完成后自动 nm 扫描产物,检出 QtPrivate::argToQString 符号直接 fail,报"头文件路径配错了"。

护栏的意义是把第四节那个阴险坑从"运行时炸"提前到"构建时拦"。经验类的东西不固化成机器检查,下一个复现的人还会再踩一遍。

产物 libflameshot_shell.so 的动态依赖干净利落:仅 Qt5 Core/Gui/Widgets/Network + libc++_shared + libc,无 DBus、无 Svg 残留。打进 HAP(Qt runtime 五个 .so 共约 120MB,加上业务库,整个 HAP 约 446MB),hvigorw assembleHap 一次通过。

签名有个 DevEco 的老坑值得复述:自动签名生成材料后,build-profile.json5products[].signingConfig: "default" 这个引用字段不会自动填,留空的话 SignHap 步骤被静默跳过,装到设备上的永远是 unsigned 包。

八、真机三连崩:每个 SIGABRT 都是一堂课

真机首启,SIGABRT。此后连崩三次,每次都是"编译期完全发现不了、只有真机能暴露"的问题------这也是交叉移植最迷人的地方。

第一崩:.hpp 不被 moc 扫描。 Qt-Color-Widgets 的头文件用 .hpp 后缀,而 CMake 里 moc 扫描的 glob 是 file(GLOB _hdrs "${_dir}/*.h")------只认 .hcolor_wheel.hpp 里的 Q_OBJECTsignals: void mouseReleaseOnColor(QColor); 从未被 moc 处理,signal 的实现体是 moc 生成的,没生成等于这个符号永久缺失,dlopen 阶段 relocation 失败。修复只需把 glob 改成同时匹配 *.h *.hpp。这类问题的恶在于:缺失是静默的,编译、链接全绿。

第二崩:被平台门控排除的文件,头文件却进了 moc。 这是最有教学价值的一崩。globalshortcutfilter.h/.cpp 是上游 Windows 专属 代码,上游用 IF (WIN32) 门控,OHOS 上两处都不生效,本该完全无关。但我们的 moc 扫描是"目录下所有头文件无差别扫",它和 controller.h 同目录,被一起扫了。这个类含 Q_OBJECT,moc 生成的实现体成了该类虚表的 key function 编译单元;而它唯一的真实虚函数 nativeEventFilter 定义在那个被排除的 .cpp 里------虚表里留下一个悬空符号。共享库链接默认不校验未解析符号,链接不报错;运行时 dlopen 才炸。

教训被固化成一条可执行的操作:给同类移植提前跑

bash 复制代码
grep -rn "IF (WIN32)\|IF (APPLE)\|IF(WIN32)\|IF(APPLE)" */CMakeLists.txt

把所有平台门控文件对逐一加入 moc 排除名单。

第三崩:SingleApplication::exit() 被沙箱击杀。 上游用跨进程 QSharedMemory 做单实例检测,QSharedMemory::create() 在 OHOS 沙箱里几乎必然失败,失败路径 abortSafely() 会调 ::exit(EXIT_FAILURE)------而鸿蒙的 appspawn 不允许沙箱进程自行 exit ,检测到直接 SIGABRT(hilog 里 [appspawn_server.c] Unexpected call: exit)。

修复没有动 vendor 代码。上游对这类受限平台早有先例:构造函数开头就有 #if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) 的早退分支,退化为普通 QApplication。我们在自己的 main_ohos.cpp(本来就是新写的平台入口)里直接构造 QApplication,效果等价于给 OHOS 开了同一个后门。何况一个 HAP bundle 本来就只有一个进程实例,这层保护本就是防御纵深,不是承重墙。

排查方法论 :三崩的定位全部依赖真机 hilog -xLastFatalMessage/fault thread,而不是 nm 静态符号比对。Qt 5.12 大量使用符号版本控制(symbol@Qt_5 vs symbol@@Qt_5),静态比对误报极多;设备上跑出来的 dlopen/appspawn 真实校验才是权威

三崩复盘有个共同规律:没有一个是 Flameshot 业务逻辑的 bug.hpp 后缀是 Qt-Color-Widgets 的选择,平台门控是上游的工程习惯,::exit() 是 SingleApplication 的错误处理设计------它们在自己的原生环境里全部正确,只是在交叉编译 + 沙箱的组合下才显形。这正是源码移植的真实战场:你的敌人从来不是上游代码,而是两套世界之间没对齐的假设。

三崩修完,进程稳定运行,不再崩溃。

图 2:真机启动,截图覆盖层激活

图 3:全屏覆盖层(蒙灰状态)

九、真机验收:从安装到覆盖层

真机安装 bm install 一次通过,桌面出现 Flameshot 图标,bm dump 确认版本 0.10.2------和上游 tag 一致,这就是源码级移植的底气。

启动后由于 OHOS 没有 QSystemTrayIconmain_ohos.cpp 里做了一个等价行为:启动即调用一次 startVisualCapture(),相当于桌面平台"点一下托盘图标"。于是应用一拉起就进入截图状态:全屏蒙灰的 CaptureWidget 覆盖层铺满桌面------这正是上游标注画布的真实渲染,包括选区高亮、尺寸指示、工具面板。

图 4:区域选择中的覆盖层

图 5:选区与标注状态

图 6:标注工具在真机上的实际效果

覆盖层能渲染、能交互,意味着 CaptureWidget 这个上游最核心的组件(约三千行标注画布逻辑)在 OHOS 的 Qt QPA 上真实跑通了;剩下的标注工具、保存、复制走的是同一套画布事件流,逐项验收在推进中。

十、诚实清单:哪些地方打了折

移植报告不说漂亮话,以下限制如实列出:

限制 原因 影响
工具栏图标空白 上游图标全是 .svg,OHOS Qt runtime 没有 libQt5Svg.so 按钮可点、功能可用,只是看不到图标;后续可做 svg→png 转换
单实例保护退化 QSharedMemory 沙箱不可用 HAP 机制本身保证同 bundle 单进程,实际无感
系统托盘 libqohos QPA 不支持 QSystemTrayIcon 用"启动即截图"等价替代
Imgur 上传 / 桌面文件 / 终端启动器 OHOS 无 .desktop 生态 保留上游真实代码,优雅降级为不可用,不崩溃

注意这些限制的共同策略:能不删上游代码就不删。降级要优雅------功能缺失可以接受,崩溃和报错刷屏不能接受。

十一、写在最后

回头看,Flameshot 移植最有复用价值的是三个判断:

  1. 先问"重写还是移植",再动手。 两者工作量可能相近,但产出的性质完全不同:移植产出的是"Flameshot 本体在鸿蒙上",重写产出的是"另一个长得像它的应用"。
  2. 版本不匹配时,复原环境而不是修改代码。 下载官方 Qt 5.12.12 头文件花十分钟,换回的是"上游代码零改动"的完整承诺。
  3. moc 扫描要显式排除平台门控文件。 条件编译和"无差别目录扫描"是天敌,链接器不报错不代表没问题,真机 dlopen 才是终审。

约 90 个上游文件原样编译、3 个新写平台文件、446MB 的 HAP、真机稳定运行------这份清单对下一个想移植 Qt 应用的团队,应该是块有用的敲门砖。鸿蒙 PC 的生态,就是这样一个应用一个应用堆出来的。


常见问题 FAQ

Q1:为什么宁可下载老版 Qt 头文件,也不改上游的 .arg() 调用?

.arg() 在上游代码里遍地都是,逐个改掉等于篡改上游逻辑,破坏"移植"的定义;而且用 5.15 头文件编出来的代码链接 5.12 运行库,dlopen 必炸。下载官方 5.12.12 头文件只花十分钟,换回的是上游代码零改动的完整承诺------复原环境比修改代码便宜得多。

Q2:截屏桥为什么用文件系统轮询,不用 NAPI?

NAPI 引入双向类型声明、线程安全和编译耦合;文件 IPC 零依赖,且天然可观测------hdc shell 进桥目录 ls 一下,req_*.jsonres_*.png.done.err 哪个缺了,链路断在哪一步一目了然。截图场景对延迟不敏感(用户感知的是画笔不是毫秒),简单性完胜。

Q3:工具栏图标是空白,是不是坏了?

没坏。上游图标全是 .svg,渲染需要 libQt5Svg.so,OHOS Qt runtime 没有这个库。按钮本身可点击、标注功能完全可用,只是看不到图标。后续可以做 svg→png 转换修复------那也是整个移植里唯一可能要动上游资源文件的地方。

Q4:为什么装完包有 446MB?

Qt runtime 占绝对大头:libqohos.so(QPA)149MB + Qt5 五件套 125MB。真实 Flameshot 业务代码编译出来只有 4.1MB。这是 Qt 路线的固有代价------要跑真 C++ 源码,就得带整个运行时。

相关推荐
心态还需努力呀2 小时前
把 Zettlr 搬上鸿蒙 PC:一次 Electron 应用移植实战
华为·electron·harmonyos
OH_TPC2 小时前
HarmonyOS APP开发---“心动卡“社交匹配App,需要用到这个库
华为·harmonyos·鸿蒙
科学实验家2 小时前
最小生成树:Prim,kruskal
数据结构·c++·算法
学习智者2 小时前
《玄》IDE v3.6.3重磅发布:全功能修复与性能飞跃
开发语言·c++·ide·算法·中文语言 玄
●VON2 小时前
Flutter 鸿蒙插件适配实战:用 is_lock_screen 2.0.0 判断真实锁屏状态
flutter·华为·harmonyos
2402_882893862 小时前
C++异常:从概念到实践的一篇博客
c++·异常
Sunny_G2 小时前
鸿蒙 Markdown 编辑器表格所见即所得:七个版本的渲染重构(CodeMirror Decoration 实战)
ai编程·harmonyos
m0_734571762 小时前
深入理解C++ 析构函数<二>析构顺序
开发语言·c++
CodeMaker8082 小时前
规则更规范,应用却起不来:我重新理解了 Qt Agent Skills
qt