【鸿蒙PC-Qt实战案例】从零自研一个 Qt 应用并交叉编译到鸿蒙 PC:IronLog 健身记录实战

【鸿蒙PC-Qt实战案例】从零自研一个 Qt 应用并交叉编译到鸿蒙 PC:IronLog 健身记录实战

欢迎加入开源鸿蒙 PC 社区:https://harmonypc.csdn.net/
新手用户重点学习一下本文章的前序流程部分,有详细的手把手教学。

项目信息

项目 内容
应用名称 IronLog(健身训练记录工具)
应用类型 自研桌面应用(非开源软件移植)
目标平台 HarmonyOS NEXT 鸿蒙 PC(2in1 / 平板)
技术栈 Qt 5.12.12 for HarmonyOS · CMake · Ninja · ArkTS(HAP工程)
代码规模 自研 C++ 约 2,800 行(4 个页面 + 自绘热力图 / 折线图 + 暗黑 QSS)
业务库 libIronLog.so 349 KB(ARM aarch64,导出 T main,4KB 页对齐)
运行依赖 Qt5 Core / Gui / Widgets / Network · libqohos.so(QPA 平台插件)· libc++_shared
HAP 体积 432 MB(19 个 .so,未 strip)
外部 C 库 零依赖(不需要交叉编译任何三方库)
开发周期 服务器交叉编译 + Mac 本地开发 + DevEco 真机部署,首次跑通 ≈ 1 天,UI 打磨 5 轮
文章定位 "从零自研一个 Qt 桌面应用 → 跑到鸿蒙 PC 真机"的完整可复现实战记录

本项目开源仓库:https://atomgit.com/weixin_52908342/OH-IronLog

应用功能

IronLog 是一个面向力量训练爱好者的桌面端训练记录工具,参考 Hevy / Strong 的桌面形态:

  • 📊 仪表盘:本周训练统计 + 12 周训练热力图 + 卧推 PR 进步曲线 + 今日计划 + 最近训练记录
  • 💪 开始训练:按计划记录每个动作的组数 / 重量 / 次数 + 计时器
  • 📈 进步曲线:单动作 1RM 估算曲线 + 容量趋势 + PR 时间线
  • 🏋️ 动作库:卧推 / 深蹲 / 硬拉 / 引体 / 肩推 等 12 个标准动作的卡片化展示

