Electron for 鸿蒙PC - 菜单栏完整开发指南:从原生菜单到自定义菜单的实现
本文介绍了基于Electron框架开发鸿蒙PC系统菜单栏的实现方法。内容涵盖原生菜单栏的基本结构、菜单项类型、键盘快捷键设置以及二级目录嵌套实现,特别针对鸿蒙系统的扁平化一级菜单限制进行了适配。文章还展示了跨平台菜单样式自动调整功能,包括10级嵌套菜单在Mac/Windows/Linux平台的展示效果。提供了完整的语言切换菜单示例代码,演示了如何通过Menu API构建菜单模板、处理菜单点击事件以
📋 目录
- Electron for 鸿蒙PC 菜单栏概述
- 原生菜单栏的使用
- 二级目录嵌套实现
- 平台检测与菜单适配
- 鸿蒙系统菜单嵌套限制
- 菜单栏的隐藏与显示
- 自定义菜单栏开发
- 功能同步与通信
- 常见问题与解决方案
- 完整代码示例
- 使用方法与快速开始
Electron for 鸿蒙PC 菜单栏概述
什么是 Electron for 鸿蒙PC 菜单栏?
Electron for 鸿蒙PC 菜单栏是应用窗口顶部的原生菜单系统,类似于传统桌面应用的菜单栏(如 Windows 的资源管理器菜单、Mac 的系统菜单栏)。它提供了标准的用户界面元素,用户可以点击菜单项来执行各种操作。

图1:Electron for 鸿蒙PC 原生菜单栏示例
菜单栏的类型
Electron for 鸿蒙PC 提供了两种菜单栏实现方式:
-
原生菜单栏(Native Menu Bar)
- 由操作系统控制样式和行为
- 符合平台规范
- 样式自定义能力有限
-
自定义菜单栏(Custom Menu Bar)
- 使用 HTML/CSS/JavaScript 实现
- 完全可控的样式和交互
- 可以实现任意复杂的UI效果

图2:Electron for 鸿蒙PC 自定义菜单栏示例
平台适配效果展示
项目会根据运行平台自动调整原生菜单结构,实现跨平台兼容:
鸿蒙平台菜单(扁平化一级菜单):

图3:鸿蒙平台 - 10个一级菜单项平铺显示
Mac/Windows/Linux平台菜单(10级嵌套菜单):

