一、写在前面

欢迎加入鸿蒙PC开发者社区,共同打造开发者工具生态:鸿蒙PC开发者社区:https://harmonypc.csdn.net/

项目开源地址:https://atomgit.com/OpenHarmonyPCDeveloper/ohos_Anki

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

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

这篇文章记录的是 Anki 在 HarmonyOS PC / OpenHarmony PC 环境中的一次 Qt 路线适配过程。

Anki 是一个很经典的间隔重复记忆卡片软件,很多人用它背单词、复习知识点、维护自己的卡片库。和一些单层桌面应用不同,Anki 的原工程不是一个简单的 Qt Widgets 或 Qt Quick 项目,它的桌面版本由多层组成:

  1. qt/aqt/:桌面 GUI 层,主要是 Python + PyQt。
  2. ts/:Svelte / TypeScript 写的 Web 前端组件。
  3. pylib/:Python 业务库,并通过 bridge 调用底层能力。
  4. rslib/:Rust 核心层,负责集合、调度、搜索、同步等核心逻辑。
  5. proto/:跨语言通信使用的 Protobuf 接口定义。

所以这次适配的重点不是把一个网页打进 HAP,也不是把原桌面所有 Python / Rust / WebView 能力一次性完整搬到鸿蒙 PC,而是先走一条可验证、可运行、可继续扩展的 Qt for HarmonyOS 路线:

  1. 新建 harmony_pc/ 鸿蒙工程壳。
  2. ArkTS 侧只负责 Stage Ability、窗口、Qt QPA 启动。
  3. Native 侧生成 libentry.so,用 Qt Quick / QML 承载界面。
  4. C++ 侧实现一个轻量本地 AnkiStore,先完成卡组、卡片、学习、导入、导出、备份等本地可用闭环。
  5. 后续再逐步替换本地 JSON 后端,接入 Anki 原有 Rust / Python 核心能力。

最终效果是:鸿蒙 PC 上可以安装并启动 HAP,应用以 Qt Quick 界面运行,支持中文界面、本地卡组、本地卡片、学习流程、卡片管理、文本导入、卡组导出、集合备份、窗口缩放适配和长页面滚动。

在这里插入图片描述

二、项目背景:Anki 原工程不是单层 Qt 应用

拿到 Anki 工程后,首先要判断它到底是什么技术栈。Anki 的目录结构比较能说明问题:

anki-main/
├── qt/
│   └── aqt/                 # PyQt 桌面 GUI
├── ts/                      # Svelte / TypeScript Web 组件
├── pylib/                   # Python 业务库
├── rslib/                   # Rust 核心层
├── proto/                   # Protobuf 接口定义
├── ftl/                     # 多语言翻译资源
├── justfile                 # 构建、测试、运行入口
└── harmony_pc/              # 本次新增的鸿蒙 PC 适配工程

这意味着它不是“直接把一个 Qt main.cpp 交给鸿蒙 CMake 编译”这么简单。原桌面 Anki 依赖 Python 运行时、PyQt、Rust 动态库、WebView、TypeScript 生成物和 Anki 自己的一套跨语言 bridge。如果第一步就追求完整移植,问题会同时集中在 Python 运行时、Rust ABI、Qt WebEngine/WebView、文件系统、插件体系、同步和打包体积上,调试面会非常大。

因此本次采用分阶段策略:

  1. 第一阶段:证明 Qt for HarmonyOS 能稳定承载 Anki 风格的桌面 UI,并做出本地可用的学习闭环。
  2. 第二阶段:把本地 JSON 数据层逐步替换为真正的 Anki Core / Rust bridge。
  3. 第三阶段:补齐 apkg / colpkg、媒体资源、模板渲染、FSRS 调度、同步和插件等能力。

这篇文章记录的是第一阶段:先把应用跑起来,并做到用户可以在鸿蒙 PC 上真实创建卡组、添加卡片、学习复习、管理数据。

在这里插入图片描述

三、适配路线:选择 Qt for HarmonyOS,而不是 ArkUI 重写

