系统托盘与菜单

深入理解 Electron 中的系统托盘(System Tray)创建、托盘菜单配置、以及原生应用菜单(Application Menu)和上下文菜单的实现。


系统托盘(System Tray)基础

系统托盘是桌面上常驻的小图标,点击可以显示菜单或窗口。

创建托盘图标

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

let tray = null;

function createTray() {
  // 创建托盘图标
  const iconPath = path.join(__dirname, 'icon.png');
  const icon = nativeImage.createFromPath(iconPath);

  // macOS 推荐使用模板图片(Template Image)
  // 模板图片会自动适配浅色/深色模式
  const templateIcon = nativeImage.createFromPath(iconPath);
  templateIcon.setTemplateImage(true);

  tray = new Tray(templateIcon);

  // 设置提示文字(鼠标悬停时显示)
  tray.setToolTip('My Electron App');

  // 设置上下文菜单
  const contextMenu = Menu.buildFromTemplate([
    { label: '显示主窗口', click: showMainWindow },
    { type: 'separator' },
    { label: '关于', click: showAbout },
    { type: 'separator' },
    { label: '退出', click: () => app.quit() }
  ]);

  tray.setContextMenu(contextMenu);

  // 点击托盘图标的事件
  tray.on('click', (event) => {
    showMainWindow();
  });

  // macOS 专用:点击时显示菜单
  tray.on('click', (event, bounds) => {
    // 在 macOS 上,点击托盘图标通常显示菜单
    // 但也可以自定义行为
  });
}

托盘图标尺寸

不同操作系统的托盘图标尺寸不同:

系统 推荐尺寸
Windows 16x16, 32x32
macOS 16x16 (@1x), 32x32 (@2x)
Linux 22x22, 32x32

动态更新托盘图标

// 更新托盘图标
function updateTrayIcon(status) {
  const iconPath = status === 'busy'
    ? path.join(__dirname, 'icon-busy.png')
    : path.join(__dirname, 'icon-idle.png');

  const newIcon = nativeImage.createFromPath(iconPath);
  tray.setImage(newIcon);
}

// 更新提示文字
tray.setToolTip(`My App - ${currentStatus}`);

托盘右键菜单

完整菜单示例

const contextMenuTemplate = [
  {
    label: '打开主窗口',
    click: () => {
      const mainWindow = BrowserWindow.getAllWindows()[0];
      if (mainWindow) {
        mainWindow.show();
        mainWindow.focus();
      }
    }
  },
  {
    label: '快速操作',
    submenu: [
      {
        label: '新建文档',
        accelerator: 'CmdOrCtrl+N',
        click: () => createNewDocument()
      },
      {
        label: '打开文件',
        accelerator: 'CmdOrCtrl+O',
        click: () => openFile()
      }
    ]
  },
  { type: 'separator' },
  {
    label: '运行状态',
    enabled: false  // 禁用项,显示当前状态
  },
  {
    label: '● 已连接',
    enabled: false
  },
  { type: 'separator' },
  {
    label: '设置',
    click: () => openSettings()
  },
  {
    label: '检查更新',
    click: () => checkForUpdates()
  },
  { type: 'separator' },
  {
    label: '关于',
    click: () => showAboutDialog()
  },
  {
    label: '退出',
    accelerator: 'CmdOrCtrl+Q',
    click: () => app.quit()
  }
];

const contextMenu = Menu.buildFromTemplate(contextMenuTemplate);
tray.setContextMenu(contextMenu);

子菜单

子菜单可以嵌套多层:

{
  label: '编辑',
  submenu: [
    { label: '撤销', role: 'undo' },
    { label: '重做', role: 'redo' },
    { type: 'separator' },
    { label: '剪切', role: 'cut' },
    { label: '复制', role: 'copy' },
    { label: '粘贴', role: 'paste' },
    { type: 'separator' },
    {
      label: '查找',
      submenu: [
        { label: '查找下一个', accelerator: 'F3', click: () => findNext() },
        { label: '查找上一个', accelerator: 'Shift+F3', click: () => findPrevious() }
      ]
    }
  ]
}

应用菜单(Application Menu)

创建应用菜单

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

