本文结合本仓库 FeatherPadNext 的实际源码、工程结构,系统总结开源轻量级多标签纯文本编辑器 FeatherPad 从 Linux 桌面到鸿蒙 PC(2in1)的完整移植过程。

移植成功后的效果:
在这里插入图片描述

一、FeatherPad 与 FeatherPadNext 在鸿蒙上的差异

FeatherPad 是 Pedram Pourang (tsujan) 开发的 Qt 桌面多标签纯文本编辑器(原始仓库:https://github.com/tsujan/FeatherPad),上游以 Linux 为主但 Qt 代码跨平台(Windows / macOS / BSD 均可编译)。本文参考的 v1.6.4 源码基于 Windows 编译版本。FeatherPadNext 是其在鸿蒙 PC 上的移植版本。

与 MeldNext 的根本差异:MeldNext 用 ArkTS 完全重写 UI 层;FeatherPadNext 走的是"Qt 引擎直接嵌入 ArkUI 容器"的路线——保留 95% Qt C++ 源码,用鸿蒙 XComponent + QAbility 作为运行宿主,仅通过 FP_OHOS 宏裁剪桌面专有代码,用 N-API 桥接对接系统服务(字体安装、打印)。

维度 原生 FeatherPad(Windows / Linux 桌面) FeatherPadNext(鸿蒙 PC)
应用形态 独立 Qt 桌面应用(Win32 或 X11/Wayland 进程) HAP 应用包(QAbility 生命周期)
UI 宿主 Win32 窗口(Windows)/ X11 / Wayland(Linux) ArkUI XComponent 嵌入 Qt 窗口(libqohos.so 平台插件)
编程语言 C++ (Qt 5) C++ (Qt 5.12.12 for OHOS) + ArkTS(壳层)
文本引擎 QPlainTextEdit + QSyntaxHighlighter 复用原版 Qt 引擎(无重写)
拼写检查 Hunspell(系统包) Hunspell 1.7.2 交叉编译 + resfile 加载
打印 Qt PrintSupport(Windows 走 WinSpool / Linux 走 CUPS) Qt PrintSupport + ohosprintsupport + Print Kit
剪贴板 QClipboard(系统剪贴板) 应用内静态剪贴板(READ_PASTEBOARD 是 system_basic 权限)
单实例 D-Bus 跨进程协调(Linux 主路径,Windows 退化为本地) 始终主实例(无 D-Bus)
SVG 图标 QSvgRenderer 头文件 + 主题色替换 libqsvg.so 插件 + QImage 逐像素着色
保存提权 pkexec / Polkit(Linux 路径)/ UAC(Windows 路径) 自动引导"另存为"(沙箱限制)
Help 文档 DATADIR/featherpad/help(Linux)/ 应用目录(Windows) :/help Qt 资源内嵌只读标签页
字体加载 系统字体安装 / Fontconfig N-API 桥接 → 下载微软雅黑 → 唤起系统字体安装器
日志 qDebug() OHOS hilog,LOG_TAG=FeatherPad
构建系统 qmake / CMake + 系统 Qt Hvigor + CMake + 内置 Qt 5.12.12 OHOS SDK
包名 / 主 Ability featherpad / 无 com.develop.opensource.featherpadnext / QAbility
设备 / SDK Windows / Linux 桌面 / 系统 Qt 2in1(鸿蒙 PC / 平板)/ HarmonyOS 6.0.1(21),兼容 5.0.5(17)

二、技术架构与技术原理

2.1 总体架构

在这里插入图片描述

2.2 核心技术原理

QAbility + XComponent 容器化:用 ArkUI 的 XComponent 控件作为 Qt 窗口的承载容器,XComponent 暴露 surfaceId,Qt 通过 libqohos.so 平台插件把 surfaceId 包装成 QWindow,Qt 的 EGL/OpenGL ES 把内容画到这个 surface 上。

FP_OHOS 宏守护桌面代码FP_OHOS=1 在 CMake 全局定义,桌面专有代码用 #ifdef FP_OHOS 双向守护(D-Bus 单实例、pkexec/UAC 提权、SpellDialog 弹窗、SpellDialog 建议词、剪贴板双缓冲、Help 路径、SVG 渲染、X11/Win32 头文件等 6+ 处)。

N-API 双向桥接

  • libfontbridge.so必须独立成库):含 Qt 依赖,独立编译后让 ArkTS 只 import 本库,避免 ArkTS import 时 dlopen 破坏 Qt 运行时加载时序
  • printbridge内嵌于 libfeatherpad.so):无 Qt 依赖,可直接编进主库
  • N-API 线程安全:C++ 侧(特别是 Qt 事件循环线程)调用 ArkTS 函数必须用 napi_threadsafe_function 派发到 JS 线程,否则直接调用会 cppcrash

