第 4 章:窗口管理与原生菜单

窗口是桌面应用的核心交互载体。本章将深入讲解 BrowserWindow API、窗口状态管理、无边框窗口、系统托盘、原生菜单和多窗口管理,让你的应用拥有专业的桌面体验。

4.1 BrowserWindow API 详解

BrowserWindow 是 Electron 中最重要的类,它负责创建和管理应用窗口。类比前端开发,它相当于浏览器中的 window 对象,但功能更强大。

类比理解

BrowserWindow 想象成你浏览器中的一个标签页,但这个标签页你可以完全控制:调整大小、隐藏边框、添加自定义按钮、控制它的显示/隐藏,甚至把它变成系统托盘图标。

常用配置选项

选项类型默认值说明
widthnumber800窗口宽度(像素)
heightnumber600窗口高度(像素)
x / ynumber居中窗口位置(屏幕坐标)
minWidth / minHeightnumber0最小尺寸限制
maxWidth / maxHeightnumber无限制最大尺寸限制
resizablebooleantrue是否可调整大小
movablebooleantrue是否可移动
minimizablebooleantrue是否可最小化
maximizablebooleantrue是否可最大化
closablebooleantrue是否可关闭
fullscreenbooleanfalse是否全屏显示
framebooleantrue是否显示窗口边框和标题栏
transparentbooleanfalse是否透明背景
alwaysOnTopbooleanfalse是否始终置顶
showbooleantrue创建后是否立即显示
titlestring"Electron"窗口标题
iconstring系统默认窗口图标路径
webPreferencesobject{}渲染进程配置(见第2章)

完整窗口配置示例

main.js - BrowserWindow 配置
const { BrowserWindow } = require('electron')
const path = require('path')

function createWindow() {
  const mainWindow = new BrowserWindow({
    width: 1200,
    height: 800,
    minWidth: 800,
    minHeight: 600,
    title: '我的 Electron 应用',
    icon: path.join(__dirname, 'assets/icon.png'),
    backgroundColor: '#1e1e1e',
    resizable: true,
    minimizable: true,
    maximizable: true,
    closable: true,
    fullscreenable: true,
    show: false,
    titleBarStyle: 'hiddenInset',
    trafficLightPosition: { x: 15, y: 12 },
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      nodeIntegration: false,
      contextIsolation: true,
      spellcheck: true
      // 注意:devTools 不是 webPreferences 的有效属性
      // 正确做法是在窗口创建后按需调用:
      // if (process.env.NODE_ENV === 'development') {
      //   mainWindow.webContents.openDevTools()
      // }
    }
  })

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

  mainWindow.on('close', (event) => {
    // event.preventDefault()
  })

  return mainWindow
}

4.2 窗口状态管理

专业的桌面应用需要记住用户的窗口偏好(位置、大小、是否最大化)。我们可以使用 electron-window-state 库来实现。

安装与使用

bash
npm install electron-window-state
main.js - 窗口状态持久化
const { app, BrowserWindow } = require('electron')
const windowStateKeeper = require('electron-window-state')
const path = require('path')

function createWindow() {
  let mainWindowState = windowStateKeeper({
    defaultWidth: 1200,
    defaultHeight: 800,
    file: 'window-state.json'
  })

  const mainWindow = new BrowserWindow({
    x: mainWindowState.x,
    y: mainWindowState.y,
    width: mainWindowState.width,
    height: mainWindowState.height,
    show: false,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true
    }
  })

  mainWindowState.manage(mainWindow)
  mainWindow.loadFile('index.html')
  
  mainWindow.once('ready-to-show', () => {
    if (mainWindowState.isMaximized) {
      mainWindow.maximize()
    }
    mainWindow.show()
  })

  return mainWindow
}

窗口控制方法

方法说明对应事件
win.minimize()最小化窗口minimize
win.maximize()最大化窗口maximize
win.unmaximize()恢复窗口unmaximize
win.restore()从最小化恢复restore
win.close()关闭窗口close
win.hide()隐藏窗口hide
win.show()显示窗口show
win.focus()聚焦窗口focus
win.blur()取消聚焦blur
win.setFullScreen(flag)设置全屏enter-full-screen / leave-full-screen
win.setAlwaysOnTop(flag)置顶/取消置顶-
win.setOpacity(opacity)设置透明度 (0.0-1.0)-
win.setBounds(bounds)设置位置和尺寸resize / move
win.getBounds()获取位置和尺寸-

