菜单与托盘 #

一、应用菜单 #

1.1 菜单概述 #

text
┌─────────────────────────────────────────────────────────────┐
│  文件   编辑   视图   窗口   帮助                            │  ← 菜单栏
├─────────────────────────────────────────────────────────────┤
│         ┌─────────────┐                                     │
│         │ 新建窗口     │                                     │
│         │ 新建标签页   │                                     │
│         ├─────────────┤                                     │
│         │ 打开...      │  Ctrl+O                            │
│         │ 最近打开     │ ►                                  │
│         ├─────────────┤                                     │
│         │ 保存        │  Ctrl+S                            │
│         │ 另存为...    │  Ctrl+Shift+S                      │
│         ├─────────────┤                                     │
│         │ 退出        │  Ctrl+Q                            │
│         └─────────────┘                                     │
│                                                             │
│                      应用内容区域                            │
│                                                             │
└─────────────────────────────────────────────────────────────┘

1.2 创建菜单 #

javascript
const { Menu, BrowserWindow, app, shell } = require('electron');

const template = [
    {
        label: '文件',
        submenu: [
            {
                label: '新建窗口',
                accelerator: 'CmdOrCtrl+N',
                click: () => createNewWindow()
            },
            {
                label: '新建标签页',
                accelerator: 'CmdOrCtrl+T',
                click: () => createNewTab()
            },
            { type: 'separator' },
            {
                label: '打开...',
                accelerator: 'CmdOrCtrl+O',
                click: async () => {
                    const result = await dialog.showOpenDialog({});
                    if (!result.canceled) {
                        openFile(result.filePaths[0]);
                    }
                }
            },
            { type: 'separator' },
            {
                label: '保存',
                accelerator: 'CmdOrCtrl+S',
                click: () => saveFile()
            },
            {
                label: '另存为...',
                accelerator: 'CmdOrCtrl+Shift+S',
                click: () => saveFileAs()
            },
            { type: 'separator' },
            {
                label: '退出',
                accelerator: process.platform === 'darwin' ? 'Cmd+Q' : 'Alt+F4',
                click: () => app.quit()
            }
        ]
    },
    {
        label: '编辑',
        submenu: [
            { role: 'undo', label: '撤销' },
            { role: 'redo', label: '重做' },
            { type: 'separator' },
            { role: 'cut', label: '剪切' },
            { role: 'copy', label: '复制' },
            { role: 'paste', label: '粘贴' },
            { role: 'delete', label: '删除' },
            { type: 'separator' },
            { role: 'selectAll', label: '全选' }
        ]
    },
    {
        label: '视图',
        submenu: [
            { role: 'reload', label: '重新加载' },
            { role: 'forceReload', label: '强制重新加载' },
            { role: 'toggleDevTools', label: '开发者工具' },
            { type: 'separator' },
            { role: 'resetZoom', label: '重置缩放' },
            { role: 'zoomIn', label: '放大' },
            { role: 'zoomOut', label: '缩小' },
            { type: 'separator' },
            { role: 'togglefullscreen', label: '全屏' }
        ]
    },
    {
        label: '窗口',
        submenu: [
            { role: 'minimize', label: '最小化' },
            { role: 'zoom', label: '缩放' },
            { type: 'separator' },
            { role: 'front', label: '前置所有窗口' },
            { type: 'separator' },
            { role: 'close', label: '关闭' }
        ]
    },
    {
        label: '帮助',
        submenu: [
            {
                label: '文档',
                click: async () => {
                    await shell.openExternal('https://example.com/docs');
                }
            },
            {
                label: '报告问题',
                click: async () => {
                    await shell.openExternal('https://github.com/user/repo/issues');
                }
            },
            { type: 'separator' },
            {
                label: '关于',
                click: () => showAboutDialog()
            }
        ]
    }
];

const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);

1.3 内置角色 #

角色 说明
undo 撤销
redo 重做
cut 剪切
copy 复制
paste 粘贴
pasteAndMatchStyle 粘贴并匹配样式
delete 删除
selectAll 全选
reload 重新加载
forceReload 强制重新加载
toggleDevTools 切换开发者工具
resetZoom 重置缩放
zoomIn 放大
zoomOut 缩小
togglefullscreen 切换全屏
minimize 最小化
close 关闭
zoom 缩放
front 前置窗口
about 关于(macOS)
services 服务(macOS)
hide 隐藏(macOS)
hideOthers 隐藏其他(macOS)
unhide 显示全部(macOS)
quit 退出(macOS)
startSpeaking 开始朗读(macOS)
stopSpeaking 停止朗读(macOS)

1.4 动态菜单 #