整套 UI 采用暗黑健身房风 (深邃 #0E0E13 底 + 渐变橙红 #FF6B5A → #FF8E5A 强调色),所有图表(热力图、折线图、PR 星标)由 QPainter 自绘,零外部图表库依赖。

这篇文章会回答的问题

  • 自研 Qt 应用如何交叉编译到鸿蒙 PC?整条链路有哪些必经步骤?
  • 自研项目和移植开源软件相比,工时和坑点差异在哪?
  • Qt-OHOS 工具链上有哪些仓库前序文档没记录过的专属 bug(QSS 不生效、font-size px 失效、qt_resourceFeatureZlib 缺失等)?
  • 真机跑通后的 UI 调优经验:鸿蒙 PC 高 DPI 下字号怎么调才"刚刚好"?

〇、写在前面

之前在仓库里整理过 DiffPDF / KDiff3 / NotePad-- 等开源 Qt 软件向鸿蒙 PC 移植的"踩坑全集",但那些都是移植别人的代码------必然背负着 qmake 工程、KDE 依赖、libpoppler 交叉编译这些历史包袱。

这次决定换个剧本:从零自研一个 Qt 应用,跑通整个鸿蒙 PC 链路 。挑了一个看起来"应用级"、UI 重、又完全不需要外部 C 库的方向------IronLog:一个力量训练记录工具,类似桌面版 Hevy / Strong。

完整链路如下:

复制代码
Mac 本地写 Qt5 源码
   ↓ tar + scp
OpenCloudOS 服务器交叉编译 (Qt-OHOS 5.12.12 + Clang 15)
   ↓ libIronLog.so (AArch64) + Qt runtime + 9 个图像插件
   ↓ scp 拉回 Mac
Mac 上 DevEco Studio 集成 → 签名 → 鸿蒙 PC 真机

事先想象的难点:4KB 对齐、host 工具是 Windows 版、SuperData ABI 错位。

实际踩到的坑:完全是另外两个 ------ 本文重点。

前序流程

1. 准备机器

你需要两类环境。

1.1 构建主机

任选一个:

text 复制代码
方案 A:Windows + WSL Ubuntu 22.04/24.04
方案 B:OpenCloudOS x86_64
方案 C:普通 Linux 服务器 x86_64

小白推荐:

text 复制代码
Windows + WSL Ubuntu

如果你现在已经有 OpenCloudOS,也可以用。注意 OpenCloudOS 建议是 x86_64,因为常见 OHOS SDK Linux 工具链是 Linux x64 主机工具。

检查架构:

bash 复制代码
uname -m

推荐看到:

text 复制代码
x86_64

1.2 鸿蒙 PC / 测试设备

用来安装和运行 HAP。你需要:

text 复制代码
鸿蒙 PC 真机
开发者模式
hdc 可用
DevEco Studio 可连接设备

2. 构建主机安装基础工具

2.1 Ubuntu / WSL

bash 复制代码
sudo apt update
sudo apt install -y \
  git cmake ninja-build \
  python3 python3-pip \
  unzip zip tar gzip xz-utils \
  pkg-config \
  build-essential \
  curl wget patch perl file

检查:

bash 复制代码
git --version
cmake --version
ninja --version
python3 --version

2.2 OpenCloudOS

bash 复制代码
sudo dnf makecache
sudo dnf install -y \
  git cmake ninja-build \
  python3 python3-pip \
  unzip zip tar gzip xz \
  pkgconf-pkg-config \
  gcc gcc-c++ make \
  curl wget patch perl file which

如果没有 dnf,用 yum

bash 复制代码
sudo yum makecache
sudo yum install -y \
  git cmake ninja-build \
  python3 python3-pip \
  unzip zip tar gzip xz \
  pkgconf-pkg-config \
  gcc gcc-c++ make \
  curl wget patch perl file which

检查:

bash 复制代码
cmake --version
ninja --version

建议 CMake 版本:

text 复制代码
3.22 或以上

如果版本太旧,后面 KDiff3 可能配置失败。

3. 准备 HarmonyOS / OpenHarmony SDK

下载地址:

http://dcp.openharmony.cn/workbench/cicd/dailybuild/dailylist

交叉编译选 ohos-sdk-full;原生编译选 ohos-sdk-public_ohos。

阶段1:下载 SDK

bash 复制代码
# 使用国内镜像下载(推荐)
wget "https://cidownload.openharmony.cn/version/Daily_Version/OpenHarmony_7.0.0.26/20260522_000324/version-Daily_Version-OpenHarmony_7.0.0.26-20260522_000324-ohos-sdk-full.tar.gz"

阶段2:解压主包

bash 复制代码
# 创建目录并解压
mkdir -p /root/ohos-sdk
tar -xzf version-Daily_Version-OpenHarmony_7.0.0.26-20260522_000324-ohos-sdk-full.tar.gz -C /root/ohos-sdk/

阶段3:解压工具链组件

bash 复制代码
# 进入 linux 目录
cd /root/ohos-sdk/ohos-sdk/linux/

# 解压 native 工具链(包含交叉编译器)
unzip native-linux-x64-26.0.0.26-Beta.zip

# 解压 toolchains(包含签名工具等)
unzip toolchains-linux-x64-26.0.0.26-Beta.zip

阶段4:设置环境变量

bash 复制代码
# 临时设置(当前会话有效)
export OHOS_SDK_ROOT=/root/ohos-sdk/ohos-sdk/linux
export PATH=$OHOS_SDK_ROOT/native/llvm/bin:$PATH

# 永久设置(写入 ~/.bashrc)
cat >> ~/.bashrc <<'EOF'
export OHOS_SDK_ROOT=/root/ohos-sdk/ohos-sdk/linux
export PATH=$OHOS_SDK_ROOT/native/llvm/bin:$PATH
EOF

# 生效环境变量
source ~/.bashrc

阶段5:验证配置

bash 复制代码
# 检查工具链文件
ls $OHOS_SDK_ROOT/native/build/cmake/ohos.toolchain.cmake

# 检查 clang 编译器
ls $OHOS_SDK_ROOT/native/llvm/bin/clang

# 检查签名工具
ls $OHOS_SDK_ROOT/toolchains/lib/binary-sign-tool

# 验证 clang 版本
clang --version

📁 最终目录结构

复制代码
/root/ohos-sdk/
└── ohos-sdk/
    └── linux/
        ├── native/
        │   ├── build/cmake/ohos.toolchain.cmake
        │   └── llvm/bin/clang
        └── toolchains/
            └── lib/binary-sign-tool

现在你已经完成了 OHOS SDK 的配置,可以开始进行 Qt 应用的鸿蒙 PC 适配开发了!🎉

4. 准备 Qt for HarmonyOS

1. 创建目录并克隆仓库

bash 复制代码
mkdir -p /opt/qt-ohos
cd /opt/qt-ohos
git clone https://atomgit.com/OpenHarmonyPCDeveloper/ohos_Qt5.12.12.git .

2. 使用 Git LFS 下载二进制包

bash 复制代码
# 确保已安装 git-lfs
git lfs pull

3. 解压 Qt 包

bash 复制代码
mkdir -p /opt/qt-ohos/qt-5.12.12-ohos
unzip /opt/qt-ohos/qt_ohos_release/qt-5.12.12-ohos_release_20260420.zip -d /opt/qt-ohos/qt-5.12.12-ohos

4. 设置环境变量

bash 复制代码
# 临时设置
export QT_OHOS_ROOT=/opt/qt-ohos/qt-5.12.12-ohos

# 永久设置
cat >> ~/.bashrc <<'EOF'
export QT_OHOS_ROOT=/opt/qt-ohos/qt-5.12.12-ohos
EOF

source ~/.bashrc

5. 验证 Qt

bash 复制代码
find $QT_OHOS_ROOT -name "Qt5Config.cmake"
# 期望输出:/opt/qt-ohos/qt-5.12.12-ohos/lib/cmake/Qt5/Qt5Config.cmake

✅ OHOS SDK 工具链文件存在

✅ clang 编译器可执行

✅ 签名工具存在

✅ Qt5Config.cmake 存在

✅ 环境变量已正确设置

🎉 配置完成! 现在你已经具备了 Qt 应用鸿蒙 PC 移植的完整开发环境。

一、为什么必须上服务器?

仓库前序文档里给过结论:交叉编译必须在 Linux x86_64 服务器上。原因不再展开,要点:

  1. OHOS SDK 工具链 (/root/ohos-sdk/...) 是 Linux x64 主机版
  2. Qt-OHOS 5.12.12 的 Qt5 模块 cmake config 在服务器上 (/opt/qt-ohos/...)
  3. Qt-OHOS 自带的 moc.exe / qmake.exe 是 Windows PE 文件,Mac 上跑不了
  4. Mac 上即使装了 brew 的 Qt5(5.15.x),版本不匹配会触发 SuperData ABI 错位

这次的工作流确定为:Mac 写代码 → tar + scp 上服务器 → 服务器 build_ohos.sh → scp 拉回 dist/

服务器环境检查(一次过):

text 复制代码
$ ssh root@129.211.223.113 "..."
--- HOST ---
VM-0-13-opencloudos
x86_64
NAME="OpenCloudOS"
VERSION="9.4"
--- ENV ---
OHOS_SDK_ROOT=/root/ohos-sdk/ohos-sdk/linux
QT_OHOS_ROOT=/opt/qt-ohos/qt-5.12.12-ohos/qt-5.12.12-ohos
--- CHECK ---
/root/ohos-sdk/ohos-sdk/linux/native/build/cmake/ohos.toolchain.cmake
/opt/qt-ohos/qt-5.12.12-ohos/qt-5.12.12-ohos/lib/cmake/Qt5/Qt5Config.cmake
/usr/bin/cmake
/usr/bin/ninja
/usr/bin/python3
/usr/bin/patchelf

ohos.toolchain.cmakeQt5Config.cmake、cmake/ninja/python3/patchelf ------ 全部就位。


二、产品定位与技术约束

#mermaid-svg-1fDWLCV2qWtbC9Xz{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1fDWLCV2qWtbC9Xz .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1fDWLCV2qWtbC9Xz .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1fDWLCV2qWtbC9Xz .error-icon{fill:#552222;}#mermaid-svg-1fDWLCV2qWtbC9Xz .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1fDWLCV2qWtbC9Xz .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1fDWLCV2qWtbC9Xz .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1fDWLCV2qWtbC9Xz .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1fDWLCV2qWtbC9Xz .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1fDWLCV2qWtbC9Xz .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1fDWLCV2qWtbC9Xz .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1fDWLCV2qWtbC9Xz .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1fDWLCV2qWtbC9Xz .marker.cross{stroke:#333333;}#mermaid-svg-1fDWLCV2qWtbC9Xz svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1fDWLCV2qWtbC9Xz p{margin:0;}#mermaid-svg-1fDWLCV2qWtbC9Xz .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-1fDWLCV2qWtbC9Xz .cluster-label text{fill:#333;}#mermaid-svg-1fDWLCV2qWtbC9Xz .cluster-label span{color:#333;}#mermaid-svg-1fDWLCV2qWtbC9Xz .cluster-label span p{background-color:transparent;}#mermaid-svg-1fDWLCV2qWtbC9Xz .label text,#mermaid-svg-1fDWLCV2qWtbC9Xz span{fill:#333;color:#333;}#mermaid-svg-1fDWLCV2qWtbC9Xz .node rect,#mermaid-svg-1fDWLCV2qWtbC9Xz .node circle,#mermaid-svg-1fDWLCV2qWtbC9Xz .node ellipse,#mermaid-svg-1fDWLCV2qWtbC9Xz .node polygon,#mermaid-svg-1fDWLCV2qWtbC9Xz .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-1fDWLCV2qWtbC9Xz .rough-node .label text,#mermaid-svg-1fDWLCV2qWtbC9Xz .node .label text,#mermaid-svg-1fDWLCV2qWtbC9Xz .image-shape .label,#mermaid-svg-1fDWLCV2qWtbC9Xz .icon-shape .label{text-anchor:middle;}#mermaid-svg-1fDWLCV2qWtbC9Xz .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-1fDWLCV2qWtbC9Xz .rough-node .label,#mermaid-svg-1fDWLCV2qWtbC9Xz .node .label,#mermaid-svg-1fDWLCV2qWtbC9Xz .image-shape .label,#mermaid-svg-1fDWLCV2qWtbC9Xz .icon-shape .label{text-align:center;}#mermaid-svg-1fDWLCV2qWtbC9Xz .node.clickable{cursor:pointer;}#mermaid-svg-1fDWLCV2qWtbC9Xz .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-1fDWLCV2qWtbC9Xz .arrowheadPath{fill:#333333;}#mermaid-svg-1fDWLCV2qWtbC9Xz .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-1fDWLCV2qWtbC9Xz .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-1fDWLCV2qWtbC9Xz .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1fDWLCV2qWtbC9Xz .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-1fDWLCV2qWtbC9Xz .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1fDWLCV2qWtbC9Xz .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-1fDWLCV2qWtbC9Xz .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-1fDWLCV2qWtbC9Xz .cluster text{fill:#333;}#mermaid-svg-1fDWLCV2qWtbC9Xz .cluster span{color:#333;}#mermaid-svg-1fDWLCV2qWtbC9Xz div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-1fDWLCV2qWtbC9Xz .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-1fDWLCV2qWtbC9Xz rect.text{fill:none;stroke-width:0;}#mermaid-svg-1fDWLCV2qWtbC9Xz .icon-shape,#mermaid-svg-1fDWLCV2qWtbC9Xz .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1fDWLCV2qWtbC9Xz .icon-shape p,#mermaid-svg-1fDWLCV2qWtbC9Xz .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-1fDWLCV2qWtbC9Xz .icon-shape .label rect,#mermaid-svg-1fDWLCV2qWtbC9Xz .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1fDWLCV2qWtbC9Xz .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-1fDWLCV2qWtbC9Xz .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-1fDWLCV2qWtbC9Xz :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 移植开源软件(DiffPDF/KDiff3...)
qmake → CMake 重写
KDE Frameworks 瘦身
libpoppler/freetype 交叉编译
moc ABI 错位修补
ELF 4KB 对齐修复
⏱ 工时 2-3 天
自研 Qt 应用(IronLog)
一开始就用 CMake
纯 Qt5 Widgets/Gui/Core
AUTOMOC 一行搞定
✅ 工时 ~ 1 天

IronLog 演示版:左侧导航 + 4 个页面(仪表盘 / 训练记录 / 力量进步 / 动作库)+ 暗黑健身房风 QSS。

技术边界(避开仓库知识库里所有红色雷区):

类别 决策
Qt 模块 只用 Core / Gui / Widgets(绝对安全)
第三方 C 库 零依赖(不碰 poppler / freetype / fontconfig)
图表 不要 QtCharts (OHOS 包里可能没有),手写 QPainter 自绘
数据持久化 不要 SQLite(演示版无需),数据写死在代码里
资源压缩 .qrc 关 zlib(这是这次第二个坑,下文有踩坑记录)

工程结构:

复制代码
IronLog/
├── CMakeLists.txt
├── build_ohos.sh             ← 服务器一键编译脚本
├── src/
│   ├── main.cpp
│   ├── MainWindow.{h,cpp}
│   ├── DashboardPage.{h,cpp}      ← 仪表盘(4 个统计卡 + 热力图 + PR 曲线 + 计划/历史)
│   ├── WorkoutPage.{h,cpp}        ← 训练记录(计时器 + 表格录入)
│   ├── ProgressPage.{h,cpp}       ← 进步图表(大折线 + 容量趋势 + PR 时间线)
│   ├── ExerciseLibraryPage.{h,cpp}← 动作库网格
│   ├── HeatmapWidget.{h,cpp}      ← QPainter 自绘热力图
│   ├── LineChartWidget.{h,cpp}    ← QPainter 自绘折线图
│   └── StatCard.{h,cpp}           ← 卡片组件
└── resources/
    ├── ironlog.qrc
    └── ironlog.qss              ← 暗黑健身房主题样式表

三、CMakeLists.txt 关键写法

这是自研应用最该抄的模板(可以直接当种子工程):

cmake 复制代码
cmake_minimum_required(VERSION 3.16)
project(IronLog CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
# Qt-OHOS 5.12.12 的 libQt5Core.so 没有导出 qt_resourceFeatureZlib 符号
# 让 rcc 关闭 zlib 压缩,避免链接错误
set(CMAKE_AUTORCC_OPTIONS "--no-compress")
set(CMAKE_AUTOUIC ON)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)

# OHOS 交叉编译时, Qt 在 sysroot 外, 需要放开 find_package
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE BOTH)

