一、写在前面

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

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

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

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

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

TupiTube Desk 不是 Electron 项目,也不是一个普通 ArkUI 应用。它原本是一个 C++ / Qt Widgets 的 2D 矢量/位图动画创作工作室(前身是 KTooN),有画布、绘图工具、图层、时间轴(Exposure Sheet)、洋葱皮、补间动画、笔刷、Papagayo 对口型、工程导入导出等一整套桌面动画软件的能力。它的代码体量很大——约 370 个 .cpp、17 万行,被拆成了一组库(framework 的 core/gui、libbase/libcolor/libtupi/libui/store)、十几个 components 组件、19 个运行时动态加载的工具插件,还内置了一套 mypaint 笔刷引擎(C 语言的 libmypaint + json-c)。

和 Web 套壳、甚至和一般的单库 Qt 程序都不同,这次适配真正难的地方在于:

  1. 怎样让一个 Qt Widgets 桌面应用进入鸿蒙 Stage 模型,并由 libentry.so 启动(和 Minitube 一脉相承的 QPA 思路)。
  2. 怎样把这套多库 + 插件 + mypaint + 工程压缩的庞大工程,全部编进一个 libentry.so——桌面版靠"分成多个 .so + 运行时加载插件"避开的问题,单体编译时全部会爆出来。
  3. 第一次能跑起来、能进主界面、能弹"新建工程"对话框,但一点"创建工程"就闪退——怎样从 hilog 的崩溃栈定位到根因。
  4. 桌面版的绘图工具是运行时从插件目录扫描 .so 动态加载的,鸿蒙沙箱不便这么做,怎样改成静态编入 + Q_IMPORT_PLUGIN,并解决 19 个插件里的同名类、moc 符号冲突。
  5. 进了工作区,按钮却是空白的、工具是纯文字没有图标——怎样把 20MB 的主题/数据打进 HAP,并在鸿蒙沙箱里把 TUPITUBE_SHARE 指对位置。
  6. 工程文件 .tup 是 zip 包,依赖 QuaZip,而鸿蒙没有现成的 QuaZip——怎样 vendor 源码、链系统 zlib。

本次适配采用逐步验证的路线:保留 TupiTube 原有 C++ 主体,新建 harmony_pc/ 作为鸿蒙工程壳;ArkTS 侧只负责 Ability、窗口和 XComponent;真正的 UI 和逻辑仍由 Qt 运行时承载;鸿蒙特有改动全部用 Q_OS_OPENHARMONY / OPENHARMONY 收敛,桌面构建完全不受影响

在这里插入图片描述

二、项目背景:TupiTube 是"多库 + 插件 + mypaint"的大型 Qt 工程

确认它是 Qt 项目很简单:根目录有 tupitube.desk.prosrc/shell/main.cpp 里用的是 QApplication,各子模块 .pro 写着 QT += opengl core gui svg xml network widgets multimediaconfigure.rb 要求 Qt 5.15.0 以上。

原始项目结构(节选)大致如下:

tupitube.desk-ohos/
├── tupitube.desk.pro          # 顶层 qmake 工程(TEMPLATE = subdirs)
├── configure / configure.rb   # 类 ./configure 的 Ruby 配置脚本
├── src/
│   ├── shell/                 # 主程序入口:main.cpp / tupmainwindow ...
│   ├── framework/             # tupifwcore + tupifwgui(核心框架库)
│   ├── libbase/ libcolor/ libtupi/ libui/ store/   # 各 tupi* 库
│   ├── components/            # workspace / timeline / player / library / papagayo ...
│   ├── plugins/               # tools(19个工具) / filters / export / import 插件
│   └── mypaint/               # libmypaint(C) + json-c(C) + qtmypaint + raster
├── themes/                    # default / dark 主题(图标、光标、配置)
└── harmony_pc/                # 本次新增的鸿蒙工程壳

鸿蒙适配工程集中放在:

tupitube.desk-ohos/harmony_pc/
├── AppScope/app.json5                  # 包名 com.tupitube.desk
├── build-profile.json5
├── entry/
│   ├── build-profile.json5             # 传 -DTUPITUBE_FULL_APP=ON 等
│   ├── libs/arm64-v8a/                  # QPA 插件 + 媒体插件 + OpenSSL
│   └── src/main/
│       ├── cpp/CMakeLists.txt          # 把整个 TupiTube 编成 libentry.so
│       ├── cpp/tupitube_static_plugins.cpp   # Q_IMPORT_PLUGIN 静态注册插件
│       ├── ets/                         # AbilityStage / EntryAbility / Index
│       ├── module.json5
│       └── resources/resfile/share/     # 打包进 HAP 的 themes + data(20MB)
├── third_party/quazip/                 # vendor 的 QuaZip 源码(工程读写需要)
├── hvigor/  oh-package.json5
└── qtforharmony_sdk/                   # 项目自带的 Qt 5.15.12 for Harmony SDK

这次没有把 TupiTube 重写成 ArkUI,而是让 Qt Widgets 继续负责界面,鸿蒙工程壳负责承载和启动。

在这里插入图片描述

三、鸿蒙工程壳: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,关键字段 launchApplication = 'libentry.so' 告诉 QPA 插件去 dlopen 哪个 .sosrc/shell/main.cpp 里加鸿蒙入口别名——QPA 插件实际调用的是 qtmain

#if defined(Q_OS_OPENHARMONY)
extern "C" int qtmain(int argc, char *argv[]) {
    return main(argc, argv);   // 转发到 TupiTube 原本的 main()
}
#endif

应用身份用 TupiTube 自己的包名 com.tupitube.desk,图标用 TupiTube 自己的 logo(从 launcher/icons/tupitube.desk.svg 渲染成鸿蒙自适应图标),签名留空交给 DevEco Studio 生成——不要把参考模板项目(如 Minitube)的 logo、包名、签名复制过来

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

适配里有一条硬性要求:Qt for Harmony SDK 必须放进当前项目目录、引用项目内自己的 SDK,而不是引用别的工程的 SDK。所以把整套 234MB 的 Qt 5.15.12 for Harmony SDK 复制到 harmony_pc/qtforharmony_sdk/CMakeLists.txtharmony_pc 反向定位项目根,并强制使用项目内置 SDK:

get_filename_component(HARMONY_PROJECT_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../../.." ABSOLUTE)
get_filename_component(TUPITUBE_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()

这个 SDK 是 Qt 5.15,后面有几个坑也和"库里写法 vs 运行环境"有关。

五、先验证链路:SHELL 壳(保证能编、能起、能画)

TupiTube 的源码量太大,直接上全量风险高。所以 CMakeLists.txt 里做了两种构建模式(TUPITUBE_FULL_APP):

  • SHELL(默认/AUTO):只编一个自包含的 tupitube_ohos_shell.cpp——一个纯 Qt Widgets 的动画工作区"仿真壳"(深色主题、工具栏、可真实绘画的画布、调色条、时间轴),不依赖 src/ 和任何 tupi 库,保证能编译、能起、能画,先把"ArkTS → XComponent → QPA → libentry.so → 可见 Qt 窗口"这条链路打通。
  • FULL-DTUPITUBE_FULL_APP=ON):编译真正的 TupiTube 全量应用。

为什么默认是 SHELL?参考的 Minitube 工程里 lib/* 是空的 git 子模块,AUTO 可以靠"源码在不在"自动回退;而 TupiTube 所有源码都在仓库里、永远存在,没法用这个信号判断,所以 AUTO 默认走 SHELL,要全量时显式打开。

SHELL 跑通后,鸿蒙真机上就能看到一个带画布、能拖动画线的 TupiTube 壳,证明整条渲染管线 OK。这一步和 Minitube 一样,主要在对付"运行时动态库链":链接 Qt5::Svg(否则 qsvg 图片插件加载失败白屏)、把 QPA 插件 / 媒体插件 / libssl/libcrypto 放进 entry/libs/arm64-v8a/

六、FULL 全量编译:把 17 万行编成一个 libentry.so

这是最硬的一关。把 framework + 各 tupi 库 + components + mypaint + shell 全部塞进一个 libentry.so,桌面版用"分库 + 运行时插件"绕开的问题全冒出来了。好在第一次全量编译就到了 312 个目标里只剩个位数错误,且高度集中。关键修法:

1)framework 只编 core+gui。 shell 桌面版链接的是 -ltupifwcore -ltupifwguiframework.proSUBDIRS 也只有 core gui——所以 soundplugins 子树不编(taudioplayer 等就是这么排除的)。

2)排除"死文件"。 我一开始用 GLOB_RECURSE 把目录里所有 .cpp 都卷进来,结果把桌面版根本不编的文件也带上了:tuptoolsdialog.cpp(里面有 insetToolTip 这种拼写错误,正因为它不参与编译才一直没被发现)、tupcanvas.pc.cpp/tupcanvas.tablet.cpp 平台变体、各组件的独立 main.cpp、被禁用的 kinas/scenes 目录等。正确做法是对齐各 .pro 的真实 SOURCES

3)mypaint 的 unity 编译。 链接时报了一片 duplicate symbol: mapping_*。排查发现 libmypaint.c 是个 unity 文件,#includemapping.c/brushmodes.c 等所有其它 .c;而 qtmypaint/mphandler.cpp 里又有一句 #include "libmypaint.c",把整个引擎都嵌进了自己。桌面版靠"libmypaint 和 qtmypaint 是两个独立 .so"避开冲突,单体编译则撞车。解法:libmypaint 的 .c 一个都不单独编,全靠 mphandler.cpp 那次 include 提供

4)一个真实的源码 bug。 报了一串 unknown type name 'TupPaintAreaStatus'。根因是 src/libtupi/tupcolorwidget.h 复制粘贴时用错了 include guard——写成了 #ifndef TUPPAINTAREASTATUS_H,和真正的 tuppaintareastatus.h 撞了;同一个编译单元里先 include 谁,另一个的类体就被整个跳过。桌面版分库编译、include 顺序不同所以没暴露,单体编译触发了它。改成正确的 TUPCOLORWIDGET_H 即可。

5)Qt 版本/重载差异。 tupsplash.cppQSplashScreen(0) 在 Qt 5.15 下构造歧义,改 QSplashScreen(static_cast<QWidget *>(nullptr));定义里去掉了会引发 QStringBuilder→QVariant 隐式转换报错的 QT_USE_QSTRINGBUILDER

6)音视频依赖先禁用。 视频导入需要 FFmpeg、音频/对口型波形需要 libsndfile,鸿蒙都没有现成 arm64 库。用一个 TUP_NO_AVMEDIA 开关把这些先关掉:把 TupAudioExtractor 桩化(无 sndfile 时 isValid() 恒为 false,papagayo 里十几处调用都已用 if (isValid()) 保护,无需逐个改),并在 tuplibrarywidget.cpp 里把"导入声音/视频"的代码段守卫起来,排除掉 tupaudiocutter/tupvideocutter/tupvideoimporterdialog/tupsounddialog。核心动画功能不依赖这些。

7)QuaZip vendor 进来。 tuppackagehandler.cpp 用 QuaZip 读写 .tup 工程包(zip 格式),这是必需的、不能砍。鸿蒙没有现成 QuaZip,但 zlib 在 OHOS sysroot 里现成可用。于是把 QuaZip 1.4 源码放进 harmony_pc/third_party/quazip,编进 libentry.so,定义 -DQUAZIP_STATIC,链接 sysroot 的 libz-lz)。

改完之后,整个 TupiTube 全量编译并链接成功libentry.so 约 112MB(debug,strip 进 HAP 后约 6.8MB)。

在这里插入图片描述

七、第一次运行:进了主界面,一点"创建工程"就闪退

装上全量 HAP,真·TupiTube 起来了:先弹出原版的"Help Us Improve TupiTube"同意对话框,关掉后是完整的主窗口——File / Edit / Import / Export / Post / Modules / Help 菜单、New Project / Open Project / Save… 工具栏、左右面板、底部时间轴。点"New Project"还能弹出完整的"Create New Project"对话框(工程名、作者、预设 1080 Full HD、尺寸、FPS)。

在这里插入图片描述

八、插件移植:静态编入 + Q_IMPORT_PLUGIN + 防御式加固

桌面版每个工具(如 PencilTool)是带 Q_PLUGIN_METADATA 的类、编成独立 .so,由 TupPluginManager 运行时 QPluginLoader 加载。鸿蒙沙箱不便这么做,改成静态编入

1)让插件管理器支持静态插件。 TupPluginManager::loadPlugins() 增加一段,遍历 QPluginLoader::staticInstances(),和动态加载一样按 qobject_cast<TupToolInterface*> 分类。

2)每个插件做成独立 OBJECT 库。 这里有个坑:每个插件的 moc 都会生成一个文件内静态qt_pluginMetaData[];如果把所有插件放进同一个编译目标,CMake 的 AUTOMOC 会把它们的 moc 合并进同一个 mocs_compilation.cpp,于是 qt_pluginMetaData 重定义。解法是每个插件(和共享的 common)各做成一个 OBJECT,各自独立 AUTOMOC,再把对象链进 entry

set(TUPITUBE_PLUGIN_DIRS
    tools/common
    tools/pencil tools/ink tools/selection tools/nodes tools/text
    tools/geometric tools/bucket tools/polyline tools/papagayo tools/eyedropper
    export/imageplugin)
foreach(_unit ${TUPITUBE_PLUGIN_DIRS})
    file(GLOB _u_src "${SRC}/plugins/${_unit}/*.cpp")
    string(REPLACE "/" "_" _tname "${_unit}")
    add_library(tupiplugin_${_tname} OBJECT ${_u_src})
    set_target_properties(tupiplugin_${_tname} PROPERTIES AUTOMOC ON POSITION_INDEPENDENT_CODE ON)
    target_compile_definitions(tupiplugin_${_tname} PRIVATE QT_STATICPLUGIN TUP_NO_AVMEDIA OPENHARMONY)
    # ... include dirs / link Qt
    list(APPEND PLUGIN_OBJECTS $<TARGET_OBJECTS:tupiplugin_${_tname}>)
endforeach()

配合定义 QT_STATICPLUGIN(让 moc 生成静态注册符号)和一个 tupitube_static_plugins.cpp

#include <QtPlugin>
Q_IMPORT_PLUGIN(PencilTool)
Q_IMPORT_PLUGIN(InkTool)
// ... selection / nodes / text / geometric / bucket / polyline / papagayo / eyedropper
Q_IMPORT_PLUGIN(ImagePlugin)   // 导出:PNG 序列 + SVG

3)筛掉过时插件、避开同名冲突。 19 个工具里:eraser/scheme/shift 用的是旧接口keys() 返回 QStringList 而非 QList<TAction::ActionId>),其实不在 tools.pro 里,桌面版也不编,排除;6 个补间工具(color/motion/opacity/rotation/scale/shear)的类都叫 Tweener,静态链接会符号冲突,先缓做。导出插件里 image 无外部依赖(纯 Qt 的 QImage/QSvgGenerator)留用,ffmpeg/theora 需外部库、apng/smile 是旧接口,均缓做。

4)防御式加固工作区。 即使个别插件缺失也不该崩——把 TupDocumentViewpencilAction 等成员指针初始化为 nullptr,并给所有 addActionif (ptr) 守卫。这样缺工具时是"优雅降级",不是段错误。

改完后 libentry.so 链接成功(约 133MB),装上去创建工程不再崩溃,进入了完整工作区:左侧出现铅笔、墨水、形状、选择、节点、补间、Papagayo 等工具,中间是画布,右侧 Exposure Sheet,底部时间轴。选铅笔,在画布上真的画出了线(即第一节那张效果图)。

在这里插入图片描述

九、空白按钮与纯文字工具:把主题/数据打进 HAP

进了工作区后还有个明显问题:很多对话框按钮是空白的,工具是纯文字没图标。根因是这些按钮/工具用的是主题目录里的图标,例如对话框基类:

okButton->setIcon(QIcon(THEME_DIR + "icons/apply.png"));     // 只有图标、没有文字
cancelButton->setIcon(QIcon(THEME_DIR + "icons/close.png"));

THEME_DIR 来自 shareDir,桌面版从环境变量 TUPITUBE_SHARE 取——鸿蒙上这些环境变量是空的,图标自然加载不到,按钮就成了空白。

解法两步:

1)把数据打进 HAP。themes/(default/dark 的图标、光标、配置)和 src/shell/data/data/xml 工具配置、翻译等)复制进 harmony_pc/entry/src/main/resources/resfile/share/(约 20MB)。鸿蒙的 resfile 资源安装后可作为真实文件访问。

2)在 main.cpp 里给鸿蒙单独解析路径。 环境变量为空,就去候选挂载路径里找打包好的数据,并把工程目录/缓存指到可写沙箱:

#elif defined(Q_OS_OPENHARMONY)
    QString ohShare;
    const QStringList shareCandidates = {
        QString::fromLocal8Bit(::getenv("TUPITUBE_SHARE")),
        "/data/storage/el1/bundle/entry/resources/resfile/share/",
        "/data/storage/el1/bundle/entry/resources/rawfile/share/"
    };
    for (const QString &c : shareCandidates)
        if (!c.isEmpty() && QFile::exists(c + "themes/default/icons/apply.png")) { ohShare = c; break; }

    QString ohHome = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
    QDir().mkpath(ohHome);
    kAppProp->setHomeDir(ohHome);
    kAppProp->setPluginDir(QString());   // 插件是静态编入的,pluginDir 留空
    kAppProp->setShareDir(ohShare);
#endif

真机日志确认命中了 resfile 路径:

OHOS shareDir resolved to "/data/storage/el1/bundle/entry/resources/resfile/share/"

装上后,对话框的 ✓/✖ 按钮显示出来了,顶部工具栏、左侧工具栏全变成了真实图标(即第八节那张工作区图的图标就是这么来的)。

十、导入与导出

把数据和插件都接好后,导入导出也顺势可用了:

  • 导入Import 菜单完整(Image / Image Sequence / SVG / SVG Sequence / Audio / Video / Library / GIMP 调色板 / Papagayo),都是内置功能(不依赖缓做的插件)。点 Open Project 会唤起鸿蒙原生文件选择器,并按 TupiTube Project Package (*.tup) 过滤——QuaZip 已经链进来,.tup 工程可读写。
  • 导出:导出向导(components/export)已编入;格式写出器里启用了无依赖的 imageplugin,可导出图片序列(PNG)+ SVG。视频导出(FFmpeg)、Theora 因需外部库继续缓做。

在这里插入图片描述

十一、这次适配的阶段性结果与边界

到目前为止,TupiTube Desk 在鸿蒙 PC 上已经完成一个核心可用的版本:

  1. 可以作为 HAP 安装到鸿蒙 PC,通过 Stage UIAbility 启动 Qt 应用。
  2. 跑的是真·原版 TupiTube 源码和界面(约 17 万行全量编进一个 libentry.so),不是仿真壳。
  3. 项目内置自己的 qtforharmony_sdk,DevEco Studio 导入后可直接构建。
  4. 核心绘画流程跑通:新建工程 → 进入工作区 → 选工具 → 在画布上绘制;工具栏图标、对话框按钮图标都正常显示。
  5. 把 19 个工具插件改成静态编入Q_IMPORT_PLUGIN + 每插件独立 OBJECT 库 + staticInstances()),并对工作区做了防御式加固。
  6. 修复了 framework 子树取舍、死文件、mypaint unity 重复符号、tupcolorwidget.h 的 include guard 真实 bug、QSplashScreen 歧义等一系列全量编译问题。
  7. vendor 了 QuaZip、链系统 zlib,.tup 工程可读写;导入走鸿蒙原生文件选择器;导出支持 PNG 序列 + SVG。
  8. 把 20MB 主题/数据打进 HAP,并在鸿蒙沙箱里把 TUPITUBE_SHARE/HOME 指对位置。

同时也明确目前的边界(后续可继续推进):

  1. 音视频媒体能力(视频导入需 FFmpeg、音频/对口型波形需 libsndfile)当前用 TUP_NO_AVMEDIA 关闭,需接入 HarmonyOS arm64 的 FFmpeg/libsndfile 后恢复。
  2. 6 个补间动画工具(同名 Tweener 类)静态链接会冲突,需重命名类后再启用;FFmpeg/Theora 导出同理待接入。
  3. 工具图标、按钮已正常;少数依赖外部库的高级特性(视频剪辑、声音导入)暂禁用。

这次迁移最大的经验是:大型 Qt 工程适配鸿蒙 PC,"能编译"只是开始。 真正耗时间的是——把"多库 + 运行时插件 + 内嵌引擎 + 第三方依赖"的桌面工程压成一个 libentry.so 时暴露的各种链接/符号/include 问题;运行时的崩溃要靠 hilog 的崩溃栈精确定位(这次的段错误就是插件为空导致 addAction(nullptr));以及"界面对了但功能空/按钮空"时,回到资源数据(主题、配置)有没有打包、路径在沙箱里指没指对这一层去解决。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