javascript
// 动态更新菜单项
function updateMenu(recentFiles) {
    const recentFilesMenu = {
        label: '最近打开',
        submenu: recentFiles.length > 0 
            ? recentFiles.map(file => ({
                label: file,
                click: () => openFile(file)
            }))
            : [{ label: '无最近文件', enabled: false }]
    };

    // 找到文件菜单并更新
    const fileMenuIndex = template.findIndex(m => m.label === '文件');
    const recentIndex = template[fileMenuIndex].submenu.findIndex(
        item => item.label === '最近打开'
    );
    
    if (recentIndex !== -1) {
        template[fileMenuIndex].submenu[recentIndex] = recentFilesMenu;
    }

    // 重建菜单
    const menu = Menu.buildFromTemplate(template);
    Menu.setApplicationMenu(menu);
}

1.5 上下文菜单 #

javascript
// 右键菜单
const contextMenu = Menu.buildFromTemplate([
    { label: '复制', role: 'copy' },
    { label: '粘贴', role: 'paste' },
    { type: 'separator' },
    {
        label: '自定义操作',
        click: () => console.log('自定义操作')
    }
]);

// 监听右键事件
mainWindow.webContents.on('context-menu', (event, params) => {
    // 根据上下文调整菜单
    if (params.selectionText) {
        contextMenu.popup(mainWindow, params.x, params.y);
    }
});

1.6 macOS 特殊菜单 #

javascript
// macOS 应用菜单
if (process.platform === 'darwin') {
    template.unshift({
        label: app.getName(),
        submenu: [
            { role: 'about' },
            { type: 'separator' },
            {
                label: '偏好设置...',
                accelerator: 'Cmd+,',
                click: () => openSettings()
            },
            { type: 'separator' },
            { role: 'services' },
            { type: 'separator' },
            { role: 'hide' },
            { role: 'hideOthers' },
            { role: 'unhide' },
            { type: 'separator' },
            { role: 'quit' }
        ]
    });
}

二、系统托盘 #

2.1 创建托盘 #

javascript
const { Tray, nativeImage, Menu } = require('electron');
const path = require('path');

let tray = null;

function createTray() {
    // 创建图标
    const iconPath = path.join(__dirname, 'assets', 'tray.png');
    const icon = nativeImage.createFromPath(iconPath);
    
    // macOS 使用模板图标
    const trayIcon = process.platform === 'darwin' 
        ? icon.resize({ width: 16, height: 16 })
        : icon;

    tray = new Tray(trayIcon);

    // 设置托盘菜单
    const contextMenu = Menu.buildFromTemplate([
        { label: '显示窗口', click: () => mainWindow.show() },
        { label: '新建窗口', click: () => createNewWindow() },
        { type: 'separator' },
        { label: '设置', click: () => openSettings() },
        { type: 'separator' },
        { label: '退出', click: () => app.quit() }
    ]);

    tray.setToolTip('我的应用');
    tray.setContextMenu(contextMenu);

    // 点击托盘图标
    tray.on('click', () => {
        mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show();
    });

    // 双击托盘图标
    tray.on('double-click', () => {
        mainWindow.show();
        mainWindow.focus();
    });
}

2.2 托盘图标 #

javascript
// 不同状态的图标
const normalIcon = nativeImage.createFromPath('tray.png');
const activeIcon = nativeImage.createFromPath('tray-active.png');
const errorIcon = nativeImage.createFromPath('tray-error.png');

// 设置图标
tray.setImage(normalIcon);

// macOS 托盘图标模板
// 使用文件名后缀 Template,如 trayTemplate.png
// 系统会自动处理深色/浅色模式
const templateIcon = nativeImage.createFromPath('trayTemplate.png');
tray.setImage(templateIcon);

2.3 动态托盘菜单 #

javascript
function updateTrayMenu(status) {
    const statusLabel = {
        idle: '空闲',
        working: '工作中...',
        error: '错误'
    };

    const contextMenu = Menu.buildFromTemplate([
        { 
            label: `状态: ${statusLabel[status]}`,
            enabled: false 
        },
        { type: 'separator' },
        { label: '显示窗口', click: () => mainWindow.show() },
        { 
            label: '开始任务', 
            enabled: status === 'idle',
            click: () => startTask() 
        },
        { 
            label: '停止任务', 
            enabled: status === 'working',
            click: () => stopTask() 
        },
        { type: 'separator' },
        { label: '退出', click: () => app.quit() }
    ]);

    tray.setContextMenu(contextMenu);

    // 更新图标
    const icons = {
        idle: normalIcon,
        working: activeIcon,
        error: errorIcon
    };
    tray.setImage(icons[status]);
}

2.4 托盘通知 #

javascript
const { Notification } = require('electron');

function showTrayNotification(title, body) {
    if (Notification.isSupported()) {
        const notification = new Notification({
            title: title,
            body: body,
            icon: path.join(__dirname, 'assets', 'icon.png')
        });

        notification.on('click', () => {
            mainWindow.show();
            mainWindow.focus();
        });

        notification.show();
    }
}

// 使用
showTrayNotification('任务完成', '文件已成功下载');

2.5 托盘气球提示(Windows) #