Hunspell resfile 加载:Hunspell 需要真实文件路径,rawfile 不能用 QFile 直接打开,必须用 resfile(运行时路径 /data/storage/el1/bundle/entry/resources/resfile/dictionaries/),所以词典文件在工程里存在两份。

Qt 主题与系统调色板对抗:OHOS 默认调色板会把 Untitled 编辑区画成"黑底蓝字",main.cpp::applyOhosUiFixes() 强制 Fusion 样式 + 覆盖 QPalette 12 个 role + 全量 QSS,在 QApplication 构造后立即调用。

三、底层依赖开源库介绍

# 版本 在 FeatherPadNext 中的角色 移植方式 复用率
1 Qt 5.12.12 for OHOS 5.12.12 整个 libfeatherpad.so 的运行时 内置到 thirdparty/qt-5.12.12-ohos-sdk/,CMake find_package 100%
2 libqohos.so 与 Qt 同 Qt 在 OHOS 的平台后端(XComponent + Ability 生命周期) 内置于 entry/libs/arm64-v8a/ 100%
3 libohosprintsupport.so 与 Qt 同 QPrinter 桥接到 BasicServicesKit.print 内置于 entry/libs/arm64-v8a/printsupport/ 100%
4 libqsvg / libqsvgicon 与 Qt 同 SVG 图像/图标插件(避开 QSvgRenderer 头依赖) 内置于 Qt SDK plugins/ 100%
5 Hunspell 1.7.2 spellChecker.cpp 调用 hunspell.hxx 交叉编译(hunspell_build.sh),预编译产物内置 ~100%
6 en_US Hunspell 词典 1.7.2 英文拼写词库 fp.qrc 嵌入 Qt 资源 + resfile/dictionaries/ 双份 100%
7 FeatherPad 21 语言高亮器 1.6.4 cmake/css/fountain/html/java/json/lua/markdown/pascal/patterns/perl-regex/regex/rest/ruby/rust/sh/tcl/toml/xml/yaml + url 源码内嵌 highlighter/0 改动 100%
8 libQt5DBus 5.12.12 桌面端单实例依赖;鸿蒙端代码裁剪后仅链接不调用 Qt SDK 自带 100%

四、移植流程

4.1 总体移植流程

在这里插入图片描述

各阶段简述:

  • 第1阶段·环境准备:DevEco Studio 6.x + 内置 Qt 5.12.12 SDK(已打包到 thirdparty/)+ OHOS Native SDK
  • 第2阶段·Hunspell 交叉编译(可选)./hunspell_build.shlibhunspell-1.7.{a,so},预编译产物已内置到仓库,首次构建可跳过
  • 第3阶段·CMake 从源码编译FEATHERPAD_BUILD_FROM_SOURCE=ON 触发 CMakeLists.featherpad-from-source.cmakeadd_library(featherpad SHARED ...50个源文件) + add_library(fontbridge SHARED font_install_napi_bridge)
  • 第4阶段·DevEco 打包:拷贝所有 .soentry/libs/arm64-v8a/ + Qt 插件 + 配置 build-profile.json5(externalNativeOptions)+ module.json5(deviceTypes/permissions)+ 拷贝词典到 resfile/
  • 第5阶段·签名 + 安装:DevEco 自动签名 → 生成 HAP → hdc install / Run

4.2 关键代码

4.2.1 关键 CMakeLists(from-source 模式核心片段)
# entry/src/main/cpp/CMakeLists.featherpad-from-source.cmake
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)

