Electron 菜单和托盘的实现
Electron 提供了强大的菜单和系统托盘功能,可以让应用更好地集成到操作系统中。本文将详细介绍如何在 Electron 中实现菜单和托盘功能。应用菜单1. 创建应用菜单// main.jsconst { app, Menu, BrowserWindow } = require('electron')let mainWindowapp.whenReady().then(() => { createWindow() createMenu()})function createMenu() { const template = [ { label: 'File', submenu: [ { label: 'New', accelerator: 'CmdOrCtrl+N', click: () => { console.log('New file') } }, { label: 'Open', accelerator: 'CmdOrCtrl+O', click: () => { console.log('Open file') } }, { type: 'separator' }, { label: 'Save', accelerator: 'CmdOrCtrl+S', click: () => { console.log('Save file') } }, { type: 'separator' }, { label: 'Quit', accelerator: 'CmdOrCtrl+Q', click: () => app.quit() } ] }, { label: 'Edit', submenu: [ { label: 'Undo', accelerator: 'CmdOrCtrl+Z', role: 'undo' }, { label: 'Redo', accelerator: 'CmdOrCtrl+Y', role: 'redo' }, { type: 'separator' }, { label: 'Cut', accelerator: 'CmdOrCtrl+X', role: 'cut' }, { label: 'Copy', accelerator: 'CmdOrCtrl+C', role: 'copy' }, { label: 'Paste', accelerator: 'CmdOrCtrl+V', role: 'paste' } ] }, { label: 'View', submenu: [ { label: 'Reload', accelerator: 'CmdOrCtrl+R', role: 'reload' }, { label: 'Toggle Developer Tools', accelerator: 'CmdOrCtrl+Shift+I', role: 'toggleDevTools' }, { type: 'separator' }, { label: 'Actual Size', accelerator: 'CmdOrCtrl+0', role: 'resetZoom' }, { label: 'Zoom In', accelerator: 'CmdOrCtrl+Plus', role: 'zoomIn' }, { label: 'Zoom Out', accelerator: 'CmdOrCtrl+-', role: 'zoomOut' }, { type: 'separator' }, { label: 'Toggle Full Screen', accelerator: 'F11', role: 'togglefullscreen' } ] }, { label: 'Window', submenu: [ { label: 'Minimize', accelerator: 'CmdOrCtrl+M', role: 'minimize' }, { label: 'Close', accelerator: 'CmdOrCtrl+W', role: 'close' } ] }, { label: 'Help', submenu: [ { label: 'About', click: () => { console.log('About') } }, { label: 'Documentation', click: () => { shell.openExternal('https://electronjs.org/docs') } } ] } ] const menu = Menu.buildFromTemplate(template) Menu.setApplicationMenu(menu)}2. 动态菜单// main.jslet recentFiles = []function updateRecentFilesMenu() { const recentMenu = recentFiles.map(file => ({ label: file, click: () => { openFile(file) } })) const template = [ { label: 'File', submenu: [ { label: 'New', accelerator: 'CmdOrCtrl+N', click: () => newFile() }, { label: 'Open', accelerator: 'CmdOrCtrl+O', click: () => openFile() }, { type: 'separator' }, { label: 'Open Recent', submenu: recentMenu.length > 0 ? recentMenu : [{ label: 'No recent files', enabled: false }] }, { type: 'separator' }, { label: 'Quit', accelerator: 'CmdOrCtrl+Q', click: () => app.quit() } ] } ] const menu = Menu.buildFromTemplate(template) Menu.setApplicationMenu(menu)}function addRecentFile(filePath) { recentFiles = recentFiles.filter(f => f !== filePath) recentFiles.unshift(filePath) recentFiles = recentFiles.slice(0, 10) // 最多保留10个 updateRecentFilesMenu()}3. 上下文菜单// main.jsconst { contextMenu } = require('electron')app.whenReady().then(() => { // 启用上下文菜单 contextMenu({ prepend: (params, browserWindow) => [ { label: 'Custom Action', click: () => { console.log('Custom action clicked') } } ] })})// 或者手动创建上下文菜单function createContextMenu() { const template = [ { label: 'Copy', accelerator: 'CmdOrCtrl+C', click: () => { const win = BrowserWindow.getFocusedWindow() win.webContents.copy() } }, { label: 'Paste', accelerator: 'CmdOrCtrl+V', click: () => { const win = BrowserWindow.getFocusedWindow() win.webContents.paste() } }, { type: 'separator' }, { label: 'Inspect Element', click: () => { const win = BrowserWindow.getFocusedWindow() win.webContents.inspectElement() } } ] const menu = Menu.buildFromTemplate(template) return menu}// 在渲染进程中使用// renderer.jsconst { remote } = require('electron')document.addEventListener('contextmenu', (event) => { event.preventDefault() const menu = remote.getGlobal('contextMenu') menu.popup({ window: remote.getCurrentWindow() })})系统托盘1. 创建托盘图标// main.jsconst { app, Tray, Menu, nativeImage } = require('electron')const path = require('path')let tray = nullapp.whenReady().then(() => { createTray()})function createTray() { // 创建托盘图标 const iconPath = path.join(__dirname, 'icon.png') const icon = nativeImage.createFromPath(iconPath) // 设置图标大小 tray = new Tray(icon.resize({ width: 16, height: 16 })) // 设置工具提示 tray.setToolTip('My Application') // 创建托盘菜单 const contextMenu = Menu.buildFromTemplate([ { label: 'Show App', click: () => { showWindow() } }, { label: 'Hide App', click: () => { hideWindow() } }, { type: 'separator' }, { label: 'Settings', click: () => { openSettings() } }, { type: 'separator' }, { label: 'Quit', click: () => { app.quit() } } ]) tray.setContextMenu(contextMenu) // 双击托盘图标 tray.on('double-click', () => { showWindow() }) // 单击托盘图标 tray.on('click', () => { toggleWindow() })}2. 托盘图标动画// main.jslet trayAnimationInterval = nullfunction startTrayAnimation() { const icons = [ path.join(__dirname, 'icon1.png'), path.join(__dirname, 'icon2.png'), path.join(__dirname, 'icon3.png') ] let currentIndex = 0 trayAnimationInterval = setInterval(() => { const icon = nativeImage.createFromPath(icons[currentIndex]) tray.setImage(icon.resize({ width: 16, height: 16 })) currentIndex = (currentIndex + 1) % icons.length }, 500)}function stopTrayAnimation() { if (trayAnimationInterval) { clearInterval(trayAnimationInterval) trayAnimationInterval = null } // 恢复默认图标 const defaultIcon = nativeImage.createFromPath(path.join(__dirname, 'icon.png')) tray.setImage(defaultIcon.resize({ width: 16, height: 16 }))}3. 托盘通知// main.jsconst { Notification } = require('electron')function showTrayNotification(title, body) { if (Notification.isSupported()) { const notification = new Notification({ title, body, icon: path.join(__dirname, 'icon.png'), silent: false }) notification.on('click', () => { showWindow() }) notification.show() }}// 使用示例ipcMain.on('show-notification', (event, { title, body }) => { showTrayNotification(title, body)})窗口控制1. 最小化到托盘// main.jslet mainWindowfunction createWindow() { mainWindow = new BrowserWindow({ width: 800, height: 600 }) mainWindow.on('minimize', (event) => { event.preventDefault() mainWindow.hide() }) mainWindow.on('close', (event) => { if (!app.isQuitting) { event.preventDefault() mainWindow.hide() } })}function showWindow() { if (mainWindow) { mainWindow.show() mainWindow.focus() }}function hideWindow() { if (mainWindow) { mainWindow.hide() }}function toggleWindow() { if (mainWindow.isVisible()) { hideWindow() } else { showWindow() }}app.on('before-quit', () => { app.isQuitting = true})2. 托盘菜单动态更新// main.jsfunction updateTrayMenu() { const isPlaying = getPlaybackState() const template = [ { label: isPlaying ? 'Pause' : 'Play', click: () => { togglePlayback() updateTrayMenu() } }, { label: 'Next Track', click: () => { nextTrack() } }, { label: 'Previous Track', click: () => { previousTrack() } }, { type: 'separator' }, { label: 'Show App', click: () => { showWindow() } }, { label: 'Quit', click: () => { app.quit() } } ] const contextMenu = Menu.buildFromTemplate(template) tray.setContextMenu(contextMenu)}跨平台兼容1. 平台特定菜单// main.jsfunction createMenu() { const template = [ { label: 'File', submenu: [ { label: 'New', accelerator: 'CmdOrCtrl+N', click: () => newFile() }, { label: 'Open', accelerator: 'CmdOrCtrl+O', click: () => openFile() }, { type: 'separator' }, { label: 'Save', accelerator: 'CmdOrCtrl+S', click: () => saveFile() } ] } ] // macOS 特定菜单 if (process.platform === 'darwin') { template.unshift({ label: app.getName(), submenu: [ { label: 'About ' + app.getName(), role: 'about' }, { type: 'separator' }, { label: 'Preferences', accelerator: 'Cmd+,', click: () => openPreferences() }, { type: 'separator' }, { label: 'Services', role: 'services', submenu: [] }, { type: 'separator' }, { label: 'Hide ' + app.getName(), accelerator: 'Command+H', role: 'hide' }, { label: 'Hide Others', accelerator: 'Command+Shift+H', role: 'hideothers' }, { label: 'Show All', role: 'unhide' }, { type: 'separator' }, { label: 'Quit', accelerator: 'Command+Q', click: () => app.quit() } ] }) } const menu = Menu.buildFromTemplate(template) Menu.setApplicationMenu(menu)}2. 平台特定快捷键// main.jsfunction getAccelerator(action) { const accelerators = { 'new': 'CmdOrCtrl+N', 'open': 'CmdOrCtrl+O', 'save': 'CmdOrCtrl+S', 'quit': process.platform === 'darwin' ? 'Cmd+Q' : 'Ctrl+Q', 'preferences': process.platform === 'darwin' ? 'Cmd+,' : 'Ctrl+,' } return accelerators[action] || ''}3. 托盘图标适配// main.jsfunction createTray() { let iconPath if (process.platform === 'darwin') { // macOS 使用模板图标 iconPath = path.join(__dirname, 'iconTemplate.png') } else if (process.platform === 'win32') { // Windows 使用 ICO 格式 iconPath = path.join(__dirname, 'icon.ico') } else { // Linux 使用 PNG 格式 iconPath = path.join(__dirname, 'icon.png') } const icon = nativeImage.createFromPath(iconPath) // macOS 需要设置模板属性 if (process.platform === 'darwin') { icon.setTemplateImage(true) } tray = new Tray(icon) tray.setToolTip('My Application')}最佳实践1. 菜单组织// main.js// 遵循平台约定function createMenu() { const template = [ // macOS: 应用名称菜单 // Windows/Linux: 文件菜单 { label: process.platform === 'darwin' ? app.getName() : 'File', submenu: [ { label: 'New', accelerator: 'CmdOrCtrl+N', click: () => newFile() }, { label: 'Open', accelerator: 'CmdOrCtrl+O', click: () => openFile() }, { type: 'separator' }, { label: 'Save', accelerator: 'CmdOrCtrl+S', click: () => saveFile() }, { type: 'separator' }, { label: 'Quit', accelerator: 'CmdOrCtrl+Q', click: () => app.quit() } ] }, // 编辑菜单 { label: 'Edit', submenu: [ { label: 'Undo', accelerator: 'CmdOrCtrl+Z', role: 'undo' }, { label: 'Redo', accelerator: 'CmdOrCtrl+Y', role: 'redo' }, { type: 'separator' }, { label: 'Cut', accelerator: 'CmdOrCtrl+X', role: 'cut' }, { label: 'Copy', accelerator: 'CmdOrCtrl+C', role: 'copy' }, { label: 'Paste', accelerator: 'CmdOrCtrl+V', role: 'paste' } ] } ] const menu = Menu.buildFromTemplate(template) Menu.setApplicationMenu(menu)}2. 托盘图标设计// main.js// 使用高对比度图标function createTray() { const iconPath = path.join(__dirname, 'icon.png') const icon = nativeImage.createFromPath(iconPath) // 确保图标在不同背景下都可见 const iconSize = process.platform === 'win32' ? 16 : 22 tray = new Tray(icon.resize({ width: iconSize, height: iconSize })) tray.setToolTip('My Application')}3. 用户体验// main.js// 提供清晰的菜单项function createTrayMenu() { const template = [ { label: 'Show Window', click: () => showWindow() }, { type: 'separator' }, { label: 'Settings', click: () => openSettings() }, { label: 'Help', click: () => openHelp() }, { type: 'separator' }, { label: 'Exit', click: () => app.quit() } ] const menu = Menu.buildFromTemplate(template) tray.setContextMenu(menu)}常见问题Q: 如何在 macOS 上创建模板图标?A: 使用 setTemplateImage(true) 方法:const icon = nativeImage.createFromPath(iconPath)icon.setTemplateImage(true)tray = new Tray(icon)Q: 如何禁用默认菜单?A: 设置空菜单:Menu.setApplicationMenu(null)Q: 如何在托盘菜单中添加复选框?A: 使用 type: 'checkbox':{ label: 'Enable Feature', type: 'checkbox', checked: true, click: (menuItem) => { console.log('Checked:', menuItem.checked) }}Q: 如何在 Windows 上创建系统托盘气球提示?A: 使用 display-balloon:tray.displayBalloon({ title: 'Notification', content: 'This is a balloon notification'})