欢迎加入开源鸿蒙 PC 社区:https://harmonypc.csdn.net/

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

适配开源地址:https://atomgit.com/OpenHarmonyPCDeveloper/ohos_QucsS

电路仿真软件是工程师桌面上最"重"的一类工具:安装包以 GB 计、菜单深似海、上手一周起步。我把电子工程圈用了二十多年的开源电路仿真器 Qucs-S 完整搬上了 HarmonyOS PC,做成应用「电路芯绘」——原理图编辑、元件库、仿真配置、工程管理、完整中文化,在 2in1 电脑上原生运行。

这不是一篇"一次过审"的爽文。第一次提审,被以「功能异常」四个字打回;从归因定位到七处补丁修复,再到复审通过正式上架,这条弯路走完恰好补齐了上架教程里最缺的一段:审核到底在检查什么,桌面应用的哪些"无伤大雅",在审核语境下都是扣分实锤。文中代码均取自上架工程的真实源码,截图来自 AGC 控制台、鸿蒙 PC 真机与应用市场实拍。

先看最终成果——在鸿蒙 PC 的应用市场里,它已经站在可以点击打开的位置:

鸿蒙 PC 应用市场上的电路芯绘

图1:上架后的应用市场详情页实拍。副标题"鸿蒙 PC 上的专业电路设计与仿真",分类「工具」,年龄分级 3+,版本 1.0.0 (1000000)。截图时正值上架初期:安装 <1万次、1 个评分 5.0——每个新应用都从这条起跑线出发。

一、作品速览:电路芯绘是什么