一开始也可以选择 ArkUI 重写,但对 Anki 这种桌面工具来说,直接重写会遇到两个问题:

  1. 原桌面 UI 和业务能力很难复用,等于重新做一个 Anki。
  2. 后续要接回原 Anki Core 时,ArkUI 页面、Python/PyQt 逻辑、Rust bridge 之间会出现新的割裂。

因此这次选择 Qt for HarmonyOS 路线。这个思路和一些 Qt 桌面应用的鸿蒙 PC 适配类似:保留 Qt 作为 UI 承载层,鸿蒙工程只做启动壳和平台适配。这里参考了 MoonPlayer Qt 适配鸿蒙 PC 的工程组织方式,但没有照搬 MoonPlayer 的播放器业务代码,Anki 这边重新实现了自己的 QML 界面和本地数据层。

本次新增的鸿蒙工程主要放在:

anki-main/harmony_pc/
├── AppScope/
│   └── app.json5
├── build-profile.json5
├── entry/
│   ├── build-profile.json5
│   ├── libs/arm64-v8a/
│   │   └── libplugins_platforms_qopenharmony.so
│   └── src/main/
│       ├── cpp/
│       │   ├── CMakeLists.txt
│       │   ├── Main.qml
│       │   ├── anki_ohos_stub.cpp
│       │   ├── anki_store.cpp
│       │   ├── anki_store.h
│       │   └── anki_ohos_resources.qrc
│       ├── ets/
│       │   ├── entryability/EntryAbility.ets
│       │   └── pages/Index.ets
│       └── module.json5
├── hvigor/
├── oh-package.json5
└── qtforharmony_sdk/

整体启动链路可以理解为:

  1. EntryAbility.ets 是鸿蒙 Stage 模型入口。
  2. Index.ets 提供 Qt QPA 需要的页面承载点。
  3. libplugins_platforms_qopenharmony.so 负责 Qt OpenHarmony QPA 对接。
  4. libentry.so 是本项目 native 入口。
  5. anki_ohos_stub.cpp 创建 QGuiApplicationQQmlApplicationEngine
  6. Main.qml 渲染 Anki 鸿蒙 PC 版界面。
  7. AnkiStore 通过 Qt 对象暴露给 QML,负责本地数据读写和学习调度。
    在这里插入图片描述

四、搭建鸿蒙 Qt 启动链路

Qt for HarmonyOS 适配里最关键的一步,是让 HAP 里的 native library 被鸿蒙 Ability 正确启动。

EntryAbility.ets 中引入 QPA 插件:

import qpa from 'libplugins_platforms_qopenharmony.so';

窗口创建后先加载 ArkTS 页面:

await windowStage.loadContent(this.loadContentUrl, localStore);

然后把鸿蒙窗口交给 Qt QPA,并启动 Qt 应用:

qpa.handleJsTopWindowCreated(this.name, this);
qpa.startQtApplication(this);

EntryAbility.ets 里还保留了 file:// / content:// URI 缓存逻辑。这个能力当前主要是为后续从系统文件选择器打开 Anki 数据包、备份文件、导入文件做准备:鸿蒙侧拿到外部 URI 后,先复制到应用 cache 目录,再把可访问的本地路径传给 Qt。

Native 入口在 anki_ohos_stub.cpp

QGuiApplication app(argc, argv);

AnkiStore store;
QQmlApplicationEngine engine;
engine.rootContext()->setContextProperty(QStringLiteral("ankiStore"), &store);
engine.load(QUrl(QStringLiteral("qrc:/anki/harmony/Main.qml")));

return app.exec();

这里没有直接启动原桌面 Anki 的 PyQt 入口,而是先启动一个面向鸿蒙 PC 的 Qt Quick 应用。QML 侧通过 ankiStore 调用 C++ 本地数据层。

CMake 侧主要处理三件事:

  1. 自动定位 Qt for HarmonyOS SDK。
  2. 链接 Qt Core / Gui / Qml / Quick / Network / Svg / Widgets 等模块。
  3. 打包 Qt QML 模块和 QML plugin,避免运行时白屏。