javascript
// Windows 气球提示
if (process.platform === 'win32') {
    tray.displayBalloon({
        icon: nativeImage.createFromPath('icon.png'),
        title: '提示标题',
        content: '这是气球提示的内容'
    });
}

三、Dock 图标(macOS) #

3.1 设置 Dock 图标 #

javascript
const { app, nativeImage } = require('electron');

// 设置 Dock 图标
const dockIcon = nativeImage.createFromPath('icon.png');
app.dock.setIcon(dockIcon);

// 设置 Dock 菜单
const dockMenu = Menu.buildFromTemplate([
    { label: '新建窗口', click: () => createNewWindow() },
    { label: '新建标签页', click: () => createNewTab() }
]);
app.dock.setMenu(dockMenu);

// 设置 Dock 徽章
app.dock.setBadge('3');  // 显示数字徽章

// 隐藏 Dock 图标
app.dock.hide();

// 显示 Dock 图标
app.dock.show();

3.2 Dock 弹跳动画 #

javascript
// 请求用户注意
app.dock.bounce();  // 弹跳一次
app.dock.bounce('critical');  // 持续弹跳直到应用获得焦点

// 取消弹跳
const bounceId = app.dock.bounce();
app.dock.cancelBounce(bounceId);

四、任务栏(Windows) #

4.1 任务栏按钮 #

javascript
const { BrowserWindow } = require('electron');

// 设置任务栏按钮
mainWindow.setThumbarButtons([
    {
        tooltip: '播放',
        icon: nativeImage.createFromPath('play.png'),
        click: () => play()
    },
    {
        tooltip: '暂停',
        icon: nativeImage.createFromPath('pause.png'),
        flags: ['disabled'],
        click: () => pause()
    },
    {
        tooltip: '停止',
        icon: nativeImage.createFromPath('stop.png'),
        click: () => stop()
    }
]);

4.2 任务栏进度条 #

javascript
// 设置进度条
mainWindow.setProgressBar(0.5);  // 50%

// 不确定进度
mainWindow.setProgressBar(2);  // 不确定进度模式

// 清除进度条
mainWindow.setProgressBar(-1);

4.3 任务栏覆盖图标 #

javascript
// 设置覆盖图标
mainWindow.setOverlayIcon(
    nativeImage.createFromPath('overlay.png'),
    '状态描述'
);

// 清除覆盖图标
mainWindow.setOverlayIcon(null, '');

五、菜单与托盘最佳实践 #

5.1 跨平台处理 #

javascript
function setupAppMenu() {
    const isMac = process.platform === 'darwin';
    
    const template = [
        // 文件菜单
        {
            label: '文件',
            submenu: [
                { label: '新建', accelerator: 'CmdOrCtrl+N', click: createNew },
                { label: '打开', accelerator: 'CmdOrCtrl+O', click: openFile },
                { type: 'separator' },
                { label: '保存', accelerator: 'CmdOrCtrl+S', click: saveFile },
                { type: 'separator' },
                isMac ? { role: 'close' } : { label: '退出', click: () => app.quit() }
            ]
        },
        // 编辑菜单
        {
            label: '编辑',
            submenu: [
                { role: 'undo' },
                { role: 'redo' },
                { type: 'separator' },
                { role: 'cut' },
                { role: 'copy' },
                { role: 'paste' }
            ]
        }
    ];

    // macOS 添加应用菜单
    if (isMac) {
        template.unshift({
            label: app.getName(),
            submenu: [
                { role: 'about' },
                { type: 'separator' },
                { role: 'preferences' },
                { type: 'separator' },
                { role: 'quit' }
            ]
        });
    }

    const menu = Menu.buildFromTemplate(template);
    Menu.setApplicationMenu(menu);
}

5.2 无菜单窗口 #

javascript
// 创建无菜单的窗口
const win = new BrowserWindow({
    autoHideMenuBar: true,  // 自动隐藏菜单栏
    // 或完全隐藏
    // frame: false
});

// 隐藏菜单栏
win.setMenuBarVisibility(false);

5.3 托盘最小化 #

javascript
// 最小化到托盘而不是任务栏
mainWindow.on('minimize', (event) => {
    event.preventDefault();
    mainWindow.hide();
    tray.displayBalloon({
        title: '应用已最小化',
        content: '点击托盘图标恢复窗口'
    });
});

// 点击托盘恢复
tray.on('click', () => {
    mainWindow.show();
    mainWindow.restore();
    mainWindow.focus();
});

六、总结 #

6.1 核心要点 #

要点 说明
应用菜单 Menu.buildFromTemplate 创建菜单
内置角色 使用 role 简化常见操作
系统托盘 Tray 创建托盘图标和菜单
跨平台 根据平台调整菜单结构
Dock/任务栏 平台特定的功能集成

6.2 下一步 #

现在你已经掌握了菜单与托盘的使用,接下来让我们学习 对话框与通知,深入了解原生对话框和系统通知的实现!

最后更新:2026-03-28