【鸿蒙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 服务器上。原因不再展开,要点:
- OHOS SDK 工具链 (
/root/ohos-sdk/...) 是 Linux x64 主机版 - Qt-OHOS 5.12.12 的 Qt5 模块 cmake config 在服务器上 (
/opt/qt-ohos/...) - Qt-OHOS 自带的
moc.exe / qmake.exe是 Windows PE 文件,Mac 上跑不了 - 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.cmake、Qt5Config.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.so会dlopen+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/moveoperator=,也能匹配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。
这次为什么没踩到?两个原因:
- 本次的 Qt-OHOS 二进制是更新版本,华为团队修过工具链默认 LDFLAGS,所有 Qt 库本身就是 4KB 对齐
- 业务库自身没有外部 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,接下来:
-
复制根目录
鸿蒙QT模板/→IronLogOhos/ -
把
dist/的所有.so倒进IronLogOhos/entry/libs/arm64-v8a/ -
改
entry/src/main/ets/common/QtAppConstants.ets:typescriptexport const APP_LIBRARY_NAME = 'libIronLog.so'; -
改
AppScope/app.json5的bundleName避免冲突 -
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.so 用 dlsym 查找名为 main 的符号------所以业务库必须:
add_library(IronLog SHARED ...)(不是add_executable)- 保留
int main(int argc, char *argv[])函数原型 - 不要加
-fvisibility=hidden,否则main不会被导出
可以用 nm -D libIronLog.so | grep ' main$' 验证,必须看到 T main(T 代表全局可见的 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% |
瘦身手段(按代价从低到高):
llvm-strip去调试符号 ------ 432MB → 约 140MB(最有效,无副作用)- 删除
imageformats/里用不到的图片格式插件(如 webp/tiff/tga)------ 节省 5-10 MB - 静态链接 Qt5 ------ 编译复杂度极高,不推荐
仓库其它项目(DiffPDF / qjackctl)也都没有在交付时 strip------优先保证调试信息完整、HAP 大小可以接受到一两百兆。
Q4:为什么 QSS 通过 .qrc 加载会失败?这个是 Qt-OHOS 的 bug 吗?
A:高度怀疑是 bug,但还没拿到确凿证据。
现象:把 QSS 放在 :/qss/ironlog.qss 资源里,C++ 代码用 QFile(":/qss/ironlog.qss") 读取后 setStyleSheet,Qt-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),QSSfont-size生效 - 通用选择器(
QLabel、QWidget)下的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 引用。
完整三步:
Project Structure → Signing Configs → Automatically generate signature------ 让 DevEco 申请 .cer / .p7b(看~/.ohos/config/应该有 4 个文件)- 打开
build-profile.json5,确认signingConfigs块已经被自动填充(cert/profile/storeFile/keyAlias/passwords 都齐全) - 手动 确认
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)。