# Qt SDK 查找:环境变量 > 内置 thirdparty/
foreach(_c "${CMAKE_CURRENT_SOURCE_DIR}/../../../../thirdparty/qt-5.12.12-ohos-sdk"
         "${CMAKE_CURRENT_SOURCE_DIR}/../../../../../qt-5.12.12-ohos-sdk")
    get_filename_component(_c_abs "${_c}" ABSOLUTE)
    if(EXISTS "${_c_abs}/lib/cmake/Qt5/Qt5Config.cmake")
        set(QT_PREFIX "${_c_abs}"); break()
    endif()
endforeach()
list(APPEND CMAKE_PREFIX_PATH "${QT_PREFIX}")
list(APPEND CMAKE_FIND_ROOT_PATH "${QT_PREFIX}")

# 50 个源文件 + 5 个 .ui + fp.qrc 编译进 libfeatherpad.so
add_library(featherpad SHARED ${featherpad_SRCS} ${featherpad_MOC_HDRS} ${featherpad_RES})
target_compile_definitions(featherpad PRIVATE FP_NO_SVG FP_OHOS=1)   # 关键宏
find_package(Qt5 REQUIRED COMPONENTS Core Gui Widgets PrintSupport)
target_link_libraries(featherpad PRIVATE Qt5::Core Qt5::Gui Qt5::Widgets Qt5::PrintSupport libhilog_ndk.z.so)

# 字体安装桥接:必须独立成库
add_library(fontbridge SHARED font_install_napi_bridge.cpp)
target_link_libraries(fontbridge PRIVATE libace_napi.z.so libhilog_ndk.z.so)
target_link_libraries(featherpad PRIVATE fontbridge)
4.2.2 QAbility 启动链
// QAbilityStage.ets —— 把 ArkUI 上下文 + Qt 库名塞给 libqohos.so
qpa.setupQtApplication({
    appContext: this.context.getApplicationContext(),
    appName: 'libfeatherpad.so',     // 与 CMake add_library(featherpad ...) 一致
    resourceDir: this.context.resourceDir,
    _unusedQChildProcess: new QChildProcess(),
});

// QAbility.ets —— 生命周期全部转发给 qpa
qpa.handleAbilityOnCreate(this, want, launchParam);
qpa.handleAbilityOnWindowStageCreate(this, windowStage);   // 此处同时调 FontInstallService.init(ctx)

// XComponent 必须用 libraryname: 'qohos' 绑定 libqohos.so 平台插件
XComponent({ type: XComponentType.NODE, id: xComponentId, libraryname: 'qohos' })
  .width('100%').height('100%')
4.2.3 N-API 字体安装桥接(独立成库 + tsfn 派发)

在这里插入图片描述

// FontInstallService.ets —— ArkTS 侧
static init(ctx) { fontbridge.initFontBridge(FontInstallService); }   // 把 installMsyh 静态方法注册给 C++

static async installMsyh() {
    // 1) 下载 msyh.ttf 到沙箱(已有则跳过)
    const req = http.createHttp();
    const resp = await req.request(MSYH_URL, { expectDataType: http.HttpDataType.ARRAY_BUFFER });
    fs.writeSync(fs.openSync(destPath, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE).fd, resp.result as ArrayBuffer);
    // 2) 唤起系统字体安装器
    await ctx.startAbility({
        bundleName: 'com.huawei.hmos.fontinstaller',
        abilityName: 'DoubleClickInstallAbility',
        action: 'ohos.want.action.viewData',
        uri: fileUri.getUriFromPath(destPath),
    });
}
// font_install_napi_bridge.cpp —— C++ 侧(tsfn 派发到 JS 线程)
extern "C" bool OhosLoadMsyhFont() {                                // Qt 按钮点击线程调用
    return napi_call_threadsafe_function(g_tsfn, nullptr, napi_tsfn_nonblocking) == napi_ok;
}

