Electron for 鸿蒙PC - 状态栏菜单嵌套扁平化与Click事件处理完整方案
本文介绍了Electron应用在鸿蒙PC平台适配过程中遇到的状态栏菜单嵌套层级限制问题及解决方案。由于鸿蒙系统状态栏菜单不支持三级及以上嵌套,导致多级菜单无法正常显示。针对这一问题,文章提出了一套菜单扁平化方案:通过递归处理将多级菜单转换为二级结构,将子菜单标题移至subTitle字段,并使用menuCode标识菜单项实现点击识别。方案包含数据转换、递归扁平化处理和事件响应机制三个关键环节,最终实
前言
在将 Electron 应用适配到鸿蒙 PC 平台时,状态栏(StatusBar)菜单遇到了嵌套层级限制问题:鸿蒙系统的状态栏菜单不支持深层嵌套,导致多级菜单无法正常显示。为了解决这个问题,我们实现了一套菜单嵌套扁平化方案,通过递归处理将多级菜单转换为扁平结构,同时通过 menuCode 和 rightMenuClick 事件实现点击处理。
本文将详细记录这个解决方案的实现原理、代码细节和最佳实践,帮助开发者理解并应用这一方案。
关键词:鸿蒙PC、Electron适配、状态栏菜单、菜单嵌套、扁平化、menuCode、click事件