电路芯绘是一款面向 HarmonyOS PC(2in1)设备的电路原理图设计与仿真配置工具,源自开源项目 Qucs-S(Quite Universal Circuit Simulator - S, https://github.com/ra3xdh/qucs_s,GPL-2.0)24.4.1 版。

打开应用,扑面而来的是完整的桌面级专业界面:顶部 文件/编辑/对齐/插入/项目/工具/仿真/视图/帮助 九个菜单,左侧是集总参数元件面板(电阻、电容、电感、带 Q 值的版本、电位计、互感器、3 线圈互感、变压器、探针、开关、继电器……支持关键字搜索),中央是无限画布的原理图编辑区:

应用介绍图:内置示例工程,仿真配置块与方程块

图2:应用介绍图。内置示例工程直接可用,原理图上以图形化"仿真块"(瞬态 TR / 交流 AC / 直流 DC)与"方程块"(K=out.v/in.v 计算增益、Pwr 计算功率)配置仿真参数——这是 Qucs 系工具链经典的"图纸即配置"工作流。

随手拖出一张单管放大电路,就能体会它的专业度:信号源 V2(200 mV、4 kHz)经耦合电容 C1 进入 Q2N2222A 三极管基极,R3/R5 构成集电极偏置,R1/R2 分压偏置,输出经 C2 耦合到负载 R4——每个元件的参数都点击可编辑,画布上直接挂仿真指令与后处理方程:

在这里插入图片描述

图3:内置示例工程 demo_amplifier 运行实拍。放大电路 + 三个仿真块 + 方程块一应俱全,元件标注、节点命名(in/out)、参数符号(R=RLoad)全部与桌面版一致。

功能面概括起来是五块:

  • 原理图编辑:wire 布线、拖放放置、旋转/镜像、文本标注、节点命名,全套 CAD 级编辑操作;
  • 元件库:集总参数元件、探针、开关继电器、互感变压器等多类元件,参数点击即改;
  • 仿真配置:瞬态(TR)、交流(AC)、直流(DC)等仿真指令以图形化块的形式挂在图纸上,起止频率、步数、积分方式逐项可配;
  • 工程管理:以 _prj 工程目录组织原理图与数据文件,项目侧栏 + 「文档」标签页双入口;
  • 完整中文化:菜单、对话框、工程分类全部中文本地化(第五节会讲这 2700 多条翻译怎么进包的)。

至于"为什么不把 SPICE 仿真内核一起搬进来"——这个问题的答案恰恰是本文第四节审核打回的引线,先按下不表。

二、上架路线:不重写,原样编译

一条与众不同的路线

我此前上架的两个鸿蒙 PC 应用(化学结构编辑器、光学设计工具)走的都是纯 ArkTS 重写路线:把上游算法逐段翻译成 ArkTS,包里没有一行 C++。电路芯绘反其道而行——上游 660 个 Qt/C++ 源文件,原样参与编译,一行业务逻辑没动。

原因是体量与复利:Qucs-S 二十多年的元件模型、绘图引擎、对话框体系全在 C++ 里,重写既不现实也不经济。移植策略一句话概括:

真实 Qucs-S 24.4.1 上游源码原样交叉编译进 libqucs_shell.so(9.1 MB),链接 Qt for OpenHarmony 5.12.12 运行时(libqohos.so,156 MB),以官方 Qt-on-OHOS 模板承载 XComponent 装进 HAP。

为什么选 24.4.1 这个版本节点也有讲究:25.x 起上游默认 Qt6,而 Qt for OpenHarmony 运行时只有 Qt 5.12.12——版本锚点必须卡在"最后一个 Qt5 时代的稳定版"上,这不是选最新,而是选"与运行时 ABI 兼容的最新"。

顺带回应一个常见疑问:这种 156 MB Qt 运行时 + 9 MB 业务 so 的"重装应用",AGC 收不收?收。上架流程与应用的内部形态完全无关——纯 ArkTS 的轻应用是几百 KB,这里是 136 MB 的 .app,落到 AGC 眼里都是"一个带合法签名的包",走同一条流水线。本篇就是这个论断的最新实证。

工程结构自证

整个工程的目录一眼看下去,"移植"二字落在实处:

ohos_QucsS/
├── native/                            # 交叉编译工程
│   ├── CMakeLists.txt                 # 交叉编译脚本(glob 上游源码 + Q_OBJECT 自动 moc + QtSvg 三件套)
│   ├── build_ohos.cmd                 # Windows 一键构建入口(SDK cmake + ninja)
│   ├── ohos/                          # 本次移植新写的 OHOS 适配层
│   │   ├── config.h                   # 替代上游 config.h.cmake
│   │   ├── env_setup.cpp              # 静态构造器:沙箱环境变量 + 首启播种示例工程
│   │   └── shims/{QPrinter,QtSvg}     # 垫片头(前置声明 / no-op)
│   ├── vendor/qucs-s/                 # 上游 24.4.1 真实源码(仅 7 处修复,全部宏门控)
│   │   └── translations/qucs_zh_CN.ts # 中文翻译源文件(606 KB)
│   ├── qt5.12.12-headers/             # 官方真实 Qt 5.12.12 头文件
│   └── qt5.12.12-win/                 # Windows 宿主工具 moc/rcc/uic/lrelease
└── harmony_pc/                        # DevEco HAP 工程(Qt for OpenHarmony 模板骨架)
    ├── AppScope/app.json5             # bundleName=com.qucs.circuit
    └── entry/
        ├── libs/arm64-v8a/
        │   ├── libqohos.so            # Qt for OpenHarmony QPA 平台运行时(156 MB)
        │   ├── libQt5{Core,Gui,Widgets,Network}.so
        │   ├── libQt5Svg.so           # 自交叉编译(1.3 MB)
        │   ├── iconengines/libqsvgicon.so   # QIcon SVG 引擎(图标渲染的关键)
        │   └── libqucs_shell.so       # 真实 Qucs-S 24.4.1 源码交叉编译产物(9.1 MB)
        └── src/main/
            ├── ets/qability/          # 官方 Qt for OpenHarmony 模板代码,零改动
            └── resources/resfile/
                ├── translations/qucs_zh_CN.qm
                └── examples/demo_amplifier.sch

对上游源码的全部改动只有 7 处修复点,每一处都用 QUCS_OHOS_PORT 宏门控——桌面端编译出口一行不变,鸿蒙端走条件分支。这个纪律是工程纪律和审核安全的双重保障,第五节逐段展开。

交叉编译体系:moc 是第一道坎

Qt 应用交叉编译的最大工作量不在 C++ 本身,而在 Qt 的元对象工具链。上游 Qucs-S 有几十个含 Q_OBJECT 的头文件,每个都要先经 moc 生成元对象代码才能编译。CMakeLists 的解法是对全部上游头文件做 grep 门控的自动 moc——只处理含 Q_OBJECT 的头,且输出文件名里掺入路径哈希避免重名冲突:

# moc: run the REAL Qt 5.12.12 Windows moc over every upstream header under
# qucs/ that contains Q_OBJECT/Q_GADGET (grep-gated: moc on a header without
# Q_OBJECT produces an empty file but still costs time).
file(GLOB_RECURSE QUCS_MOC_HEADERS "${QUCS_DIR}/*.h")
list(FILTER QUCS_MOC_HEADERS EXCLUDE REGEX "/xspice/|/python/|/octave/")

foreach(_hdr ${QUCS_MOC_HEADERS})
  file(READ "${_hdr}" _content)
  string(FIND "${_content}" "Q_OBJECT" _qobj)
  if(NOT _qobj EQUAL -1)
    get_filename_component(_base "${_hdr}" NAME_WE)
    string(MD5 _hash "${_hdr}")
    string(SUBSTRING "${_hash}" 0 8 _hash8)
    set(_out "${MOC_OUT_DIR}/moc_${_base}_${_hash8}.cpp")
    if(NOT EXISTS "${_out}" OR "${_hdr}" IS_NEWER_THAN "${_out}")
      execute_process(COMMAND "${MOC_EXE}" ${MOC_INCLUDE_ARGS} "${_hdr}" -o "${_out}")
    endif()
    if(EXISTS "${_out}")
      list(APPEND QUCS_MOC_SOURCES "${_out}")
    endif()
  endif()
endforeach()

源码清单直接递归 glob 上游目录,排除三个与 GUI 无关的子目录,再追加两个适配层文件:

file(GLOB_RECURSE QUCS_SOURCES "${QUCS_DIR}/*.cpp")
list(FILTER QUCS_SOURCES EXCLUDE REGEX "/printerwriter\\.cpp$|/xspice/|/python/|/octave/")
list(APPEND QUCS_SOURCES
  "${CMAKE_CURRENT_SOURCE_DIR}/ohos/env_setup.cpp"
  "${CMAKE_CURRENT_SOURCE_DIR}/ohos/printerwriter_ohos.cpp")

add_library(qucs_shell SHARED
  ${QUCS_SOURCES}
  ${QUCS_MOC_SOURCES}
  "${QRC_OUT}")        # rcc 编译的元件位图资源

链接段值得注意的一点:业务 so 必须链接 HAP 内自带的那套 Qt 运行时(entry/libs/arm64-v8a 下的 libQt5*.so),而不是系统库——沙箱里没有"系统 Qt"这种东西:

target_link_directories(qucs_shell PRIVATE "${QUCS_QT_LIBS}")
target_link_libraries(qucs_shell PRIVATE
  Qt5Widgets
  Qt5Gui
  Qt5Network
  Qt5Core
  c++_shared
  dl)

一个差点让工具栏全空成灰条的坑:QtSvg 三件套

真机首启验收时踩到一个代表性问题:应用能启动、菜单齐全,但工具栏所有按钮的图标全空,只剩一排扁平的灰色按钮——而左侧元件面板的图标却完好无损。

排查到底,根因藏在 Qucs 的资源加载方式里:上游每个工具栏动作的图标都是 SVG 资源(QIcon(":/bitmaps/svg/*.svg")),QIcon 走 SVG 需要 Qt 的图标引擎插件 iconengines/libqsvgicon.so,背后还要 libQt5Svg.so——而预置的 Qt-for-OHOS 运行时两样都不带。元件面板图标不受影响纯属侥幸:它们是 QPainter 直接画的,不走 QIcon。

解法没有捷径:从官方 Qt 5.12.12 的 qtsvg 源码把三件套自己交叉编译出来——libQt5Svg.so(1.3 MB)、图标引擎插件、QPixmap 的 SVG 读取插件,全部链到 HAP 自带的 Qt 运行时上:

# QtSvg runtime — cross-compiled from official Qt 5.12.12 qtsvg sources.
# Every toolbar QAction icon in qucs-s is an SVG resource
# (QIcon(":/bitmaps/svg/*.svg")); QIcon requires the iconengines/qsvgicon
# plugin backed by libQt5Svg.so. The prebuilt Qt-for-OHOS runtime ships
# neither, so all toolbar icons render empty.
file(GLOB ZLIB_SOURCES "${QTBASE_SRC}/src/3rdparty/zlib/src/*.c")
file(GLOB QTSVG_SOURCES "${QTSVG_SRC}/src/svg/*.cpp")
add_library(Qt5Svg SHARED ${QTSVG_SOURCES} ${QTSVG_MOC_SOURCES} ${ZLIB_SOURCES})
target_link_libraries(Qt5Svg PRIVATE Qt5Widgets Qt5Gui Qt5Core c++_shared)

# --- iconengines/libqsvgicon.so (the QIcon SVG engine — the actual fix) ---
file(GLOB QSVGICON_SOURCES "${QTSVG_SRC}/src/plugins/iconengines/svgiconengine/*.cpp")
add_library(qsvgicon SHARED ${QSVGICON_SOURCES} "${QTSVG_MOC_OUT}/moc_qsvgiconengine.cpp")
target_compile_definitions(qsvgicon PRIVATE -DQT_NO_DEBUG -DQT_PLUGIN)

(ZLIB_SOURCES 一并打进来是因为 OHOS 运行时的 libQt5Core.so 不导出内置 zlib 符号,SVG 渲染需要的 zlib 得自带。)三个产物 POST_BUILD 时自动拷进 HAP 的 libs 树对应目录。这段插曲的意义在后文审核环节才完全显形:如果图标是空的,任何一张真机截图都会立刻露馅——审核员看到的第一个界面就是工具栏。

一键构建脚本

以上全部被 build_ohos.cmd 收敛成一条命令:清理构建目录 → SDK 自带 cmake 以 ohos.toolchain.cmake 配置(OHOS_ARCH=arm64-v8a)→ ninja 全量编译 → 用 llvm-readelf 校验产物依赖 → 把 libqucs_shell.so 安装进 HAP 的 libs 目录:

cmake -G Ninja ^
  -DCMAKE_TOOLCHAIN_FILE="%OHOS_SDK_NATIVE%\build\cmake\ohos.toolchain.cmake" ^
  -DOHOS_ARCH=arm64-v8a ^
  -DOHOS_SDK_NATIVE="%OHOS_SDK_NATIVE%" .
cmake --build .
llvm-readelf -d libqucs_shell.so | findstr NEEDED
copy /y libqucs_shell.so ..\harmony_pc\entry\libs\arm64-v8a\

全量重编 413 个上游 C++ 文件 + 适配层,产出 9.1 MB 的 libqucs_shell.so;随后 hvigor assembleHap 出包。补丁轮的整个"改代码 → 出包 → 真机回归"循环,靠的就是这条流水线的分钟级反馈。
运行实拍:内置 demo_amplifier 示例工程

三、上架流水线:从 AGC 建应用到 .app 包

1. 包名与版本号。 AGC 创建应用时绑定包名,一经上架锁死。AppScope/app.json5:

{
  "app": {
    "bundleName": "com.qucs.circuit",
    "vendor": "qucs-s",
    "versionCode": 1000000,     // 1.0.0:主版本占高位,次版本依次排开
    "versionName": "1.0.0",
    "icon": "$media:app_icon",
    "label": "$string:app_name"
  }
}

versionCode 沿用"主版本占高位"的十进制编码:1.0.0 → 1000000。AGC 要求每次提审 versionCode 严格递增,规则定早了,发版不用现想。

2. deviceTypes 必须有 2in1。 鸿蒙 PC 应用在 module.json5 里声明设备类型,漏了电脑端市场直接搜不到。顺带一提工程里留着一行有价值的注释——Qt-OHOS 模板自己记录了一个已知问题:

"deviceTypes": [
  // When 'phone' is added to the list, the main window does not restore itself
  // to the desired state after being minimized (QTFOROH-1076). Before it is
  // added back, be sure that the issue has been resolved.
  "tablet",
  "2in1"
],

3. 权限逐项核对——桌面移植应用最容易翻车的地方。 惯性做法是"上游要什么权限就声明什么",但鸿蒙的权限语义完全不同,多声明一项就多一份隐私材料与审核问询面。本工程的权限表最终收敛为 5 项,每一项都在源码里写明了"为什么非它不可",这段配置本身就是上架材料的底稿:

"requestPermissions": [
  {
    // READ_PASTEBOARD(受限开放权限):原理图编辑器的复制/粘贴功能,
    // Qt 剪贴板读取需要该权限;无此权限粘贴功能不可用。
    "name": "ohos.permission.READ_PASTEBOARD",
    "reason": "$string:perm_pasteboard_reason",
    "usedScene": { "abilities": ["QAbility"], "when": "inuse" }
  },
  {
    // 通过系统文件选择器打开/保存原理图文件后,持久化所授权的文件访问权限,
    // 使用户下次打开同一文件无需重新授权。
    "name": "ohos.permission.FILE_ACCESS_PERSIST",
    "reason": "$string:perm_file_persist_reason",
    "usedScene": { "abilities": ["QAbility"], "when": "inuse" }
  },
  {
    // Qt QPA 层(libqohos)窗口关闭拦截回调:退出应用前允许上游代码
    // 提示保存未完成的原理图编辑内容。
    "name": "ohos.permission.PREPARE_APP_TERMINATE",
    "reason": "$string:perm_terminate_reason",
    "usedScene": { "abilities": ["QAbility"], "when": "always" }
  },
  {
    // 保存应用设置(QSettings)与工作区目录等本地持久化数据。
    "name": "ohos.permission.STORE_PERSISTENT_DATA",
    "reason": "$string:perm_store_reason",
    "usedScene": { "abilities": ["QAbility"], "when": "inuse" }
  },
  {
    // 打开/保存原理图文件的系统文件选择器中显示文件图标。
    "name": "ohos.permission.GET_FILE_ICON",
    "reason": "$string:perm_file_icon_reason",
    "usedScene": { "abilities": ["QAbility"], "when": "inuse" }
  }
]

注意没有 INTERNET——桌面 Qucs-S 的联网场景是调用外部仿真内核与更新检查,鸿蒙版两者都不存在,网络权限自然清零。每一项权限的 reason 都会在隐私材料与审核问询里被引用,写 reason 的过程就是权限自查的过程:写不出"非它不可"理由的权限,就不该出现在表里。其中 READ_PASTEBOARD 属受限开放权限,AGC 提审时会要求如实说明使用场景——"编辑器复制粘贴"这个理由简洁、真实、可验证。

4. 发布签名与 .app 包。 平时 hdc 直装用 debug 自动签名,提审必须发布证书 + 发布 Profile 签出的包。DevEco 里 Build > Build App(s) 产出 .app 应用级发布包。最终上传 AGC 的软件包 harmony_pc-default-signed.app,136.10 MB,版本 1.0.0 (1000000)。

5. 软件包加密:这次轮到"有 .so 的一方"来面对它。 AGC 的加密选项提示写得很清楚:包内 appx 格式文件支持加密,.so 格式文件不支持加密、明文打进应用包。纯 ArkTS 应用可以宣称"加密覆盖全部核心代码";而本应用的核心业务逻辑恰恰在 libqucs_shell.so 里——落在加密范围之外。

怎么办?看清一个事实就不焦虑了:Qucs-S 本身是 GPL 开源软件,源码在 GitHub 上公开,so 里没有任何需要保密的东西。加密保护的是"编译产物不被人轻易逆向",而对一个本来就开源的项目,这层保护的意义天然有限。所以结论很简单:加密选项照选"加密(推荐)"(没有额外成本),同时清楚它的边界——真正需要保密的逻辑,要么放 ArkTS 层吃加密保护,要么(像本项目这样)本来就是公开源码,谈不上泄密。

四、第一次提审:打回「功能异常」

10 月中旬首包提审。几天后状态跳红:审核不通过,理由「功能异常」。

AGC 的驳回理由就这四个字,没有堆栈、没有截图、没有复现步骤。怎么办?答案只有一条:把自己当审核员,在真机上把应用的每个入口点一遍——每个菜单、每个工具栏按钮、每个侧栏标签、每个对话框。审核员真的会这么点。

这么点一遍,两处"点了不对劲"浮出水面:

现象①:工具菜单弹出「无法启动程序」

桌面版 Qucs-S 的「工具」菜单下挂着九件套:文本编辑器、滤波器综合、有源滤波器、传输线计算、匹配电路综合、衰减器计算、功率合成、RF Layout、S 参数查看器。它们的实现方式惊人地统一——每一个都是独立的外部可执行程序,主程序用 QProcess 拉起。统一入口 launchTool(qucs_actions.cpp):

// qucs_actions.cpp —— 桌面版启动外部子工具的统一入口
void QucsApp::launchTool(const QString& prog, const QString& progDesc,
                         const QStringList &args, bool qucs_tool)
{
  QProcess *tool = new QProcess();
  QString tooldir;
  if (qucs_tool) tooldir = QucsSettings.QucsatorDir;
  else           tooldir = QucsSettings.BinDir;
  // 按 OS 拼 Cmd:tooldir + prog(Windows 补 .exe,macOS 走 .app 包内路径)
  tool->setWorkingDirectory(tooldir);
  tool->start(cmd, args);

  if(!tool->waitForStarted(1000) ) {
    QMessageBox::critical(this, tr("Error"),
                          tr("Cannot start %1 program! \n\n(%2)").arg(progDesc, cmd));
    delete tool;
    return;
  }
}

问题清清楚楚:这些 qucs-filter、qucs-attenuator、qucs-spar-viewer 子工具一个都没打进 HAP,而鸿蒙应用沙箱根本禁止 fork/exec 子进程。waitForStarted(1000) 必然超时,审核员点一个弹一个错误框——中文界面下就是那句「无法启动程序」。在桌面上是"没装全家桶"的正常体验,在审核环境里就是连续弹错的"功能异常"。

现象②:「文档」标签页点开没有内容

Qucs 的工程模型有个容易踩的隐性约定:只有目录名以 _prj 结尾的文件夹才算工程,工程列表只显示目录;而「文档」(Content)标签页本质是"当前工程的文件管理器",把工程内文件按 10 个分类展示(原理图、符号、数据集、数据显示页、Verilog、VHDL、SPICE、备注、其他……)。

移植首启时只把示例原理图 demo_amplifier.sch 拷进了工作区——一个裸 .sch 文件。后果链式爆发:工程列表里没有任何可打开的工程 → 「文档」标签页无工程可展示 → 10 个分类里 9 个永远为空(沙箱里没有仿真数据、没有 Octave、没有 HDL)→ 双击空分类零响应。审核员视角:这个标签页就是"点不开、空的、坏的"。

归因:不是功能缺失,是"裁剪不彻底"

两条现象指向同一个根因:桌面行为的平台化裁剪做了一半。沙箱禁 exec 决定了所有依赖外部子进程的功能都不可能工作,这个事实从移植第一天就清楚;但"不可能工作"和"审核员看不见"之间差着一整层 UI 收口——菜单还挂着、按钮还在弹错误框、空分类还立在那里。桌面端的"体验瑕疵",在审核语境下全部升级为"功能异常"实锤。审核员没有义务替你区分"有意裁剪"与"意外损坏",他只看入口背后的行为。

五、修复实录:一处宏门控,七处补丁

修复原则延续移植纪律:全部改动用 #ifdef QUCS_OHOS_PORT 门控,桌面端代码一行不动。修复点对应表:

#文件现象动作
1qucs_init.cpp工具菜单弹「无法启动程序」菜单只留应用内实现的入口,9 个外部子工具全部不挂
2qucs_init.cpp仿真菜单/工具栏依赖外部内核整菜单 setEnabled(false);仿真工具栏不创建
3qucs_init.cpp状态栏常驻 “no warnings” 属于无效装饰保留对象、清空文案
4qucs.cpp / projectView.cpp「文档」空分类点不开不添加无数据的器件库标签页;refresh 时删除全部空分类
5projectView.cpp删分类后导出逻辑写死行号错位按 tr() 表头文字定位
6env_setup.cpp + main.cpp首启无工程、翻译与示例路径失效首启播种完整 _prj 工程;路径指向 resfile、默认简体中文
7misc.cpp窗口标题沿用"Qucs-S 24.4.1"英文格式本地化为「电路芯绘」

补丁①:工具菜单——只让审核员看见能工作的东西

保留的两项 callMatch(匹配电路综合)与 callConverter 是应用内 Qt 对话框实现(slotCallMatch 直接 new MatchDialog(this); d->exec(),沙箱内安全),其余全部不挂:

  toolMenu = new QMenu(tr("&Tools"));  // menuBar entry toolMenu
#ifdef QUCS_OHOS_PORT
  // OHOS: only keep tools that run inside the app (Qt dialogs). All external
  // tool actions (text editor / filter / active-filter / trans / attenuator /
  // powercombining / RF-layout / spar-viewer / compact modelling) spawn child
  // processes via QProcess, which the HarmonyOS sandbox forbids — they all pop
  // "Cannot start program". Hide them so the app only exposes working features.
  toolMenu->addAction(callMatch);
  toolMenu->addAction(callConverter);
#else
  toolMenu->addAction(callEditor);      // 桌面版:九件套全部挂载
  toolMenu->addAction(callFilter);
  // ... callActiveFilter / callLine / callAtt / callPwrComb
  // ... callRFLayout / callSPAR_Viewer + Compact modelling 子菜单
#endif

补丁②:仿真菜单、仿真工具栏与状态栏

仿真菜单同理——桌面版八项动作全部依赖外部仿真内核或外部查看器,鸿蒙端整个菜单置灰:

  simMenu = new QMenu(tr("&Simulation"));
#ifdef QUCS_OHOS_PORT
  // OHOS: no simulator kernel (qucsator/ngspice) or external viewer/editor is
  // shipped; every entry here either execs a child process or shows a netlist
  // in the external text editor — all fail in the sandbox. Disable the whole
  // simulation menu so the reviewer only sees features that work.
  simMenu->setEnabled(false);
#else
  simMenu->addAction(simulate);
  // ... tune / dpl_sch / dcbias / showMsg / showNet / save_netlist / simSettings
#endif

同一判据推到工具栏:桌面版工具栏上有仿真器下拉框 + 仿真/调谐/显示等五个动作,全靠外部进程,鸿蒙端整个工具栏不创建:

#ifdef QUCS_OHOS_PORT
  // OHOS: simulation toolbar exposes simulator selector + simulate/tune, all
  // of which need an external simulator process. Omit the toolbar entirely.
#else
  simulateToolbar = new QToolBar(tr("Simulate"));
  this->addToolBar(simulateToolbar);
  simulateToolbar->addWidget(reinterpret_cast<QWidget *>(simulatorsCombobox));
  simulateToolbar->addAction(simulate);
  simulateToolbar->addAction(tune);
  // ... dpl_sch / setMarker / setDiagramLimits
#endif

状态栏还有个更隐蔽的细节:桌面版常驻一个 “no warnings”(无警告)标签,本意是仿真警告的占位显示。沙箱里永远不会有仿真警告,一行常驻的"无警告"沦为纯装饰噪音——处理方式很克制:保留标签对象(别的槽函数还在引用它,删对象会编译失败),只清空初始文案:

  WarningLabel = new QLabel(tr("no warnings"), statusBar());
#ifdef QUCS_OHOS_PORT
  // OHOS: no simulation kernels are shipped, so a permanent "no warnings"
  // caption is pure chrome noise. Keep the label object (slotShowWarnings
  // still uses it) but start it empty.
  WarningLabel->setText(QString());
#endif
  statusBar()->addWidget(WarningLabel, 0);

同一轮把项目菜单的 Verilog-A 模块构建/加载(依赖外部 admsXml/openvaf)、视图菜单的 Octave 停靠(依赖外部 Octave 进程)、无数据的器件库标签页一并收掉。判据始终只有一条:这个入口被点击后,在沙箱内是否有真实、正确的行为。

补丁④:「文档」分类——把"空"从界面上抹掉

工程文件管理器的 refresh() 末尾追加一段:自后向前删除所有空分类行(倒序删除保证前面的行号不失效):

#ifdef QUCS_OHOS_PORT
  // OHOS: on HarmonyOS there is no simulator/Octave/HDL backend, so categories
  // such as Datasets / Data Displays / VHDL / Octave stay empty forever. An
  // empty category row looks "broken" (double-click does nothing). Remove all
  // empty categories so every visible row is a real, openable entry.
  for (int row = m_model->rowCount() - 1; row >= 0; --row) {
    QStandardItem *cat = m_model->item(row, 0);
    if (cat != nullptr && cat->rowCount() == 0) {
      m_model->removeRow(row);
    }
  }
#endif

补丁⑤:被自己的修复打破的下游假设

删行有个隐蔽的连锁反应:exportSchematic() 原来写死取第 6 行(桌面版固定布局里"原理图"永远在第 6 行)——分类一删,行号全乱。修复方式是按表头文字定位,并且必须用 tr() 包裹,与翻译系统联动(中文环境下表头已是"原理图"):

#ifdef QUCS_OHOS_PORT
  // OHOS removes empty categories, so "Schematics" is no longer at fixed row 6.
  // Locate it by its (translated) label instead of a hard-coded index.
  for (int row = 0; row < m_model->rowCount(); ++row) {
    QStandardItem *cat = m_model->item(row, 0);
    if (cat != nullptr && cat->text() == tr("Schematics")) {
      item = cat;
      break;
    }
  }
  if (item == nullptr) {
    return list;
  }
#else
  item = m_model->item(6, 0);   // 桌面版:固定布局
#endif

这类"被自己的修复打破的下游假设"最值得警惕——如果只删分类不查引用,导出原理图功能就静默坏掉,下一轮审核换一个地方打回。

补丁⑥:首启播种完整工程 + 沙箱路径与语言

env_setup.cpp 是本移植新增的 OHOS 适配层(不属上游文件),以静态构造器方式在 main() 之前执行。前半段把 HOME/XDG/TMPDIR 指进应用沙箱可写目录——QSettings、QStandardPaths、工作区目录的落盘全指望它们;后半段首启播种一个完整的 demo_amplifier_prj 工程——注意是目录,不是裸文件,Qucs 只认 _prj 结尾的目录为工程:

// env_setup.cpp(OHOS 适配层,static-initializer,先于 main() 执行)
struct OhosEnvSetup {
    OhosEnvSetup()
    {
        static const char* kCandidates[] = {
            "/data/storage/el2/base/haps/entry/files",
            "/data/storage/el2/base/files",
            nullptr,
        };
        const char* home = kCandidates[0];
        for (int i = 0; kCandidates[i] != nullptr; ++i) {
            if (QDir(QString::fromUtf8(kCandidates[i])).exists()) {
                home = kCandidates[i];
                break;
            }
        }
        const std::string homeStr(home);
        setenv("HOME", home, 1);
        setenv("XDG_CONFIG_HOME", (homeStr + "/.config").c_str(), 1);
        setenv("XDG_DATA_HOME", (homeStr + "/.local/share").c_str(), 1);
        setenv("TMPDIR", (homeStr + "/tmp").c_str(), 1);
        QDir().mkpath(QString::fromUtf8(home) + QStringLiteral("/.config"));
        QDir().mkpath(QString::fromUtf8(home) + QStringLiteral("/tmp"));

        // Pre-seed the workspace with a demo PROJECT on first run. Qucs only
        // treats directories ending in "_prj" as projects — a bare .sch file
        // would be invisible, which is why we build a proper "<name>_prj/"
        // folder containing the schematic.
        const QString demo = QStringLiteral(
            "/data/storage/el1/bundle/entry/resources/resfile/examples/demo_amplifier.sch");
        const QString ws   = QString::fromUtf8(home) + QStringLiteral("/QucsWorkspace");
        const QString proj = ws + QStringLiteral("/demo_amplifier_prj");
        QDir().mkpath(proj);
        const QString dst = proj + QStringLiteral("/demo_amplifier.sch");
        if (QFile::exists(demo) && !QFile::exists(dst)) {
            QFile::copy(demo, dst);
        }
    }
};
static const OhosEnvSetup g_ohosEnvSetup;   // 静态构造:保证先于 QApplication

配套的资源路径重定向在 main.cpp:桌面版假设 share/qucs/lang、share/qucs/examples 的安装布局,鸿蒙上不存在,全部改指 HAP 的 resfile 目录;且默认语言直接定为简体中文——沙箱里 QLocale::system() 不可靠,与其赌系统区域,不如按目标用户明确指定:

  // main.cpp —— OHOS port: .qm translations are shipped inside the HAP
  // resfile directory; the conventional share/qucs/lang layout does not
  // exist on device. Language defaults to Simplified Chinese per the port's
  // target audience; QLocale::system() is unreliable in the app sandbox.
  QucsSettings.LangDir =
      QStringLiteral("/data/storage/el1/bundle/entry/resources/resfile/translations/");
  if (QucsSettings.Language.isEmpty())
      QucsSettings.Language = QStringLiteral("zh_CN");
  QucsSettings.ExamplesDir =
      QStringLiteral("/data/storage/el1/bundle/entry/resources/resfile/examples/");

这样首启即有:工程列表里有可打开的工程 → 「文档」标签页有内容 → 每个可见分类下都有真实可双击的条目,而且界面天然就是中文。给审核准备的"第一分钟体验",靠的不是运气,是首启播种。

补丁⑦:窗口标题——应用名的落地点

一个小而有趣的收尾:桌面版窗口标题是"Qucs-S 24.4.1"式的版本串(还会截掉尾部 .0、拼 Git 哈希),移植版的标题则在这一处统一本地化——「电路芯绘」四个字的代码落点就在这:

// misc.cpp
QString misc::getWindowTitle()
{
#ifdef QUCS_OHOS_PORT
    // Localized window title for the HarmonyOS PC port.
    return QStringLiteral("电路芯绘");
#else
    QString title = QStringLiteral("%1 %2").arg(QUCS_NAME, PACKAGE_VERSION);
    // ... 桌面版:截 .0 尾巴、拼 GIT 哈希
    return title;
#endif
}

中文本地化:翻译文件的手工编译

修复轮顺带补齐了「文档」标签页的中文表头。.ts 翻译源文件是标准 Qt 语言学 XML,ProjectView 上下文里的分类名一条条补上:

    <message>
        <location line="+3"/>
        <source>Datasets</source>
        <translation>数据集</translation>
    </message>
    <message>
        <location line="+1"/>
        <source>Data Displays</source>
        <translation>数据显示页</translation>
    </message>

坑在于:上游 CMake 不负责编译 Qt 翻译文件,.ts → .qm 需要用宿主工具手工完成:

lrelease.exe qucs_zh_CN.ts -qm resfile\translations\qucs_zh_CN.qm

整包中文翻译实测 2728 条,随 resfile 进 HAP。这也是应对审核环境的重要一环——第六节会看到,审核环境正是"简体中文"。

构建与回归

build_ohos.cmd 一键交叉编译(413 个上游 C++ 文件 + 适配层 → libqucs_shell.so 9.1 MB),hvigor assembleHap 出包。真机回归的标准动作就一条:把审核员的角色再演一遍——九个菜单逐个点开、每个 dock 拉一遍、「文档」标签页逐分类双击、示例工程打开逐元件点选。两处打回现象确认消失,无一新增异常。

六、复审通过:审核意见里的真机环境

10 月 23 日 13:44 上传修复后的 harmony_pc-default-signed.app(136.10 MB)重新提审;10 月 24 日在真机上做最后一轮复核——新建原理图,把探针、电位器、互感变压器、开关、继电器逐个摆上画布,编辑交互全部正常:

真机复核:新建原理图逐一验证元件放置与编辑

图4:修复版真机复核实拍(HUAWEI MateBook Pro)。新建 untitled 工程,电流探针 Pr1/Pr2、电位器 POT1、三只互感变压器、开关 S2、继电器 S1 逐一放置验证——补丁轮改的是"入口行为",复核就要把入口走遍。

10 月 30 日 16:28,AGC 状态跳绿:

AGC 审核通过、正式上架

图5:AGC 控制台"已上架"页实拍。状态:已上架;审核意见:通过——并且附了完整的审核环境记录。

这次审核意见的内容非常值得读,原文照录:

应用审核意见:通过。
WIFI联网,HarmonyOS7.0.0(HUAWEI MateBook Pro),简体中文环境。

短短一行,把审核的真实测试条件交了底,每一项都对应着上架前的功课:

  • HUAWEI MateBook Pro 真机——不是模拟器。第一轮打回的两处问题(弹错误框、空标签页)恰恰只有在真机沙箱里才会暴露;工具栏图标是否正常渲染,也只有真机上的 Qt 插件加载说了算;
  • WIFI 联网——网络权限声明、联网行为要经得起在线环境的检验(本应用未申请网络权限,反而少了一项被检验的面);
  • 简体中文环境——2728 条翻译、分类表头中文化、zh_CN 默认语言,在这里全部被实际点亮。如果「文档」标签页还挂着英文表头,或者 exportSchematic 还写死第 6 行导致中文环境下定位错乱,复审结果都可能不同。

审核不是黑盒抽签,而是一次可预期的真机验收——你在自己的 MateBook 上把每个入口点绿,审核员在同款设备上做的事一模一样。

七、上架之后

上架即分发:市场详情页(图1)展示介绍图、功能说明与分类,用户点击即装,后续版本过审后自动向已安装用户推送更新——对桌面软件来说,这是过去需要自己搭更新服务器、管升级包才能做到的事。

代码侧的演进方向也已排好。最值得做的一件:把 SPICE 仿真内核从"外部进程"改为"进程内库"——ngspice 本身支持以共享库形式嵌入,编译进 so 后仿真就能在沙箱内闭环,届时再按本文第五节的同一套门控纪律把仿真菜单重新点亮,走一遍 versionCode 递增 → 发布签名 → 上传提审的成熟流水线。

八、上架踩坑清单(浓缩版)

#坑结论
1外部子工具用 QProcess 拉起,沙箱禁 exec沙箱内无法 fork/exec,依赖外部可执行文件的入口必须收口或改为进程内实现
2只拷裸 .sch,没建 _prj 工程Qucs 只认 _prj 结尾目录为工程;首启播种完整工程目录
310 个分类 9 个空,双击零响应空分类看起来就是"坏的";refresh 时删除所有空分类
4删分类后 exportSchematic 写死 item(6,0) 错位修复常有连锁反应;布局改动态后,定位逻辑改为按 tr() 表头文字查找
5翻译 .ts 不进 CMake用宿主 lrelease 手工编 .qm 进 resfile(实测 2728 条)
6工具栏图标全空QIcon 的 SVG 图标需要 libQt5Svg.so + libqsvgicon 插件,预置 Qt-OHOS 运行时不带,须自编译三件套
7沙箱里 QLocale::system() 不可靠界面语言明确指定默认值(zh_CN),不赌系统区域
8驳回理由只有「功能异常」四个字自己当审核员:真机把每个菜单/标签/按钮点一遍,异常入口自己先找到
9.so 不在软件包加密范围内开源软件无秘密可保;需保密逻辑放 ArkTS 层或另做加固
10debug 签名包拿去提审提审必须发布证书 + 发布 Profile 签出的 .app
11deviceTypes 漏 2in1PC 端市场直接搜不到
12versionCode 不递增AGC 拒收新包;版本编码规则早定

九、常见问题 QA

Q1:Qt/C++ 的桌面应用也能上架鸿蒙 PC 吗?

能。上架流程只管包的合法性与一致性,不管包里的技术形态。本应用带着 156 MB 的 Qt 运行时 so、136.10 MB 的 .app 包,与几百 KB 的纯 ArkTS 应用走完全相同的"创建应用 → 签名 → 上传 → 审核 → 上架"流水线,最终正常上架(图1、图5)。

Q2:审核驳回只说「功能异常」,怎么定位问题?

自己当审核员。在真机(与审核环境同款设备最好)上把应用每个入口点一遍:九个菜单逐项、每个 dock、每个标签页、每个对话框、双击每个列表项。“点了没反应”“弹错误框”"页面为空"三类现象是审核驳回的高频实锤。本文两处问题(工具菜单弹错、空分类)都是这样自检出来的。

Q3:沙箱里确实跑不了的功能,怎么处理?

两个动作:UI 收口——菜单不挂、工具栏不建、标签页不加,让用户与审核员根本看不见;能力替代——能改为进程内实现就改(如本文保留的匹配综合就是应用内对话框)。原则是"宁可少而真实,不要多而报错"。同时把替代方案留好:ngspice 支持进程内库嵌入,仿真闭环可以作为下个版本亮点回归。

Q4:.so 不被软件包加密覆盖,敏感逻辑怎么办?

先问自己 so 里有没有真秘密。开源项目(GPL 等)源码本来公开,无所谓泄密;闭源项目的敏感算法要么上移到 ArkTS 层(吃 appx 加密),要么接受 so 明文的事实并另做加固(字符串混淆、反调试等),要么干脆评估这条技术路线的保密水位。本文的选择是:核心逻辑本来就是公开源码,加密照选,边界看清。

Q5:Qt 应用的中文化怎么进包?

.ts 源文件补齐译文后,用宿主 lrelease 手工编译成 .qm,放进 resfile 随包分发(上游 CMake 通常不含翻译构建步骤)。注意两条联动:一是运行时语言要明确指定默认值(沙箱里 QLocale::system() 不可靠);二是代码里凡依赖界面文字的定位逻辑(如本文按"原理图"表头找分类)必须用 tr() 包裹,中英文环境才都正确。

Q6:首启体验为什么要刻意设计?

审核窗口只有几分钟,用户耐心只有几十秒,两者需求一致:打开即有内容、每步有响应。本文的做法是首启播种一个完整示例工程(含 _prj 目录结构),让工程列表、「文档」标签页、画布三处第一时间都有真实内容——这比任何引导页都有说服力。

Q7:被拒后重新提审,周期多久?

本次时间线:10-21 打回 → 当晚定位修复 → 10-23 上传新包 → 10-30 复审通过上架,修复轮总耗时约一周半。其中真正写代码的时间不到一晚,大头在自检回归与等审核排队。

Q8:桌面端代码真的一行没动吗?

业务逻辑一行未动,7 处移植性修复全部 #ifdef QUCS_OHOS_PORT 宏门控(本文第五节逐段展示了与上架直接相关的部分),另加 env_setup.cpp 等独立适配层文件。同一份上游源码,桌面端编译出口行为不变,鸿蒙端走条件分支——这是"移植不残害上游"的底线,也让上游版本升级时的合并成本可控。

Q9:.app 和 .hap 到底传哪个?

单 HAP 应用两种格式 AGC 都能解析,但 .app 是应用级发布包,是市场分发的标准形态。DevEco 里 Build > Build App(s) 产出,本应用上传的即 harmony_pc-default-signed.app(136.10 MB)。

Q10:为什么选择整体移植而不是像化学编辑器、光学工具那样重写?

看体量与复利:Qucs-S 二十余年积累的 660 个源文件里是元件模型、绘图引擎、对话框体系的全部细节,重写等于放弃这些资产;而交叉编译路线让鸿蒙版与桌面版共享同一份源码,上游每个版本升级都能低成本跟进。两条路线没有优劣,只有适配场景——重写适合"核心算法可收敛为数千行"的工具,整体移植适合"体系庞大、交互厚重"的桌面软件。

Q11:权限表里的"受限开放权限"会不会卡审?

如实申报就不卡。READ_PASTEBOARD 这类受限开放权限,AGC 提审时会核对使用场景说明。诀窍还是那句:写不出"非它不可"理由的权限就从表里删掉——本应用为粘贴功能保留它,reason 一句话写清"原理图编辑器复制粘贴",与实际行为严格一致,审核问询自然答得上。

十、结语

回头看这次上架,最贵的学费花在第一次打回上:桌面软件的移植完成度,不能用自己的"核心功能能跑"来衡量,要用"每一个入口都经得起点击"来衡量。审核员点开的每一个菜单,都是一次独立的功能验收——弹错误框的外部工具、空空如也的分类页,在桌面上是"没装全家桶",在审核语境里就是「功能异常」。

修复的动作其实朴素得近乎笨拙:把每个跑不通的入口藏起来,把每个空白的页面填上真实内容,把每个分类的中文名补齐,然后自己把九个菜单重新点一遍。但正是这份笨拙,换来了复审意见里那句干净的"通过",和一行珍贵的审核环境记录——它反过来证明,上架从来不是运气:你在什么环境里验收,审核员就在什么环境里验收;你把每个入口点绿的那天,上架就只是时间问题。


Logo

加入「COC·上海城市开发者社区」,成就更好的自己!

更多推荐