static napi_value InitFontBridge(napi_env env, napi_callback_info info) {
    napi_value installFunc;
    napi_get_named_property(env, argv[0], "installMsyh", &installFunc);
    napi_value resourceName;
    napi_create_string_utf8(env, "fontInstall", NAPI_AUTO_LENGTH, &resourceName);
    napi_create_threadsafe_function(env, installFunc, nullptr, resourceName,
        0, 1, nullptr, nullptr, nullptr, CallInstallMsyhOnJsThread, &g_tsfn);
    napi_ref_threadsafe_function(env, g_tsfn);                       // 防止 GC
    return undefined;
}
4.2.4 N-API 打印桥接(内嵌 + napi_ref 缓存)
// PrintService.ets
static init() { printbridge.initPrintBridge(); }                    // 一次性注册 PrintService 类
static async printHtml(html, jobName) {
    await print.print([html], {
        jobName, pageSize: { id: 'ISO_A4', name: 'A4' },
        margin: { top: 20, bottom: 20, left: 20, right: 20 },
        orientation: print.PrintDocumentOrientation.PORTRAIT,
        colorMode: print.PrintColorMode.MONOCHROME,
        duplexMode: print.PrintDuplexMode.ONE_SIDED
    });
}
// ohos_print_adapter.cpp(Qt 侧)—— 一行桥接到 C++
bool OHOSPrintAdapter::callNativePrint(const QString &html, const QString &job) {
    return CallPrintService(html.toUtf8().constData(), job.toUtf8().constData());
}
// print_napi_bridge.cpp(内嵌于 libfeatherpad.so)—— 存 napi_ref + napi_call_function
extern "C" bool CallPrintService(const char* html, const char* job) {
    napi_value svc, fn;
    napi_get_reference_value(g_env, g_printServiceRef, &svc);
    napi_get_named_property(g_env, svc, "printHtml", &fn);
    napi_value args[2];
    napi_create_string_utf8(g_env, html, NAPI_AUTO_LENGTH, &args[0]);
    napi_create_string_utf8(g_env, job, NAPI_AUTO_LENGTH, &args[1]);
    napi_call_function(g_env, svc, fn, 2, args, nullptr);
    return true;
}
4.2.5 SVG 双路径渲染(关键差异点)
// svgicons.cpp::symbolicIconEngine::paint()
#ifndef FP_NO_SVG
    // 桌面端:QSvgRenderer 解析 + 字符串替换 #000
    QSvgRenderer renderer;
    renderer.load(bytes.replace("#000", col.name().toLatin1()));
    renderer.render(&p, QRect(0, 0, rect.width(), rect.height()));