目录
欢迎加入开源鸿蒙PC社区:https://harmonypc.csdn.net/
问题现象与限制分析
1.1 问题现象
在鸿蒙 PC 上使用状态栏菜单时,遇到以下问题:
错误现象:
- 多级嵌套菜单无法正常显示
- 子菜单的子菜单(三级及以上)显示异常
- 菜单结构复杂时,部分菜单项丢失
表现:
原始菜单结构:
文件 (File)
├─ 新建 (New)
│ ├─ 文档 (Document) ❌ 无法显示
│ └─ 文件夹 (Folder) ❌ 无法显示
└─ 打开 (Open)
└─ 最近文件 (Recent) ❌ 无法显示
├─ 文件1 ❌ 无法显示
└─ 文件2 ❌ 无法显示
1.2 鸿蒙系统限制
根据 HarmonyOS 状态栏扩展能力文档,状态栏菜单的限制:
限制说明:
- ✅ 支持一级菜单(主菜单项)
- ✅ 支持二级菜单(子菜单)
- ❌ 不支持三级及以上菜单嵌套
技术原因:
- 状态栏菜单的 UI 组件限制
- 系统级别的菜单层级限制
- 无法通过配置绕过
菜单扁平化方案设计
2.1 核心思路
扁平化策略:
- 将多级嵌套菜单转换为二级菜单结构
- 子菜单的
title移到subTitle - 通过
menuCode标识每个菜单项的唯一ID - 点击时通过
menuCode识别具体菜单项
转换示例:
转换前(三级嵌套):
{
"label": "文件",
"submenu": [
{
"label": "新建",
"submenu": [
{"label": "文档", "commandId": 1},
{"label": "文件夹", "commandId": 2}
]
}
]
}
转换后(扁平化):
{
"title": "文件",
"subMenu": [
{
"title": "新建",
"subMenu": [
{
"subTitle": "文档", // title 移到 subTitle
"menuCode": "1"
},
{
"subTitle": "文件夹",
"menuCode": "2"
}
]
}
]
}
2.2 方案架构
原始菜单数据(JSON)
↓
transformJsonToMenus() // 转换为 StatusBarMenuItem
↓
processData() // 扁平化处理
↓
recurse() // 递归处理嵌套菜单
↓
扁平化后的菜单数据
↓
updateStatusBarMenu() // 更新状态栏菜单
↓
用户点击菜单
↓
rightMenuClick 事件
↓
通过 menuCode 识别菜单项
↓
执行对应操作
完整实现方案
3.1 菜单数据转换
transformMenuItem 函数:将 Electron 菜单数据转换为鸿蒙状态栏菜单格式
// StatusBarManager.ets
private transformMenuItem(jsonItem: MenuRawData): statusBarManager.StatusBarMenuItem | statusBarManager.StatusBarSubMenuItem {
// 创建菜单动作,使用 commandId 作为 menuCode
const menuAction: statusBarManager.StatusBarMenuAction = {
abilityName: 'StatusBarEntryAbility',
moduleName: 'entry',
notifyOnly: true,
menuCode: jsonItem.commandId?.toString() // ⚠️ 关键:使用 commandId 作为 menuCode
};
const menuItem: statusBarManager.StatusBarMenuItem = {
title: jsonItem.label as string,
menuAction: menuAction
};
// 递归处理子菜单
if (jsonItem.submenu && jsonItem.submenu.length > 0) {
menuItem.subMenu = jsonItem.submenu.map((subItem) =>
this.transformMenuItem(subItem) as statusBarManager.StatusBarSubMenuItem
);
}
return menuItem;
}
关键点:
- ✅ 使用
commandId作为menuCode,用于点击识别 - ✅ 递归处理子菜单,支持任意层级
- ✅ 保持菜单结构,为后续扁平化做准备
3.2 菜单扁平化处理
recurse 函数:递归处理菜单嵌套,将子菜单的 title 移到 subTitle
// StatusBarManager.ets
private recurse(items: MenuItem[] | undefined, isTopLevel: boolean = true): undefined | MenuItem[] {
if (items === undefined) {
return items;
}
items.map((item: MenuItem) => {
// ⚠️ 关键:如果不是顶级菜单,将 title 移到 subTitle
if (!isTopLevel) {
item.subTitle = item.title; // 保存原始标题到 subTitle
item.title = undefined; // 清空 title
}
// 递归处理子菜单
this.recurse(item.subMenu, false);
});
return items;
}
处理逻辑:
- ✅ 顶级菜单(
isTopLevel = true):保持title不变 - ✅ 子菜单(
isTopLevel = false):将title移到subTitle,清空title - ✅ 递归处理所有层级的子菜单
processData 函数:调用递归函数处理菜单数据
// StatusBarManager.ets
private processData(rawData: statusBarManager.StatusBarMenuItem[]): MenuItem[] {
// 深拷贝数据,避免修改原始数据
let modifiedData: MenuItem[] = JSON.parse(JSON.stringify(rawData)) as MenuItem[];
// 调用递归函数扁平化处理
return this.recurse(modifiedData) as MenuItem[];
}
3.3 菜单设置流程
SetContextMenu 函数:设置状态栏菜单的完整流程
// StatusBarManager.ets
@LogMethod
SetContextMenu(menu_model: string, onCompleted: (ret: boolean) => void, callback: (id: Number) => void) {
LogUtil.debug(TAG, `JS SetContextMenu:: ${menu_model}`);
try {
// 1. 解析 JSON 菜单数据
let menu_json: MenuRawData[] = JSON.parse(menu_model) as MenuRawData[];
// 2. 转换为鸿蒙菜单格式
const items: statusBarManager.StatusBarMenuItem[] = this.transformJsonToMenus(menu_json);
// 3. 扁平化处理
const modified_items: statusBarManager.StatusBarMenuItem[] = this.processData(items) as statusBarManager.StatusBarMenuItem[];
// 4. 包装为菜单组
const menus: statusBarManager.StatusBarGroupMenu[] = [modified_items];
// 5. 更新状态栏菜单
try {
statusBarManager.updateStatusBarMenu(
this.ctxAdapter.getActiveContext(),
menus,
() => {
onCompleted && onCompleted(true);
}
);
} catch (err) {
LogUtil.info(TAG, `updateStatusBarMenu failed. err: ${JSON.stringify(err)}`);
onCompleted && onCompleted(false);
}
// 6. 保存点击回调函数
this.rightMenuCallback = callback;
} catch (err) {
LogUtil.error(TAG, 'SetContextMenu Error: ' + err);
onCompleted && onCompleted(false);
}
}
Click事件处理机制
4.1 事件监听注册
在 SetImage 函数中注册事件监听:
// StatusBarManager.ets
statusBarManager.addToStatusBar(this.ctxAdapter.getActiveContext(), {
icons: { /* ... */ },
quickOperation: { /* ... */ },
statusBarGroupMenu: [],
}, () => {
// 注册左键点击事件
this.leftClickCallback = callback;
statusBarManager.on('statusBarIconClick', () => {
this.leftClickCallback?.();
});
// ⚠️ 关键:注册右键菜单点击事件
let rightMenuClickCallback = (eventData: emitter.EventData) => {
LogUtil.info(TAG, `rightMenuClickCallback is called. menuCode: ${eventData?.data?.menuCode}`);
// 通过 menuCode 调用回调函数
this.rightMenuCallback?.(Number(eventData?.data?.menuCode));
}
statusBarManager.on('rightMenuClick', rightMenuClickCallback);
this.isCreated = true;
onCompleted && onCompleted(true);
});
关键点:
- ✅ 监听
rightMenuClick事件 - ✅ 从
eventData.data.menuCode获取菜单项ID - ✅ 调用保存的回调函数,传递
menuCode
4.2 menuCode 传递流程
完整流程:
Electron 菜单数据
↓
commandId: 123 // Electron 菜单项的 commandId
↓
transformMenuItem()
↓
menuCode: "123" // 转换为字符串作为 menuCode
↓
updateStatusBarMenu() // 设置到状态栏
↓
用户点击菜单项
↓
rightMenuClick 事件触发
↓
eventData.data.menuCode = "123"
↓
rightMenuCallback(123) // 调用回调,传递 menuCode
↓
Electron 主进程处理菜单命令
4.3 事件清理
RemoveFromStatusBar 函数:移除事件监听
// StatusBarManager.ets
@LogMethod
async RemoveFromStatusBar(onCompleted: (ret: boolean) => void) {
try {
statusBarManager.removeFromStatusBar(this.ctxAdapter.getActiveContext(), () => {
this.isCreated = false;
onCompleted && onCompleted(true);
});
} catch (err) {
LogUtil.info(TAG, `removeFromStatusBar failed. err: ${JSON.stringify(err)}`);
onCompleted && onCompleted(false);
} finally {
// ⚠️ 关键:清理事件监听
statusBarManager.off('statusBarIconClick');
statusBarManager.off('rightMenuClick');
}
}
最佳实践与注意事项
5.1 menuCode 设计原则
推荐做法:
// ✅ 好:使用唯一的 commandId 作为 menuCode
menuCode: jsonItem.commandId?.toString()
// ❌ 不好:使用重复的 menuCode
menuCode: "1" // 多个菜单项使用相同的 menuCode
注意事项:
- ✅ 确保每个菜单项有唯一的
commandId - ✅
menuCode必须是字符串类型 - ✅ 如果没有
commandId,需要生成唯一ID
5.2 菜单扁平化最佳实践
推荐做法:
// ✅ 好:深拷贝数据后再处理
let modifiedData: MenuItem[] = JSON.parse(JSON.stringify(rawData)) as MenuItem[];
// ❌ 不好:直接修改原始数据
let modifiedData = rawData; // 会修改原始数据
注意事项:
- ✅ 使用深拷贝避免修改原始数据
- ✅ 递归处理所有层级的子菜单
- ✅ 保持菜单结构,只调整 title/subTitle
5.3 事件处理最佳实践
推荐做法:
// ✅ 好:保存回调函数,在事件触发时调用
this.rightMenuCallback = callback;
statusBarManager.on('rightMenuClick', (eventData) => {
this.rightMenuCallback?.(Number(eventData?.data?.menuCode));
});
// ❌ 不好:直接在事件中处理业务逻辑
statusBarManager.on('rightMenuClick', (eventData) => {
// 直接处理业务逻辑,耦合度高
});
注意事项:
- ✅ 使用回调函数模式,解耦事件处理和业务逻辑
- ✅ 检查回调函数是否存在再调用
- ✅ 正确转换
menuCode类型(字符串转数字)
常见问题解答
Q1: 为什么需要扁平化菜单?
A: 鸿蒙系统的状态栏菜单不支持三级及以上嵌套,必须将多级菜单扁平化为二级结构才能正常显示。
Q2: menuCode 的作用是什么?
A: menuCode 用于标识每个菜单项的唯一ID,当用户点击菜单时,通过 menuCode 识别具体是哪个菜单项被点击,然后执行对应的操作。
Q3: 扁平化后菜单结构会丢失吗?
A: 不会。扁平化只是将子菜单的 title 移到 subTitle,菜单的层级结构仍然保留,只是显示方式改变。
Q4: 如何处理没有 commandId 的菜单项?
A: 需要为每个菜单项生成唯一的ID,可以使用自增ID或者基于菜单路径生成唯一标识。
总结与展望
7.1 核心要点总结
通过本文的深入分析,我们了解到:
- 菜单扁平化的必要性:鸿蒙系统限制导致必须扁平化多级菜单
- 递归处理机制:通过
recurse()函数递归处理所有层级的菜单 - menuCode 标识机制:使用
commandId作为menuCode标识菜单项 - Click事件处理:通过
rightMenuClick事件和menuCode实现点击处理
7.2 技术价值
这个解决方案不仅解决了菜单嵌套显示问题,还带来了以下好处:
- ✅ 兼容鸿蒙系统限制:通过扁平化适配系统限制
- ✅ 保持菜单功能:所有菜单项都能正常显示和点击
- ✅ 代码可维护性:清晰的递归处理逻辑,易于理解和维护
7.3 适用场景
这套方案适用于:
- ✅ 所有使用状态栏菜单的 Electron 应用
- ✅ 菜单结构复杂的应用
- ✅ 在鸿蒙 PC 上运行的 Electron 应用
- ✅ 需要多级菜单嵌套的应用
相关资源
HarmonyOS 官方文档:
Electron 官方文档:
更多推荐

所有评论(0)