Skip to content

Electron 窗口管理与菜单系统完全指南

前言

窗口和菜单是桌面应用的基础界面元素。一个好的桌面应用不仅功能强大,还要有流畅的窗口交互和直观的菜单系统。

在这篇文章中,我们将深入学习 Electron 的窗口管理和菜单系统,包括窗口创建、多窗口管理、菜单定制等实用技能。

窗口管理基础

创建窗口

javascript
const { BrowserWindow } = require('electron')

function createWindow() {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    
    // 窗口位置
    x: 100,
    y: 100,
    center: true,  // 居中显示
    
    // 窗口样式
    title: 'My App',
    backgroundColor: '#ffffff',
    show: false,  // 先隐藏,准备好后再显示
    
    // 窗口特性
    resizable: true,
    minimizable: true,
    maximizable: true,
    closable: true,
    
    // 尺寸限制
    minWidth: 400,
    minHeight: 300,
    maxWidth: 1920,
    maxHeight: 1080,
    
    // 窗口类型
    frame: true,  // 显示边框
    transparent: false,  // 透明窗口
    alwaysOnTop: false,  // 置顶
    
    // macOS 特性
    titleBarStyle: 'default',  // 'default' | 'hidden' | 'hiddenInset'
    
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true,
      nodeIntegration: false
    }
  })

  // 等待ready-to-show事件
  win.once('ready-to-show', () => {
    win.show()
  })

  win.loadFile('index.html')
  
  return win
}

窗口管理器

javascript
// window-manager.js
class WindowManager {
  constructor() {
    this.windows = new Map()
  }

  create(id, options) {
    if (this.windows.has(id)) {
      const existingWindow = this.windows.get(id)
      existingWindow.focus()
      return existingWindow
    }

    const window = new BrowserWindow(options)
    
    this.windows.set(id, window)
    
    window.on('closed', () => {
      this.windows.delete(id)
    })

    return window
  }

  get(id) {
    return this.windows.get(id)
  }

  getAll() {
    return Array.from(this.windows.values())
  }

  closeAll() {
    this.windows.forEach(window => window.close())
  }
}

module.exports = new WindowManager()

窗口事件

javascript
const win = new BrowserWindow({...})

// 窗口生命周期
win.on('ready-to-show', () => {
  console.log('窗口准备显示')
})

win.on('show', () => {
  console.log('窗口已显示')
})

win.on('hide', () => {
  console.log('窗口已隐藏')
})

win.on('close', (event) => {
  console.log('窗口即将关闭')
  // 可以阻止关闭
  // event.preventDefault()
})

win.on('closed', () => {
  console.log('窗口已关闭')
  win = null
})

// 窗口状态变化
win.on('maximize', () => console.log('窗口最大化'))
win.on('unmaximize', () => console.log('取消最大化'))
win.on('minimize', () => console.log('窗口最小化'))
win.on('restore', () => console.log('窗口恢复'))

// 窗口焦点
win.on('focus', () => console.log('窗口获得焦点'))
win.on('blur', () => console.log('窗口失去焦点'))

// 窗口移动/调整大小
win.on('move', () => {
  const [x, y] = win.getPosition()
  console.log(`窗口移动到: ${x}, ${y}`)
})

win.on('resize', () => {
  const [width, height] = win.getSize()
  console.log(`窗口大小: ${width}x${height}`)
})

// 响应式
win.on('enter-full-screen', () => console.log('进入全屏'))
win.on('leave-full-screen', () => console.log('退出全屏'))

多窗口应用

主窗口和子窗口

javascript
// 创建主窗口
let mainWindow = new BrowserWindow({
  width: 1000,
  height: 700
})

// 创建子窗口
let settingsWindow = new BrowserWindow({
  width: 600,
  height: 400,
  parent: mainWindow,  // 设置父窗口
  modal: true,  // 模态窗口
  show: false
})

settingsWindow.once('ready-to-show', () => {
  settingsWindow.show()
})

窗口间数据共享

javascript
// main.js
const windows = {
  main: null,
  settings: null,
  about: null
}