find_package(Qt5 REQUIRED COMPONENTS Core Gui Widgets)

set(IRONLOG_SRCS
    src/main.cpp src/MainWindow.cpp src/MainWindow.h
    src/DashboardPage.cpp src/DashboardPage.h
    src/WorkoutPage.cpp src/WorkoutPage.h
    src/ProgressPage.cpp src/ProgressPage.h
    src/ExerciseLibraryPage.cpp src/ExerciseLibraryPage.h
    src/HeatmapWidget.cpp src/HeatmapWidget.h
    src/LineChartWidget.cpp src/LineChartWidget.h
    src/StatCard.cpp src/StatCard.h
    resources/ironlog.qrc
)

# ⭐ 关键:OHOS 下生成 SHARED 库,桌面下生成 executable
if(OHOS OR DEFINED OHOS_ARCH)
    message(STATUS ">>>> IronLog: 鸿蒙交叉编译模式 (生成 SHARED 库) <<<<")
    add_library(IronLog SHARED ${IRONLOG_SRCS})
else()
    add_executable(IronLog ${IRONLOG_SRCS})
endif()

target_link_libraries(IronLog PRIVATE Qt5::Core Qt5::Gui Qt5::Widgets)

要点:

  • add_library(... SHARED) ------ 鸿蒙下输出 libIronLog.so,因为 libqohos.sodlopen + dlsym("main")
  • CMAKE_AUTORCC_OPTIONS "--no-compress" ------ 本次踩坑的核心修复,下文展开
  • CMAKE_FIND_ROOT_PATH_MODE_PACKAGE BOTH ------ OHOS 工具链默认只在 sysroot 找 package,Qt 在外面要放开