图4:Mac/Windows/Linux平台 - 10级嵌套菜单结构
原生菜单栏的使用
基本结构
Electron for 鸿蒙PC 的原生菜单栏通过 Menu API 创建,使用模板(Template)定义菜单结构:
const { Menu } = require('electron');
const template = [
{
label: '一级菜单名称',
submenu: [
{
label: '二级菜单项',
click: () => {
// 点击处理逻辑
}
}
]
}
];
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
菜单项类型
Electron 支持多种菜单项类型:
1. 普通菜单项(normal,默认)
{
label: '新建',
click: () => {
console.log('新建文件');
}
}
2. 分隔线(separator)
{ type: 'separator' }
3. 单选按钮(radio)
{
label: '中文',
type: 'radio',
checked: true, // 是否选中
click: () => {
console.log('切换到中文');
}
}
4. 复选框(checkbox)
{
label: '显示工具栏',
type: 'checkbox',
checked: true,
click: (menuItem) => {
console.log('工具栏显示状态:', menuItem.checked);
}
}
添加键盘快捷键
{
label: '新建',
accelerator: 'CmdOrCtrl+N', // Mac: Cmd+N, Windows/Linux: Ctrl+N
click: () => {
console.log('新建文件');
}
}
使用系统角色
Electron for 鸿蒙PC 提供了一些内置角色,可以直接使用:
{
label: '编辑',
submenu: [
{ role: 'undo', label: '撤销' },
{ role: 'redo', label: '重做' },
{ role: 'cut', label: '剪切' },
{ role: 'copy', label: '复制' },
{ role: 'paste', label: '粘贴' }
]
}
二级目录嵌套实现
实现原理
二级目录嵌套是通过在菜单项的 submenu 属性中添加子菜单项来实现的:
{
label: '语言',
submenu: [
{
label: '中文',
type: 'radio',
checked: true,
click: () => {
console.log('切换到中文');
}
},
{
label: '英文',
type: 'radio',
checked: false,
click: () => {
console.log('切换到英文');
}
}
]
}
完整示例:语言切换菜单
const { app, BrowserWindow, Menu } = require('electron');
// 当前选中的语言
let currentLanguage = '中文';
let currentLanguageCode = 'zh-CN';
// 语言切换处理函数
function handleLanguageChange(language, languageCode) {
currentLanguage = language;
currentLanguageCode = languageCode;
// 更新菜单选中状态
updateLanguageMenu();
// 发送消息到渲染进程
BrowserWindow.getAllWindows().forEach(win => {
win.webContents.send('language-changed', {
language: language,
languageCode: languageCode
});
});
console.log(`语言已切换到: ${language} (${languageCode})`);
}
// 创建应用菜单
function createMenu() {
const template = [
{
label: '语言',
submenu: [
{
label: '中文',
type: 'radio',
checked: currentLanguage === '中文',
click: () => {
handleLanguageChange('中文', 'zh-CN');
}
},
{
label: '英文',
type: 'radio',
checked: currentLanguage === '英文',
click: () => {
handleLanguageChange('英文', 'en-US');
}
}
]
}
];
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
return menu;
}
// 更新语言菜单的选中状态
function updateLanguageMenu() {
// 重新创建菜单以更新选中状态
createMenu();
}
function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
});
win.loadFile('index.html');
createMenu();
}
app.whenReady().then(createWindow);
三级嵌套菜单
Electron for 鸿蒙PC 支持多级嵌套菜单:
{
label: '设置',
submenu: [
{
label: '外观',
submenu: [
{
label: '字体',
submenu: [
{ label: '微软雅黑', click: () => {} },
{ label: '宋体', click: () => {} }
]
},
{
label: '颜色',
submenu: [
{ label: '蓝色', click: () => {} },
{ label: '绿色', click: () => {} }
]
}
]
}
]
}
平台检测与菜单适配
为什么需要平台检测?
不同平台对 Electron 原生菜单的支持程度不同:
- Mac/Windows/Linux:支持深层嵌套菜单(理论上可以无限嵌套)
- 鸿蒙系统:由于系统限制,最多支持 2-3 级嵌套
为了在不同平台上都能提供最佳用户体验,我们需要根据平台类型动态调整菜单结构。
平台检测实现
1. 检测函数
// 检测是否为鸿蒙平台
function isHarmonyOSPlatform() {
// 方法1: 检查process.platform(鸿蒙可能返回特定值)
const platform = process.platform;
// 方法2: 检查环境变量(如果有鸿蒙特定的环境变量)
const harmonyEnv = process.env.HARMONY_OS || process.env.OHOS_PLATFORM;
// 如果platform不是常见的darwin/win32/linux,可能是鸿蒙
const commonPlatforms = ['darwin', 'win32', 'linux'];
if (!commonPlatforms.includes(platform)) {
console.log('检测到非标准平台,可能是鸿蒙系统:', platform);
return true;
}
// 如果有鸿蒙环境变量
if (harmonyEnv) {
console.log('检测到鸿蒙环境变量:', harmonyEnv);
return true;
}
// 默认:如果是darwin(Mac)、win32(Windows)、linux,则不是鸿蒙
return false;
}
2. 根据平台创建不同菜单结构
// 创建扁平化的一级菜单(用于鸿蒙平台)
function createFlatMenu() {
const numbers = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十'];
// 将所有10个菜单项都创建为一级菜单项
return numbers.map((label, index) => {
return {
label: label,
click: () => {
console.log(`点击了第${index + 1}级菜单:${label}`);
}
};
});
}
// 根据平台创建不同深度的嵌套菜单
function createNestedMenuByPlatform(maxLevel) {
const numbers = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十'];
function createMenuItem(level) {
if (level > maxLevel) {
return null;
}
const menuItem = {
label: numbers[level - 1]
};
// 如果不是最后一级,添加子菜单
if (level < maxLevel) {
const childMenu = createMenuItem(level + 1);
if (childMenu) {
menuItem.submenu = [childMenu];
}
} else {
// 最后一级添加点击事件
menuItem.click = () => {
console.log(`点击了第${level}级菜单:${numbers[level - 1]}`);
};
}
return menuItem;
}
return createMenuItem(1);
}
3. 菜单创建逻辑
// 创建应用菜单
function createMenu() {
// 检测平台类型
const isHarmonyOS = isHarmonyOSPlatform();
const platform = process.platform;
console.log('当前平台:', platform);
console.log('是否为鸿蒙平台:', isHarmonyOS);
let menuItems;
let maxLevel;
if (isHarmonyOS) {
// 鸿蒙平台:将所有10级菜单展开为一级目录(平铺显示)
maxLevel = 1;
console.log('鸿蒙平台:使用扁平化一级菜单(10个菜单项平铺)');
menuItems = createFlatMenu();
} else {
// Mac/Windows/Linux平台:使用10级嵌套
maxLevel = 10;
console.log('Mac/Windows/Linux平台:使用10级嵌套菜单');
const nestedMenu = createNestedMenuByPlatform(10);
menuItems = [nestedMenu];
}
const template = [
// 添加菜单项(根据平台不同:鸿蒙平台是10个一级菜单,其他平台是1个10级嵌套菜单)
...menuItems,
// 保留原有的语言菜单
{
label: '语言',
submenu: [
{
label: '中文',
type: 'radio',
checked: currentLanguage === '中文',
click: (menuItem) => {
handleLanguageChange('中文', 'zh-CN');
}
},
{
label: '英文',
type: 'radio',
checked: currentLanguage === '英文',
click: (menuItem) => {
handleLanguageChange('英文', 'en-US');
}
}
]
}
];
try {
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
if (isHarmonyOS) {
console.log('菜单创建成功(鸿蒙平台:10个一级菜单项平铺),已设置到应用菜单栏');
} else {
console.log(`菜单创建成功(${maxLevel}级嵌套),已设置到应用菜单栏`);
}
return menu;
} catch (error) {
console.error('菜单创建失败:', error);
// 错误处理逻辑...
}
}
菜单结构对比
鸿蒙平台菜单结构

图3:鸿蒙平台 - 10个一级菜单项平铺显示
菜单栏显示:
├─ 一(一级菜单)
├─ 二(一级菜单)
├─ 三(一级菜单)
├─ 四(一级菜单)
├─ 五(一级菜单)
├─ 六(一级菜单)
├─ 七(一级菜单)
├─ 八(一级菜单)
├─ 九(一级菜单)
├─ 十(一级菜单)
└─ 语言(一级菜单)
├─ 中文(二级菜单)
└─ 英文(二级菜单)
特点:
- ✅ 10个菜单项(一、二、三…十)都作为一级菜单显示
- ✅ 避免嵌套限制,所有功能都可以直接访问
- ✅ 菜单项平铺显示,无需展开
Mac/Windows/Linux平台菜单结构

