【开源软件移植】把 CopyQ 搬上鸿蒙 PC:主窗口启动的适配实录—实战适配笔记记录
【开源软件移植】把 CopyQ 搬上鸿蒙 PC:主窗口启动的适配实录—实战适配笔记记录
欢迎加入开源鸿蒙 PC 社区:https://harmonypc.csdn.net/
注意(本部分必看):
开始本文工作前需要完成:
从0创建项目指南,新手先看这篇,都写到这篇文章内了,重复的步骤不过多重复写,后续工作都在用HAP 壳工程:
https://blog.csdn.net/weixin_52908342/article/details/161343743
准备 OpenHarmony SDK
准备 Qt for HarmonyOS
复现最小 Qt Widgets Demo
准备 DevEco HAP 壳工程
完成上面步骤,即可跟着本文进行手把手教学适配CopyQ!
项目开源仓库:https://atomgit.com/weixin_52908342/OH-CopyQ
项目信息
| 项 | 内容 |
|---|---|
| 项目名称 | CopyQ |
| 上游版本 | 9.1.0 |
| 上游仓库 | https://github.com/hluk/CopyQ |
| 开源协议 | GPL-3.0-or-later(适配产物同协议开源) |
| 主要语言 | C++17 |
| GUI 框架 | Qt 5 Widgets(无 QML / 无 OpenGL) |
| 源码规模 | ~10 万行,98 个 .cpp 翻译单元 |
| 项目类型 | 跨平台剪贴板管理器(client/server 单进程双角色) |
| 项 | 内容 |
|---|---|
| 操作系统 | HarmonyOS Next PC |
| 设备形态 | PC(2in1) |
| CPU 架构 | aarch64 (ARM64) |
| ABI | aarch64-linux-ohos |
| 运行时 | qt-ohos(Qt 5.12.12 鸿蒙移植版) |
编译环境
| 项 | 内容 |
|---|---|
| 构建主机 | OpenCloudOS Server 9(x86_64,腾讯云 4C8G) |
| OHOS NDK | /root/ohos-sdk/ohos-sdk/linux/native/(自带 Clang 15.0.4) |
| Qt-OHOS | /opt/qt-ohos/qt-5.12.12-ohos/qt-5.12.12-ohos/ |
| 构建工具 | CMake 3.x + Ninja/Make |
| HAP 打包工具 | DevEco Studio(Mac 端) |
| 真机 | HarmonyOS Next PC(hdc 调试) |
本文记录的是通用 Qt 桌面应用 → 鸿蒙 PC的适配实战。如果你是:
- 🎯 想把自家/开源 Qt 5 应用移植到鸿蒙 PC 的开发者
- 🎯 想了解 HAP dlopen 模型与传统 Qt 可执行文件区别的研究者
- 🎯 在排查鸿蒙 PC 上
dlopen failed、Napi::Error: too frequently、main returned 1等崩溃问题的运维 / 测试工程师
一、为什么选 CopyQ?
我们清单里 100 多个 Qt 开源软件中,CopyQ 属于"中等偏难"档次:
- ✅ 优势:Qt5 Widgets 纯 GUI,没有 OpenGL/QML 重度依赖,源码 ~10 万行规模可控
- ❌ 难点:典型 client/server 架构(一个进程同时是 server 和 client),启动逻辑高度依赖命令行参数;用了
QSystemTrayIcon(系统托盘);用了QSettings读 ini
后来证明,这三个"难点"分别在启动流程的三个不同阶段把我们绊倒了。
二、技术总路线
鸿蒙 PC 没有原生 Qt runtime,业界目前的主流方案是 qt-ohos 平台插件:

也就是说,Qt 应用必须先被改造成动态库(.so),让 HAP 用 dlopen + dlsym("main") 反向调起来。这跟传统的"独立可执行文件"模型完全相反。