ipcMain.handle('open-settings', () => {
  if (windows.settings) {
    windows.settings.focus()
    return
  }

  windows.settings = new BrowserWindow({
    width: 600,
    height: 400,
    parent: windows.main
  })

  windows.settings.loadFile('settings.html')
  
  windows.settings.on('closed', () => {
    windows.settings = null
  })
})

// 窗口间传递数据
ipcMain.on('update-from-settings', (event, data) => {
  if (windows.main) {
    windows.main.webContents.send('settings-updated', data)
  }
})

窗口状态持久化

javascript
const Store = require('electron-store')
const store = new Store()

function createWindow() {
  // 恢复窗口状态
  const windowState = store.get('windowState', {
    width: 800,
    height: 600,
    x: undefined,
    y: undefined
  })

  const win = new BrowserWindow({
    ...windowState,
    show: false
  })

  // 保存窗口状态
  const saveWindowState = () => {
    const bounds = win.getBounds()
    store.set('windowState', bounds)
  }

  win.on('close', saveWindowState)
  win.on('resize', debounce(saveWindowState, 500))
  win.on('move', debounce(saveWindowState, 500))

  return win
}

菜单系统

应用菜单(菜单栏)

javascript
const { Menu } = require('electron')

function createMenu() {
  const template = [
    {
      label: '文件',
      submenu: [
        {
          label: '新建',
          accelerator: 'CmdOrCtrl+N',
          click: () => {
            console.log('新建文件')
          }
        },
        {
          label: '打开',
          accelerator: 'CmdOrCtrl+O',
          click: async () => {
            const { filePaths } = await dialog.showOpenDialog({
              properties: ['openFile']
            })
            if (filePaths.length > 0) {
              openFile(filePaths[0])
            }
          }
        },
        {
          label: '保存',
          accelerator: 'CmdOrCtrl+S',
          click: () => {
            saveFile()
          }
        },
        { type: 'separator' },
        {
          label: '退出',
          accelerator: process.platform === 'darwin' ? 'Cmd+Q' : 'Alt+F4',
          click: () => {
            app.quit()
          }
        }
      ]
    },
    {
      label: '编辑',
      submenu: [
        { role: 'undo', label: '撤销' },
        { role: 'redo', label: '重做' },
        { type: 'separator' },
        { role: 'cut', label: '剪切' },
        { role: 'copy', label: '复制' },
        { role: 'paste', label: '粘贴' },
        { role: 'delete', label: '删除' },
        { type: 'separator' },
        { role: 'selectAll', label: '全选' }
      ]
    },
    {
      label: '查看',
      submenu: [
        { role: 'reload', label: '重新加载' },
        { role: 'forceReload', label: '强制重新加载' },
        { role: 'toggleDevTools', label: '开发者工具' },
        { type: 'separator' },
        { role: 'resetZoom', label: '实际大小' },
        { role: 'zoomIn', label: '放大' },
        { role: 'zoomOut', label: '缩小' },
        { type: 'separator' },
        { role: 'togglefullscreen', label: '全屏' }
      ]
    },
    {
      label: '窗口',
      submenu: [
        { role: 'minimize', label: '最小化' },
        { role: 'zoom', label: '缩放' },
        { type: 'separator' },
        { role: 'front', label: '前置所有窗口' }
      ]
    },
    {
      label: '帮助',
      submenu: [
        {
          label: '关于',
          click: () => {
            createAboutWindow()
          }
        },
        {
          label: '文档',
          click: () => {
            shell.openExternal('https://electronjs.org/docs')
          }
        }
      ]
    }
  ]

  // macOS 特殊菜单
  if (process.platform === 'darwin') {
    template.unshift({
      label: app.name,
      submenu: [
        { role: 'about', label: '关于 ' + app.name },
        { type: 'separator' },
        { role: 'services', label: '服务' },
        { type: 'separator' },
        { role: 'hide', label: '隐藏 ' + app.name },
        { role: 'hideOthers', label: '隐藏其他' },
        { role: 'unhide', label: '显示全部' },
        { type: 'separator' },
        { role: 'quit', label: '退出 ' + app.name }
      ]
    })
  }

  const menu = Menu.buildFromTemplate(template)
  Menu.setApplicationMenu(menu)
}

