Skip to content

Electron 原生集成

系统托盘、原生菜单、通知、对话框与快捷键

目录


系统托盘

Tray 基础

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

let tray = null;

app.whenReady().then(() => {
  tray = new Tray(path.join(__dirname, 'icon.png'));
  tray.setToolTip('My App');

  const contextMenu = Menu.buildFromTemplate([
    { label: '显示', click: () => mainWindow.show() },
    { label: '隐藏', click: () => mainWindow.hide() },
    { type: 'separator' },
    { label: '退出', click: () => app.quit() }
  ]);

  tray.setContextMenu(contextMenu);

  tray.on('click', () => {
    mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show();
  });
});

动态图标与角标

typescript
// macOS 角标(未读数量)
app.dock?.setBadge('3');
app.dock?.setBadge('');

// 动态托盘图标(不同状态)
tray.setImage(path.join(__dirname, 'icon-normal.png'));
tray.setImage(path.join(__dirname, 'icon-active.png'));

原生菜单

应用菜单

typescript
const { Menu } = require('electron');

const template = [
  {
    label: '文件',
    submenu: [
      { label: '新建', accelerator: 'CmdOrCtrl+N', click: () => {} },
      { label: '打开', accelerator: 'CmdOrCtrl+O', click: () => {} },
      { type: 'separator' },
      { label: '退出', accelerator: 'CmdOrCtrl+Q', role: 'quit' }
    ]
  },
  {
    label: '编辑',
    submenu: [
      { label: '撤销', role: 'undo' },
      { label: '重做', role: 'redo' },
      { type: 'separator' },
      { label: '剪切', role: 'cut' },
      { label: '复制', role: 'copy' },
      { label: '粘贴', role: 'paste' }
    ]
  },
  {
    label: '视图',
    submenu: [
      { label: '刷新', role: 'reload' },
      { label: '开发者工具', role: 'toggleDevTools' }
    ]
  }
];

Menu.setApplicationMenu(Menu.buildFromTemplate(template));

窗口上下文菜单

typescript
// 渲染进程通过 preload 请求
// preload.js
contextBridge.exposeInMainWorld('api', {
  showContextMenu: () => ipcRenderer.invoke('show-context-menu')
});

// main.js
const { Menu } = require('electron');

ipcMain.handle('show-context-menu', (event) => {
  const template = [
    { label: '复制', role: 'copy' },
    { label: '粘贴', role: 'paste' }
  ];
  const menu = Menu.buildFromTemplate(template);
  menu.popup({ window: BrowserWindow.fromWebContents(event.sender) });
});

role 内置角色

role说明
undo / redo撤销 / 重做
cut / copy / paste剪贴板
reload / forceReload刷新
toggleDevTools开发者工具
quit退出应用

通知与对话框

系统通知

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

// 需用户授权(macOS)
if (Notification.isSupported()) {
  new Notification({
    title: '新消息',
    body: '您有一条未读消息',
    icon: path.join(__dirname, 'icon.png')
  }).show();
}

原生对话框

typescript
const { dialog } = require('electron');

// 打开文件
const { filePaths, canceled } = await dialog.showOpenDialog(mainWindow, {
  title: '选择文件',
  defaultPath: app.getPath('documents'),
  filters: [
    { name: '文本', extensions: ['txt'] },
    { name: '全部', extensions: ['*'] }
  ],
  properties: ['openFile', 'multiSelections']
});

// 保存文件
const { filePath } = await dialog.showSaveDialog(mainWindow, {
  defaultPath: 'untitled.txt',
  filters: [{ name: '文本', extensions: ['txt'] }]
});

// 消息框
const { response } = await dialog.showMessageBox(mainWindow, {
  type: 'question',
  title: '确认',
  message: '确定要删除吗?',
  buttons: ['取消', '删除']
});

与渲染进程联动

typescript
// preload 暴露
contextBridge.exposeInMainWorld('api', {
  openFile: () => ipcRenderer.invoke('dialog:openFile'),
  showMessage: (opts) => ipcRenderer.invoke('dialog:message', opts)
});

// main 处理
ipcMain.handle('dialog:openFile', async () => {
  const result = await dialog.showOpenDialog(mainWindow, { /* ... */ });
  return result;
});

剪贴板与快捷键

剪贴板

typescript
const { clipboard } = require('electron');

// 读写文本
clipboard.writeText('Hello');
const text = clipboard.readText();

// 读写 HTML
clipboard.writeHTML('<b>Bold</b>');
const html = clipboard.readHTML();

// 读写图片(NativeImage)
clipboard.writeImage(nativeImage);
const image = clipboard.readImage();

全局快捷键

typescript
const { globalShortcut } = require('electron');

app.whenReady().then(() => {
  globalShortcut.register('CommandOrControl+Shift+X', () => {
    mainWindow?.webContents.send('shortcut-triggered', 'custom');
  });
});

app.on('will-quit', () => {
  globalShortcut.unregisterAll();
});

窗口内快捷键

通过菜单 accelerator 或渲染进程监听 keydown 实现,无需 globalShortcut。


与 Web 页面交互

模式一:主进程直接调用

主进程持有 BrowserWindow,可直接调用 dialogMenu 等,再通过 webContents.send 将结果推送给渲染进程。

模式二:渲染进程发起请求

typescript
// 渲染进程无法直接调 dialog,需通过 IPC
// 1. preload 暴露 invoke
// 2. main 执行 dialog.showOpenDialog
// 3. 返回结果给渲染进程

模式三:Preload 暴露只读能力

typescript
// 剪贴板、部分只读 API 可在 preload 中直接暴露
contextBridge.exposeInMainWorld('api', {
  readClipboard: () => clipboard.readText()
});
// 注意:preload 可 require('electron'),但需谨慎暴露

高频面试题

Q1: Tray 与 Dock 的区别?

特性TrayDock
位置系统托盘(任务栏旁)macOS 底部 / Windows 任务栏
用途后台常驻、快捷入口应用图标、窗口切换
平台全平台macOS 特有 Dock API

Q2: 如何实现「点击托盘显示/隐藏窗口」?

typescript
tray.on('click', () => {
  mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show();
});

Q3: 全局快捷键为什么要 unregister?

避免应用退出后快捷键仍被占用,导致其他应用无法使用。在 will-quit 中调用 globalShortcut.unregisterAll()

前端面试知识库