📋 目录

  1. Electron for 鸿蒙PC 菜单栏概述
  2. 原生菜单栏的使用
  3. 二级目录嵌套实现
  4. 平台检测与菜单适配
  5. 鸿蒙系统菜单嵌套限制
  6. 菜单栏的隐藏与显示
  7. 自定义菜单栏开发
  8. 功能同步与通信
  9. 常见问题与解决方案
  10. 完整代码示例
  11. 使用方法与快速开始

Electron for 鸿蒙PC 菜单栏概述

什么是 Electron for 鸿蒙PC 菜单栏?

Electron for 鸿蒙PC 菜单栏是应用窗口顶部的原生菜单系统,类似于传统桌面应用的菜单栏(如 Windows 的资源管理器菜单、Mac 的系统菜单栏)。它提供了标准的用户界面元素,用户可以点击菜单项来执行各种操作。

在这里插入图片描述

图1:Electron for 鸿蒙PC 原生菜单栏示例

菜单栏的类型

Electron for 鸿蒙PC 提供了两种菜单栏实现方式:

  1. 原生菜单栏(Native Menu Bar)

    • 由操作系统控制样式和行为
    • 符合平台规范
    • 样式自定义能力有限
  2. 自定义菜单栏(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级嵌套菜单(一 → 二 → … → 十)
  • ✅ 菜单结构更紧凑,节省菜单栏空间
  • ✅ 支持深层嵌套,符合传统桌面应用习惯

优势

  1. 避免嵌套限制:鸿蒙平台使用一级菜单,不受子窗口嵌套限制
  2. 功能完整:所有10个菜单项都可以访问
  3. 平台适配:Mac/Windows/Linux 仍使用10级嵌套
  4. 自动检测:无需手动配置,自动识别平台

鸿蒙系统菜单嵌套限制

问题描述

在鸿蒙系统上使用 Electron 原生菜单时,发现深层嵌套菜单(超过2-3级)无法正常显示。

错误信息

从日志中可以看到以下错误:

[ERROR:ohos_popup.cc(129)] Cannot create subwindow from another subwindow

这个错误表明:鸿蒙系统不允许从子窗口再创建子窗口,这限制了 Electron 原生菜单的嵌套深度。

限制说明

鸿蒙系统的限制
  1. 子窗口限制:鸿蒙系统的窗口管理机制不允许从子窗口(subwindow)再创建子窗口
  2. 菜单实现机制:Electron 的原生菜单在鸿蒙系统上通过创建子窗口来实现子菜单
  3. 嵌套深度:由于上述限制,原生菜单最多支持 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菜单栏 理论上无限 复杂菜单结构

注意事项

  1. 这是鸿蒙系统平台的限制,不是 Electron 本身的限制
  2. 在 Windows、Mac、Linux 平台上,Electron 原生菜单支持更深层的嵌套
  3. 推荐使用平台检测,自动适配不同平台的菜单结构

菜单栏的隐藏与显示

方法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>

总结

关键要点

  1. 原生菜单栏

    • 使用 Menu.buildFromTemplate() 创建
    • 样式由操作系统控制
    • 适合简单的菜单需求
  2. 自定义菜单栏

    • 使用 HTML/CSS/JavaScript 实现
    • 完全可控的样式和交互
    • 适合需要复杂UI的场景
  3. 二级目录嵌套

    • 原生菜单:使用 submenu 属性
    • 自定义菜单:使用 CSS 和 JavaScript 控制显示/隐藏
  4. 功能同步

    • 使用 IPC 通信实现主进程和渲染进程的同步
    • 统一的处理函数确保状态一致
  5. 常见问题

    • 菜单无法展开 → 检查 CSS 和事件绑定
    • 点击无效 → 检查 pointer-events 和事件处理
    • 状态不同步 → 重新创建菜单或使用 IPC 同步

最佳实践

  1. 平台适配:使用平台检测自动适配不同平台的菜单结构
  2. 菜单结构设计
    • 鸿蒙平台:使用扁平化菜单,避免嵌套限制
    • 其他平台:可以设计深层嵌套菜单
  3. 状态管理:使用统一的处理函数管理状态
  4. IPC 通信:确保消息格式一致
  5. 错误处理:添加适当的错误处理和日志
  6. 用户体验:提供清晰的视觉反馈和流畅的动画
  7. 测试:在不同平台上测试菜单功能,确保兼容性

使用方法与快速开始

环境要求

  • 操作系统:鸿蒙PC(HarmonyOS PC)或 Mac/Windows/Linux
  • 开发工具:DevEco Studio 6.0.0 或更高版本(鸿蒙平台)
  • Node.js:建议使用 Node.js 16.x 或更高版本
  • Electron:项目内置 Electron 运行时

安装步骤

  1. 克隆或下载项目

    git clone <repository-url>
    cd Directory_nesting_demo
    
  2. 安装依赖

    # 进入Electron应用目录
    cd web_engine/src/main/resources/resfile/resources/app
    
    # 安装Electron依赖
    npm install
    
  3. 运行项目

    方式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:平台检测不准确

解决方案

  1. 查看控制台输出的 process.platform

    Mac/Windows/Linux平台日志示例

    在这里插入图片描述

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

    鸿蒙平台日志示例

    在这里插入图片描述

    图6:鸿蒙平台 - 控制台日志输出

  2. 根据实际值调整 isHarmonyOSPlatform() 函数

  3. 可以添加环境变量检测作为补充

问题2:鸿蒙平台菜单无法展开

解决方案

  1. 确认使用的是扁平化菜单(createFlatMenu()
  2. 检查是否有错误日志
  3. 如果仍有问题,可以尝试只创建前5个菜单项
问题3:其他平台菜单嵌套失败

解决方案

  1. 检查菜单结构是否正确
  2. 确保有子菜单的菜单项没有 click 事件
  3. 尝试降低嵌套深度(如改为5级)

参考资料

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