鸿蒙PC迁移:Tux Paint 儿童绘画程序鸿蒙PC适配全记录
一、写在前面
欢迎加入鸿蒙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 跑在鸿蒙上"的现成先例。
所以这次适配真正要回答的问题是:
- 一个 SDL2 的应用,到底要不要、能不能套用这套 Qt 的鸿蒙适配框架?
- 在没有把 SDL2 全家桶交叉编译到鸿蒙之前,怎样先拿出一个能装、能跑、能交互的版本?
- 怎样让这个版本忠实于 Tux Paint 的经典样子,而不是随便画个界面?
- 怎样让左边那一排工具真的能用(画笔、形状、填充、魔法、撤销、保存……),而不是点了没反应的"假按钮"?
- 在拿不到鸿蒙真机交叉编译链的本机上,怎样提前验证这份 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.h里TOOL_*枚举顺序就是左侧工具栏的顺序:Paint、Stamp、Lines、Shapes、Text、Label、Fill、Magic、Undo、Redo、Eraser、New、Open、Save、Print、Quit,外加每个工具的tool_names[]显示名和tool_tips[]提示语;src/colors.h里default_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.ets 在 onCreate 里 qpa.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.36,module.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)和多媒体后端 .so。CMakeLists.txt 从 cpp 目录反向定位到 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.txt 用 TUXPAINT_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.h的tool_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 上完成了一个界面忠实、工具可交互的版本:
- 可作为 HAP 安装到鸿蒙 PC,通过 Stage
UIAbility启动,走 ArkTS → XComponent → Qt QPA →libentry.so的标准链路。 - 界面忠实复刻 Tux Paint 经典布局:16 个工具、17 个默认色、Tux 提示栏,名称/提示/颜色全部逐字取自
tools.h/colors.h。 - 左侧工具真实可用:画笔/橡皮、直线、形状(9 种)、文字、填充(真·漫水填充)、图章、魔法(9 种整图特效)、撤销/重做、新建/打开/保存/退出,全部接了真实逻辑。
- 项目内置自己的
qtforharmony_sdk,DevEco Studio 导入后可直接构建;签名留空交给开发者自己配置。 - 提供本机 Qt5 语法校验流程,已抓修
QPen参数错误等问题,并对std::function/M_PI做了可移植性加固。

同时也如实说明当前的边界(后续可继续推进):
- 这是基于 Qt Widgets 的 SHELL 复刻,跑的不是 Tux Paint 原版的 SDL2 代码——它证明了链路、复刻了界面、做活了工具,但底层不是原引擎。
- 真正的 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已写清这条路线。 - 印章目前用 emoji 占位、魔法是一组通用图像特效,尚未接 Tux Paint 原版的真实印章素材与 magic 插件——这些都可以在后续替换为真实资源。
这次迁移最大的经验是:适配框架要跟着生态走,而不是跟着原项目的技术栈走。 Tux Paint 原生是 SDL2,但本地鸿蒙PC的成熟通路是 Qt;与其孤军深入去趟 SDL2-on-OHOS,不如先用同一套 Qt SHELL 拿出能装、能跑、能交互、且长得就是 Tux Paint 的版本,把链路和体验先立住,再把"换上真·SDL2 引擎"作为有清晰路线图的后续工作。而 SHELL 也不能只做"门面"——工具点了要真的能用,这才是这个版本从"像 Tux Paint"走到"是个能用的画图工具"的关键。
更多推荐




所有评论(0)