四、build_ohos.sh:一键服务器编译

脚本做 4 件事:CMake 配置 → ninja 编译 → 体检 ELF → 收集 runtime libs 到 dist/

关键片段:

bash 复制代码
cmake -S . -B build-ohos -GNinja \
    -DCMAKE_TOOLCHAIN_FILE="$OHOS_SDK_ROOT/native/build/cmake/ohos.toolchain.cmake" \
    -DOHOS_ARCH=arm64-v8a \
    -DOHOS_PLATFORM=OHOS \
    -DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE=BOTH \
    -DCMAKE_PREFIX_PATH="$QT_OHOS_ROOT" \
    -DCMAKE_BUILD_TYPE=Release \
    -DQt5_DIR="$QT_OHOS_ROOT/lib/cmake/Qt5" \
    -DQt5Core_DIR="$QT_OHOS_ROOT/lib/cmake/Qt5Core" \
    -DQt5Gui_DIR="$QT_OHOS_ROOT/lib/cmake/Qt5Gui" \
    -DQt5Widgets_DIR="$QT_OHOS_ROOT/lib/cmake/Qt5Widgets"

ninja -C build-ohos

# 收集到 dist/
cp build-ohos/libIronLog.so                       dist/
cp $QT_OHOS_ROOT/lib/libQt5{Core,Gui,Widgets}.so   dist/
cp $QT_OHOS_ROOT/plugins/platforms/libqohos.so     dist/
cp $QT_OHOS_ROOT/plugins/platforms/libqohos.so     dist/platforms/   # ⭐ 必须双份
cp $QT_OHOS_ROOT/plugins/styles/libqohosstyle.so   dist/styles/
cp $QT_OHOS_ROOT/plugins/imageformats/*.so         dist/imageformats/
cp $OHOS_SDK_ROOT/native/llvm/lib/aarch64-linux-ohos/libc++_shared.so dist/

关键约定(仓库里反复强调):

  • libqohos.so 要放两份 :外层一份 + platforms/ 一份
  • libc++_shared.so 必须打进来(NDK 的 C++ 运行时)
  • imageformats/ 里 9 个图像插件全拷上,不然 PNG/JPG 等加载会失败

五、踩坑记录(完整真实输出)

虽然提前预想了 4KB 对齐、host 工具版本错位等老坑,但这次实际踩到的是两个新坑------都不在仓库知识库里写过,特别值得记录。

🕳️ 坑 1:QStringList 列表初始化在 Clang 15 下歧义

第一次跑 bash build_ohos.sh,CMake 配置秒过,ninja 编译到第 6/14 个文件就 FAIL:

text 复制代码
[6/14] Building CXX object CMakeFiles/IronLog.dir/src/LineChartWidget.cpp.o
FAILED: CMakeFiles/IronLog.dir/src/LineChartWidget.cpp.o
...
LineChartWidget.cpp:10:15: error: use of overloaded operator '=' is ambiguous
                          (with operand types 'QStringList' and 'void')
    m_xLabels = {"12月", "", "", "1月", "", ...};
    ~~~~~~~~~ ^ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
qstringlist.h:99:7: note: candidate function (the implicit move assignment operator)
qstringlist.h:99:7: note: candidate function (the implicit copy assignment operator)
qstringlist.h:113:18: note: candidate function
    QStringList &operator=(const QList<QString> &other)
qstringlist.h:116:18: note: candidate function
    QStringList &operator=(QList<QString> &&other) Q_DECL_NOTHROW
1 error generated.

根因

  • 我写的 m_xLabels = {"12月", ...}已声明成员的赋值 ,需要走 operator=
  • Qt 5.12 时代 QStringList 没有定义 operator=(std::initializer_list<QString>)
  • Clang 15 严格模式下,{...} 既能匹配隐式 copy/move operator=,也能匹配 operator=(QList<QString> &&) ------ 歧义

老一些的 GCC / 老的 Clang 在这种场景会偷偷选一个,Clang 15 直接 error: ambiguous

修复 :所有"赋值场景"显式构造类型(声明 + 初始化场景没问题,只有"先声明、后赋值"才歧义):

cpp 复制代码
// ❌ 老写法(在 Clang 15 + Qt 5.12 下歧义)
m_xLabels = {"12月", "", ""};

// ✅ 修复
m_xLabels = QStringList{"12月", "", ""};

// 函数参数也一样
chart->setSeries(QVector<double>{72, 75, 78}, QStringList{"Q1", "Q2", "Q3"});

经验:Qt 5.12 + Clang 15 的组合是个非主流现代搭配 ,写代码时尽量避免 obj = {...} 这种依赖隐式构造的写法。

🕳️ 坑 2:链接报 undefined symbol: qt_resourceFeatureZlib

修完 QStringList 后再跑,前 13/14 个 .o 全部编译通过,链接时炸:

text 复制代码
[14/14] Linking CXX shared library libIronLog.so
FAILED: libIronLog.so
ld.lld: error: undefined symbol: qt_resourceFeatureZlib
>>> referenced by qrc_ironlog.cpp
>>>     CMakeFiles/IronLog.dir/IronLog_autogen/3YJK5W5UP7/qrc_ironlog.cpp.o:(qCleanupResources_ironlog())
>>> referenced by qrc_ironlog.cpp
>>>     CMakeFiles/IronLog.dir/IronLog_autogen/3YJK5W5UP7/qrc_ironlog.cpp.o:((anonymous namespace)::initializer::~initializer())

根因

  • Qt 的 rcc 工具默认会对体积超过阈值(约 100 字节)的资源做 zlib 压缩
  • 生成的 qrc_xxx.cpp 里会引用 qt_resourceFeatureZlib 这个内部符号
  • 这个符号在 Qt-OHOS 5.12.12 的 libQt5Core.so没有导出------疑似 Qt-OHOS 在裁剪二进制时把 zlib 资源支持给砍了

定位:随手用 nm -D libQt5Core.so | grep qt_resource 验证(如果你想自己确认),就能看到这个符号确实不存在。

修复 :让 rcc 关闭 zlib 压缩。两种方式都可行,最干净的是改 CMake

cmake 复制代码
set(CMAKE_AUTORCC_OPTIONS "--no-compress")

这一行让 AUTORCC 在跑 rcc 时加 --no-compress,生成的 qrc_xxx.cpp 就不会引用 zlib 符号。资源会以原始字节存在 .so 里------对一个几 KB 的 QSS 文件来说,体积差异可以忽略。

经验:Qt-OHOS 是裁剪过的运行时,不要假设它和桌面 Qt5 完全等价。链接报"undefined symbol: qt_xxx"时,先怀疑这是被裁掉的特性而不是你代码的问题。


六、编译成功 · 完整真实输出

修完两个坑,第三次跑 bash build_ohos.sh,从头到尾真实终端输出如下(节选关键部分):

text 复制代码
============================================
 IronLog · 鸿蒙 PC 交叉编译
============================================
 OHOS_SDK_ROOT = /root/ohos-sdk/ohos-sdk/linux
 QT_OHOS_ROOT  = /opt/qt-ohos/qt-5.12.12-ohos/qt-5.12.12-ohos
============================================

>>> [1/2] CMake 配置...
-- The CXX compiler identification is Clang 15.0.4
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /root/ohos-sdk/ohos-sdk/linux/native/llvm/bin/clang++ - skipped
-- >>>> IronLog: 鸿蒙交叉编译模式 (生成 SHARED 库) <<<<
-- Configuring done (0.1s)
-- Generating done (0.0s)

>>> [2/2] Ninja 编译...
[ 1/14] Automatic MOC and UIC for target IronLog
[ 2/14] Automatic RCC for resources/ironlog.qrc
[ 3/14] Building CXX object src/StatCard.cpp.o
[ 4/14] Building CXX object IronLog_autogen/.../qrc_ironlog.cpp.o
[ 5/14] Building CXX object src/main.cpp.o
[ 6/14] Building CXX object src/HeatmapWidget.cpp.o
[ 7/14] Building CXX object IronLog_autogen/mocs_compilation.cpp.o
[ 8/14] Building CXX object src/MainWindow.cpp.o
[ 9/14] Building CXX object src/LineChartWidget.cpp.o
[10/14] Building CXX object src/ProgressPage.cpp.o
[11/14] Building CXX object src/ExerciseLibraryPage.cpp.o
[12/14] Building CXX object src/DashboardPage.cpp.o
[13/14] Building CXX object src/WorkoutPage.cpp.o
[14/14] Linking CXX shared library libIronLog.so

============================================
 验证产物 libIronLog.so
============================================

🔎 文件类型:
build-ohos/libIronLog.so: ELF 64-bit LSB shared object, ARM aarch64,
  version 1 (SYSV), dynamically linked,
  BuildID[sha1]=d32ef4e0e4b6c7c53926db08ab945e2ca24aa633,
  with debug_info, not stripped

🔎 ELF 头:
  Class:                             ELF64
  Type:                              DYN (Shared object file)
  Machine:                           AArch64

🔎 main 符号 (必须是 T main):
0000000000016d44 T main

🔎 NEEDED 依赖:
  Shared library: [libQt5Widgets.so]
  Shared library: [libQt5Gui.so]
  Shared library: [libQt5Core.so]
  Shared library: [libc++_shared.so]
  Shared library: [libc.so]

🔎 LOAD 段对齐 (关注是否 0x10000 需要修复):
  LOAD  0x000000  0x0000000000000000  0x014e1c  R    0x1000
  LOAD  0x014e1c  0x0000000000015e1c  0x016054  R E  0x1000
  LOAD  0x02ae70  0x000000000002ce70  0x001d70  RW   0x1000
  LOAD  0x02cbe0  0x000000000002fbe0  0x000288  RW   0x1000

逐项体检

检查项 期望值 实际
文件类型 ARM aarch64 ✅ ELF 64-bit, ARM aarch64
ELF 类型 DYN (共享对象) ✅ DYN
Machine AArch64 ✅ AArch64
main 符号 T main (已导出) 0000000000016d44 T main
NEEDED 依赖 无绝对路径 ✅ 全是干净相对名
LOAD 段对齐 0x1000 (4KB) 全部 4 个段都是 0x1000

业务库 333 KB、依赖闭包整洁、main 已导出、ELF 4KB 对齐------一次干净的产物

4KB 对齐意外惊喜

fix_elf_align_v2.py 兜底修复时,输出显示所有 19 个 .so 都已经是 4KB 对齐,无需修复

text 复制代码
=== 处理 19 个 .so 文件 ===
--- libIronLog.so ---
  ✓ dist/libIronLog.so: 已对齐,无需修复
--- libQt5Core.so ---
  ✓ dist/libQt5Core.so: 已对齐,无需修复
--- libQt5Gui.so ---
  ✓ dist/libQt5Gui.so: 已对齐,无需修复
... (省略)
=== 完成: 19/19 ===

对比仓库前序文档里 DiffPDF 时代记录的致命 4KB / 64KB 页对齐冲突 ------那时候自编的 libpoppler.so / libfreetype.so 都是 64KB 对齐(musl loader 在鸿蒙 PC 上 mmap 时直接 SEGV_MAPERR),需要 Python 脚本暴力重写 ELF Program Header。

这次为什么没踩到?两个原因

  1. 本次的 Qt-OHOS 二进制是更新版本,华为团队修过工具链默认 LDFLAGS,所有 Qt 库本身就是 4KB 对齐
  2. 业务库自身没有外部 C 库依赖 ,自己只链了 Qt5,Qt-OHOS 工具链的默认链接参数已经是 -Wl,-z,max-page-size=0x1000

也就是说:只用 Qt 模块的纯 Qt 自研应用,4KB 对齐已经不再是问题。这个老坑只在交叉编译第三方 C 库(如 poppler)时才会复活。


七、产物清单

最终 dist/ 目录(432 MB,绝大部分是 libqohos.so 的 149 MB QPA 平台插件):

text 复制代码
IronLog/dist/
├── libIronLog.so          333 K   ← 业务库(导出 main 符号)
├── libQt5Core.so           34 M
├── libQt5Gui.so            37 M
├── libQt5Widgets.so        36 M
├── libQt5Network.so        12 M   ← 演示版没用到,留着备用
├── libQt5OhosExtras.so    5.1 M   ← 鸿蒙特定扩展
├── libqohos.so            149 M   ← QPA 插件 + ArkTS 桥接(大头在这)
├── libc++_shared.so       1.2 M
├── platforms/
│   └── libqohos.so        149 M   ← Qt 插件加载机制要求的"双份"
├── styles/
│   └── libqohosstyle.so   1.9 M
└── imageformats/
    ├── libqgif.so         332 K
    ├── libqicns.so        374 K
    ├── libqico.so         333 K
    ├── libqjpeg.so        1.4 M
    ├── libqsvg.so         250 K
    ├── libqtga.so         301 K
    ├── libqtiff.so        1.4 M
    ├── libqwbmp.so        252 K
    └── libqwebp.so        1.9 M

整个 dist/ 拉回 Mac 后,下一步就是把它倒进 DevEco Studio 的 HAP 工程的 entry/libs/arm64-v8a/

九、鸿蒙工程搭建

IronLog/ 整个目录可以作为 "Qt 自研鸿蒙 PC 应用"种子工程,复制改名即可:

如果复用工程有不明白了,可参考文章:https://blog.csdn.net/weixin_52908342/article/details/161343743

text 复制代码
IronLog/
├── CMakeLists.txt           ← 关键:OHOS 分支 + AUTORCC --no-compress
├── build_ohos.sh            ← 一键服务器编译
├── src/                     ← 改成你的业务
└── resources/
    ├── ironlog.qrc
    └── ironlog.qss

UI 迭代历程:V1 → V5 字号专项打磨

真机部署后做了 5 轮 UI 调整,是这次自研项目里最有价值的产出 ------每一轮都对应一个仓库前序文档没记录过的"鸿蒙 PC + Qt-OHOS"专属坑点:

关键发现(仓库前序文档零记录)

版本 发现 修复
V1 :/qss/ironlog.qss 通过 .qrc 加载在 Qt-OHOS 上不生效(怀疑 rcc + AUTORCC 在 OHOS 模式下资源数据格式异常) 改为 C++ raw string 内联 + setStyleSheet(QString)
V4 Qt-OHOS 的 QSS font-size: Npx 对很多 widget 不生效 ------只有写了具体 objectName 的才生效 全部改用 C++ 代码 widget->setFont(QFont) 显式控字
V5 鸿蒙 PC 高 DPI 下,QPainter 自绘字体(热力图标签、折线图坐标)和 QLabel 的字号呈现不同------前者按 pt 换算偏大,后者偏小 分类微调:QPainter 字号 → 9-10pt(显得精致),QLabel 字号 → 16-18pt(显得舒服)

dist/ 目录已经回到 Mac,接下来:

  1. 复制根目录 鸿蒙QT模板/IronLogOhos/

  2. dist/ 的所有 .so 倒进 IronLogOhos/entry/libs/arm64-v8a/

  3. entry/src/main/ets/common/QtAppConstants.ets

    typescript 复制代码
    export const APP_LIBRARY_NAME = 'libIronLog.so';
  4. AppScope/app.json5bundleName 避免冲突

  5. DevEco Studio → Signing Configs → Sign → Run

十、总结

这次自研 IronLog 跑通"从零写代码 → 鸿蒙 PC 真机运行"的完整链路,相比之前移植 DiffPDF / KDiff3 等开源软件的 2-3 天工时,自研项目从 init 到第一次跑通只花了大约 1 天 ------这个差距不是因为偷工减料,而是因为自研项目天然规避了所有"历史包袱型坑"

  • ❌ 不用做 qmake → CMake 重写
  • ❌ 不用瘦身 KDE Frameworks 依赖
  • ❌ 不用交叉编译 Poppler / FreeType / Fontconfig 等三方库
  • ❌ 不用打 moc ABI 错位补丁
  • ❌ 不用为不同 Qt 版本写兼容宏

只剩纯净的鸿蒙 PC + Qt-OHOS 链路 :写 Qt 代码 → CMake 加 add_library(... SHARED) → 服务器 build_ohos.sh 一键编译 → .so 塞进 HAP 模板 → DevEco 签名 Run。

十一、FAQ

整理读者最常问的问题(也是我自己第一次走这条路时困惑过的问题)。

Q1:Mac 本地能不能直接编出 .so?为什么必须上 Linux 服务器?

A :理论上可以,但强烈不建议

Qt-OHOS 5.12.12 的官方分发包里,host 工具(moc/uic/rcc/lrelease)是为 Linux x86_64 编译的------Mac 上跑不起来。如果用 Mac 本地的 brew install qt5 替代,版本会是 5.15.x,和目标 Qt-OHOS 5.12.12 的 ABI 不匹配,会触发"SuperData::link<...> 错位"等一连串编译错误。

正确路径就是:Mac 写代码 → rsync/scp 上服务器 → 服务器 bash build_ohos.sh → scp 拉回 .so。仓库里所有成功案例(DiffPDF / KDiff3 / NotePad-- / IronLog)都是这条链路。

Q2:业务代码必须编成 SHARED 库吗?能不能直接编可执行文件?

A :必须编成 SHARED 库 ,而且必须导出 main 符号

鸿蒙 PC 上启动一个 Qt 应用的真实流程是:

复制代码
ArkTS 入口 → libqohos.so (QPA 平台插件) → dlopen("libIronLog.so") + dlsym("main") → 调用 main()

libqohos.sodlsym 查找名为 main 的符号------所以业务库必须:

  • add_library(IronLog SHARED ...)(不是 add_executable
  • 保留 int main(int argc, char *argv[]) 函数原型
  • 不要加 -fvisibility=hidden ,否则 main 不会被导出

可以用 nm -D libIronLog.so | grep ' main$' 验证,必须看到 T mainT 代表全局可见的 text 段符号)。

Q3:HAP 包为什么这么大(432 MB)?能不能瘦身?

A:能,但需要权衡。

HAP 大头是 Qt5 + QPA 插件未 strip 状态

文件 大小 占比
libqohos.so 149 MB 35%
libQt5Gui.so 37 MB 9%
libQt5Widgets.so 36 MB 8%
libQt5Core.so 34 MB 8%
业务库 + 其它 176 MB 40%

瘦身手段(按代价从低到高):

  1. llvm-strip 去调试符号 ------ 432MB → 约 140MB(最有效,无副作用)
  2. 删除 imageformats/ 里用不到的图片格式插件(如 webp/tiff/tga)------ 节省 5-10 MB
  3. 静态链接 Qt5 ------ 编译复杂度极高,不推荐

仓库其它项目(DiffPDF / qjackctl)也都没有在交付时 strip------优先保证调试信息完整、HAP 大小可以接受到一两百兆。

Q4:为什么 QSS 通过 .qrc 加载会失败?这个是 Qt-OHOS 的 bug 吗?

A:高度怀疑是 bug,但还没拿到确凿证据。

现象:把 QSS 放在 :/qss/ironlog.qss 资源里,C++ 代码用 QFile(":/qss/ironlog.qss") 读取后 setStyleSheetQt-OHOS 上读到空字符串 / 异常字节,导致样式不生效。

排除原因:

  • .qrc 在桌面 Qt(Linux/Mac/Windows)上 100% 正常
  • ✅ AUTORCC 编译过程没有任何错误或警告
  • ✅ rcc 生成的 qrc_*.cpp 文件本身正常(grep 能看到 QSS 内容)
  • ❌ 但运行时读到的不是预期内容

兜底解法(IronLog 在用):把 QSS 写成 C++ raw string literal 内联到 main.cpp

cpp 复制代码
static const char kIronLogStyleSheet[] = R"QSS(
    QMainWindow { background: #0E0E13; }
    /* ... 其它样式 ... */
)QSS";