三、Phase 1:把 CopyQ 改造成 .so
3.1 交叉编译环境
服务器是 OpenCloudOS Server 9(x86_64),工具链路径如下:
| 项目 | 路径 |
|---|---|
| OHOS NDK Clang++ | /root/ohos-sdk/ohos-sdk/linux/native/llvm/bin/aarch64-unknown-linux-ohos-clang++ |
| OHOS sysroot | /root/ohos-sdk/ohos-sdk/linux/native/sysroot |
| Qt-OHOS 5.12.12 | /opt/qt-ohos/qt-5.12.12-ohos/qt-5.12.12-ohos/ |
| Target triple | aarch64-linux-ohos |
3.2 CMake toolchain
CopyQ 用 CMake,写一个标准的 ohos-toolchain.cmake,关键开关:
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)
set(CMAKE_C_COMPILER ${OHOS_SDK}/llvm/bin/aarch64-unknown-linux-ohos-clang)
set(CMAKE_CXX_COMPILER ${OHOS_SDK}/llvm/bin/aarch64-unknown-linux-ohos-clang++)
# 关键:visibility 默认 default + export-dynamic
# 否则 HAP 壳 dlsym("main") 会找不到符号
add_compile_options(-fvisibility=default -fPIC)
add_link_options(-Wl,--export-dynamic)
3.3 把可执行目标改成共享库
CopyQ 原本生成 copyq 可执行文件。我们改 src/CMakeLists.txt:
- add_executable(copyq main.cpp ...)
+ add_library(copyq SHARED main.cpp ...)
注意 main.cpp 里的 int main(int argc, char **argv) 一行不动——HAP 壳就是按这个签名 dlsym("main") 然后调用的。
3.4 CMake 配置阶段实测

几个关键信号:
- ✅
Clang 15.0.4—— OHOS NDK 自带的 LLVM - ✅
aarch64-unknown-linux-ohos-clang—— target triple 正确,是鸿蒙 - ✅
OHOS build: using platform/dummy backend—— 让 CopyQ 用 dummy 平台后端(无 X11/Win32 依赖) - ⚠️
CMAKE_DISABLE_FIND_PACKAGE_X11警告 —— 无视,这是我们主动塞的
3.5 编译阶段实测(98 个 .cpp 翻译单元)