4.3 无边框窗口与自定义标题栏

现代桌面应用(如 VS Code、Spotify)普遍采用无边框窗口 + 自定义标题栏的设计。Electron 通过 frame: false 轻松实现。

设计趋势

无边框窗口让应用看起来更现代、更沉浸。但你需要自己实现拖拽区域和窗口控制按钮(最小化、最大化、关闭)。

创建无边框窗口

main.js - 无边框窗口
const { BrowserWindow } = require('electron')

function createWindow() {
  const mainWindow = new BrowserWindow({
    width: 1200,
    height: 800,
    frame: false,
    transparent: false,
    titleBarStyle: 'hidden',
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true
    }
  })
  mainWindow.loadFile('index.html')
  return mainWindow
}

自定义标题栏(HTML + CSS)

index.html - 自定义标题栏
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>无边框应用</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
      background: #1e1e1e;
      color: #d4d4d4;
      overflow: hidden;
    }
    .titlebar {
      height: 40px;
      background: #2d2d2d;
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: 0 15px;
      -webkit-app-region: drag;
      user-select: none;
      border-bottom: 1px solid #3e3e3e;
    }
    .titlebar .controls {
      -webkit-app-region: no-drag;
      display: flex;
      gap: 8px;
    }
    .titlebar .btn {
      width: 14px;
      height: 14px;
      border-radius: 50%;
      border: none;
      cursor: pointer;
      transition: opacity 0.2s;
    }
    .titlebar .btn:hover { opacity: 0.8; }
    .btn-minimize { background: #ffbd2e; }
    .btn-maximize { background: #28c840; }
    .btn-close { background: #ff5f57; }
    .content {
      height: calc(100vh - 40px);
      padding: 20px;
      overflow-y: auto;
    }
  </style>
</head>
<body>
  <div class="titlebar">
    <div class="app-title">我的应用</div>
    <div class="controls">
      <button class="btn btn-minimize" id="min-btn" title="最小化"></button>
      <button class="btn btn-maximize" id="max-btn" title="最大化"></button>
      <button class="btn btn-close" id="close-btn" title="关闭"></button>
    </div>
  </div>
  <div class="content">
    <h1>无边框窗口示例</h1>
    <p>这是一个自定义标题栏的 Electron 应用。</p>
  </div>
  <script src="renderer.js"></script>
</body>
</html>

窗口控制 IPC

preload.js - 暴露窗口控制 API
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('windowAPI', {
  minimize: () => ipcRenderer.send('window:minimize'),
  maximize: () => ipcRenderer.send('window:maximize'),
  close: () => ipcRenderer.send('window:close'),
  onMaximizeChange: (callback) => {
    ipcRenderer.on('window:maximized', (_, isMaximized) => callback(isMaximized))
  }
})
main.js - 处理窗口控制
const { ipcMain } = require('electron')

let mainWindow

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 1200,
    height: 800,
    frame: false,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true
    }
  })
  
  mainWindow.on('maximize', () => {
    mainWindow.webContents.send('window:maximized', true)
  })
  
  mainWindow.on('unmaximize', () => {
    mainWindow.webContents.send('window:maximized', false)
  })
  
  return mainWindow
}

ipcMain.on('window:minimize', () => {
  mainWindow?.minimize()
})

ipcMain.on('window:maximize', () => {
  if (mainWindow?.isMaximized()) {
    mainWindow.unmaximize()
  } else {
    mainWindow?.maximize()
  }
})

ipcMain.on('window:close', () => {
  mainWindow?.close()
})
CSS 关键属性

-webkit-app-region: drag 是让 HTML 元素可拖拽窗口的关键。注意:按钮等可交互元素需要设置 -webkit-app-region: no-drag,否则无法点击。

4.4 系统托盘(Tray)

系统托盘图标让应用可以在后台运行,用户可以通过右键菜单快速操作。类比前端,它相当于网页的 "后台运行" + "快捷操作菜单"。

main.js - 系统托盘实现
const { app, Tray, Menu, BrowserWindow, nativeImage } = require('electron')
const path = require('path')

let tray = null
let mainWindow = null