app.setStyleSheet(QString::fromUtf8(kIronLogStyleSheet));

绕过资源系统,100% 必生效。

Q5:QSS 的 font-size: Npx 为什么对部分 widget 不生效?

A :这是 Qt-OHOS 的另一个字号继承链异常

现象:在 QSS 里写 QWidget { font-size: 18px }QLabel { font-size: 16px },部分 widget 完全不响应,依然显示默认字号。

经过 V3 → V4 → V5 三轮排查,发现:

  • objectName + setObjectName 显式锚定的 widget(如 #statCardValue),QSS font-size 生效
  • 通用选择器(QLabelQWidget)下的 font-size 对很多原生 widget 不生效
  • 不分原因,最稳的做法就是不用 QSS 控字号

兜底解法(IronLog V4 起在用):所有字号通过 C++ 代码 widget->setFont(QFont) 显式控制

cpp 复制代码
QFont f;
f.setPointSize(18);
f.setWeight(QFont::Medium);
label->setFont(f);

代价是要给每个 QLabel/QListWidget 写一行 setFont,但 100% 跨平台稳定。

Q6:自研 vs 移植开源软件,哪个更适合作为入门 Qt 鸿蒙 PC 开发的第一步?

A首推自研一个简单的 Qt Widgets 应用,原因:

维度 自研(如 IronLog) 移植开源(如 DiffPDF)
一上手能学到的 鸿蒙 PC + Qt-OHOS 链路本身 链路 + 历史包袱填坑
第一次跑通工时 0.5 - 1 天 2 - 5 天
卡点种类 少而集中 多而分散(qmake/KDE/三方库... )
成就感曲线 平稳上升 反复挫败

入门建议:先自研一个 3 个页面 + 1 个 SQLite + 1 个 QSS 主题的小工具(番茄钟、记账本、待办清单都行),跑通后再去挑战 KDE 移植类项目。

Q7:DevEco Studio 自动签名失败,hap 一直是 unsigned,怎么办?

A :这是 DevEco 的一个反直觉设计------自动签名 UI 只填证书,不填 products.signingConfig 引用

完整三步:

  1. Project Structure → Signing Configs → Automatically generate signature ------ 让 DevEco 申请 .cer / .p7b(看 ~/.ohos/config/ 应该有 4 个文件)
  2. 打开 build-profile.json5,确认 signingConfigs已经被自动填充(cert/profile/storeFile/keyAlias/passwords 都齐全)
  3. 手动 确认 products.default 块里有 "signingConfig": "default" 这一行------这一步 DevEco 不会自动加

跑 Build 后看 hap 文件名:

  • entry-default-unsigned.hap ❌(第 3 步没做)
  • entry-default-signed.hap

Q8:鸿蒙 PC 真机 Run 后窗口尺寸不对、字号偏小?

A :鸿蒙 PC 是 2.5K / 3K 高分屏,DPI 缩放后所有按 px 单位的字会偏小。两个解法:

方案 A(推荐) :所有字号用 pointSize 而不是 pixelSize,让 Qt 自动按 DPI 缩放:

cpp 复制代码
QFont f;
f.setPointSize(16);  // 不是 setPixelSize

方案 B(保险) :在 main() 开头主动开启 Qt 的高 DPI 缩放(注意 Qt-OHOS 5.12 上不一定生效,作为可选保险):

cpp 复制代码
QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);

IronLog 的实测经验:主要靠 pointSize 控字 + 关键大字号在代码里写明 setFont(fontPt(36)),整体效果"接近 Mac 视网膜屏"的舒适度。

Q9:能不能不用服务器,直接在 Mac 上用 Docker 跑这套交叉编译?

A :技术上可以,仓库 知识库/ 里有 Docker 方案的文档。但实际上:

  • Docker 镜像包含完整 OHOS SDK(约 10 GB) + Qt-OHOS(约 600 MB),下载耗时
  • Mac 上 Docker 跑 Linux 容器是虚拟化,编译速度比真 Linux 服务器慢 2-3 倍
  • 调试服务器问题(看日志、改文件、ssh 进容器)麻烦

如果你只是偶尔编一次,Docker 方案可行;如果你打算长期做鸿蒙 PC + Qt 开发,租一台云服务器更省心------OpenCloudOS / Ubuntu 22.04 都可以,2 核 4G 起步即可。

本文所有代码与命令均经服务器真实跑通(OpenCloudOS 9 / Clang 15.0.4 / Qt-OHOS 5.12.12)。