function createApplicationMenu() {
  const template = [
    {
      label: app.name,  // macOS 会显示为应用名
      submenu: [
        { label: '关于', role: 'about' },
        { type: 'separator' },
        { label: '设置', accelerator: 'Cmd+,', click: () => openSettings() },
        { type: 'separator' },
        { label: '隐藏', role: 'hide' },
        { label: '隐藏其他', role: 'hideOthers' },
        { label: '显示全部', role: 'unhide' },
        { type: 'separator' },
        { label: '退出', role: 'quit' }
      ]
    },
    {
      label: '文件',
      submenu: [
        { label: '新建', accelerator: 'CmdOrCtrl+N', click: () => newFile() },
        { label: '打开', accelerator: 'CmdOrCtrl+O', click: () => openFile() },
        {
          label: '打开最近',
          role: 'recentDocuments',
          submenu: [
            { label: '清除最近文件', role: 'clearRecentDocuments' }
          ]
        },
        { type: 'separator' },
        { label: '保存', accelerator: 'CmdOrCtrl+S', click: () => saveFile() },
        { label: '另存为', accelerator: 'CmdOrCtrl+Shift+S', click: () => saveFileAs() },
        { type: 'separator' },
        { label: '导出 PDF', click: () => exportPDF() }
      ]
    },
    {
      label: '编辑',
      submenu: [
        { label: '撤销', role: 'undo' },
        { label: '重做', role: 'redo' },
        { type: 'separator' },
        { label: '剪切', role: 'cut' },
        { label: '复制', role: 'copy' },
        { label: '粘贴', role: 'paste' },
        { label: '删除', role: 'delete' },
        { type: 'separator' },
        { label: '全选', role: 'selectAll' }
      ]
    },
    {
      label: '视图',
      submenu: [
        { label: '重新加载', role: 'reload' },
        { label: '强制重新加载', role: 'forceReload' },
        { label: '切换开发者工具', role: 'toggleDevTools' },
        { type: 'separator' },
        { label: '放大', role: 'zoomIn' },
        { label: '缩小', role: 'zoomOut' },
        { label: '重置缩放', role: 'resetZoom' },
        { type: 'separator' },
        { label: '全屏', role: 'togglefullscreen' }
      ]
    },
    {
      label: '窗口',
      submenu: [
        { label: '最小化', role: 'minimize' },
        { label: '缩放', role: 'zoom' },
        { type: 'separator' },
        { label: '前置全部窗口', role: 'front' }
      ]
    },
    {
      label: '帮助',
      submenu: [
        {
          label: '文档',
          click: () => {
            shell.openExternal('https://electronjs.org/docs');
          }
        },
        {
          label: '报告问题',
          click: () => {
            shell.openExternal('https://github.com/electron/electron/issues');
          }
        }
      ]
    }
  ];

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

macOS 特有菜单行为

在 macOS 上,第一个菜单项的名称是应用名称,并且有些 role 行为不同:

{
  label: app.name,  // macOS 会使用应用的实际名称
  submenu: [
    { label: '关于', role: 'about' },
    { type: 'separator' },
    { label: '服务', role: 'services', submenu: [] },  // macOS 专用
    { type: 'separator' },
    { label: '隐藏', role: 'hide' },
    { label: '隐藏其他', role: 'hideOthers' },
    { label: '显示全部', role: 'unhide' },
    { type: 'separator' },
    { label: '退出', role: 'quit' }
  ]
}

上下文菜单(Context Menu)

在渲染进程触发右键菜单

// preload.js
contextBridge.exposeInMainWorld('electronAPI', {
  showContextMenu: (menuTemplate) => {
    return ipcRenderer.invoke('show-context-menu', menuTemplate);
  }
});
// main process
ipcMain.handle('show-context-menu', async (event, menuTemplate) => {
  const menu = Menu.buildFromTemplate(menuTemplate);

  // 在当前窗口显示上下文菜单
  const win = BrowserWindow.fromWebContents(event.sender);
  menu.popup({ window: win });
});

使用示例

// renderer.js
document.addEventListener('contextmenu', (e) => {
  e.preventDefault();

  const menuTemplate = [
    {
      label: '复制',
      accelerator: 'CmdOrCtrl+C',
      click: () => document.execCommand('copy')
    },
    {
      label: '粘贴',
      accelerator: 'CmdOrCtrl+V',
      click: () => document.execCommand('paste')
    },
    { type: 'separator' },
    {
      label: '全选',
      click: () => document.execCommand('selectAll')
    }
  ];

  window.electronAPI.showContextMenu(menuTemplate);
});

动态上下文菜单

根据选中内容显示不同菜单:

document.addEventListener('contextmenu', (e) => {
  e.preventDefault();

  const selection = window.getSelection().toString();
  const hasSelection = selection.length > 0;

  const menuTemplate = [
    {
      label: '复制',
      enabled: hasSelection,
      click: () => document.execCommand('copy')
    },
    {
      label: '剪切',
      enabled: hasSelection,
      click: () => document.execCommand('cut')
    },
    { type: 'separator' },
    {
      label: '搜索:' + (hasSelection ? `"${selection}"` : ''),
      enabled: hasSelection,
      click: () => {
        shell.openExternal(`https://www.google.com/search?q=${encodeURIComponent(selection)}`);
      }
    },
    {
      label: '在词典中查找',
      enabled: hasSelection,
      click: () => {
        shell.openExternal(`https://dict.cn/${encodeURIComponent(selection)}`);
      }
    }
  ];

  window.electronAPI.showContextMenu(menuTemplate);
});

菜单项属性详解

完整菜单项配置

{
  label: '菜单项名称',           // 显示文字
  type: 'normal',               // 类型:normal | separator | submenu | checkbox | radio
  enabled: true,                // 是否可用
  visible: true,                // 是否可见
  accelerator: 'CmdOrCtrl+S',   // 快捷键
  toolTip: '保存文件',           // 提示文字
  icon: nativeImage,            // 图标(可选)
  sublabel: 'Ctrl+S',           // 副标签(macOS)

  // 点击回调
  click: () => { /* action */ },

  // 角色(使用系统预设行为)
  role: 'undo'  // undo, redo, cut, copy, paste, delete, selectAll, reload, forceReload,
                 // toggleDevTools, togglefullscreen, minimize, zoom, close, quit,
                 // about, hide, hideOthers, unhide, front, help, services
}

Checkbox 菜单项

{
  label: '自动保存',
  type: 'checkbox',
  checked: autoSaveEnabled,
  click: (menuItem) => {
    autoSaveEnabled = menuItem.checked;
    updateSettings('autoSave', autoSaveEnabled);
  }
}

Radio 菜单项

{
  label: '视图模式',
  submenu: [
    {
      label: '紧凑',
      type: 'radio',
      checked: viewMode === 'compact',
      click: () => setViewMode('compact')
    },
    {
      label: '舒适',
      type: 'radio',
      checked: viewMode === 'comfortable',
      click: () => setViewMode('comfortable')
    },
    {
      label: '扩展',
      type: 'radio',
      checked: viewMode === 'expanded',
      click: () => setViewMode('expanded')
    }
  ]
}

快捷键(Accelerator)

格式说明

修饰键 Windows/Linux macOS
Ctrl Ctrl Ctrl
Alt Alt Alt
Shift Shift Shift
Cmd 不支持 Cmd
CmdOrCtrl Ctrl Cmd
Plus + +

示例

// 单键
{ label: '保存', accelerator: 'CmdOrCtrl+S' }

// 组合键
{ label: '强制刷新', accelerator: 'CmdOrCtrl+Shift+R' }

// 功能键
{ label: '全屏', accelerator: 'F11' }
{ label: '退出', accelerator: 'Cmd+Q' }  // macOS 常用

// 空格键
{ label: '播放/暂停', accelerator: 'Space' }

本地化快捷键显示

不同系统显示不同:

  • Windows: Ctrl+S
  • macOS: ⌘S

Electron 会自动根据系统渲染 accelerator。


Dock 菜单(macOS)

macOS 独有的 Dock 图标右键菜单:

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

function createDockMenu() {
  const dockMenu = Menu.buildFromTemplate([
    {
      label: '新建窗口',
      click: () => {
        createNewWindow();
      }
    },
    {
      label: '新建文档',
      click: () => {
        createNewDocument();
      }
    },
    { type: 'separator' },
    {
      label: '最近文档',
      submenu: [
        { label: '文档 1.pdf' },
        { label: '文档 2.pdf' }
      ]
    }
  ]);

  app.dock.setMenu(dockMenu);
}

Dock Badge

显示通知数量:

// 设置 badge(macOS)
app.dock.setBadge('5');  // 显示 "5"
app.dock.setBadge('');   // 清除

// Windows/Linux 可以用 tray 设置
tray.setToolTip('5 条新消息');

菜单设计最佳实践

1. 遵循平台惯例

  • macOS:使用标准 role,尽量让系统处理菜单行为
  • Windows/Linux:支持 CmdOrCtrl 组合键

2. 常用快捷键对照

操作 Windows macOS
保存 Ctrl+S ⌘S
复制 Ctrl+C ⌘C
粘贴 Ctrl+V ⌘V
撤销 Ctrl+Z ⌘Z
全选 Ctrl+A ⌘A
查找 Ctrl+F ⌘F
关闭 Ctrl+W ⌘W
退出 Alt+F4 ⌘Q

3. 菜单分层不要过深

建议最多两层子菜单,过深的菜单难以使用。

4. 动态更新菜单

function updateFileMenu(hasUnsavedChanges) {
  const menu = Menu.getApplicationMenu();
  const fileMenu = menu.items.find(item => item.label === '文件');

  if (hasUnsavedChanges) {
    // 添加或更新"保存"菜单项
  }
}

这一章想说的

系统托盘和菜单是桌面应用的重要组成部分:

  1. 系统托盘:常驻图标 + 右键菜单,适合后台运行应用
  2. 应用菜单:顶栏菜单,包含文件、编辑、视图等标准项
  3. 上下文菜单:右键弹出,根据选中内容动态变化
  4. Dock 菜单:macOS 专用,Dock 图标右键菜单

良好的菜单设计应遵循平台惯例,提供清晰的快捷键支持。


延展阅读