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,可直接调用 dialog、Menu 等,再通过 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 的区别?
| 特性 | Tray | Dock |
|---|---|---|
| 位置 | 系统托盘(任务栏旁) | macOS 底部 / Windows 任务栏 |
| 用途 | 后台常驻、快捷入口 | 应用图标、窗口切换 |
| 平台 | 全平台 | macOS 特有 Dock API |
Q2: 如何实现「点击托盘显示/隐藏窗口」?
typescript
tray.on('click', () => {
mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show();
});Q3: 全局快捷键为什么要 unregister?
避免应用退出后快捷键仍被占用,导致其他应用无法使用。在 will-quit 中调用 globalShortcut.unregisterAll()。