function createTray() {
  const iconPath = path.join(__dirname, 'assets/tray-icon.png')
  const icon = nativeImage.createFromPath(iconPath)
  
  if (process.platform === 'darwin') {
    icon.setTemplateImage(true)
  }
  
  tray = new Tray(icon)
  tray.setToolTip('我的 Electron 应用')
  
  const contextMenu = Menu.buildFromTemplate([
    {
      label: '显示应用',
      click: () => {
        if (mainWindow) {
          mainWindow.show()
          mainWindow.focus()
        }
      }
    },
    {
      label: '最小化到托盘',
      click: () => {
        mainWindow?.hide()
      }
    },
    { type: 'separator' },
    {
      label: '设置',
      click: () => {
        createSettingsWindow()
      }
    },
    { type: 'separator' },
    {
      label: '退出',
      click: () => {
        app.quit()
      }
    }
  ])
  
  tray.setContextMenu(contextMenu)
  
  tray.on('click', () => {
    if (process.platform !== 'darwin') {
      if (mainWindow?.isVisible()) {
        mainWindow.hide()
      } else {
        mainWindow?.show()
      }
    }
  })
  
  tray.on('double-click', () => {
    mainWindow?.show()
  })
}

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 1200,
    height: 800,
    show: false,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true
    }
  })
  
  mainWindow.loadFile('index.html')
  
  mainWindow.once('ready-to-show', () => {
    mainWindow.show()
  })
  
  mainWindow.on('close', (event) => {
    if (!app.isQuiting) {
      event.preventDefault()
      mainWindow.hide()
    }
  })
  
  return mainWindow
}

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

app.on('before-quit', () => {
  app.isQuiting = true
})
平台差异注意
  • Windows/Linux: 单击托盘图标通常切换窗口显示/隐藏
  • macOS: 单击托盘图标通常显示菜单,双击显示窗口
  • 图标尺寸: Windows 16x16,macOS 18x18(模板图),Linux 22x22

4.5 原生菜单(Menu)

Electron 支持创建原生应用菜单和上下文菜单。菜单是桌面应用的重要交互方式,比网页的导航栏更强大。

应用菜单(Application Menu)

main.js - 应用菜单
const { app, Menu, shell, dialog } = require('electron')

function createMenu() {
  const template = [
    ...(process.platform === 'darwin' ? [{
      label: app.getName(),
      submenu: [
        { label: '关于', role: 'about' },
        { type: 'separator' },
        { label: '服务', role: 'services', submenu: [] },
        { type: 'separator' },
        { label: '隐藏', role: 'hide' },
        { label: '隐藏其他', role: 'hideOthers' },
        { label: '显示全部', role: 'unhide' },
        { type: 'separator' },
        { label: '退出', role: 'quit' }
      ]
    }] : []),
    
    {
      label: '文件',
      submenu: [
        {
          label: '新建',
          accelerator: 'CmdOrCtrl+N',
          click: () => {
            createNewDocument()
          }
        },
        {
          label: '打开',
          accelerator: 'CmdOrCtrl+O',
          click: async () => {
            const result = await dialog.showOpenDialog({
              properties: ['openFile'],
              filters: [
                { name: '文本文档', extensions: ['txt', 'md'] }
              ]
            })
            if (!result.canceled) {
              openFile(result.filePaths[0])
            }
          }
        },
        { type: 'separator' },
        {
          label: '保存',
          accelerator: 'CmdOrCtrl+S',
          click: () => {
            saveCurrentDocument()
          }
        },
        { type: 'separator' },
        process.platform === 'darwin' ? { role: 'close' } : { role: 'quit' }
      ]
    },
    
    {
      label: '编辑',
      submenu: [
        { role: 'undo', label: '撤销' },
        { role: 'redo', label: '重做' },
        { type: 'separator' },
        { role: 'cut', label: '剪切' },
        { role: 'copy', label: '复制' },
        { role: 'paste', label: '粘贴' },
        { 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: 'close', label: '关闭' }
      ]
    },
    
    {
      label: '帮助',
      submenu: [
        {
          label: '文档',
          click: () => {
            shell.openExternal('https://electronjs.org')
          }
        },
        {
          label: '检查更新',
          click: () => {
            checkForUpdates()
          }
        }
      ]
    }
  ]
  
  const menu = Menu.buildFromTemplate(template)
  Menu.setApplicationMenu(menu)
}

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

上下文菜单(右键菜单)

main.js - 右键菜单
const { ipcMain, Menu, clipboard } = require('electron')

