A
Air官方
【Air 系列 · 鸿蒙 PC 适配全记录】16 / 56
■ 应用:AirGis(from QGIS)(AirGis)
■ 定位:QGIS 移植,地理信息与地图
■ 上游:QGIS | 路线:Qt6 6.11.1+QPA | 版本:1.2.0
■ 开源: https://gitcode.com/Andy88/Harmony-QGIS
■ 市场状态:已上架 AGC 应用页: https://appgallery.huawei.com/app/6917614293273594893
■ 官网(免费下载): https://www.shunidc.cn/
————————————————————
开源鸿蒙 PC 社区: https://harmonypc.csdn.net/
PC 社区项目申请: https://atomgit.com/OpenHarmonyPCDeveloper
适配开源地址:https://gitcode.com/Andy88/Harmony-QGIS
环境搭建文章:https://blog.csdn.net/weixin_52908342/article/details/161343743
▍一、为什么要适配 AirGis
QGIS 是开源地理信息系统领域的桌面标杆,菜单栏、工具栏、Browser 与 Layers 停靠面板、地图画布、状态栏构成了完整的桌面工作流,底下的依赖栈包括 Qt、GDAL、PROJ、GEOS、Spatialite 等重型组件。把这样体量的应用带上鸿蒙 PC,检验的不是一个控件能否渲染,而是整套桌面应用范式——模态对话框、弹出菜单、停靠面板、多实例窗口——能否在无窗口管理器的环境里成立。
AirGis 的答案是明确的:不重写界面,不截取子集,直接让 QGIS 4.3.0(master)的上游桌面本体在鸿蒙上运行。工程包名 com.airgis.app,版本 1.0.0,deviceTypes 覆盖 tablet、2in1 与 phone,声明 ohos.permission.INTERNET 权限(WMS/WMTS 图层需要),并设置 compressNativeLibs: false。HAP 内打包 150 个交叉编译的 .so,约 264 MB,另在 rawfile 携带 qgis-resources、proj-data、gdal-data 三套数据。
这个工程在 Air 系列中还有一层身份:native/sources/ 保存着 qtbase、qt5compat、qtsvg、qttools 的 6.11.1 源码包,native/scripts/build_qt6.sh 在本工程完成 Qt 6.11.1 栈的交叉编译,QScintilla 2.14.1、QCA 2.3.10、OpenSSL 3.4.1、protobuf 25.3、libzip、zstd 等配套依赖也在此编译。它是组内 Qt 6.11.1 预编译栈的源头,也是 QGIS 桌面路线的完整技术档案——PORTING.md 用 31 个小节记录了每一次故障的根因与修法。
▍二、先划清边界:不把编译通过当成适配完成
QGIS 在桌面上运行时依赖 X11/Wayland 提供窗口系统、依赖窗口管理器提供层序与几何约束、依赖系统包管理器安装 GIS 库。编译通过只意味着代码能生成动态库;要让"完整桌面应用"成立,必须回答窗口从哪来、事件怎么投、多窗口怎么摆。工程划定的边界如下。
层次 传统桌面 QGIS 鸿蒙侧当前方案
应用入口 main() + QApplication Stage 模型 EntryAbility + Index.ets 宿主壳
窗口系统 X11/Wayland + 窗口管理器 全屏 XComponent(SURFACE) + 自研 QPA 插件 libqohos.so
应用本体 桌面可执行档 libqgis_core_ohos.so 内完整 QgisApp(GUI 直渲)
线程模型 Qt 主线程 = main 线程 Qt 主线程 = RenderThread,JS 线程零 Qt 调用
窗口几何 WM 管理层序/钳制 QPA 内实现钳制、半屏平铺、cascade 与真子窗口
GIS 依赖 系统包管理器安装 交叉编译随 HAP 分发(libgdal/libproj/libgeos/libspatialite)
数据与配置 ~/.config 直读 沙箱 + Download/<bundle>/ 工作目录,收藏与最近工程启动重建
早期版本(v1)走的是"offscreen 渲染 + ArkTS 自绘 UI"路线,已被整体废弃;v2 对齐 AirKrita 范式,ArkTS 只做宿主壳,UI 完全交给 Qt Widgets 本体。这个决定意味着所有桌面交互问题都必须在 QPA 与补丁层解决,而不是绕开 Qt 自己画一套假界面。
▍三、鸿蒙版的整体架构
分层为四段:ArkTS 宿主壳负责 Ability 生命周期、rawfile 数据拷贝、Splash 与就绪轮询;libqgisbridge.so 是只链系统库的薄桥,承接 XComponent 回调、按序 dlopen 依赖链并通过 qt_plugin_query_metadata_v2 内联静态注册 Qt 插件;libqgis_core_ohos.so 通过 18 个 QgisOhos_* C 符号被 dlsym 调用,内部构造完整 QgsApplication 与 QgisApp;libqohos.so 作为 QT_QPA_PLATFORM=ohos 的 QPA 插件,把 raster 图像经 GL 纹理与 EGL 直上屏幕。
text
EntryAbility(Stage 模型)
├── rawfile 数据拷贝(qgis-resources / proj / gdal)
└── Index.ets
├── 全屏 XComponent(SURFACE) + Splash
├── pumpQtEvents 16ms 定时器
└── 「打开数据」浮动钮
│ NAPI
libqgisbridge.so(napi_init.cpp,只链系统库)
├── Surface/触摸/鼠标/键盘回调
├── warmup dlopen 链(Qt → QGIS → QPA)
├── RegisterQtPluginsInline(QPA/图片格式/图标引擎内联注册)
└── RenderThread:QgisOhos_Step 循环(事件驱动 + 8ms 节拍)
│ dlopen / dlsym(18 个 QgisOhos_* C 符号)
libqgis_core_ohos.so(qgis_ohos_entry.cpp)
├── QgsApplication(..., "desktop") 完整 GUI
├── profile 解析 → init → createDatabase
├── QSplashScreen(1x1) + new QgisApp(...) + show()
└── 环境注入:QGIS_PREFIX_PATH / HOME / XDG / PROJ_LIB / GDAL_DATA
│ QT_QPA_PLATFORM=ohos
libqohos.so(自研 QPA 插件,native/qpa_ohos/)
├── QOhosIntegration / QOhosScreen(96×dpr 单源高 DPI)
├── QOhosBackingStore:raster QImage → GL 纹理 → blit → eglSwapBuffers
└── QOhos_SetNativeWindow / SetDisplayMetrics / FirstSwapShown
主要目录如下:
text
AirGis/
├── AppScope/app.json5 # 包名与版本
├── PORTING.md # 架构、依赖栈、31 节坑位实录
├── ai_log/ # 历次修复实录(根因分析 + 实测验证)
├── entry/
│ ├── libs/arm64-v8a/ # 150 个交叉编译 .so(QGIS/Qt6/GIS 依赖)
│ └── src/main/
│ ├── module.json5 # INTERNET 权限、compressNativeLibs:false
│ ├── ets/
│ │ ├── pages/Index.ets # 全屏 XComponent + Splash + 事件泵
│ │ ├── pages/DialogPage.ets # 弹窗子窗口自绘标题条
│ │ ├── managers/DialogWindowManager.ets # 子窗口创建与回退
│ │ ├── managers/ImmersiveManager.ets
│ │ ├── utils/FileManager.ets # Download 工作目录与 picker 桥
│ │ └── model/LayerInfo.ets
│ ├── resources/rawfile/ # qgis-resources / proj-data / gdal-data
│ └── cpp/ # libqgisbridge 与 libqgis_core_ohos(hvigor 直编)
└── native/
├── patches/qgis_ohos/ # 上游补丁(欢迎页替身、OHOS 守护等)
├── patches/qt-translations/ # qtbase_zh_CN.qm 预编译
├── qpa_ohos/ # libqohos.so QPA 插件源码
├── scripts/ # env.sh 与全套构建脚本
├── sources/ # Qt 6.11.1 / QScintilla / QCA / OpenSSL 等源码包
├── generated/ # 构建树缺失期的生成头替身
└── install/ # 交叉编译安装前缀
▍四、核心适配过程
### 1. 从 offscreen 到 v2:ArkTS 壳加上完整 QgisApp
v1 方案让 QgsApplication(GUI=false) 离屏渲染、ArkTS 用 bindMenu 自绘图层面板,实测证明这条路做不出可用的桌面应用。v2 把 XComponent 从 TEXTURE 换成 SURFACE(EGL 直连),QPA 从 offscreen 换成自研 libqohos.so,UI 全部交还给 Qt 本体。QOhosBackingStore 的上屏链路是 raster QImage 上传 GL 纹理、全屏 triangle-strip blit、eglSwapBuffers 直出;主窗口与模态对话框共享单个 EGLSurface。PORTING.md 第 1 节的 v1/v2 差异表逐项对比了五处替换,是理解该工程的第一份材料。
### 2. 线程模型:把 Qt 主线程搬进 RenderThread
最初 QgisApp 创建在 ArkTS JS 线程上,实测是死路:任何模态 QDialog::exec() 进入嵌套事件循环后,与 JS 同线程的 ArkUI 触摸回调全部饿死,对话框永远无法关闭。v2 定论(2026-08-15)是把 QgisOhos_Init 全部构造改由 RenderThread 执行——Qt 把首个触碰 QThread::currentThread() 的线程记为主线程,RenderThread 从此就是 Qt 主线程。JS 线程绝不再进 Qt:pumpQtEvents 变为 no-op,initQgis 是纯异步 kickoff(2026-09-02 起 JS 线程 9ms 内返回,彻底消除 AppFreeze THREAD_BLOCK_6S)。模态 exec 落在 RenderThread 时,Step 循环里补装的 8ms 输入泵 QTimer 在嵌套循环内持续投递触摸,交互不中断。
### 3. QPA 的窗口治理:钳制、平铺、z 序与真子窗口
没有窗口管理器,Qt 的多窗口假设全部失效,工程在 qohosintegration.cpp 内逐项补齐:对请求几何超屏的对话框做 WM 式钳制并按 QPA 请求/应答契约回告实际几何,compose 叠层从"拉伸到全屏"改为与表面求交裁剪,解决超大对话框按钮物理不可达的问题;对打印布局设计器这类多实例顶层窗做半屏平铺(slot0 右半屏、slot1 左半屏)与平台侧承接的 raise/lower;弹窗型窗口引入 z 序单一事实源、cascade 层叠摆放与按压置顶。终态方案(6.22)是合格的顶层窗经 QOhosDialogOps 回调与 NAPI TSFN 接到 ArkTS DialogWindowManager,创建鸿蒙真子窗口,由 DialogPage 自绘 40vp 标题条与关闭钮,帧经专属 EGLSurface 直投;子窗口创建失败的设备自动回退共享合成治理。
### 4. 上游补丁与依赖栈组织
上游修改全部以补丁形式登记在 native/patches/:欢迎页换为纯 Widgets 替身(qgswelcomescreen_{h,cpp},类名与 API 与上游一致);qgisapp.cpp 在 QGIS_OHOS_PLATFORM 守护下把 createDatabase 失败的模态 QMessageBox::critical 降级为日志、跳过 enableFontDownloadsForSession() 字体下载;libqgis_app 编为共享库并 EXCLUDE_FROM_ALL 排除可执行档。依赖栈按 env.sh → build_deps_{basic,ext,geo}.sh → build_qt6.sh → build_qca.sh → build_qscintilla.sh → build_qgis.sh → build_qpa_ohos.sh 的顺序交叉编译,sync_hap_libs.sh 做 BFS 闭包同步、NEEDED 原位去版本号、ELF 对齐修复。ELF 纪律明确:NEEDED 无版本号无绝对路径、禁 patchelf、hvigor strip:false、Qt 插件走 bare-name dlopen 与内联静态注册。
### 5. 中文化、文件链路与浏览器可达性
中文化踩了三层坑:QGIS 4.x 翻译包是 BCP47 命名(qgis_zh-Hans.qm),需按 language+script 映射;QgsApplication::init() 之后必须二次 setTranslation 才能真正加载;OHOS 上 Qt 无系统 locale 后端,由 EntryAbility 取 config.language(实测 zh-Hans-CN)注入 QGIS_LANG 环境变量兜底。Qt 标准按钮的"确定/取消"来自 qttranslations,工程用 lrelease 预编译 qtbase_zh_CN.qm(1786 条)随包分发。文件链路上,QOhosTheme 覆写 themeNames() 后 QFileDialog 接入鸿蒙 DocumentViewPicker;Download/<bundle>/ 作为用户工作目录直读直写。浏览器面板经三轮终案修到可用:13 个沙箱内零内容的死根条目用 hidePath 裁剪,行单击直调 doubleClicked 实现"单击工程即打开"。
▍五、适配中遇到的几个难点
### 难点1:启动期模态对话框等于 JS 线程死锁
现象是启动卡死、Splash 永不撤除。根因是 createDatabase 失败时上游弹模态 QMessageBox::critical,嵌套 exec 阻塞 JS 线程,而 Splash 与所有轮询定时器都在 JS 线程上。解法分三层:上游补丁把启动期模态降级为日志;Splash 改为监听 QPA 首帧(QOhos_FirstSwapShown)即撤;后续再把库加载与 Init 整体移出 JS 线程。同类问题在 2026-09-02 还以"隐形模态"形式复发——子窗口创建失败后 Qt 窗口保持已路由态,不可见但模态在场,全部触摸被吞;修复是增加 QOhos_UnrouteDialog 回退链路,让失败窗口回到共享合成。
### 难点2:屏幕几何硬编码导致菜单命中偏移半行
现象是点击「工程属性」菜单行实际命中「捕捉选项」,popup 弹在错误位置。根因链有两级:QOhosScreen 构造把屏幕写死为 1920x1080 物理(逻辑 960x540),而真实分辨率注入发生在 QgsApplication 构造之前、被空指针分支跳过,此后无人再更新——Qt 永远按假屏幕摆放与钳制菜单;叠加事件直投 popup QWindow 时 QMenu 恒按第 1 行响应的 Qt6.11 行为,错位被进一步放大。解法是构造改读已注入的真实几何,事件一律投主窗口、由 Qt 的 forwardToPopup 按 globalPos 映射进活动弹窗。修复后菜单逐行精确命中。
### 难点3:QWSI 坐标语义的翻案
排查"欢迎屏点击无响应"时曾把鼠标坐标按逻辑坐标投递,结果全 UI 错位 2.38 倍。PORTING.md 6.12 用探针实证:Qt 6.11 的 QWindowSystemInterface::handleMouseEvent 接收的 local/global 必须是物理像素,Qt 内部自行除以 dpr 转逻辑坐标,预除一次就是二次换算。这条翻案记录的价值在于否定了更早的"真机画布交互不可达"结论——真凶是空列表与对话框遮挡,坐标层没有问题。
### 难点4:provider 插件依赖漏包引发闪退
点击「新建网格图层」图标必闪退(SIGSEGV)。取证链:sync_hap_libs.sh 的 BFS 闭包从核心种子出发,provider 插件是末尾平铺复制、其 NEEDED 依赖不入闭包——libprovider_mdal.so 依赖的 libxml2.so 未入包,dlopen 失败,provider 注册表查不到 mdal,QgsNewMeshLayerDialog 走早退分支留下未构造成员,随后 setCrs() 空指针解引。修复是三管齐下:上游 setCrs 判空、sync 脚本新增 provider 依赖补包段、libxml2.so 手动入包。
### 难点5:签名配置复发导致全设备拒装
真机安装报 9568322 "not trusted app source",两台设备全部拒装。排查排除过 UDID 覆盖与证书链,最终用 hap-sign-tool 的 verify-app 看到输出 profile type is: release 才定位:build-profile.json5 的 products[0] 被指到 release 签名,release profile 不嵌设备 UDID 白名单,任何真机直装都会被拒。改回 default(debug)签名后恢复。该问题 2026-09-07 又复发一次,工程随后在配置处就地加注释防线,并把"改签名出包后必须回置 default"写进文档。
▍六、编译、安装与启动
依赖与 QGIS/QPA 已预编入库(entry/libs 150 个 .so 加 rawfile 数据),日常构建直接走 hvigor。macOS 命令行构建如下:
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
仅当交叉依赖变更时才需要全量重建,顺序为 native/scripts/ 下的 env.sh → build_deps_* → build_qt6 → build_qca → build_qscintilla → build_qgis → build_qpa_ohos,再执行 sync_hap_libs.sh 同步入包。签名产物位于 entry/build/default/outputs/default/entry-default-signed.hap,真机部署必须使用 default(debug)签名。
bash
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b com.airgis.app
PORTING.md 第 7 节同时给出诊断手段:hilog 按 QgisCore|QOhosProbe|AirGisNAPI 过滤可看到从库加载到首帧的完整启动里程碑,沙箱 qgis-share/qgis_init_diag.txt 记录指针事件几何快照。
▍七、当前功能边界
依据 PORTING.md 当前状态与 README 已验证能力,当前版本已覆盖:
- 完整 QGIS 桌面 UI 全屏渲染:菜单栏、工具栏、Browser 与 Layers 停靠面板、地图画布、状态栏;
- 中文界面与 Qt 标准按钮中文化(qgis_zh-Hans.qm + qtbase_zh_CN.qm 双层翻译);
- 菜单逐行精确命中、模态对话框(工程属性 15 页全渲染验证)、popup 开合与残影清除;
- 触摸/鼠标/键盘输入链路,打开数据到工程加载;
- 系统 DocumentViewPicker 打开/保存,Download/<bundle>/ 工作目录免拷贝直读直写;
- 弹窗真子窗口(PC 端,自绘标题条与拖拽)与共享合成自动回退(平板);
- 多实例顶层窗半屏平铺、raise/lower、cascade 层叠与模态守卫置顶;
- 启动期 JS 线程零阻塞(initQgis 全异步化),MatePad 11.5 多轮操作 0 faultlog。
尚未覆盖或已知遗留(均有 PORTING.md 登记依据):工程 .qgz/附件存档保存失败(上游 mFiles 混入空串条目,修复需 QGIS 源码树恢复,见 6.25);保存选择器实际落点为「个人」目录导致全部走 staging 回拷(功能等价);MatePad 11.5 不支持 createSubWindowWithOptions,对话框在该设备无标题条子窗口;浏览器树触摸滑动仍是桌面行为(拖动为多选而非滚动,待 QScroller 类适配);地图画布的渲染专项验证在后续路线表中仍标记为待办;首启动约 25 秒(264 MB 数据拷贝加 150 个 so 加载),按需拷贝在评估中。
因此,当前版本更准确的定位是"完整 QGIS 桌面在鸿蒙 PC 上的可交互移植验证版":桌面应用范式已经在无窗口管理器环境里成立,剩余工作集中在保存链路修复与设备差异适配。
▍八、总结
AirGis 证明了一件事:像 QGIS 这种量级的桌面应用,不需要重写界面也不需要阉割功能,可以在鸿蒙 PC 上以完整本体运行——前提是把窗口系统、线程模型、事件投递和窗口治理这四件 WM 通常包办的事,在自研 QPA 插件里逐项补齐。31 节坑位实录的价值不在罗列故障,而在于每一条都给出了现象、根因、修法与真机验证四个环节,后续任何 Qt 桌面应用移植都可以按图索骥。
后续工作重心集中在三处:修复依赖源码树的 .qgz 保存缺陷;按设备能力探测分流对话框子窗口方案;以及针对首启动 25 秒的数据拷贝做按需化。每一步仍以 PORTING.md 的实录纪律推进——先复现取证,再定根因,最后真机验证闭环。
■ 应用:AirGis(from QGIS)(AirGis)
■ 定位:QGIS 移植,地理信息与地图
■ 上游:QGIS | 路线:Qt6 6.11.1+QPA | 版本:1.2.0
■ 开源: https://gitcode.com/Andy88/Harmony-QGIS
■ 市场状态:已上架 AGC 应用页: https://appgallery.huawei.com/app/6917614293273594893
■ 官网(免费下载): https://www.shunidc.cn/
————————————————————
开源鸿蒙 PC 社区: https://harmonypc.csdn.net/
PC 社区项目申请: https://atomgit.com/OpenHarmonyPCDeveloper
适配开源地址:https://gitcode.com/Andy88/Harmony-QGIS
环境搭建文章:https://blog.csdn.net/weixin_52908342/article/details/161343743
▍一、为什么要适配 AirGis
QGIS 是开源地理信息系统领域的桌面标杆,菜单栏、工具栏、Browser 与 Layers 停靠面板、地图画布、状态栏构成了完整的桌面工作流,底下的依赖栈包括 Qt、GDAL、PROJ、GEOS、Spatialite 等重型组件。把这样体量的应用带上鸿蒙 PC,检验的不是一个控件能否渲染,而是整套桌面应用范式——模态对话框、弹出菜单、停靠面板、多实例窗口——能否在无窗口管理器的环境里成立。
AirGis 的答案是明确的:不重写界面,不截取子集,直接让 QGIS 4.3.0(master)的上游桌面本体在鸿蒙上运行。工程包名 com.airgis.app,版本 1.0.0,deviceTypes 覆盖 tablet、2in1 与 phone,声明 ohos.permission.INTERNET 权限(WMS/WMTS 图层需要),并设置 compressNativeLibs: false。HAP 内打包 150 个交叉编译的 .so,约 264 MB,另在 rawfile 携带 qgis-resources、proj-data、gdal-data 三套数据。
这个工程在 Air 系列中还有一层身份:native/sources/ 保存着 qtbase、qt5compat、qtsvg、qttools 的 6.11.1 源码包,native/scripts/build_qt6.sh 在本工程完成 Qt 6.11.1 栈的交叉编译,QScintilla 2.14.1、QCA 2.3.10、OpenSSL 3.4.1、protobuf 25.3、libzip、zstd 等配套依赖也在此编译。它是组内 Qt 6.11.1 预编译栈的源头,也是 QGIS 桌面路线的完整技术档案——PORTING.md 用 31 个小节记录了每一次故障的根因与修法。
▍二、先划清边界:不把编译通过当成适配完成
QGIS 在桌面上运行时依赖 X11/Wayland 提供窗口系统、依赖窗口管理器提供层序与几何约束、依赖系统包管理器安装 GIS 库。编译通过只意味着代码能生成动态库;要让"完整桌面应用"成立,必须回答窗口从哪来、事件怎么投、多窗口怎么摆。工程划定的边界如下。
层次 传统桌面 QGIS 鸿蒙侧当前方案
应用入口 main() + QApplication Stage 模型 EntryAbility + Index.ets 宿主壳
窗口系统 X11/Wayland + 窗口管理器 全屏 XComponent(SURFACE) + 自研 QPA 插件 libqohos.so
应用本体 桌面可执行档 libqgis_core_ohos.so 内完整 QgisApp(GUI 直渲)
线程模型 Qt 主线程 = main 线程 Qt 主线程 = RenderThread,JS 线程零 Qt 调用
窗口几何 WM 管理层序/钳制 QPA 内实现钳制、半屏平铺、cascade 与真子窗口
GIS 依赖 系统包管理器安装 交叉编译随 HAP 分发(libgdal/libproj/libgeos/libspatialite)
数据与配置 ~/.config 直读 沙箱 + Download/<bundle>/ 工作目录,收藏与最近工程启动重建
早期版本(v1)走的是"offscreen 渲染 + ArkTS 自绘 UI"路线,已被整体废弃;v2 对齐 AirKrita 范式,ArkTS 只做宿主壳,UI 完全交给 Qt Widgets 本体。这个决定意味着所有桌面交互问题都必须在 QPA 与补丁层解决,而不是绕开 Qt 自己画一套假界面。
▍三、鸿蒙版的整体架构
分层为四段:ArkTS 宿主壳负责 Ability 生命周期、rawfile 数据拷贝、Splash 与就绪轮询;libqgisbridge.so 是只链系统库的薄桥,承接 XComponent 回调、按序 dlopen 依赖链并通过 qt_plugin_query_metadata_v2 内联静态注册 Qt 插件;libqgis_core_ohos.so 通过 18 个 QgisOhos_* C 符号被 dlsym 调用,内部构造完整 QgsApplication 与 QgisApp;libqohos.so 作为 QT_QPA_PLATFORM=ohos 的 QPA 插件,把 raster 图像经 GL 纹理与 EGL 直上屏幕。
text
EntryAbility(Stage 模型)
├── rawfile 数据拷贝(qgis-resources / proj / gdal)
└── Index.ets
├── 全屏 XComponent(SURFACE) + Splash
├── pumpQtEvents 16ms 定时器
└── 「打开数据」浮动钮
│ NAPI
libqgisbridge.so(napi_init.cpp,只链系统库)
├── Surface/触摸/鼠标/键盘回调
├── warmup dlopen 链(Qt → QGIS → QPA)
├── RegisterQtPluginsInline(QPA/图片格式/图标引擎内联注册)
└── RenderThread:QgisOhos_Step 循环(事件驱动 + 8ms 节拍)
│ dlopen / dlsym(18 个 QgisOhos_* C 符号)
libqgis_core_ohos.so(qgis_ohos_entry.cpp)
├── QgsApplication(..., "desktop") 完整 GUI
├── profile 解析 → init → createDatabase
├── QSplashScreen(1x1) + new QgisApp(...) + show()
└── 环境注入:QGIS_PREFIX_PATH / HOME / XDG / PROJ_LIB / GDAL_DATA
│ QT_QPA_PLATFORM=ohos
libqohos.so(自研 QPA 插件,native/qpa_ohos/)
├── QOhosIntegration / QOhosScreen(96×dpr 单源高 DPI)
├── QOhosBackingStore:raster QImage → GL 纹理 → blit → eglSwapBuffers
└── QOhos_SetNativeWindow / SetDisplayMetrics / FirstSwapShown
主要目录如下:
text
AirGis/
├── AppScope/app.json5 # 包名与版本
├── PORTING.md # 架构、依赖栈、31 节坑位实录
├── ai_log/ # 历次修复实录(根因分析 + 实测验证)
├── entry/
│ ├── libs/arm64-v8a/ # 150 个交叉编译 .so(QGIS/Qt6/GIS 依赖)
│ └── src/main/
│ ├── module.json5 # INTERNET 权限、compressNativeLibs:false
│ ├── ets/
│ │ ├── pages/Index.ets # 全屏 XComponent + Splash + 事件泵
│ │ ├── pages/DialogPage.ets # 弹窗子窗口自绘标题条
│ │ ├── managers/DialogWindowManager.ets # 子窗口创建与回退
│ │ ├── managers/ImmersiveManager.ets
│ │ ├── utils/FileManager.ets # Download 工作目录与 picker 桥
│ │ └── model/LayerInfo.ets
│ ├── resources/rawfile/ # qgis-resources / proj-data / gdal-data
│ └── cpp/ # libqgisbridge 与 libqgis_core_ohos(hvigor 直编)
└── native/
├── patches/qgis_ohos/ # 上游补丁(欢迎页替身、OHOS 守护等)
├── patches/qt-translations/ # qtbase_zh_CN.qm 预编译
├── qpa_ohos/ # libqohos.so QPA 插件源码
├── scripts/ # env.sh 与全套构建脚本
├── sources/ # Qt 6.11.1 / QScintilla / QCA / OpenSSL 等源码包
├── generated/ # 构建树缺失期的生成头替身
└── install/ # 交叉编译安装前缀
▍四、核心适配过程
### 1. 从 offscreen 到 v2:ArkTS 壳加上完整 QgisApp
v1 方案让 QgsApplication(GUI=false) 离屏渲染、ArkTS 用 bindMenu 自绘图层面板,实测证明这条路做不出可用的桌面应用。v2 把 XComponent 从 TEXTURE 换成 SURFACE(EGL 直连),QPA 从 offscreen 换成自研 libqohos.so,UI 全部交还给 Qt 本体。QOhosBackingStore 的上屏链路是 raster QImage 上传 GL 纹理、全屏 triangle-strip blit、eglSwapBuffers 直出;主窗口与模态对话框共享单个 EGLSurface。PORTING.md 第 1 节的 v1/v2 差异表逐项对比了五处替换,是理解该工程的第一份材料。
### 2. 线程模型:把 Qt 主线程搬进 RenderThread
最初 QgisApp 创建在 ArkTS JS 线程上,实测是死路:任何模态 QDialog::exec() 进入嵌套事件循环后,与 JS 同线程的 ArkUI 触摸回调全部饿死,对话框永远无法关闭。v2 定论(2026-08-15)是把 QgisOhos_Init 全部构造改由 RenderThread 执行——Qt 把首个触碰 QThread::currentThread() 的线程记为主线程,RenderThread 从此就是 Qt 主线程。JS 线程绝不再进 Qt:pumpQtEvents 变为 no-op,initQgis 是纯异步 kickoff(2026-09-02 起 JS 线程 9ms 内返回,彻底消除 AppFreeze THREAD_BLOCK_6S)。模态 exec 落在 RenderThread 时,Step 循环里补装的 8ms 输入泵 QTimer 在嵌套循环内持续投递触摸,交互不中断。
### 3. QPA 的窗口治理:钳制、平铺、z 序与真子窗口
没有窗口管理器,Qt 的多窗口假设全部失效,工程在 qohosintegration.cpp 内逐项补齐:对请求几何超屏的对话框做 WM 式钳制并按 QPA 请求/应答契约回告实际几何,compose 叠层从"拉伸到全屏"改为与表面求交裁剪,解决超大对话框按钮物理不可达的问题;对打印布局设计器这类多实例顶层窗做半屏平铺(slot0 右半屏、slot1 左半屏)与平台侧承接的 raise/lower;弹窗型窗口引入 z 序单一事实源、cascade 层叠摆放与按压置顶。终态方案(6.22)是合格的顶层窗经 QOhosDialogOps 回调与 NAPI TSFN 接到 ArkTS DialogWindowManager,创建鸿蒙真子窗口,由 DialogPage 自绘 40vp 标题条与关闭钮,帧经专属 EGLSurface 直投;子窗口创建失败的设备自动回退共享合成治理。
### 4. 上游补丁与依赖栈组织
上游修改全部以补丁形式登记在 native/patches/:欢迎页换为纯 Widgets 替身(qgswelcomescreen_{h,cpp},类名与 API 与上游一致);qgisapp.cpp 在 QGIS_OHOS_PLATFORM 守护下把 createDatabase 失败的模态 QMessageBox::critical 降级为日志、跳过 enableFontDownloadsForSession() 字体下载;libqgis_app 编为共享库并 EXCLUDE_FROM_ALL 排除可执行档。依赖栈按 env.sh → build_deps_{basic,ext,geo}.sh → build_qt6.sh → build_qca.sh → build_qscintilla.sh → build_qgis.sh → build_qpa_ohos.sh 的顺序交叉编译,sync_hap_libs.sh 做 BFS 闭包同步、NEEDED 原位去版本号、ELF 对齐修复。ELF 纪律明确:NEEDED 无版本号无绝对路径、禁 patchelf、hvigor strip:false、Qt 插件走 bare-name dlopen 与内联静态注册。
### 5. 中文化、文件链路与浏览器可达性
中文化踩了三层坑:QGIS 4.x 翻译包是 BCP47 命名(qgis_zh-Hans.qm),需按 language+script 映射;QgsApplication::init() 之后必须二次 setTranslation 才能真正加载;OHOS 上 Qt 无系统 locale 后端,由 EntryAbility 取 config.language(实测 zh-Hans-CN)注入 QGIS_LANG 环境变量兜底。Qt 标准按钮的"确定/取消"来自 qttranslations,工程用 lrelease 预编译 qtbase_zh_CN.qm(1786 条)随包分发。文件链路上,QOhosTheme 覆写 themeNames() 后 QFileDialog 接入鸿蒙 DocumentViewPicker;Download/<bundle>/ 作为用户工作目录直读直写。浏览器面板经三轮终案修到可用:13 个沙箱内零内容的死根条目用 hidePath 裁剪,行单击直调 doubleClicked 实现"单击工程即打开"。
▍五、适配中遇到的几个难点
### 难点1:启动期模态对话框等于 JS 线程死锁
现象是启动卡死、Splash 永不撤除。根因是 createDatabase 失败时上游弹模态 QMessageBox::critical,嵌套 exec 阻塞 JS 线程,而 Splash 与所有轮询定时器都在 JS 线程上。解法分三层:上游补丁把启动期模态降级为日志;Splash 改为监听 QPA 首帧(QOhos_FirstSwapShown)即撤;后续再把库加载与 Init 整体移出 JS 线程。同类问题在 2026-09-02 还以"隐形模态"形式复发——子窗口创建失败后 Qt 窗口保持已路由态,不可见但模态在场,全部触摸被吞;修复是增加 QOhos_UnrouteDialog 回退链路,让失败窗口回到共享合成。
### 难点2:屏幕几何硬编码导致菜单命中偏移半行
现象是点击「工程属性」菜单行实际命中「捕捉选项」,popup 弹在错误位置。根因链有两级:QOhosScreen 构造把屏幕写死为 1920x1080 物理(逻辑 960x540),而真实分辨率注入发生在 QgsApplication 构造之前、被空指针分支跳过,此后无人再更新——Qt 永远按假屏幕摆放与钳制菜单;叠加事件直投 popup QWindow 时 QMenu 恒按第 1 行响应的 Qt6.11 行为,错位被进一步放大。解法是构造改读已注入的真实几何,事件一律投主窗口、由 Qt 的 forwardToPopup 按 globalPos 映射进活动弹窗。修复后菜单逐行精确命中。
### 难点3:QWSI 坐标语义的翻案
排查"欢迎屏点击无响应"时曾把鼠标坐标按逻辑坐标投递,结果全 UI 错位 2.38 倍。PORTING.md 6.12 用探针实证:Qt 6.11 的 QWindowSystemInterface::handleMouseEvent 接收的 local/global 必须是物理像素,Qt 内部自行除以 dpr 转逻辑坐标,预除一次就是二次换算。这条翻案记录的价值在于否定了更早的"真机画布交互不可达"结论——真凶是空列表与对话框遮挡,坐标层没有问题。
### 难点4:provider 插件依赖漏包引发闪退
点击「新建网格图层」图标必闪退(SIGSEGV)。取证链:sync_hap_libs.sh 的 BFS 闭包从核心种子出发,provider 插件是末尾平铺复制、其 NEEDED 依赖不入闭包——libprovider_mdal.so 依赖的 libxml2.so 未入包,dlopen 失败,provider 注册表查不到 mdal,QgsNewMeshLayerDialog 走早退分支留下未构造成员,随后 setCrs() 空指针解引。修复是三管齐下:上游 setCrs 判空、sync 脚本新增 provider 依赖补包段、libxml2.so 手动入包。
### 难点5:签名配置复发导致全设备拒装
真机安装报 9568322 "not trusted app source",两台设备全部拒装。排查排除过 UDID 覆盖与证书链,最终用 hap-sign-tool 的 verify-app 看到输出 profile type is: release 才定位:build-profile.json5 的 products[0] 被指到 release 签名,release profile 不嵌设备 UDID 白名单,任何真机直装都会被拒。改回 default(debug)签名后恢复。该问题 2026-09-07 又复发一次,工程随后在配置处就地加注释防线,并把"改签名出包后必须回置 default"写进文档。
▍六、编译、安装与启动
依赖与 QGIS/QPA 已预编入库(entry/libs 150 个 .so 加 rawfile 数据),日常构建直接走 hvigor。macOS 命令行构建如下:
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
仅当交叉依赖变更时才需要全量重建,顺序为 native/scripts/ 下的 env.sh → build_deps_* → build_qt6 → build_qca → build_qscintilla → build_qgis → build_qpa_ohos,再执行 sync_hap_libs.sh 同步入包。签名产物位于 entry/build/default/outputs/default/entry-default-signed.hap,真机部署必须使用 default(debug)签名。
bash
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b com.airgis.app
PORTING.md 第 7 节同时给出诊断手段:hilog 按 QgisCore|QOhosProbe|AirGisNAPI 过滤可看到从库加载到首帧的完整启动里程碑,沙箱 qgis-share/qgis_init_diag.txt 记录指针事件几何快照。
▍七、当前功能边界
依据 PORTING.md 当前状态与 README 已验证能力,当前版本已覆盖:
- 完整 QGIS 桌面 UI 全屏渲染:菜单栏、工具栏、Browser 与 Layers 停靠面板、地图画布、状态栏;
- 中文界面与 Qt 标准按钮中文化(qgis_zh-Hans.qm + qtbase_zh_CN.qm 双层翻译);
- 菜单逐行精确命中、模态对话框(工程属性 15 页全渲染验证)、popup 开合与残影清除;
- 触摸/鼠标/键盘输入链路,打开数据到工程加载;
- 系统 DocumentViewPicker 打开/保存,Download/<bundle>/ 工作目录免拷贝直读直写;
- 弹窗真子窗口(PC 端,自绘标题条与拖拽)与共享合成自动回退(平板);
- 多实例顶层窗半屏平铺、raise/lower、cascade 层叠与模态守卫置顶;
- 启动期 JS 线程零阻塞(initQgis 全异步化),MatePad 11.5 多轮操作 0 faultlog。
尚未覆盖或已知遗留(均有 PORTING.md 登记依据):工程 .qgz/附件存档保存失败(上游 mFiles 混入空串条目,修复需 QGIS 源码树恢复,见 6.25);保存选择器实际落点为「个人」目录导致全部走 staging 回拷(功能等价);MatePad 11.5 不支持 createSubWindowWithOptions,对话框在该设备无标题条子窗口;浏览器树触摸滑动仍是桌面行为(拖动为多选而非滚动,待 QScroller 类适配);地图画布的渲染专项验证在后续路线表中仍标记为待办;首启动约 25 秒(264 MB 数据拷贝加 150 个 so 加载),按需拷贝在评估中。
因此,当前版本更准确的定位是"完整 QGIS 桌面在鸿蒙 PC 上的可交互移植验证版":桌面应用范式已经在无窗口管理器环境里成立,剩余工作集中在保存链路修复与设备差异适配。
▍八、总结
AirGis 证明了一件事:像 QGIS 这种量级的桌面应用,不需要重写界面也不需要阉割功能,可以在鸿蒙 PC 上以完整本体运行——前提是把窗口系统、线程模型、事件投递和窗口治理这四件 WM 通常包办的事,在自研 QPA 插件里逐项补齐。31 节坑位实录的价值不在罗列故障,而在于每一条都给出了现象、根因、修法与真机验证四个环节,后续任何 Qt 桌面应用移植都可以按图索骥。
后续工作重心集中在三处:修复依赖源码树的 .qgz 保存缺陷;按设备能力探测分流对话框子窗口方案;以及针对首启动 25 秒的数据拷贝做按需化。每一步仍以 PORTING.md 的实录纪律推进——先复现取证,再定根因,最后真机验证闭环。
楼主
8 天前