其中 QT_PREFIX 一开始是最容易出错的地方。构建时如果找不到:

lib/cmake/Qt5/Qt5Config.cmake

就会出现类似错误:

QT_PREFIX must point to a Qt 5 for HarmonyOS SDK with lib/cmake/Qt5/Qt5Config.cmake

为了解决这个问题,CMake 做了多路查找:

set(ANKI_QT_PREFIX_CANDIDATES)

if (QT_PREFIX)
    ...
endif()

if (DEFINED ENV{QT_FOR_HARMONY_PREFIX})
    ...
endif()

list(APPEND ANKI_QT_PREFIX_CANDIDATES
    "${HARMONY_PROJECT_ROOT}/qtforharmony_sdk"
    "${WORKSPACE_ROOT}/ok-moonplayer-develop/moonplayer-ohos/harmony_pc/qtforharmony_sdk"
)

也就是说,本项目优先使用自己的 harmony_pc/qtforharmony_sdk,本地迭代时也允许临时复用旁边 MoonPlayer 项目的 Qt for Harmony SDK。这样做只是为了复用本机已经验证过的 Qt SDK 路径,不复制 MoonPlayer 的业务逻辑。

另外,白屏问题也和 QML 运行时模块有关。Main.qml 虽然是我们自己的文件,但 Qt Quick 本身还依赖 QtQmlQtQuick.2QtQuick/Window.2 等模块。CMake 中将这些模块写入生成的 qrc,并把对应 .so plugin 复制到 native 输出目录:

set(ANKI_QT_QML_MODULE_ROOTS
    QtQml
    QtQuick.2
    QtQuick/Window.2
)

在这里插入图片描述

五、从白屏到可用:先做本地 Anki MVP

第一版跑起来后,问题并不是一下就结束了。实际在鸿蒙 PC 设备上验证时,经历了几个典型问题:

  1. 启动白屏:Qt QML 模块或 plugin 没有被正确打包。
  2. 启动闪退:native 入口、QPA 绑定或依赖库路径不完整。
  3. 页面能显示但点击无反应:QML 交互层和布局覆盖关系需要重新检查。
  4. 窗口放大后,内容没有同步放大:原始固定尺寸 UI 不适配鸿蒙 PC 窗口。
  5. 页面内容显示不全,鼠标滚轮不能滚动:长页面没有显式滚动容器高度。
  6. 管理页“导入卡片”按钮点击无反应:按钮所在区域被后续备份区域覆盖,视觉上看得到,实际点击命中了上层布局。

这些问题说明,桌面工具上鸿蒙 PC 后,不能只看“画面出现了没有”,还必须逐项验证窗口缩放、滚轮、鼠标点击、长页面、输入框、弹窗和数据写入。

本次第一阶段没有直接接入完整 Anki 后端,而是实现了一个 C++ / Qt 本地数据层 AnkiStore。它负责把数据保存到应用沙箱 JSON 文件:

/data/app/el2/100/base/net.ankiweb.anki/files/anki_harmony_collection.json

当前本地 MVP 支持:

  1. 卡组列表、卡组新增、卡组重命名、卡组删除。
  2. 卡片新增、编辑、删除、搜索。
  3. 学习页显示问题、显示答案、选择“重来 / 困难 / 良好 / 简单”。
  4. 简化调度逻辑,根据回答结果更新到期时间、间隔和复习次数。
  5. 文本导入卡片。
  6. 当前卡组文本导出。
  7. 集合 JSON 导入 / 导出 / 备份。
  8. 中文 / 英文界面切换。

学习调度是一个简化版本,目标是先证明学习闭环成立:

  1. Again:当天继续复习,并降低 ease。
  2. Hard:下一天复习。
  3. Good:按当前 interval 增长。
  4. Easy:更快增长 interval。