// 处理渲染进程请求的右键菜单
ipcMain.on('show-context-menu', (event, params) => {
  const template = [
    {
      label: '复制',
      role: 'copy',
      enabled: params.selectionText && params.selectionText.length > 0
    },
    {
      label: '粘贴',
      role: 'paste'
    },
    { type: 'separator' },
    {
      label: '全选',
      role: 'selectAll'
    }
  ]
  
  // 如果是链接,添加打开选项
  if (params.linkURL) {
    template.unshift({
      label: '在新窗口打开链接',
      click: () => {
        shell.openExternal(params.linkURL)
      }
    })
  }
  
  const menu = Menu.buildFromTemplate(template)
  menu.popup({
    window: BrowserWindow.fromWebContents(event.sender)
  })
})

4.6 多窗口管理

复杂的桌面应用通常需要多个窗口协作。Electron 提供了多种窗口管理方式。

窗口管理策略对比

策略适用场景实现方式优缺点
单实例多窗口 IDE、编辑器 一个主进程管理多个 BrowserWindow 资源共享,窗口间通信方便;但一个窗口崩溃可能影响其他窗口
多实例独立进程 浏览器标签页 每个窗口独立进程 隔离性好;但资源开销大,进程间通信复杂
主窗口 + 子窗口 设置面板、对话框 使用 parent 选项创建子窗口 子窗口跟随父窗口;但子窗口不能超出父窗口范围
模态对话框 确认框、警告框 使用 modal: true 阻塞父窗口交互;用户体验好

多窗口管理示例

main.js - 多窗口管理
const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('path')

// 存储所有窗口的 Map
const windows = new Map()
let windowIdCounter = 0

function createWindow(options = {}) {
  const windowId = ++windowIdCounter
  
  const window = new BrowserWindow({
    width: options.width || 1200,
    height: options.height || 800,
    parent: options.parent,  // 父窗口
    modal: options.modal || false,  // 是否为模态窗口
    show: false,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true,
      // 为每个窗口提供唯一 ID
      additionalArguments: [`--window-id=${windowId}`]
    }
  })
  
  // 存储窗口引用
  windows.set(windowId, window)
  
  // 加载内容
  if (options.url) {
    window.loadURL(options.url)
  } else {
    window.loadFile(options.file || 'index.html')
  }
  
  window.once('ready-to-show', () => {
    window.show()
    if (options.focus !== false) {
      window.focus()
    }
  })
  
  // 窗口关闭时清理
  window.on('closed', () => {
    windows.delete(windowId)
  })
  
  return { window, windowId }
}

// 创建主窗口
function createMainWindow() {
  const { window, windowId } = createWindow({
    width: 1200,
    height: 800,
    file: 'index.html'
  })
  
  return window
}

// 创建子窗口(如设置窗口)
function createSettingsWindow() {
  const { window, windowId } = createWindow({
    width: 600,
    height: 500,
    parent: mainWindow,
    modal: true,
    file: 'settings.html'
  });
  return window;
}

// 获取所有窗口列表
function getAllWindows() {
  return Array.from(windows.values());
}

// 向指定窗口发送消息
function sendToWindow(windowId, channel, data) {
  const win = windows.get(windowId);
  if (win && !win.isDestroyed()) {
    win.webContents.send(channel, data);
  }
}

// 广播消息到所有窗口
function broadcast(channel, data) {
  windows.forEach((win) => {
    if (!win.isDestroyed()) {
      win.webContents.send(channel, data);
    }
  });
}

module.exports = { createWindow, createSettingsWindow, getAllWindows, sendToWindow, broadcast };
本章小结

本章核心要点:

  • BrowserWindow 是 Electron 窗口管理的核心,支持丰富的配置选项
  • 窗口状态管理 使用 electron-window-state 记住用户偏好(位置、大小)
  • 无边框窗口 + 自定义标题栏打造现代化 UI,-webkit-app-region: drag 是关键 CSS 属性
  • 系统托盘 让应用在后台运行,注意 Windows/macOS 的交互差异
  • 原生菜单 提供应用菜单和右键上下文菜单,使用 Menu.buildFromTemplate 构建
  • 多窗口管理 使用 Map 维护窗口引用,支持单实例多窗口、模态对话框等多种策略

下一章我们将学习系统交互与文件操作,让你的应用真正具备桌面级能力。