乐闻世界logo
搜索文章和话题

Implementation of Electron Menu and Tray

2月18日 10:35

Electron provides powerful menu and system tray features that allow applications to better integrate with the operating system. This article will detail how to implement menu and tray functionality in Electron.

javascript
// main.js const { app, Menu, BrowserWindow } = require('electron') let mainWindow app.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) }
javascript
// main.js let 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) // Keep at most 10 updateRecentFilesMenu() }
javascript
// main.js const { contextMenu } = require('electron') app.whenReady().then(() => { // Enable context menu contextMenu({ prepend: (params, browserWindow) => [ { label: 'Custom Action', click: () => { console.log('Custom action clicked') } } ] }) }) // Or manually create context menu 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 } // Use in renderer process // renderer.js const { remote } = require('electron') document.addEventListener('contextmenu', (event) => { event.preventDefault() const menu = remote.getGlobal('contextMenu') menu.popup({ window: remote.getCurrentWindow() }) })

System Tray

1. Creating Tray Icon

javascript
// main.js const { app, Tray, Menu, nativeImage } = require('electron') const path = require('path') let tray = null app.whenReady().then(() => { createTray() }) function createTray() { // Create tray icon const iconPath = path.join(__dirname, 'icon.png') const icon = nativeImage.createFromPath(iconPath) // Set icon size tray = new Tray(icon.resize({ width: 16, height: 16 })) // Set tooltip tray.setToolTip('My Application') // Create tray menu 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) // Double-click tray icon tray.on('double-click', () => { showWindow() }) // Single-click tray icon tray.on('click', () => { toggleWindow() }) }

2. Tray Icon Animation

javascript
// main.js let trayAnimationInterval = null function 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 } // Restore default icon const defaultIcon = nativeImage.createFromPath(path.join(__dirname, 'icon.png')) tray.setImage(defaultIcon.resize({ width: 16, height: 16 })) }

3. Tray Notifications

javascript
// main.js const { 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() } } // Usage example ipcMain.on('show-notification', (event, { title, body }) => { showTrayNotification(title, body) })

Window Control

1. Minimize to Tray

javascript
// main.js let mainWindow function 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 })
javascript
// main.js function 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) }

Cross-Platform Compatibility

javascript
// main.js function 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 specific menu 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. Platform-Specific Accelerators

javascript
// main.js function 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. Tray Icon Adaptation

javascript
// main.js function createTray() { let iconPath if (process.platform === 'darwin') { // macOS uses template icon iconPath = path.join(__dirname, 'iconTemplate.png') } else if (process.platform === 'win32') { // Windows uses ICO format iconPath = path.join(__dirname, 'icon.ico') } else { // Linux uses PNG format iconPath = path.join(__dirname, 'icon.png') } const icon = nativeImage.createFromPath(iconPath) // macOS needs to set template property if (process.platform === 'darwin') { icon.setTemplateImage(true) } tray = new Tray(icon) tray.setToolTip('My Application') }

Best Practices

javascript
// main.js // Follow platform conventions function createMenu() { const template = [ // macOS: App name menu // Windows/Linux: File menu { 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() } ] }, // Edit menu { 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. Tray Icon Design

javascript
// main.js // Use high contrast icons function createTray() { const iconPath = path.join(__dirname, 'icon.png') const icon = nativeImage.createFromPath(iconPath) // Ensure icon is visible on different backgrounds const iconSize = process.platform === 'win32' ? 16 : 22 tray = new Tray(icon.resize({ width: iconSize, height: iconSize })) tray.setToolTip('My Application') }

3. User Experience

javascript
// main.js // Provide clear menu items 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) }

Common Questions

Q: How to create template icon on macOS?A: Use setTemplateImage(true) method:

javascript
const icon = nativeImage.createFromPath(iconPath) icon.setTemplateImage(true) tray = new Tray(icon)

Q: How to disable default menu?A: Set empty menu:

javascript
Menu.setApplicationMenu(null)

Q: How to add checkbox in tray menu?A: Use type: 'checkbox':

javascript
{ label: 'Enable Feature', type: 'checkbox', checked: true, click: (menuItem) => { console.log('Checked:', menuItem.checked) } }

Q: How to create system tray balloon notification on Windows?A: Use display-balloon:

javascript
tray.displayBalloon({ title: 'Notification', content: 'This is a balloon notification' })

标签:Electron