UI 侧主要集中在 Main.qml。为了适配鸿蒙 PC 窗口,做了几个关键处理:

  1. 使用设计宽高和 uiScale,让窗口放大时内容同步放大。
  2. 长页面使用显式 contentHeight,避免 Flickable 判断不到可滚动区域。
  3. 管理页和浏览页增加右侧“上 / 下”滚动按钮,方便在鼠标滚轮不稳定时也能滚动。
  4. 管理页重新调整导入、导出、备份区域高度,避免按钮被后续区域覆盖。
  5. 页面底部导航、主要按钮、输入框、列表项都使用较大的点击区域,更适合 PC 鼠标操作。

其中“导入卡片点击无反应”的问题比较典型。表面看按钮在界面上,但由于页面布局高度不足,下面的备份区域实际覆盖到了导入区域上方,导致鼠标点击命中了覆盖层。修复方式不是只改按钮事件,而是重新梳理管理页内容高度、区块间距和 Flickable 的 contentHeight,让每个可点击控件都处在真实可交互区域内。

在这里插入图片描述

六、学习闭环验证:添加、浏览、学习、写回

一个记忆卡片工具是否真的可用,不能只看首页,而要验证完整闭环:

  1. 创建或选择一个卡组。
  2. 在“新增”页添加问题和答案。
  3. 在“浏览”页能搜索、编辑、删除卡片。
  4. 在“学习”页能看到问题。
  5. 点击“显示答案”后出现答案和评分按钮。
  6. 点击“良好”等评分按钮后,卡片状态写回本地 JSON。
  7. 关闭并重新打开应用后,数据仍然存在。

本次在设备上验证过学习流程:点击“显示答案”后,可以看到答案区域和四个评分按钮;点击“良好”后,卡片的 repsintervaldueDay 等字段会更新,并写入:

/data/app/el2/100/base/net.ankiweb.anki/files/anki_harmony_collection.json

可以用下面命令把设备上的集合文件拉回本机检查:

/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains/hdc file recv \
/data/app/el2/100/base/net.ankiweb.anki/files/anki_harmony_collection.json \
~/XM/anki-main/harmony_pc/anki_harmony_collection_device.json

安装和启动命令如下:

cd ~/XM/anki-main

/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains/hdc install -r harmony_pc/entry/build/default/outputs/default/entry-default-signed.hap

/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains/hdc shell aa force-stop net.ankiweb.anki

/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains/hdc shell aa start -b net.ankiweb.anki -a EntryAbility

在这里插入图片描述

七、当前完成度与后续方向

到这里,Anki 鸿蒙 PC Qt 适配已经完成了第一阶段目标:应用能作为 HAP 安装运行,并且用户可以在鸿蒙 PC 上完成本地记忆卡片的基础使用。

当前已经完成:

  1. harmony_pc/ 鸿蒙 PC 工程壳。
  2. ArkTS Stage Ability + Qt QPA 启动链路。
  3. libentry.so native 入口。
  4. Qt Quick / QML 主界面。
  5. C++ / Qt 本地 AnkiStore 数据层。
  6. 本地 JSON 持久化。
  7. 卡组、卡片、学习、浏览、管理。
  8. 文本导入、卡组导出、集合备份。
  9. 中文 / 英文界面切换。
  10. 鸿蒙 PC 窗口缩放适配。
  11. 长页面滚动和管理页点击问题修复。

这次适配最大的经验是:复杂桌面软件迁移到鸿蒙 PC,不一定要一开始就重写所有功能。对 Qt 项目,尤其是有桌面工具属性的项目,可以先把 Qt for HarmonyOS 启动链路打通,再把核心使用路径做成本地可用 MVP。这样可以尽早在真实设备上发现白屏、闪退、缩放、滚动、点击覆盖、输入等问题,也能让后续接入原核心能力时有一个稳定的 UI 和工程基础。

对于 Anki 这类软件来说,第一阶段最重要的不是“看起来像”,而是“真的能学一张卡、写回一条复习记录”。只要这个闭环成立,后续再逐步把本地 MVP 替换成完整 Anki Core,就有了比较清晰的演进路线。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