一、写在前面

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

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

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

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

这篇文章记录的是 Tux Paint 在 HarmonyOS PC / OpenHarmony PC 环境中的一次完整适配过程。

Tux Paint 是一款经典的、面向 3~12 岁儿童的开源绘画程序(GPL,当前版本 0.9.36):卡通企鹅 Tux 当向导、左侧一排大按钮工具(画笔、印章、直线、形状、文字、填充、魔法、橡皮……)、中间白画布、底部一排大色块,主打"简单、好玩、跨平台"。它在桌面端覆盖 Linux / Windows / macOS / Android / iOS。

但这次适配有一个和前面那些 Qt 项目(TupiTube、Krita、Natron…)根本不同的地方:

它的技术栈是 C 语言 + SDL2(外加 SDL2_image / SDL2_ttf / SDL2_mixer / SDL2_Pango、Cairo、librsvg、libpng/jpeg/tiff……),约 117 个 .c 文件,用 GNU Make 构建。而本地这一系列鸿蒙PC适配走的全是同一条**“Qt for HarmonyOS”**的路子——ArkTS 起窗口 + XComponent + Qt OpenHarmony QPA 插件 + libentry.so。手上又没有任何"SDL2 跑在鸿蒙上"的现成先例。

所以这次适配真正要回答的问题是:

  1. 一个 SDL2 的应用,到底要不要、能不能套用这套 Qt 的鸿蒙适配框架?
  2. 在没有把 SDL2 全家桶交叉编译到鸿蒙之前,怎样先拿出一个能装、能跑、能交互的版本?
  3. 怎样让这个版本忠实于 Tux Paint 的经典样子,而不是随便画个界面?
  4. 怎样让左边那一排工具真的能用(画笔、形状、填充、魔法、撤销、保存……),而不是点了没反应的"假按钮"?
  5. 在拿不到鸿蒙真机交叉编译链的本机上,怎样提前验证这份 C++ 至少能编过?

本次适配采用和 Krita / TupiTube 一致的逐步验证路线:桌面原项目(SDL2 的 Tux Paint)保持不动,新建 harmony_pc/ 作为鸿蒙工程壳;先交付一个功能可用的 Qt Widgets SHELL(忠实复刻 Tux Paint 界面、工具真实生效),把"ArkTS → XComponent → QPA → libentry.so → 可见可交互的窗口"整条链路打通;真正的 SDL2 全量移植(FULL)作为后续路线,并在 CMakeLists.txt 与文档里写清楚需要交叉编译哪些依赖。

在这里插入图片描述

二、项目背景:Tux Paint 是 SDL2 写的儿童绘画程序

确认它是 SDL2 项目也很简单:根目录有 Makefile(不是 .pro、不是 CMakeLists),src/tuxpaint.c 顶部 #include "SDL.h"#include "SDL_image.h"#include "SDL_ttf.h"#include "SDL_mixer.h" 一应俱全,整套界面(工具栏、画布、选择器、调色板、Tux 提示、文字输入、魔法对话框)全是直接往 SDL surface 上 blit 出来的,没有任何窗口控件库。

原始项目结构(节选):

tuxpaint/
├── Makefile                 # GNU Make 构建(非 Qt)
├── src/
│   ├── tuxpaint.c           # 主程序(SDL2 主循环、全部 UI 绘制)
│   ├── tools.h              # 16 个工具的枚举与显示名/提示语
│   ├── colors.h             # 17 个默认颜色的 RGB 值
│   ├── fonts.c im.c i18n.c  # 字体 / 输入法 / 国际化
│   └── ...                  # 共约 117 个 .c
├── magic/                   # "魔法工具"特效插件
├── data/ fonts/ stamps/ starters/ templates/   # 素材资源
└── harmony_pc/              # 本次新增的鸿蒙工程壳

这里有个很有用的细节:Tux Paint 把工具清单调色板都写死在两个头文件里,正好拿来当"忠实复刻"的数据源:

  • src/tools.hTOOL_* 枚举顺序就是左侧工具栏的顺序:Paint、Stamp、Lines、Shapes、Text、Label、Fill、Magic、Undo、Redo、Eraser、New、Open、Save、Print、Quit,外加每个工具的 tool_names[] 显示名和 tool_tips[] 提示语;
  • src/colors.hdefault_color_hexes[] 是 17 个默认色的精确 RGB(黑、深灰、浅灰、白、红、橙、黄、浅绿、深绿、天蓝、蓝、薰衣草、紫、粉、棕、棕褐、米色)。