100% 干净链接,零警告零错误产出 libcopyq.so。
四、Phase 2:第一次崩溃 —— 符号缺失
打包成 HAP,推到鸿蒙 PC,点启动……闪退。抓 hilog:
05-29 23:11:35.559 W MUSL-LDSO: relocating failed: symbol not found.
dso=/data/storage/el1/bundle/libs/arm64/libcopyq.so
s=qt_resourceFeatureZlib
05-29 23:11:35.559 F dlopen() failed to open library 'libcopyq.so':
Error relocating libcopyq.so: qt_resourceFeatureZlib: symbol not found
根因
CopyQ 链了 Qt5::Core,CMake 自动把 Qt 的 zlib feature 探测代码也拉了进来,引用了 qt_resourceFeatureZlib。但鸿蒙的 Qt-OHOS 5.12.12 库里 没有这个符号——这是 Qt 5.13+ 才引入的内部 API。
修复
在 link 阶段加 -Wl,--unresolved-symbols=ignore-in-shared-libs,并清理 .qrc 资源里的压缩开关:
target_link_options(copyq PRIVATE
-Wl,--unresolved-symbols=ignore-in-shared-libs)
重新链接后验证(真实服务器输出):
$ /root/ohos-sdk/ohos-sdk/linux/native/llvm/bin/llvm-nm -D --undefined-only \
/root/CopyQGOGO/artifacts/libcopyq.so | grep -c qt_resourceFeatureZlib
0
✅ 符号引用清零。
五、Phase 3:第二次崩溃 —— main 跑了 1 秒就 return 1
第一坑修完,再启动。这次 dlopen 成功、main 也调到了,但不到 1 秒应用窗口就消失。日志:
05-29 23:20:07.046 D opened library '/data/storage/el1/bundle/libs/arm64/libcopyq.so' with main function
05-29 23:20:07.046 D 'main' function argv[0]='/data/storage/el1/bundle/libs/arm64/libcopyq.so'
05-29 23:20:07.046 D 'main' function argv[1]='' ← 注意!空字符串
05-29 23:20:08.065 D 'main' function in library returned 1 ← 1 秒后退出
05-29 23:20:08.065 I Qt: asynchronously terminating remaining QAbility instances
根因
读 CopyQ 源码 src/main.cpp:
int startApplication(int argc, char **argv) {
const QStringList arguments = platformNativeInterface()->getCommandLineArguments(argc, argv);
int skipArguments = 0;
const QString sessionName = getSessionName(arguments, &skipArguments);
// ...
// 关键判断:没有任何参数 → 启动 server(GUI 模式)
if (skipArguments == arguments.size())
return startServer(argc, argv, sessionName);
// 有参数 → 当作命令传给 client
return startClient(argc, argv, arguments.mid(skipArguments), sessionName);
}
HAP 壳调 main 时 argv[1] = ""(空串),但 argc=2。CopyQ 把空串当成了一个有效 argument,于是 arguments.size()=1, skipArguments=0,走进了 client 分支——尝试连接已运行的 server,连不上,return 1。
这是 OHOS HAP 壳的"特色"——它会给 main 塞一个空串占位,普通 Qt 应用对此免疫,但 CopyQ 这种带命令行解析的应用就栽了。
修复
在 main() 入口先过滤空串:
int main(int argc, char **argv) {
// OHOS HAP shell may pass empty string args; filter them out
int newArgc = 0;
for (int i = 0; i < argc; ++i) {
if (argv[i] && argv[i][0] != '\0') {
argv[newArgc++] = argv[i];
}
}
argc = newArgc;
try {
return startApplication(argc, argv);
} ...
}
服务器上验证 patch 已落地(真实输出):
$ grep -A2 "OHOS HAP" /root/CopyQGOGO/CopyQ-9.1.0/src/main.cpp | head -10
// OHOS HAP shell may pass empty string args; filter them out
int newArgc = 0;
for (int i = 0; i < argc; ++i) {
增量重编:
$ cd /root/CopyQGOGO/build && make -C src copyq -j4
[ 2%] Building CXX object src/CMakeFiles/copyq.dir/main.cpp.o
[ 3%] Linking CXX shared library libcopyq.so
[100%] Built target copyq
六、Phase 4:第三次崩溃 —— SystemTrayIcon API 限流
修完前两个坑,再启动。这次进度条又往前推了一大步,已经能看到 ArkUI 把窗口画出来了……然后又崩。这次有完整的 cppcrash 文件:
LastFatalMessage: terminating due to uncaught exception of type
Napi::Error: The API is being called too frequently.
Tid:55895, Name:mple.qtohosdemo
#11 pc 002c9e50 libcopyq.so MainWindow::loadSettings(QSettings&, AppConfig*)+5020
#12 pc 0020d124 libcopyq.so ClipboardServer::loadSettings(AppConfig*)+2648
#13 pc 0020ae34 libcopyq.so ClipboardServer::ClipboardServer(QApplication*, QString const&)+2460
#14 pc 00324540 libcopyq.so main+4212
完整调用链(向上展开):
main ← Phase 3 修过,已能进入
└─ ClipboardServer 构造
└─ loadSettings()
└─ MainWindow::loadSettings()
└─ QSystemTrayIcon::setIcon(QIcon&) ← 设置托盘图标
└─ libqohos.so / libQt5Widgets.so
└─ runInJsThreadAndWait ← 桥到 ArkTS JS 线程
└─ NAPI: statusBarManager.setIcon(...)
└─ throw Napi::Error("The API is being called too frequently")
└─ 没人 catch
└─ __cxa_rethrow → abort
根因
鸿蒙 PC 没有"系统托盘"概念,Qt-OHOS 平台插件用顶部状态栏 statusBarManager API 模拟 QSystemTrayIcon。该 API 有调用频率限制,CopyQ 启动时短时间内连发多次 setIcon()(主图标 + 小图标 + 监听图标)→ 触发限流 → 抛异常 → 没 catch → abort。
修复
src/gui/mainwindow.cpp:2802,在 loadSettings() 中:
- setTrayEnabled( !appConfig->option<Config::disable_tray>() );
+ setTrayEnabled(false); // OHOS: disable tray to avoid statusBarManager rate limit
理由:
- CopyQ 主窗口(剪贴板历史 + 工具栏)不依赖托盘,照样能用
- 鸿蒙 PC 已有自己的 dock 栏管理图标,托盘是 "Linux/Windows 桌面"概念,不适用
- 最小侵入:1 行改动
服务器上验证(真实输出):
$ grep "OHOS:" /root/CopyQGOGO/CopyQ-9.1.0/src/gui/mainwindow.cpp
setTrayEnabled(false); // OHOS: disable tray to avoid statusBarManager rate limit
七、最终成果验证

上图浓缩了下面 7.1~7.3 全部验证项,可作整体一览。下面分项展开:
7.1 服务器产物(真实命令输出)
$ ls -la /root/CopyQGOGO/artifacts/
-rwxr-xr-x 1 root root 4883776 May 29 23:28 libcopyq.so
$ /root/ohos-sdk/ohos-sdk/linux/native/llvm/bin/llvm-readelf -h \
/root/CopyQGOGO/artifacts/libcopyq.so | head -10
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Shared object file)
Machine: AArch64
Version: 0x1
7.2 依赖共享库(真实输出)
$ /root/ohos-sdk/ohos-sdk/linux/native/llvm/bin/llvm-readelf -d \
/root/CopyQGOGO/artifacts/libcopyq.so | grep NEEDED
0x0000000000000001 (NEEDED) Shared library: [libQt5Svg.so]
0x0000000000000001 (NEEDED) Shared library: [libQt5Xml.so]
0x0000000000000001 (NEEDED) Shared library: [libQt5Qml.so]
0x0000000000000001 (NEEDED) Shared library: [libQt5Widgets.so]
0x0000000000000001 (NEEDED) Shared library: [libQt5Gui.so]
0x0000000000000001 (NEEDED) Shared library: [libQt5Network.so]
0x0000000000000001 (NEEDED) Shared library: [libQt5Core.so]
0x0000000000000001 (NEEDED) Shared library: [libc++_shared.so]
0x0000000000000001 (NEEDED) Shared library: [libc.so]
干净!9 个动态库依赖,全部为 Qt-OHOS 标准件 + libc。无任何 desktop 残留(如 libGL、libX11、libdbus)。
7.3 main 符号成功导出(真实输出)
$ /root/ohos-sdk/ohos-sdk/linux/native/llvm/bin/llvm-nm -D --defined-only \
/root/CopyQGOGO/artifacts/libcopyq.so | grep -E " T main$"
0000000000323484 T main
$ /root/ohos-sdk/ohos-sdk/linux/native/llvm/bin/llvm-nm -D --undefined-only \
/root/CopyQGOGO/artifacts/libcopyq.so | grep -c qt_resourceFeatureZlib
0
✅ main 符号导出(HAP 壳能 dlsym 到)
✅ qt_resourceFeatureZlib 未引用计数 = 0(Phase 2 patch 仍生效)
📸 Phase 3 增量重编日志(patch 后只重编 main.cpp + 重链接,6 秒搞定)