图4:Mac/Windows/Linux平台 - 10级嵌套菜单结构
菜单栏显示:
├─ 一(一级菜单)
│ └─ 二(二级菜单)
│ └─ 三(三级菜单)
│ └─ ... └─ 十(十级菜单)
└─ 语言(一级菜单)
├─ 中文(二级菜单)
└─ 英文(二级菜单)
特点:
- ✅ 10级嵌套菜单(一 → 二 → … → 十)
- ✅ 菜单结构更紧凑,节省菜单栏空间
- ✅ 支持深层嵌套,符合传统桌面应用习惯
优势
- 避免嵌套限制:鸿蒙平台使用一级菜单,不受子窗口嵌套限制
- 功能完整:所有10个菜单项都可以访问
- 平台适配:Mac/Windows/Linux 仍使用10级嵌套
- 自动检测:无需手动配置,自动识别平台
鸿蒙系统菜单嵌套限制
问题描述
在鸿蒙系统上使用 Electron 原生菜单时,发现深层嵌套菜单(超过2-3级)无法正常显示。
错误信息
从日志中可以看到以下错误:
[ERROR:ohos_popup.cc(129)] Cannot create subwindow from another subwindow
这个错误表明:鸿蒙系统不允许从子窗口再创建子窗口,这限制了 Electron 原生菜单的嵌套深度。
限制说明
鸿蒙系统的限制
- 子窗口限制:鸿蒙系统的窗口管理机制不允许从子窗口(subwindow)再创建子窗口
- 菜单实现机制:Electron 的原生菜单在鸿蒙系统上通过创建子窗口来实现子菜单
- 嵌套深度:由于上述限制,原生菜单最多支持 2-3 级嵌套
实际测试结果
- ✅ 2级嵌套:可以正常工作(一级菜单 → 二级菜单)
- ⚠️ 3级嵌套:可能可以工作,但需要测试
- ❌ 4级及以上嵌套:无法工作,会出现
Cannot create subwindow from another subwindow错误
解决方案
方案1:平台检测 + 扁平化菜单(推荐)
使用平台检测,在鸿蒙平台上将深层菜单展开为一级菜单:
if (isHarmonyOS) {
// 鸿蒙平台:扁平化菜单
menuItems = createFlatMenu(); // 10个一级菜单项
} else {
// 其他平台:嵌套菜单
menuItems = [createNestedMenuByPlatform(10)]; // 10级嵌套
}
方案2:使用自定义HTML菜单栏
自定义HTML菜单栏不受此限制,可以支持任意深度的嵌套:
<!-- 支持10级嵌套 -->
<div class="menu-item">
<span>一</span>
<div class="submenu">
<div class="submenu-item">
<span>二</span>
<div class="submenu-nested">
<!-- 可以继续嵌套到10级 -->
</div>
</div>
</div>
</div>
方案3:限制原生菜单嵌套深度
如果必须使用原生菜单,建议限制在2-3级:
{
label: '一',
submenu: [
{
label: '二',
submenu: [
{
label: '三',
click: () => {
console.log('点击了三');
}
}
]
}
]
}
对比表
| 菜单类型 | 支持嵌套深度 | 平台限制 | 推荐使用场景 |
|---|---|---|---|
| 原生菜单(Windows/Mac/Linux) | 理论上无限 | 无 | 简单菜单结构 |
| 原生菜单(鸿蒙系统) | 2-3级 | 子窗口限制 | 简单菜单结构 |
| 自定义HTML菜单栏 | 理论上无限 | 无 | 复杂菜单结构 |
注意事项
- 这是鸿蒙系统平台的限制,不是 Electron 本身的限制
- 在 Windows、Mac、Linux 平台上,Electron 原生菜单支持更深层的嵌套
- 推荐使用平台检测,自动适配不同平台的菜单结构
菜单栏的隐藏与显示
方法1:完全移除菜单栏
function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
});
win.loadFile('index.html');
// 完全移除菜单栏
Menu.setApplicationMenu(null);
}
效果:菜单栏完全隐藏,无法通过任何方式显示。
方法2:隐藏但保留(Windows/Linux)
function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
});
win.loadFile('index.html');
// 创建菜单
createMenu();
// 隐藏菜单栏(可以通过Alt键显示)
if (process.platform !== 'darwin') {
win.setMenuBarVisibility(false);
}
}
效果:菜单栏隐藏,但可以通过 Alt 键临时显示。
方法3:自动隐藏(Windows/Linux)
function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
autoHideMenuBar: true, // 自动隐藏
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
});
win.loadFile('index.html');
createMenu();
}
效果:菜单栏自动隐藏,鼠标移到窗口顶部时自动显示。
方法4:平台适配(推荐)
function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
});
win.loadFile('index.html');
// Mac:显示在系统菜单栏
// Windows/Linux:隐藏窗口菜单栏
if (process.platform === 'darwin') {
createMenu();
} else {
createMenu();
win.setMenuBarVisibility(false);
}
}
自定义菜单栏开发
为什么需要自定义菜单栏?
原生菜单栏有以下限制:
- 样式由操作系统控制,无法自定义颜色、字体、大小等
- 二级菜单的样式完全无法调整
- 无法实现复杂的动画效果
自定义菜单栏可以:
- 完全控制样式(颜色、字体、大小、动画等)
- 实现任意复杂的交互效果
- 与页面设计风格保持一致
HTML 结构
<div class="custom-menu-bar">
<div class="menu-item" id="languageMenu">
<span id="languageMenuLabel">语言</span>
<div class="submenu" id="languageSubmenu">
<div class="submenu-item chinese-item" data-lang="zh-CN" data-label="中文">
<span>中文</span>
</div>
<div class="submenu-item english-item" data-lang="en-US" data-label="英文">
<span>英文</span>
</div>
</div>
</div>
</div>
CSS 样式
/* 菜单栏容器 */
.custom-menu-bar {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 50px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-bottom: 2px solid #5568d3;
display: flex;
align-items: center;
padding: 0 20px;
z-index: 1000;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
/* 一级菜单项 */
.menu-item {
position: relative;
padding: 0 20px;
height: 50px;
display: flex;
align-items: center;
cursor: pointer;
color: white;
font-size: 14px;
font-weight: 500;
transition: all 0.3s ease;
user-select: none;
}
.menu-item:hover {
background: rgba(255, 255, 255, 0.1);
}
.menu-item.active {
background: rgba(255, 255, 255, 0.2);
}
/* 二级菜单 */
.submenu {
display: none;
position: absolute;
top: 100%;
left: 0;
background: white;
border: 1px solid #e0e0e0;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
min-width: 180px;
padding: 6px 0;
margin-top: 4px;
z-index: 1001;
pointer-events: auto;
animation: slideDown 0.2s ease;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.menu-item:hover .submenu,
.menu-item.active .submenu {
display: block;
}
/* 二级菜单项 */
.submenu-item {
padding: 10px 20px;
cursor: pointer;
color: #333;
font-size: 14px;
transition: all 0.2s ease;
display: flex;
align-items: center;
position: relative;
pointer-events: auto;
user-select: none;
}
.submenu-item:hover {
background: #f0f0f0;
}
.submenu-item.selected {
font-weight: 500;
}
.submenu-item.selected::before {
content: '✓';
margin-right: 8px;
font-weight: bold;
}
/* 中文菜单项 - 红色 */
.submenu-item.chinese-item {
color: #ff4d4f;
}
.submenu-item.chinese-item:hover {
background: #fff1f0;
color: #ff4d4f;
}
.submenu-item.chinese-item.selected {
background: #fff1f0;
color: #ff4d4f;
}
.submenu-item.chinese-item.selected::before {
color: #ff4d4f;
}
/* 英文菜单项 - 蓝色 */
.submenu-item.english-item {
color: #1890ff;
}
.submenu-item.english-item:hover {
background: #e7f3ff;
color: #1890ff;
}
.submenu-item.english-item.selected {
background: #e7f3ff;
color: #1890ff;
}
.submenu-item.english-item.selected::before {
color: #1890ff;
}
JavaScript 交互逻辑
const { ipcRenderer } = require('electron');
// 多语言文本资源
const translations = {
'zh-CN': {
languageMenuLabel: '语言',
// ... 其他翻译
},
'en-US': {
languageMenuLabel: 'Language',
// ... 其他翻译
}
};
let currentLang = 'zh-CN';
// 初始化自定义菜单
function initCustomMenu() {
const languageMenu = document.getElementById('languageMenu');
const submenuItems = document.querySelectorAll('.submenu-item');
// 菜单项点击处理
submenuItems.forEach(item => {
item.addEventListener('click', (e) => {
e.stopPropagation();
e.preventDefault();
const langCode = item.dataset.lang;
const langLabel = item.dataset.label;
// 更新选中状态
submenuItems.forEach(i => {
i.classList.remove('selected');
});
item.classList.add('selected');
// 更新语言
updatePageLanguage(langCode);
// 发送消息到主进程(同步原生菜单栏)
ipcRenderer.send('custom-menu-language-changed', {
language: langLabel,
languageCode: langCode
});
// 关闭子菜单
setTimeout(() => {
languageMenu.classList.remove('active');
}, 100);
});
});
// 点击展开/关闭
languageMenu.addEventListener('click', (e) => {
if (e.target.closest('.submenu-item')) {
return;
}
e.stopPropagation();
languageMenu.classList.toggle('active');
});
// 悬停展开
let menuTimeout = null;
languageMenu.addEventListener('mouseenter', () => {
if (menuTimeout) {
clearTimeout(menuTimeout);
menuTimeout = null;
}
languageMenu.classList.add('active');
});
languageMenu.addEventListener('mouseleave', () => {
menuTimeout = setTimeout(() => {
languageMenu.classList.remove('active');
}, 150);
});
// 子菜单区域保持打开
const submenu = document.getElementById('languageSubmenu');
submenu.addEventListener('mouseenter', () => {
if (menuTimeout) {
clearTimeout(menuTimeout);
menuTimeout = null;
}
});
submenu.addEventListener('mouseleave', () => {
languageMenu.classList.remove('active');
});
// 点击外部关闭菜单
document.addEventListener('click', (e) => {
if (!languageMenu.contains(e.target)) {
languageMenu.classList.remove('active');
}
});
}
// 更新页面语言
function updatePageLanguage(languageCode) {
currentLang = languageCode;
const t = translations[languageCode] || translations['zh-CN'];
// 更新菜单栏标签
document.getElementById('languageMenuLabel').textContent = t.languageMenuLabel;
// 更新其他页面内容
// ...
}
// 监听来自主进程的语言切换消息
ipcRenderer.on('language-changed', (event, data) => {
const languageCode = data.languageCode || data;
updatePageLanguage(languageCode);
// 更新菜单选中状态
const submenuItems = document.querySelectorAll('.submenu-item');
submenuItems.forEach(item => {
item.classList.remove('selected');
if (item.dataset.lang === languageCode) {
item.classList.add('selected');
}
});
});
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', () => {
initCustomMenu();
});
功能同步与通信
IPC 通信架构
┌─────────────────┐ IPC ┌─────────────────┐
│ 主进程 │ ◄──────────────────► │ 渲染进程 │
│ (main.js) │ │ (index.html) │
│ │ │ │
│ - 原生菜单栏 │ │ - 自定义菜单栏 │
│ - 状态管理 │ │ - 页面内容 │
└─────────────────┘ └─────────────────┘
主进程代码
const { app, BrowserWindow, Menu, ipcMain } = require('electron');
let currentLanguage = '中文';
let currentLanguageCode = 'zh-CN';
// 统一的语言切换处理函数
function handleLanguageChange(language, languageCode) {
currentLanguage = language;
currentLanguageCode = languageCode;
// 更新原生菜单栏
updateLanguageMenu();
// 发送消息到所有渲染进程窗口
BrowserWindow.getAllWindows().forEach(win => {
win.webContents.send('language-changed', {
language: language,
languageCode: languageCode
});
});
console.log(`语言已切换到: ${language} (${languageCode})`);
}
// 监听来自渲染进程的语言切换消息(自定义菜单栏)
ipcMain.on('custom-menu-language-changed', (event, data) => {
console.log('收到自定义菜单栏的语言切换消息:', data);
if (data && data.languageCode) {
handleLanguageChange(data.language, data.languageCode);
}
});
渲染进程代码
const { ipcRenderer } = require('electron');
// 发送消息到主进程(自定义菜单栏切换)
ipcRenderer.send('custom-menu-language-changed', {
language: '英文',
languageCode: 'en-US'
});
// 监听来自主进程的语言切换消息
ipcRenderer.on('language-changed', (event, data) => {
const languageCode = data.languageCode || data;
updatePageLanguage(languageCode);
// 更新自定义菜单栏的选中状态
updateCustomMenuSelection(languageCode);
});
常见问题与解决方案
问题1:二级菜单无法展开
症状:点击一级菜单项后,二级菜单不显示。
原因:
- CSS 选择器不正确
- JavaScript 事件绑定失败
- z-index 层级问题
解决方案:
/* 确保子菜单可以显示 */
.menu-item:hover .submenu,
.menu-item.active .submenu {
display: block !important; /* 使用 !important 确保优先级 */
z-index: 1001; /* 确保在最上层 */
}
// 确保事件正确绑定
languageMenu.addEventListener('click', (e) => {
e.stopPropagation();
languageMenu.classList.toggle('active');
});
问题2:点击菜单项后菜单不关闭
症状:点击二级菜单项后,子菜单仍然显示。
解决方案:
item.addEventListener('click', (e) => {
e.stopPropagation();
// 执行操作...
// 延迟关闭菜单
setTimeout(() => {
languageMenu.classList.remove('active');
}, 100);
});
问题3:原生菜单栏状态不同步
症状:自定义菜单栏切换语言后,原生菜单栏的选中状态不更新。
原因:Electron for 鸿蒙PC 的菜单状态需要重新创建才能更新。
解决方案:
// 更新语言菜单的选中状态
function updateLanguageMenu() {
// 重新创建菜单以更新选中状态(最可靠的方法)
createMenu();
}
问题4:英文切换无效
症状:点击英文菜单项后,页面内容没有切换为英文。
原因:
- IPC 消息格式不正确
- 翻译资源缺失
- 更新函数未正确调用
解决方案:
// 确保消息格式正确
ipcRenderer.send('custom-menu-language-changed', {
language: '英文', // 必须与主进程期望的格式一致
languageCode: 'en-US' // 语言代码
});
// 确保翻译资源完整
const translations = {
'zh-CN': { /* ... */ },
'en-US': { /* ... */ } // 确保英文翻译存在
};
// 确保更新函数被调用
function updatePageLanguage(languageCode) {
const t = translations[languageCode];
if (!t) {
console.error('翻译资源不存在:', languageCode);
return;
}
// 更新所有文本...
}
问题5:菜单项无法点击
症状:二级菜单项显示正常,但无法点击。
原因:
pointer-events被禁用- 事件被阻止
- z-index 层级问题
解决方案:
.submenu {
pointer-events: auto; /* 确保可以点击 */
z-index: 1001; /* 确保在最上层 */
}
.submenu-item {
pointer-events: auto; /* 确保可以点击 */
cursor: pointer; /* 显示手型光标 */
}
// 确保事件不被阻止
item.addEventListener('click', (e) => {
e.stopPropagation(); // 阻止冒泡,但不阻止默认行为
// 不要使用 e.preventDefault(),除非必要
});
问题6:菜单栏样式无法自定义
症状:原生菜单栏的样式无法调整。
原因:Electron for 鸿蒙PC 原生菜单栏的样式由操作系统控制。
解决方案:
- 使用自定义 HTML 菜单栏
- 或者接受原生样式,只调整菜单内容
问题7:点击语言菜单无法展开二级目录
症状:点击"语言"菜单项后,二级菜单不显示。
原因:
- CSS 选择器只支持
:hover,不支持点击 - JavaScript 事件未正确绑定
active类未正确添加
解决方案:
/* 同时支持悬停和点击 */
.menu-item:hover .submenu,
.menu-item.active .submenu {
display: block;
}
// 添加点击事件处理
languageMenu.addEventListener('click', (e) => {
// 如果点击的是子菜单项,不处理
if (e.target.closest('.submenu-item')) {
return;
}
e.stopPropagation();
// 切换active状态
languageMenu.classList.toggle('active');
});
完整代码示例
main.js(主进程)
const { app, BrowserWindow, Menu, ipcMain } = require('electron');
const path = require('path');
// 当前选中的语言
let currentLanguage = '中文';
let currentLanguageCode = 'zh-CN';
// 语言切换处理函数
function handleLanguageChange(language, languageCode) {
currentLanguage = language;
currentLanguageCode = languageCode;
// 更新菜单选中状态
updateLanguageMenu();
// 发送消息到所有渲染进程窗口
BrowserWindow.getAllWindows().forEach(win => {
win.webContents.send('language-changed', {
language: language,
languageCode: languageCode
});
});
console.log(`语言已切换到: ${language} (${languageCode})`);
}
// 创建应用菜单
function createMenu() {
const template = [
{
label: '语言',
submenu: [
{
label: '中文',
type: 'radio',
checked: currentLanguage === '中文',
click: () => {
handleLanguageChange('中文', 'zh-CN');
}
},
{
label: '英文',
type: 'radio',
checked: currentLanguage === '英文',
click: () => {
handleLanguageChange('英文', 'en-US');
}
}
]
}
];
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
return menu;
}
// 更新语言菜单的选中状态
function updateLanguageMenu() {
createMenu();
}
function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
});
win.loadFile('index.html');
// 可选:隐藏原生菜单栏
// Menu.setApplicationMenu(null);
// 或
// win.setMenuBarVisibility(false);
// 创建原生菜单栏
createMenu();
}
// 监听来自渲染进程的语言切换消息(自定义菜单栏)
ipcMain.on('custom-menu-language-changed', (event, data) => {
console.log('收到自定义菜单栏的语言切换消息:', data);
if (data && data.languageCode) {
handleLanguageChange(data.language, data.languageCode);
}
});
app.whenReady().then(createWindow);
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
index.html(渲染进程)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title id="pageTitle">二级目录嵌套Demo - Electron for 鸿蒙PC</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', sans-serif;
background: #f5f5f5;
padding-top: 50px;
}
/* 自定义菜单栏样式 */
.custom-menu-bar {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 50px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-bottom: 2px solid #5568d3;
display: flex;
align-items: center;
padding: 0 20px;
z-index: 1000;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
.menu-item {
position: relative;
padding: 0 20px;
height: 50px;
display: flex;
align-items: center;
cursor: pointer;
color: white;
font-size: 14px;
font-weight: 500;
transition: all 0.3s ease;
user-select: none;
}
.menu-item:hover {
background: rgba(255, 255, 255, 0.1);
}
.menu-item.active {
background: rgba(255, 255, 255, 0.2);
}
.submenu {
display: none;
position: absolute;
top: 100%;
left: 0;
background: white;
border: 1px solid #e0e0e0;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
min-width: 180px;
padding: 6px 0;
margin-top: 4px;
z-index: 1001;
pointer-events: auto;
animation: slideDown 0.2s ease;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.menu-item:hover .submenu,
.menu-item.active .submenu {
display: block;
}
.submenu-item {
padding: 10px 20px;
cursor: pointer;
color: #333;
font-size: 14px;
transition: all 0.2s ease;
display: flex;
align-items: center;
position: relative;
pointer-events: auto;
user-select: none;
}
.submenu-item.selected {
font-weight: 500;
}
.submenu-item.selected::before {
content: '✓';
margin-right: 8px;
font-weight: bold;
}
.submenu-item.chinese-item {
color: #ff4d4f;
}
.submenu-item.chinese-item:hover {
background: #fff1f0;
color: #ff4d4f;
}
.submenu-item.chinese-item.selected {
background: #fff1f0;
color: #ff4d4f;
}
.submenu-item.chinese-item.selected::before {
color: #ff4d4f;
}
.submenu-item.english-item {
color: #1890ff;
}
.submenu-item.english-item:hover {
background: #e7f3ff;
color: #1890ff;
}
.submenu-item.english-item.selected {
background: #e7f3ff;
color: #1890ff;
}
.submenu-item.english-item.selected::before {
color: #1890ff;
}
.container {
max-width: 800px;
margin: 40px auto;
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
</style>
</head>
<body>
<!-- 自定义菜单栏 -->
<div class="custom-menu-bar">
<div class="menu-item" id="languageMenu">
<span id="languageMenuLabel">语言</span>
<div class="submenu" id="languageSubmenu">
<div class="submenu-item chinese-item" data-lang="zh-CN" data-label="中文">
<span>中文</span>
</div>
<div class="submenu-item english-item" data-lang="en-US" data-label="英文">
<span>英文</span>
</div>
</div>
</div>
</div>
<!-- 内容区域 -->
<div class="container">
<h1 id="mainTitle">欢迎使用 Electron for 鸿蒙PC!</h1>
<div class="info-box" id="infoBox">
<p id="infoText">这是运行在鸿蒙PC系统上的 Electron 应用</p>
<p id="menuHint">点击顶部自定义菜单栏的"语言"菜单,可以选择"中文"或"英文"</p>
</div>
<div class="language-status">
<p id="currentLang">当前语言: <strong id="langDisplay">中文 (zh-CN)</strong></p>
<p id="langNote">语言切换功能已启用,点击自定义菜单栏的"语言"菜单可以切换语言</p>
</div>
</div>
<script>
const { ipcRenderer } = require('electron');
// 多语言文本资源
const translations = {
'zh-CN': {
title: '二级目录嵌套Demo - Electron for 鸿蒙PC',
mainTitle: '欢迎使用 Electron for 鸿蒙PC!',
infoText: '这是运行在鸿蒙PC系统上的 Electron 应用',
menuHint: '点击顶部自定义菜单栏的"语言"菜单,可以选择"中文"或"英文"',
currentLang: '当前语言:',
langDisplay: '中文 (zh-CN)',
langNote: '语言切换功能已启用,点击自定义菜单栏的"语言"菜单可以切换语言',
languageMenuLabel: '语言'
},
'en-US': {
title: 'Nested Menu Demo - Electron for HarmonyOS PC',
mainTitle: 'Welcome to Electron for HarmonyOS PC!',
infoText: 'This is an Electron application running on HarmonyOS PC',
menuHint: 'Click the "Language" menu in the custom menu bar to select "Chinese" or "English"',
currentLang: 'Current Language:',
langDisplay: 'English (en-US)',
langNote: 'Language switching is enabled. Click the "Language" menu in the custom menu bar to switch languages',
languageMenuLabel: 'Language'
}
};
let currentLang = 'zh-CN';
// 更新页面语言
function updatePageLanguage(languageCode) {
currentLang = languageCode;
const t = translations[languageCode] || translations['zh-CN'];
document.getElementById('pageTitle').textContent = t.title;
document.getElementById('mainTitle').textContent = t.mainTitle;
document.getElementById('infoText').textContent = t.infoText;
document.getElementById('menuHint').textContent = t.menuHint;
document.getElementById('currentLang').textContent = t.currentLang + ':';
document.getElementById('langDisplay').textContent = t.langDisplay;
document.getElementById('langNote').textContent = t.langNote;
document.getElementById('languageMenuLabel').textContent = t.languageMenuLabel;
document.documentElement.lang = languageCode;
}
// 初始化自定义菜单
function initCustomMenu() {
const languageMenu = document.getElementById('languageMenu');
const submenuItems = document.querySelectorAll('.submenu-item');
// 菜单项点击处理
submenuItems.forEach(item => {
item.addEventListener('click', (e) => {
e.stopPropagation();
e.preventDefault();
const langCode = item.dataset.lang;
const langLabel = item.dataset.label;
submenuItems.forEach(i => {
i.classList.remove('selected');
});
item.classList.add('selected');
updatePageLanguage(langCode);
ipcRenderer.send('custom-menu-language-changed', {
language: langLabel,
languageCode: langCode
});
setTimeout(() => {
languageMenu.classList.remove('active');
}, 100);
});
});
// 点击展开/关闭
languageMenu.addEventListener('click', (e) => {
if (e.target.closest('.submenu-item')) {
return;
}
e.stopPropagation();
languageMenu.classList.toggle('active');
});
// 悬停展开
let menuTimeout = null;
languageMenu.addEventListener('mouseenter', () => {
if (menuTimeout) {
clearTimeout(menuTimeout);
menuTimeout = null;
}
languageMenu.classList.add('active');
});
languageMenu.addEventListener('mouseleave', () => {
menuTimeout = setTimeout(() => {
languageMenu.classList.remove('active');
}, 150);
});
const submenu = document.getElementById('languageSubmenu');
submenu.addEventListener('mouseenter', () => {
if (menuTimeout) {
clearTimeout(menuTimeout);
menuTimeout = null;
}
});
submenu.addEventListener('mouseleave', () => {
languageMenu.classList.remove('active');
});
document.addEventListener('click', (e) => {
if (!languageMenu.contains(e.target)) {
languageMenu.classList.remove('active');
}
});
// 初始化选中状态
const currentItem = Array.from(submenuItems).find(
item => item.dataset.lang === currentLang
);
if (currentItem) {
currentItem.classList.add('selected');
}
}
// 监听来自主进程的语言切换消息
ipcRenderer.on('language-changed', (event, data) => {
const languageCode = data.languageCode || data;
updatePageLanguage(languageCode);
const submenuItems = document.querySelectorAll('.submenu-item');
submenuItems.forEach(item => {
item.classList.remove('selected');
if (item.dataset.lang === languageCode) {
item.classList.add('selected');
}
});
});
document.addEventListener('DOMContentLoaded', () => {
updatePageLanguage(currentLang);
initCustomMenu();
});
</script>
</body>
</html>
总结
关键要点
-
原生菜单栏:
- 使用
Menu.buildFromTemplate()创建 - 样式由操作系统控制
- 适合简单的菜单需求
- 使用
-
自定义菜单栏:
- 使用 HTML/CSS/JavaScript 实现
- 完全可控的样式和交互
- 适合需要复杂UI的场景
-
二级目录嵌套:
- 原生菜单:使用
submenu属性 - 自定义菜单:使用 CSS 和 JavaScript 控制显示/隐藏
- 原生菜单:使用
-
功能同步:
- 使用 IPC 通信实现主进程和渲染进程的同步
- 统一的处理函数确保状态一致
-
常见问题:
- 菜单无法展开 → 检查 CSS 和事件绑定
- 点击无效 → 检查
pointer-events和事件处理 - 状态不同步 → 重新创建菜单或使用 IPC 同步
最佳实践
- 平台适配:使用平台检测自动适配不同平台的菜单结构
- 菜单结构设计:
- 鸿蒙平台:使用扁平化菜单,避免嵌套限制
- 其他平台:可以设计深层嵌套菜单
- 状态管理:使用统一的处理函数管理状态
- IPC 通信:确保消息格式一致
- 错误处理:添加适当的错误处理和日志
- 用户体验:提供清晰的视觉反馈和流畅的动画
- 测试:在不同平台上测试菜单功能,确保兼容性
使用方法与快速开始
环境要求
- 操作系统:鸿蒙PC(HarmonyOS PC)或 Mac/Windows/Linux
- 开发工具:DevEco Studio 6.0.0 或更高版本(鸿蒙平台)
- Node.js:建议使用 Node.js 16.x 或更高版本
- Electron:项目内置 Electron 运行时
安装步骤
-
克隆或下载项目
git clone <repository-url> cd Directory_nesting_demo -
安装依赖
# 进入Electron应用目录 cd web_engine/src/main/resources/resfile/resources/app # 安装Electron依赖 npm install -
运行项目
方式1:在DevEco Studio中运行(鸿蒙平台)
- 在DevEco Studio中打开项目
- 选择运行配置
- 点击运行按钮
方式2:使用命令行运行(Mac/Windows/Linux)
cd web_engine/src/main/resources/resfile/resources/app npm start
平台检测使用方法
1. 自动平台检测
代码会自动检测运行平台,无需手动配置:
// 自动检测平台
const isHarmonyOS = isHarmonyOSPlatform();
if (isHarmonyOS) {
// 鸿蒙平台:使用扁平化菜单
menuItems = createFlatMenu();
} else {
// 其他平台:使用嵌套菜单
menuItems = [createNestedMenuByPlatform(10)];
}
2. 查看平台信息
运行应用后,查看控制台输出可以确认平台检测结果:
Mac/Windows/Linux平台控制台日志:

图5:Mac/Windows/Linux平台 - 控制台日志输出
日志输出示例:
当前平台: darwin
是否为鸿蒙平台: false
Mac/Windows/Linux平台:使用10级嵌套菜单
菜单创建成功(10级嵌套),已设置到应用菜单栏
鸿蒙平台控制台日志:

图6:鸿蒙平台 - 控制台日志输出
日志输出示例:
当前平台: ohos
是否为鸿蒙平台: true
鸿蒙平台:使用扁平化一级菜单(10个菜单项平铺)
菜单创建成功(鸿蒙平台:10个一级菜单项平铺),已设置到应用菜单栏
日志说明:
- 日志会显示当前运行的平台类型(
process.platform) - 显示平台检测结果(是否为鸿蒙平台)
- 显示使用的菜单类型(扁平化菜单或嵌套菜单)
- 显示菜单创建是否成功
3. 手动调整平台检测
如果自动检测不准确,可以手动调整 isHarmonyOSPlatform() 函数:
function isHarmonyOSPlatform() {
const platform = process.platform;
// 根据实际平台值调整
if (platform === 'ohos' || platform === 'harmonyos') {
return true;
}
// 检查环境变量
if (process.env.HARMONY_OS || process.env.OHOS_PLATFORM) {
return true;
}
// 其他检测逻辑...
return false;
}
菜单功能说明
鸿蒙平台菜单
- 菜单结构:10个一级菜单项(一、二、三…十)平铺显示
- 点击行为:每个菜单项都可以直接点击
- 语言菜单:支持中文/英文切换
- 优势:避免嵌套限制,所有功能都可以直接访问
Mac/Windows/Linux平台菜单
- 菜单结构:10级嵌套菜单(一 → 二 → … → 十)
- 点击行为:需要逐级展开才能访问深层菜单
- 语言菜单:支持中文/英文切换
- 优势:菜单结构更紧凑,节省菜单栏空间
常见使用场景
场景1:开发跨平台应用
// 自动适配不同平台
function createMenu() {
const isHarmonyOS = isHarmonyOSPlatform();
if (isHarmonyOS) {
// 鸿蒙平台:扁平化菜单
return createFlatMenu();
} else {
// 其他平台:嵌套菜单
return createNestedMenuByPlatform(10);
}
}
场景2:需要统一菜单结构
如果需要在所有平台使用相同的扁平化菜单:
function createMenu() {
// 所有平台都使用扁平化菜单
const menuItems = createFlatMenu();
const template = [
...menuItems,
{ label: '语言', submenu: [...] }
];
return Menu.buildFromTemplate(template);
}
场景3:需要统一嵌套结构
如果需要在所有平台使用嵌套菜单(注意鸿蒙平台限制):
function createMenu() {
const isHarmonyOS = isHarmonyOSPlatform();
// 根据平台限制嵌套深度
const maxLevel = isHarmonyOS ? 2 : 10;
const nestedMenu = createNestedMenuByPlatform(maxLevel);
const template = [
nestedMenu,
{ label: '语言', submenu: [...] }
];
return Menu.buildFromTemplate(template);
}
故障排除
问题1:平台检测不准确
解决方案:
-
查看控制台输出的
process.platform值Mac/Windows/Linux平台日志示例:

图5:Mac/Windows/Linux平台 - 控制台日志输出
鸿蒙平台日志示例:

图6:鸿蒙平台 - 控制台日志输出
-
根据实际值调整
isHarmonyOSPlatform()函数 -
可以添加环境变量检测作为补充
问题2:鸿蒙平台菜单无法展开
解决方案:
- 确认使用的是扁平化菜单(
createFlatMenu()) - 检查是否有错误日志
- 如果仍有问题,可以尝试只创建前5个菜单项
问题3:其他平台菜单嵌套失败
解决方案:
- 检查菜单结构是否正确
- 确保有子菜单的菜单项没有
click事件 - 尝试降低嵌套深度(如改为5级)
参考资料
更多推荐



所有评论(0)