后面 SHELL 的工具名、提示语、颜色,全部逐字取自这两个文件,保证复刻出来就是 Tux Paint 的样子,而不是凭感觉编。

在这里插入图片描述

三、关键抉择:SDL2 的应用,为什么用 Qt 去适配鸿蒙

先把"要不要用 Qt"这个问题讲清楚。

我把本地已经完成的鸿蒙PC适配挨个看了一遍——Krita、TupiTube、Natron、Scribus、Avogadro、Photoflare……无一例外,全部是同一套"Qt for HarmonyOS"方案:

ArkTS UIAbility → 全窗口 XComponent → Qt OpenHarmony QPA 插件 → libentry.so → 可见的 Qt 窗口

每个项目都自带一份 234MB 的 qtforharmony_sdk(Qt 5.15 移植版),并且都有两种构建模式:SHELL(一个自包含、必定能编能跑的 Qt Widgets 仿真壳,用来打通管线)和 FULL(真正的原应用)。

Tux Paint 的麻烦在于它是 SDL2。理论上 SDL2 新版本确实有 OpenHarmony 的后端(SDL_ohos),可以走"SDL2 直接在鸿蒙起窗口"的路线,但那是另一套体系,本地没有任何先例,且需要把 SDL2 + SDL2_image/ttf/mixer/Pango、Pango、Cairo、FriBidi、HarfBuzz、FreeType、librsvg…… 一整串依赖全部交叉编译到 OHOS arm64。

结论很清晰:和整个系列保持一致、最稳、当下就能交付的做法,是套用 Qt 的 SHELL 模式——用 Qt Widgets 写一个忠实复刻 Tux Paint 经典界面、且工具真实可用的外壳,先把鸿蒙这条渲染管线跑通;真正的 SDL2 移植作为 FULL 模式的后续工作。这正是 Krita / TupiTube 交付的可运行基线的思路。

所以这篇的答案是:是的,这次也是用 Qt 适配鸿蒙——哪怕原版是 SDL2。

四、鸿蒙工程壳:Ability + XComponent + Qt QPA

Qt for Harmony 的关键思路是:ArkTS 侧创建鸿蒙窗口,页面里放一个 XComponent,再由 Qt OpenHarmony QPA 插件把 Qt 窗口挂上去。Index.ets 很薄,核心就是这个 XComponent

XComponent({
  id: this.windowId,
  type: XComponentType.NODE,
  libraryname: 'plugins_platforms_qopenharmony'
})
  .width('100%').height('100%');

EntryAbility.ets 负责窗口生命周期并启动 Qt,MyAbilityStage.etsonCreateqpa.attachAbilityStage(this)onAcceptWant 里返回实例 key(这里改成了 Tux Paint 自己的 tuxpaint_main_window)。QPA 插件会去 dlopen 名为 libentry.so 的库,并调用它的 qtmain 入口。SHELL 的 C++ 里就提供这个入口:

int main(int argc, char **argv) { return runTuxPaintShell(argc, argv); }

#if defined(OPENHARMONY)
// Qt OpenHarmony QPA 插件 dlopen libentry.so 后调用的就是 qtmain
extern "C" int qtmain(int argc, char *argv[]) {
    return runTuxPaintShell(argc, argv);
}
#endif

应用身份用 Tux Paint 自己的包名 org.tuxpaint.tuxpaint、版本 0.9.36module.json5 里因为 Tux Paint 是纯离线应用,去掉了网络权限,只保留文件访问权限(用于保存画作)。签名留空,交给 DevEco Studio 自己生成——不把参考模板项目的包名、logo、签名复制过来。

五、自带 Qt SDK:放进自己项目、引用自己的

适配里有一条硬性要求:Qt for Harmony SDK 必须放进当前项目目录、引用项目内自己的 SDK,而不是引用别的工程的 SDK。所以把整套 234MB 的 Qt 5.15 for Harmony SDK 复制到 harmony_pc/qtforharmony_sdk/entry/libs/arm64-v8a/ 里放上配套的 QPA 插件(libplugins_platforms_qopenharmony.so)、TLS(libssl/libcrypto)和多媒体后端 .soCMakeLists.txtcpp 目录反向定位到 harmony_pc,再强制使用项目内置 SDK:

# .../harmony_pc/entry/src/main/cpp -> .../harmony_pc
get_filename_component(HARMONY_PROJECT_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../../.." ABSOLUTE)
get_filename_component(TUXPAINT_ROOT "${HARMONY_PROJECT_ROOT}/.." ABSOLUTE)

set(QT_PREFIX "qtforharmony_sdk" CACHE PATH "Qt for HarmonyOS SDK path")
if (NOT IS_ABSOLUTE "${QT_PREFIX}")
    get_filename_component(QT_PREFIX "${HARMONY_PROJECT_ROOT}/${QT_PREFIX}" ABSOLUTE)
endif()
if (NOT EXISTS "${QT_PREFIX}/lib/cmake/Qt5/Qt5Config.cmake")
    message(FATAL_ERROR "QT_PREFIX must point to this project's bundled Qt 5 SDK")
endif()

entry/build-profile.json5 默认就把这个相对路径传进去:

"arguments": "-DQT_PREFIX=qtforharmony_sdk -DTUXPAINT_FULL_APP=AUTO"

六、两种构建模式:SHELL 先跑通,FULL 留路线

和 TupiTube / Krita 一样,CMakeLists.txtTUXPAINT_FULL_APP(AUTO / ON / OFF)分两种模式:

  • SHELL(默认 AUTO/OFF):只编一个自包含的 tuxpaint_ohos_shell.cpp,不依赖 src/、不依赖 SDL2,必定能编、能起、能画,把鸿蒙渲染管线打通。链接 Qt5::Core/Gui/Widgets/Svg/Network——其中 Qt5::Svg 必须链上,否则 hvigor 收集的 qsvg 图片插件加载 libentry.so 时会失败、白屏。
  • FULL-DTUXPAINT_FULL_APP=ON):编译真正的 Tux Paint。当前不可构建,CMake 会直接以清晰的 FATAL_ERROR 停下,并列出需要先做的事:把 SDL2 全家桶 + Pango/Cairo/FriBidi/HarfBuzz/FreeType/librsvg/libpng… 交叉编译到 OHOS arm64,再把 src/*.c 编进 libentry.so、用 OPENHARMONY 守卫掉 X11/win32/macos/android 平台文件、把 data/ fonts/ stamps/… 打进 HAP。

为什么默认是 SHELL、FULL 直接报错而不是"自动回退"?因为 Tux Paint 的源码都在仓库里、永远存在,没法用"源码在不在"来判断;而它又不是 Qt 程序,FULL 在没有 SDL2 鸿蒙库之前根本无从编起。所以让 SHELL 当默认交付物,FULL 给出明确的、可执行的路线说明。

七、忠实复刻 Tux Paint 的界面

SHELL 不是随便画个窗口,而是照着 Tux Paint 的经典五区布局来搭,数据全部取自上面那两个头文件:

+--------+---------------------------+----------+
| 工具栏 |        黄色标题栏          | 选择器   |
| 16 个  +---------------------------+ (随工具  |
| 按钮   |                           |  变化)   |
| 两列   |        白色画布            |          |
|        |     (天蓝色衬底)          |          |
+--------+---------------------------+----------+
|              底部 17 色调色板 + 彩虹取色器       |
+-------------------------------------------------+
| 🐧 Tux |  提示语(随工具/颜色变化)              |
+-------------------------------------------------+
  • 左侧工具栏:16 个按钮,名称和顺序逐字来自 tools.h
  • 画布:白色 QImage,画在天蓝色的"画纸衬垫"上,和原版一样;
  • 右侧选择器:随当前工具切换内容——选 Paint 显示笔刷、选 Shapes 显示形状列表、选 Magic 显示效果列表……外加一个画笔粗细滑块;
  • 底部调色板:17 个色块用 colors.h 的精确 RGB 渲染,外加一个彩虹"取色"按钮(点开 QColorDialog 任选颜色);
  • 底部 Tux 提示栏:黄色背景 + 企鹅,显示当前工具的提示语(也来自 tools.htool_tips[])。

整个外观用一段 Fusion 风格 + 浅灰/亮黄的 QSS 统一,尽量贴近 Tux Paint 明快、友好的儿童风。

在这里插入图片描述

八、从"假按钮"到"真能用":让每个工具真正生效

这是这次最关键、也最容易被忽略的一步。

第一版 SHELL 我犯了个典型错误:它只是个"门面"。点左边的工具按钮,只做了三件表面的事——改标题文字、改底部提示语、换右侧选择器列表。真正的绘图逻辑一个都没接:除了画笔能画、New 能清空,其它 Stamp / Lines / Shapes / Text / Fill / Magic / Eraser 点了之后画布行为不变,Undo / Redo / Open / Save / Print / Quit 全是空操作。一上手就会发现"左边这些功能点了没用"。

于是我把绘图核心整个重写成**工具感知(tool-aware)**的:画布持有"当前工具 + 变体 + 颜色 + 笔宽 + 撤销/重做栈",鼠标事件按当前工具分发:

void Canvas::mousePressEvent(QMouseEvent *e) {
    QPoint p = mapToImage(e->pos());
    switch (tool_) {
    case TOOL_PAINT:
    case TOOL_ERASER: pushUndo(); dragging_ = true; last_ = p; drawDab(p); break;
    case TOOL_LINES:
    case TOOL_SHAPES: pushUndo(); dragging_ = true; start_ = cur_ = p; update(); break;
    case TOOL_FILL:   pushUndo(); floodFill(p, penColor_); update(); break;
    case TOOL_TEXT:
    case TOOL_LABEL:  placeText(p); break;          // 弹输入框,把文字画上去
    case TOOL_STAMP:  pushUndo(); placeStamp(p); update(); break;   // 盖一个图案
    case TOOL_MAGIC:  pushUndo(); applyMagic(); update(); break;    // 整图特效
    }
}

逐个工具落地后的真实行为:

  • Paint / Eraser:自由涂画,橡皮就是用白色画;
  • Lines:按下定起点,拖动实时橡皮筋预览,松手提交直线;
  • Shapes:同样橡皮筋预览,支持方形/矩形/圆/椭圆/三角/五边形/菱形/八边形/星形(用正多边形与星形的参数方程画出来);
  • Fill:真正的漫水填充——基于扫描线的 4-邻接 flood fill,从点击点按相同颜色的连通区域铺色;
  • Text / Label:点击弹 QInputDialog 输入文字,按当前颜色/字号绘制到画布;
  • Stamp:在点击处盖一个 emoji 图案(★♥✿☀☽☺🐱🌳🚗,右侧选择器切换);
  • Magic:整图特效,按右侧变体应用——反相 invertPixels()、着色、淡化、模糊(box blur)、变亮/变暗、灰度、水平镜像、垂直翻转;
  • Undo / Redo:真实的双栈撤销/重做(每次改动前 pushUndo(),最多保留 24 步);
  • New / Open / Save / Print / Quit:New 确认后清空;Open/Save 用 Qt 自带(非原生)文件对话框读写 PNG;Print 提示预览版暂不支持;Quit 确认后退出。

还有两个交互细节:动作类按钮(Undo/Save/Quit…)执行完会自动把选中态还原到你之前的绘图工具,不会卡在"Save 被按下"的状态;右侧选择器选中行通过 currentRowChanged 回写到画布的"变体",所以换形状、换魔法效果立即生效。

在这里插入图片描述

九、本机编译验证:在没有鸿蒙链的情况下提前抓错

鸿蒙的交叉编译要在 DevEco Studio 里跑,链路长、反馈慢。好在这份 SHELL 是标准 Qt 5 Widgets 代码,可以先在 Mac 本机用 Homebrew 的 Qt 5(/opt/homebrew/opt/qt@5)做一次"只编译不链接"的语法检查,提前把 API 错误抓出来:

QT5=/opt/homebrew/opt/qt@5
clang++ -std=c++17 -fsyntax-only -DOPENHARMONY \
  -I"$QT5/include" -I"$QT5/include/QtCore" \
  -I"$QT5/include/QtGui" -I"$QT5/include/QtWidgets" \
  tuxpaint_ohos_shell.cpp

第一次就抓到一个真实错误——画形状描边时 QPen 的参数顺序写错了:

// 错误:第 4 个参数应是"线帽样式"(PenCapStyle),却传了"连接样式"(PenJoinStyle)
QPen(penColor_, w, Qt::SolidLine, Qt::RoundJoin)
// 修正:补全 RoundCap, RoundJoin
QPen(penColor_, w, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin)

为了让代码在鸿蒙 LLVM/musl 工具链上更稳,还顺手做了两处加固:补 #include <functional>(用了 std::function 当提示回调)、给非标准宏 M_PI#ifndef 兜底。改完后 -fsyntax-only 退出码 0,干净通过,连 OPENHARMONY 分支的 qtmain 一起覆盖。这一步省掉了好几轮"打包→装机→白屏→猜"的来回。

之后回到 DevEco Studio 重新 build entry,CMake 会重新编出 libentry.so,重新安装即可。

十、构建与运行

桌面版(不受影响):照常 make 即可,本次适配没有动 Tux Paint 的任何原始源码与 Makefile

鸿蒙 PC,命令行:

cd harmony_pc
export DEVECO_SDK_HOME=<command-line-tools>/sdk
<command-line-tools>/bin/ohpm install
<command-line-tools>/bin/hvigorw --mode module -p module=entry@default \
  -p product=default assembleHap --no-daemon

产物(未签名):

harmony_pc/entry/build/default/outputs/default/entry-default-unsigned.hap

DevEco Studio:把 harmony_pc/ 作为工程根目录打开,配置好自己的签名,Build entry HAP,再装到鸿蒙 PC / 模拟器上运行。

在这里插入图片描述

十一、适配成果与边界

到目前为止,Tux Paint 在鸿蒙 PC 上完成了一个界面忠实、工具可交互的版本:

  1. 可作为 HAP 安装到鸿蒙 PC,通过 Stage UIAbility 启动,走 ArkTS → XComponent → Qt QPA → libentry.so 的标准链路。
  2. 界面忠实复刻 Tux Paint 经典布局:16 个工具、17 个默认色、Tux 提示栏,名称/提示/颜色全部逐字取自 tools.h / colors.h
  3. 左侧工具真实可用:画笔/橡皮、直线、形状(9 种)、文字、填充(真·漫水填充)、图章、魔法(9 种整图特效)、撤销/重做、新建/打开/保存/退出,全部接了真实逻辑。
  4. 项目内置自己的 qtforharmony_sdk,DevEco Studio 导入后可直接构建;签名留空交给开发者自己配置。
  5. 提供本机 Qt5 语法校验流程,已抓修 QPen 参数错误等问题,并对 std::function/M_PI 做了可移植性加固。

在这里插入图片描述

同时也如实说明当前的边界(后续可继续推进):

  1. 这是基于 Qt Widgets 的 SHELL 复刻,跑的不是 Tux Paint 原版的 SDL2 代码——它证明了链路、复刻了界面、做活了工具,但底层不是原引擎。
  2. 真正的 FULL(SDL2 全量移植)尚未可构建:需要先把 SDL2 + SDL2_image/ttf/mixer/gfx/Pango、Pango、Cairo、FriBidi、HarfBuzz、FreeType、librsvg、libpng/jpeg/tiff/webp、zlib 交叉编译到 OHOS arm64,再把 src/*.c 编进 libentry.so 并把素材打进 HAP。CMakeLists.txt 的 FULL 分支与 HARMONYOS_PORT.md 已写清这条路线。
  3. 印章目前用 emoji 占位、魔法是一组通用图像特效,尚未接 Tux Paint 原版的真实印章素材与 magic 插件——这些都可以在后续替换为真实资源。

这次迁移最大的经验是:适配框架要跟着生态走,而不是跟着原项目的技术栈走。 Tux Paint 原生是 SDL2,但本地鸿蒙PC的成熟通路是 Qt;与其孤军深入去趟 SDL2-on-OHOS,不如先用同一套 Qt SHELL 拿出能装、能跑、能交互、且长得就是 Tux Paint 的版本,把链路和体验先立住,再把"换上真·SDL2 引擎"作为有清晰路线图的后续工作。而 SHELL 也不能只做"门面"——工具点了要真的能用,这才是这个版本从"像 Tux Paint"走到"是个能用的画图工具"的关键。


Logo

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

更多推荐