如何在基于React的Electron应用工具栏中添加自定义菜单项?
在Electron+React环境中添加自定义菜单项
你可以通过两种方式实现需求:系统原生菜单(主进程创建,适配系统菜单规范)和自定义渲染菜单(与现有工具栏样式统一,完全由React控制),以下是具体实现方案:
一、系统原生菜单(主进程实现)
Electron的Menu API可以创建系统级菜单,支持快捷键、系统菜单集成等特性,适合需要原生体验的场景。
主进程代码(main.ts)
import { app, BrowserWindow, Menu, ipcMain } from 'electron'; import path from 'path'; // 窗口创建函数(假设你已有该逻辑) function createWindow() { const mainWindow = new BrowserWindow({ width: 800, height: 600, webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true, nodeIntegration: false }, frame: false // 配合自定义标题栏隐藏原生边框 }); mainWindow.loadURL('http://localhost:3000'); // 你的React开发服务地址 return mainWindow; } // 创建系统菜单 function buildApplicationMenu() { const menuTemplate = [ { label: 'File', submenu: [ { label: 'New Text File', accelerator: 'CmdOrCtrl+N', click: () => { const activeWindow = BrowserWindow.getFocusedWindow(); activeWindow?.webContents.send('app-command', 'new-text-file'); } }, { label: 'New File', click: () => { const activeWindow = BrowserWindow.getFocusedWindow(); activeWindow?.webContents.send('app-command', 'new-file'); } }, { label: 'New Window', accelerator: 'CmdOrCtrl+Shift+N', click: createWindow } ] }, { label: 'Edit', submenu: [ { label: 'Undo', accelerator: 'CmdOrCtrl+Z', click: () => { const activeWindow = BrowserWindow.getFocusedWindow(); activeWindow?.webContents.send('app-command', 'undo'); } }, { label: 'Redo', accelerator: 'CmdOrCtrl+Shift+Z', click: () => { const activeWindow = BrowserWindow.getFocusedWindow(); activeWindow?.webContents.send('app-command', 'redo'); } } ] }, { label: 'Help', submenu: [ { label: 'Get Started', click: () => { const activeWindow = BrowserWindow.getFocusedWindow(); activeWindow?.webContents.send('app-command', 'show-get-started'); } }, { label: 'About', click: () => app.showAboutPanel() } ] } ]; const appMenu = Menu.buildFromTemplate(menuTemplate); Menu.setApplicationMenu(appMenu); } // 主进程启动逻辑 app.whenReady().then(() => { createWindow(); buildApplicationMenu(); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); }); // 监听渲染进程的菜单指令 ipcMain.on('menu-command', (_, action) => { const activeWindow = BrowserWindow.getFocusedWindow(); switch (action) { case 'new-text-file': activeWindow?.webContents.send('app-command', 'new-text-file'); break; case 'new-file': activeWindow?.webContents.send('app-command', 'new-file'); break; case 'new-window': createWindow(); break; case 'undo': activeWindow?.webContents.send('app-command', 'undo'); break; case 'redo': activeWindow?.webContents.send('app-command', 'redo'); break; case 'show-get-started': activeWindow?.webContents.send('app-command', 'show-get-started'); break; case 'about': app.showAboutPanel(); break; } }); app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); });
二、自定义渲染菜单(与现有Titlebar集成)
如果需要菜单和自定义工具栏样式完全统一,可在React中实现下拉菜单组件,通过IPC与主进程通信。
1. 实现MenuBar组件(MenuBar.tsx)
import React, { useState, useEffect } from 'react'; import styles from './MenuBar.module.scss'; // 声明全局Electron API类型(需配合preload脚本) declare global { interface Window { electronAPI: { sendMenuCommand: (action: string) => void; }; } } interface SubMenuItem { label: string; action: string; accelerator?: string; } interface MenuConfig { [key: string]: SubMenuItem[]; } const MenuBar = () => { const [activeMenu, setActiveMenu] = useState<string | null>(null); // 菜单配置 const menuConfig: MenuConfig = { File: [ { label: 'New Text File', action: 'new-text-file', accelerator: 'Ctrl+N' }, { label: 'New File', action: 'new-file' }, { label: 'New Window', action: 'new-window', accelerator: 'Ctrl+Shift+N' } ], Edit: [ { label: 'Undo', action: 'undo', accelerator: 'Ctrl+Z' }, { label: 'Redo', action: 'redo', accelerator: 'Ctrl+Shift+Z' } ], Help: [ { label: 'Get Started', action: 'show-get-started' }, { label: 'About', action: 'about' } ] }; // 切换菜单展开状态 const toggleMenu = (menuName: string) => { setActiveMenu(prev => prev === menuName ? null : menuName); }; // 点击菜单项发送指令到主进程 const handleMenuItemClick = (action: string) => { setActiveMenu(null); window.electronAPI.sendMenuCommand(action); }; // 点击页面其他区域关闭菜单 useEffect(() => { const handleClickOutside = (e: MouseEvent) => { const menuBar = document.getElementById('custom-menu-bar'); if (menuBar && !menuBar.contains(e.target as Node)) { setActiveMenu(null); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); return ( <div id="custom-menu-bar" className={styles.menuBar}> {Object.keys(menuConfig).map(menuName => ( <div key={menuName} className={styles.menuContainer}> <button className={`${styles.menuButton} ${activeMenu === menuName ? styles.active : ''}`} onClick={() => toggleMenu(menuName)} > {menuName} </button> {activeMenu === menuName && ( <ul className={styles.submenu}> {menuConfig[menuName].map((item, idx) => ( <li key={idx} className={styles.submenuItem} onClick={() => handleMenuItemClick(item.action)} > <span>{item.label}</span> {item.accelerator && <span className={styles.accelerator}>{item.accelerator}</span>} </li> ))} </ul> )} </div> ))} </div> ); }; export default MenuBar;
2. 菜单样式(MenuBar.module.scss)
.menuBar { display: flex; margin-left: 16px; gap: 4px; } .menuContainer { position: relative; } .menuButton { background: transparent; border: none; color: inherit; padding: 8px 12px; cursor: pointer; font-size: 14px; border-radius: 4px; &:hover, &.active { background: rgba(255, 255, 255, 0.1); } } .submenu { position: absolute; top: calc(100% + 4px); left: 0; background: #2d2d2d; border-radius: 6px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); padding: 6px 0; min-width: 180px; z-index: 9999; list-style: none; margin: 0; } .submenuItem { display: flex; justify-content: space-between; align-items: center; padding: 6px 16px; cursor: pointer; color: #fff; font-size: 14px; &:hover { background: rgba(255, 255, 255, 0.1); } .accelerator { color: #aaa; font-size: 12px; } }
3. Preload脚本(preload.js)
用于在渲染进程安全暴露IPC能力:
const { contextBridge, ipcRenderer } = require('electron'); contextBridge.exposeInMainWorld('electronAPI', { sendMenuCommand: (action) => ipcRenderer.send('menu-command', action) });
三、渲染进程接收指令(可选)
在React主组件中监听主进程发来的指令,执行对应UI逻辑:
import React, { useEffect } from 'react'; declare global { interface Window { electronAPI: { onAppCommand: (callback: (action: string) => void) => void; }; } } function App() { useEffect(() => { const handleAppCommand = (action: string) => { switch (action) { case 'new-text-file': // 执行新建文本文件的UI逻辑 console.log('创建新文本文件'); break; case 'show-get-started': // 打开引导页面 console.log('显示引导页面'); break; // 其他指令处理 } }; // 需在preload中补充onAppCommand的暴露 window.electronAPI.onAppCommand(handleAppCommand); return () => window.electronAPI.onAppCommand(() => {}); }, []); return <div className="App">{/* 你的应用内容 */}</div>; } export default App;
内容的提问来源于stack exchange,提问作者izle_r
相关产品推荐
相关产品推荐