#else
    // 鸿蒙端:QImage 读 SVG(依赖 libqsvg.so 插件)+ 逐像素着色
    QImage img(fileName);                       // libqsvg.so 解析 SVG
    img = img.scaled(rect.width(), rect.height(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
    for (int y = 0; y < img.height(); ++y)
        for (int x = 0; x < img.width(); ++x) {
            QColor c = img.pixelColor(x, y);
            if (c.alpha() > 0)
                img.setPixelColor(x, y, QColor(col.red(), col.green(), col.blue(), c.alpha()));
        }
    p.drawImage(0, 0, img);
#endif
4.2.6 桌面代码裁剪实例
// fpwin.cpp:3554 —— saveFile 失败时
#ifdef FP_OHOS
    showWarningBar("Please use Save As to save to a writable location.", 15);
#else
    fileProcess->start("pkexec", QStringList() << "--disable-internal-agent" << "cp" << fname << target);
#endif

// textedit.cpp —— 双缓冲剪贴板
#ifdef FP_OHOS
static QString internalClipboard_;
#endif
void TextEdit::copy() {
    QApplication::clipboard()->setText(text);
#ifdef FP_OHOS
    internalClipboard_ = text;   // 同步写一份应用内静态变量,绕开 READ_PASTEBOARD system_basic 权限
#endif
}
void TextEdit::paste() {
#ifdef FP_OHOS
    if (!internalClipboard_.isEmpty()) { insertPlainText(internalClipboard_); return; }
#endif
    QPlainTextEdit::paste();
}

4.3 关键配置文件

文件 关键作用
build-profile.json5(根) targetSdkVersion=6.0.1(21),compatibleSdkVersion=5.0.5(17),runtimeOS=HarmonyOS,arkTSVersion=1.1
entry/build-profile.json5 externalNativeOptions.path=./src/main/cpp/CMakeLists.txt,arguments=-DFEATHERPAD_BUILD_FROM_SOURCE=ON,abiFilters=["arm64-v8a"]
entry/src/main/module.json5 deviceTypes=["2in1"],requestPermissions=[ohos.permission.PRINT, ohos.permission.INTERNET],mainElement=QAbility
entry/src/main/cpp/CMakeLists.txt 顶层 CMake 入口,from-source 开关(OFF 时用预编译 libfeatherpad.so)
entry/src/main/cpp/CMakeLists.featherpad-from-source.cmake from-source 编译配置(Qt 查找、Hunspell 路径、宏定义、库链接)
entry/src/main/ets/qability/QAbility.ets UIAbility 生命周期,调用 libqohos.so + FontInstallService.init()
entry/src/main/ets/qabilitystage/QAbilityStage.ets AbilityStage,调用 qpa.setupQtApplication()
entry/src/main/ets/common/QtAppConstants.ets APP_LIBRARY_NAME = 'libfeatherpad.so'(与 CMake 产物名一致)
entry/src/main/ets/pages/MainWindowNativeNode.ets XComponent 容器,libraryname: 'qohos'
entry/src/main/cpp/data/fp.qrc Qt 资源清单(help、icons/*.svg、dictionaries)
entry/libs/arm64-v8a/libqohos.so Qt OHOS 平台插件(HAP 打包必需)
entry/libs/arm64-v8a/lib{fontbridge,ohosprintsupport,qsvg,qsvgicon}.so 各类 Qt 插件
entry/libs/arm64-v8a/libhunspell-1.7.{a,so} 预编译 Hunspell
entry/libs/arm64-v8a/libQt5*.so Qt 5 运行时 7 个核心库(Core/Gui/Widgets/PrintSupport/Svg/DBus/Concurrent)
thirdparty/qt-5.12.12-ohos-sdk/ 内置 Qt SDK(含已修复硬编码路径的 cmake 配置)
entry/src/main/resources/resfile/dictionaries/ 运行时可被 QFile 打开的 Hunspell 词典

4.4 注意事项

  1. Qt SDK 路径硬编码已修复:旧版 thirdparty/qt-5.12.12-ohos-sdk/lib/cmake/Qt5Gui/Qt5GuiConfigExtras.cmake 写死 D:/SDK/23/native/...,新版本按 OHOS_SDK_NATIVE 环境变量 → 自动检测 DevEco 默认路径 → 兜底报错的顺序查找

  2. Hunspell 词典双份拷贝:必须在 resfile/dictionaries/ 留一份(运行时路径),rawfile/dictionaries/ 可选(用于 Qt 资源访问)

  3. N-API 桥接库的"独立 vs 内嵌"

    • libfontbridge.so → 必须独立(含 Qt 依赖的库会让 ArkTS 提前 dlopen,破坏 Qt 运行时加载时序)
    • printbridge → 可以内嵌于 libfeatherpad.so(无 Qt 依赖)
  4. N-API 线程安全:C++ 侧调用 ArkTS 函数(特别是 Qt 事件循环线程)必须用 napi_threadsafe_function 派发到 JS 线程,否则直接调用 NAPI 会 cppcrash

  5. 剪贴板方案:copy/cut 时同时写 QApplication::clipboard()(系统剪贴板) internalClipboard_(应用内静态),形成双缓冲

  6. OHOS 调色板对抗:必须在 QApplication 构造后立即调 applyOhosUiFixes(),否则 Untitled 编辑区会显示成黑底蓝字

  7. 打包 .so 完整性:HAP 打包时所有运行时 .so 必须放 entry/libs/<abi>/,包括 Qt 插件(platforms/、printsupport/、imageformats/、iconengines/)。缺一个就启动失败

  8. 目标设备:本工程 deviceTypes: ["2in1"],仅支持鸿蒙 PC / 平板,不支持手机

  9. 首次编译时长:~50 个 C++ 源文件 + 5 个 .ui + 1 个 .qrc,3-8 分钟。后续为秒级增量

  10. C++ 改动后无效果:2in1 target 的 CMake 增量检测偶有失效。Build → Clean Project 或删 entry/build/entry/.cxx/.hvigor/ 后重编

五、移植总结

5.1 移植模式

模式 适用场景 代表项目 复杂度
Qt 应用嵌入 ArkUI 容器(FeatherPadNext) 原项目本身就是 Qt 应用,希望保留 Qt 引擎层 + 极小代价上鸿蒙 FeatherPad、KDE 应用 ★★☆
ArkTS UI 重写 + C++ 引擎(MeldNext) 原项目是 Python/GTK/其他 UI,需要完全重写 UI 层 MeldNext 等 ★★★
HNP 命令行工具 纯命令行工具,无 UI Git、SVN、diffutils ★☆☆

FeatherPadNext 走的是"最小代价"路线——保留 95% 的 Qt C++ 源码,只做四类工作:

  1. FP_OHOS 宏裁剪桌面专有代码(D-Bus、pkexec/UAC、X11/Win32、SpellDialog)
  2. N-API 桥接系统服务(字体、打印)
  3. Qt SDK 路径可移植化(让仓库换电脑也能编)
  4. .so 全量打包进 HAP

5.2 关键技术决策

  1. 不重写 UI 层:直接复用 Qt Widgets + QSyntaxHighlighter,开发量最小、风险最低、原作者升级时合并最容易
  2. FP_OHOS 宏守护桌面代码:而非 fork 上游仓库,让所有 OHOS 适配与上游代码并存可读
  3. Qt 主题适配下沉到 main.cppapplyOhosUiFixes() 一次性解决调色板 + QSS 问题,不污染业务代码
  4. N-API 桥接库按需独立:libfontbridge 必须独立(Qt 依赖加载时序),libprintbridge 可内嵌(无 Qt 依赖)
  5. Hunspell 走 resfile 而非 rawfile:满足 QFile::open 路径要求,同时保留 rawfile 备份
  6. Qt SDK 内置到仓库:让工程自带 thirdparty/qt-5.12.12-ohos-sdk/,构建时无需额外下载 Qt
  7. saveFile 失败时引导 Save As:而非走 pkexec 提权,符合鸿蒙沙箱安全模型

5.3 效果

指标 说明
代码复用率 Qt 引擎层 ~95% 未修改,高亮器 100% 复用
新写代码量 ArkTS 壳层 ~10 个文件 / 300 行;N-API 桥接 ~250 行(font + print);CMake 改造 ~150 行
功能完整性 多标签编辑、21 语言语法高亮、查找替换、跳转行号、偏好设置、会话恢复、侧边栏、拼写检查、打印、字体下载安装、Help 文档 均正常运行
架构 / SDK arm64-v8a / HarmonyOS 6.0.1(21),兼容 5.0.5(17)
设备形态 2in1(鸿蒙 PC / 平板)
首编译时长 3-8 分钟(50 个 C++ 文件);增量秒级
HAP 体积 ~30 MB(含 Qt 运行时)

5.4 适用建议

什么项目适合走"Qt 嵌入 ArkUI 容器"路线?

  • 原项目是 Qt 5/6 C++ 应用,体量较大(>50 个源文件)
  • 重写 UI 工作量不亚于移植本身
  • 团队对 Qt 熟悉,对 ArkTS 还在学习
  • 鸿蒙端功能以"展示+编辑"为主,不需要深度集成 ArkUI 控件

什么项目建议走"ArkTS UI 重写 + C++ 引擎"路线?

  • 原项目 UI 是 Python/GTK/Electron 等,重写 ArkTS 反而更轻
  • 需要深度使用 ArkUI 控件(如 Navigation、List、Grid 组件)
  • 原项目 C++ 引擎层逻辑独立(如 MeldNext 的 diff 引擎、文件加载器)

5.5 参考资源

  • 原始 FeatherPad 仓库:https://github.com/tsujan/FeatherPad
  • FeatherPadNext 鸿蒙开源地址:https://gitcode.com/OpenHarmonyPCDeveloper/ohos_FeatherPadNext
  • Qt for OpenHarmony 官方模板:https://gitcode.com/openharmony-sig/qt
  • OHOS Native SDK:https://developer.huawei.com/consumer/cn/deveco-studio/
  • OpenHarmony 三方库移植指导:https://gitcode.com/openharmony-sig/tpc_c_cplusplus
  • Hunspell 拼写检查引擎:https://github.com/hunspell/hunspell

版权声明:本文为原创技术总结,遵循 CC 4.0 BY-SA 版权协议。FeatherPad 上游代码遵循 GPL-3.0+ 协议。

Logo

赋能鸿蒙PC开发者,共建全场景原生生态,共享一次开发多端部署创新价值。

更多推荐