7.4 HAP 工程产物(真实 Mac 端输出)

$ ls -la /Users/zhubo/Downloads/QtOnHarmonyOSPC/CopyQ-OHOS/CopyQ-HAP/entry/libs/arm64-v8a/
-rw-r--r-- 35506944 libQt5Core.so
-rw-r--r-- 39324352 libQt5Gui.so
-rw-r--r-- 12139200 libQt5Network.so
-rw-r--r-- 5339904 libQt5OhosExtras.so
-rw-r--r-- 42080712 libQt5Qml.so
-rw-r--r-- 2561816 libQt5Svg.so
-rw-r--r-- 37788352 libQt5Widgets.so
-rw-r--r-- 1202968 libQt5Xml.so
-rw-r--r-- 1267392 libc++_shared.so
-rwxr-xr-x 4883776 libcopyq.so ← 我们的产物
-rw-r--r-- 156203776 libqohos.so ← qt-ohos 平台插件
drwxr-xr-x platforms/
drwxr-xr-x styles/
$ file libcopyq.so
libcopyq.so: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV),
dynamically linked, with debug_info, not stripped
八、踩坑沉淀:Qt → 鸿蒙 PC 适配方法论
经过 CopyQ + 之前 9 个项目,我把"通用三连击坑"总结成:
| 阶段 | 现象 | 根因 | 修复 |
|---|---|---|---|
| dlopen 阶段 | symbol not found |
Qt 版本符号差异 | --unresolved-symbols=ignore-in-shared-libs |
| main 入口阶段 | main returned 1/直接退出 |
argv[1]=“” 空串占位 | 入口处过滤空字符串 argv |
| GUI 初始化阶段 | Napi::Error: too frequently |
调用 OHOS 受限 API 过快 | 禁用 QSystemTrayIcon / 加节流 |
只要把这三关都过了,剩下的问题基本都是业务逻辑层面的、可控的细节。
不通用的坑:CopyQ 特色
- ✅ client/server 单 main 入口:必须先过 argv 过滤,否则进 client 分支必崩
- ✅ 托盘启动顺序:
MainWindow::loadSettings里就调setTrayEnabled,比创建主窗口还早 - ⚠️ session 名单:CopyQ 用
QSettings持久化 session,鸿蒙沙箱目录权限要确认(本次没踩到)
九、最终启动效果