app.whenReady().then(() => {
  createMenu()
  createWindow()
})

动态菜单

javascript
let menu = null

function updateMenu(isDocumentOpen) {
  const template = [
    {
      label: '文件',
      submenu: [
        {
          label: '新建',
          accelerator: 'CmdOrCtrl+N',
          click: createNewDocument
        },
        {
          label: '打开',
          accelerator: 'CmdOrCtrl+O',
          click: openDocument
        },
        {
          label: '保存',
          accelerator: 'CmdOrCtrl+S',
          enabled: isDocumentOpen,  // 根据状态启用/禁用
          click: saveDocument
        },
        {
          label: '另存为',
          accelerator: 'CmdOrCtrl+Shift+S',
          enabled: isDocumentOpen,
          click: saveDocumentAs
        }
      ]
    }
  ]

  menu = Menu.buildFromTemplate(template)
  Menu.setApplicationMenu(menu)
}

// 文档打开时更新菜单
ipcMain.on('document-opened', () => {
  updateMenu(true)
})

// 文档关闭时更新菜单
ipcMain.on('document-closed', () => {
  updateMenu(false)
})

上下文菜单(右键菜单)

javascript
// preload.js
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('electronAPI', {
  showContextMenu: () => ipcRenderer.send('show-context-menu')
})

// main.js
const { Menu } = require('electron')

ipcMain.on('show-context-menu', (event) => {
  const template = [
    {
      label: '复制',
      click: () => {
        event.sender.send('context-menu-command', 'copy')
      }
    },
    {
      label: '粘贴',
      click: () => {
        event.sender.send('context-menu-command', 'paste')
      }
    },
    { type: 'separator' },
    {
      label: '删除',
      click: () => {
        event.sender.send('context-menu-command', 'delete')
      }
    }
  ]

  const menu = Menu.buildFromTemplate(template)
  menu.popup(BrowserWindow.fromWebContents(event.sender))
})

// renderer.js
window.addEventListener('contextmenu', (e) => {
  e.preventDefault()
  window.electronAPI.showContextMenu()
})

window.electronAPI.onContextMenuCommand((command) => {
  switch (command) {
    case 'copy':
      document.execCommand('copy')
      break
    case 'paste':
      document.execCommand('paste')
      break
    case 'delete':
      // 删除逻辑
      break
  }
})

托盘菜单

javascript
const { Tray, Menu } = require('electron')

let tray = null

function createTray() {
  tray = new Tray('path/to/icon.png')

  const contextMenu = Menu.buildFromTemplate([
    {
      label: '显示窗口',
      click: () => {
        mainWindow.show()
      }
    },
    {
      label: '隐藏窗口',
      click: () => {
        mainWindow.hide()
      }
    },
    { type: 'separator' },
    {
      label: '设置',
      click: () => {
        openSettings()
      }
    },
    { type: 'separator' },
    {
      label: '退出',
      click: () => {
        app.quit()
      }
    }
  ])

  tray.setToolTip('My Application')
  tray.setContextMenu(contextMenu)

  // 点击托盘图标
  tray.on('click', () => {
    mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show()
  })
}

app.whenReady().then(() => {
  createTray()
})

Dock 菜单(macOS)

javascript
if (process.platform === 'darwin') {
  const dockMenu = Menu.buildFromTemplate([
    {
      label: '新建窗口',
      click: () => {
        createWindow()
      }
    },
    {
      label: '新建隐身窗口',
      click: () => {
        createIncognitoWindow()
      }
    }
  ])

  app.dock.setMenu(dockMenu)
}

高级窗口特性

无边框窗口

javascript
const win = new BrowserWindow({
  width: 800,
  height: 600,
  frame: false,  // 无边框
  transparent: true,  // 透明(可选)
  webPreferences: {
    preload: path.join(__dirname, 'preload.js')
  }
})

