You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.07 14:40:48