修完三个 patch、重编 4.66MB 的 libcopyq.so、推到鸿蒙 PC:




十、常见问题(FAQ)
Q1:argv[1]="" 这个空串到底是谁塞进去的?能在 HAP 层修复吗?
A:是 Qt-OHOS 平台插件的实现细节:它会把 QAbility 拉起时拿到的 Want 参数转成 argv 传给 main,即使上层没传任何参数,为了保证 argc>=1 且与 POSIX 习惯一致,插件会主动填一个 argv[0]=".so 路径" + argv[1]=""。HAP 层动不了(除非你同时修改 qt-ohos 插件源码并重编 libqohos.so,代价太大)。最佳实践是在应用的 main() 入口头加三行空串过滤,这不仅适用于 CopyQ,凡是用了 QCommandLineParser / getopt 的 Qt 应用都建议默认上这个 patch。
Q2:禁掉 QSystemTrayIcon 之后,用户怎么快速唤出 CopyQ 历史面板?
A:原生 Linux/Windows 上是点托盘图标或全局快捷键(Ctrl+Shift+V)。鸿蒙 PC 上:
- 代替方案 1:dock 栏。HAP 安装后默认在鸿蒙 PC 的应用抽屉 / dock 里可点,不需额外入口。
- 代替方案 2:HAP 侧注册全局快捷键。鸿蒙提供
inputMonitor/globalShortcutAPI(需声明权限),可以在 ArkTS 侧抓Ctrl+Shift+V后通过 NAPI 跳回 C++ 层调MainWindow::showAt(),这样不需要托盘 API 也能实现原本习惯。本次适配为了快速交付,第一期只做了方案 1;方案 2 量大金在处,会放到 v1.1 迫加。
更多推荐





所有评论(0)