// 自定义标题栏
// index.html
<div class="titlebar">
  <div class="title">My App</div>
  <div class="window-controls">
    <button id="min-btn">─</button>
    <button id="max-btn">□</button>
    <button id="close-btn">×</button>
  </div>
</div>

// renderer.js
document.getElementById('min-btn').addEventListener('click', () => {
  window.electronAPI.minimizeWindow()
})

document.getElementById('max-btn').addEventListener('click', () => {
  window.electronAPI.maximizeWindow()
})

document.getElementById('close-btn').addEventListener('click', () => {
  window.electronAPI.closeWindow()
})

窗口拖拽

css
/* 可拖拽区域 */
.titlebar {
  -webkit-app-region: drag;
}

/* 按钮不可拖拽 */
.window-controls button {
  -webkit-app-region: no-drag;
}

毛玻璃效果(macOS/Windows)

javascript
const win = new BrowserWindow({
  width: 800,
  height: 600,
  transparent: true,
  vibrancy: 'under-window',  // macOS
  backgroundMaterial: 'acrylic'  // Windows 11
})

多显示器支持

javascript
const { screen } = require('electron')

function createWindowOnDisplay(displayId) {
  const displays = screen.getAllDisplays()
  const targetDisplay = displays.find(d => d.id === displayId) || screen.getPrimaryDisplay()

  const win = new BrowserWindow({
    width: 800,
    height: 600,
    x: targetDisplay.bounds.x + 50,
    y: targetDisplay.bounds.y + 50
  })

  return win
}

// 监听显示器变化
screen.on('display-added', (event, newDisplay) => {
  console.log('新显示器:', newDisplay)
})

screen.on('display-removed', (event, oldDisplay) => {
  console.log('显示器移除:', oldDisplay)
})

实战案例

完整的窗口管理系统

javascript
// window-system.js
const { BrowserWindow, ipcMain } = require('electron')
const path = require('path')

class WindowSystem {
  constructor() {
    this.windows = new Map()
    this.setupIPC()
  }

  createWindow(id, options = {}) {
    if (this.windows.has(id)) {
      const win = this.windows.get(id)
      win.focus()
      return win
    }

    const defaultOptions = {
      width: 800,
      height: 600,
      show: false,
      webPreferences: {
        preload: path.join(__dirname, 'preload.js'),
        contextIsolation: true,
        nodeIntegration: false
      }
    }

    const win = new BrowserWindow({
      ...defaultOptions,
      ...options
    })

    win.once('ready-to-show', () => {
      win.show()
    })

    win.on('closed', () => {
      this.windows.delete(id)
    })

    this.windows.set(id, win)
    return win
  }

  getWindow(id) {
    return this.windows.get(id)
  }

  closeWindow(id) {
    const win = this.windows.get(id)
    if (win) {
      win.close()
    }
  }

  setupIPC() {
    ipcMain.handle('window:create', (event, id, options) => {
      return this.createWindow(id, options)
    })

    ipcMain.handle('window:close', (event, id) => {
      this.closeWindow(id)
    })

    ipcMain.handle('window:minimize', (event) => {
      const win = BrowserWindow.fromWebContents(event.sender)
      win.minimize()
    })

    ipcMain.handle('window:maximize', (event) => {
      const win = BrowserWindow.fromWebContents(event.sender)
      if (win.isMaximized()) {
        win.unmaximize()
      } else {
        win.maximize()
      }
    })
  }
}

module.exports = new WindowSystem()

总结

✅ 核心知识点

  1. 窗口管理

    • 创建和配置窗口
    • 窗口生命周期
    • 多窗口协作
  2. 菜单系统

    • 应用菜单
    • 上下文菜单
    • 托盘菜单
    • Dock 菜单
  3. 高级特性

    • 无边框窗口
    • 自定义标题栏
    • 多显示器支持

下一篇文章,我们将学习 Electron 的原生功能与系统集成。


相关文章推荐:

有问题欢迎留言讨论!