主题切换
第 4 章:窗口管理与原生菜单
窗口是桌面应用的核心交互载体。本章将深入讲解 BrowserWindow API、窗口状态管理、无边框窗口、系统托盘、原生菜单和多窗口管理,让你的应用拥有专业的桌面体验。
4.1 BrowserWindow API 详解
BrowserWindow 是 Electron 中最重要的类,它负责创建和管理应用窗口。类比前端开发,它相当于浏览器中的 window 对象,但功能更强大。
类比理解
把 BrowserWindow 想象成你浏览器中的一个标签页,但这个标签页你可以完全控制:调整大小、隐藏边框、添加自定义按钮、控制它的显示/隐藏,甚至把它变成系统托盘图标。
BrowserWindow 配置项一览
BrowserWindow 的构造函数接受丰富的配置选项,以下是常用配置项:
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
width | number | 800 | 窗口宽度(像素) |
height | number | 600 | 窗口高度(像素) |
x | number | 居中 | 窗口 X 坐标 |
y | number | 居中 | 窗口 Y 坐标 |
frame | boolean | true | false 时创建无边框窗口 |
titleBarStyle | string | default | macOS 标题栏样式:hidden/hiddenInset/customButtonsOnHover |
show | boolean | true | false 时窗口创建后不立即显示 |
backgroundColor | string | #FFF | 窗口背景色(在页面加载前显示,可避免白屏闪烁) |
transparent | boolean | false | 是否启用窗口透明 |
resizable | boolean | true | 是否允许用户调整窗口大小 |
minWidth/minHeight | number | 0 | 窗口最小尺寸 |
maxWidth/maxHeight | number | 不限 | 窗口最大尺寸 |
alwaysOnTop | boolean | false | 是否始终置顶 |
skipTaskbar | boolean | false | 是否在任务栏中隐藏 |
icon | string | 默认图标 | 窗口图标路径 |
parent | BrowserWindow | null | 父窗口(子窗口始终在父窗口之上) |
modal | boolean | false | 是否为模态窗口 |
webPreferences | object | — | 网页相关配置(安全相关,见下文详述) |
窗口创建完整示例
javascript
const { BrowserWindow } = require('electron')
const path = require('path')
const mainWindow = new BrowserWindow({
width: 1200,
height: 800,
minWidth: 800,
minHeight: 600,
backgroundColor: '#1e1e1e',
show: false, // 先隐藏,ready-to-show 后再显示,避免白屏闪烁
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true, // 必须开启:隔离渲染进程上下文
nodeIntegration: false, // 必须关闭:禁止渲染进程直接使用 Node.js
sandbox: true, // 开启沙盒(安全加固)
webSecurity: true, // 开启 Web 安全策略
spellcheck: false, // 桌面应用通常不需要拼写检查
}
})
mainWindow.once('ready-to-show', () => {
mainWindow.show()
mainWindow.focus()
})
mainWindow.loadFile('index.html')窗口控制 IPC
通过 IPC 通道让渲染进程控制窗口行为(最小化、最大化、关闭):
javascript
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))
}
})javascript
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()
})4.2 窗口状态管理
用户期望应用能记住他们的偏好——窗口放在哪个位置、多大尺寸、是否最大化。electron-window-state 是一个轻量级库,能自动保存和恢复窗口状态。
安装
bash
npm install electron-window-state基本使用
javascript
const windowStateKeeper = require('electron-window-state')
const { BrowserWindow } = require('electron')
const path = require('path')
function createWindow() {
// 加载上次保存的窗口状态
const mainWindowState = windowStateKeeper({
defaultWidth: 1200,
defaultHeight: 800,
// 可选:保存最大化/全屏状态
maximize: true,
fullScreen: false,
})
const mainWindow = new BrowserWindow({
// 使用保存的位置和尺寸
x: mainWindowState.x,
y: mainWindowState.y,
width: mainWindowState.width,
height: mainWindowState.height,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
}
})
// 监听窗口状态变化,自动保存
mainWindowState.manage(mainWindow)
mainWindow.loadFile('index.html')
return mainWindow
}状态管理要点
windowStateKeeper将状态保存在app.getPath('userData')下的 JSON 文件中,应用卸载重装也不会丢失manage()方法会自动监听resize、move、close、maximize、unmaximize等事件- 如果上次是最大化关闭,下次启动会自动恢复最大化;如果上次是正常窗口,则恢复位置和尺寸
4.3 无边框窗口与自定义标题栏
无边框窗口是打造现代化 UI 的基础——你可以完全自定义标题栏的外观和行为,实现浏览器无法做到的视觉效果。
创建无边框窗口
javascript
const mainWindow = new BrowserWindow({
width: 1200,
height: 800,
frame: false, // 隐藏系统标题栏
titleBarStyle: 'hidden', // macOS: 隐藏标题栏但保留红绿灯按钮
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
}
})自定义标题栏 Vue 组件
vue
<template>
<div class="title-bar" @dblclick="toggleMaximize">
<div class="title-bar-drag">
<img src="@/assets/icon.png" class="app-icon" />
<span class="app-title">{{ title }}</span>
</div>
<div class="window-controls">
<button class="control-btn minimize" @click="minimize" title="最小化">

</button>
<button class="control-btn maximize" @click="toggleMaximize" title="最大化">
{{ isMaximized ? '' : '' }}
</button>
<button class="control-btn close" @click="closeWindow" title="关闭">

</button>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const title = ref('我的应用')
const isMaximized = ref(false)
function minimize() {
window.windowAPI?.minimize()
}
async function toggleMaximize() {
window.windowAPI?.maximize()
}
function closeWindow() {
window.windowAPI?.close()
}
onMounted(() => {
window.windowAPI?.onMaximizeChange((maximized) => {
isMaximized.value = maximized
})
})
</script>
<style scoped>
.title-bar {
display: flex;
align-items: center;
justify-content: space-between;
height: 38px;
background: var(--bg-secondary);
-webkit-app-region: drag; /* 关键:使标题栏可拖拽移动窗口 */
user-select: none;
padding: 0 8px;
}
.title-bar-drag {
display: flex;
align-items: center;
gap: 8px;
}
.app-icon {
width: 18px;
height: 18px;
}
.window-controls {
display: flex;
-webkit-app-region: no-drag; /* 按钮区域不可拖拽 */
}
.control-btn {
width: 46px;
height: 32px;
border: none;
background: transparent;
color: var(--text-primary);
font-size: 10px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.control-btn:hover {
background: rgba(255, 255, 255, 0.1);
}
.control-btn.close:hover {
background: #e81123;
color: #fff;
}
</style>CSS 关键属性
-webkit-app-region: drag 是让 HTML 元素可拖拽窗口的关键。注意:按钮等可交互元素需要设置 -webkit-app-region: no-drag,否则无法点击。
4.4 系统托盘(Tray)
系统托盘图标让应用可以在后台运行,用户可以通过右键菜单快速操作。类比前端,它相当于网页的 "后台运行" + "快捷操作菜单"。
javascript
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()
})
// 注意:Electron API 中使用 will-quit 事件
app.on('will-quit', () => {
app.isQuiting = true
})平台差异注意
- Windows/Linux: 单击托盘图标通常切换窗口显示/隐藏
- macOS: 单击托盘图标通常显示菜单,双击显示窗口
- 图标尺寸: Windows 16x16,macOS 18x18(模板图),Linux 22x22
4.5 原生菜单(Menu)
Electron 支持创建原生应用菜单和上下文菜单。菜单是桌面应用的重要交互方式,比网页的导航栏更强大。
应用菜单(Application Menu)
javascript
const { app, Menu, shell, dialog } = require('electron')
function createMenu() {
const template = [
// macOS 需要在最前面添加应用菜单
...(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()
})上下文菜单(右键菜单)
javascript
const { ipcMain, Menu, clipboard, BrowserWindow, shell } = 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 | 阻塞父窗口交互;用户体验好 |
多窗口管理示例
javascript
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 }4.7 单实例锁定
对于 RPA 桌面应用,通常只需要运行一个实例。重复启动可能导致自动化任务冲突。Electron 提供了 app.requestSingleInstanceLock() 来解决这个问题。
javascript
const { app, BrowserWindow } = require('electron')
const gotTheLock = app.requestSingleInstanceLock()
if (!gotTheLock) {
// 已有实例在运行,直接退出
app.quit()
} else {
// 当用户尝试启动第二个实例时,聚焦到已有窗口
app.on('second-instance', (event, commandLine, workingDirectory) => {
const mainWindow = BrowserWindow.getAllWindows()[0]
if (mainWindow) {
// macOS 上可能需要先恢复窗口
if (mainWindow.isMinimized()) mainWindow.restore()
mainWindow.show()
mainWindow.focus()
}
})
app.whenReady().then(() => {
createWindow()
})
}单实例锁的 RPA 场景
- 防止用户误操作启动多个自动化任务
- 外部系统调用应用时,确保任务进入队列而非重复执行
- 配合
second-instance事件,可以将命令行参数传递给已有实例(如通过自定义协议打开特定任务)
本章小结
本章核心要点:
- BrowserWindow 是 Electron 窗口管理的核心,支持丰富的配置选项来控制窗口外观和行为
- 窗口状态管理 使用
electron-window-state记住用户偏好(位置、大小、最大化状态),提升用户体验 - 无边框窗口 + 自定义标题栏打造现代化 UI,
-webkit-app-region: drag是关键 CSS 属性,按钮区域需设置no-drag - 系统托盘 让应用在后台运行,注意 Windows/macOS 的交互差异(单击 vs 双击)
- 原生菜单 提供应用菜单和右键上下文菜单,使用
Menu.buildFromTemplate构建,支持快捷键和角色预设 - 多窗口管理 使用 Map 维护窗口引用,支持单实例多窗口、模态对话框等多种策略
- 单实例锁定 使用
requestSingleInstanceLock()防止重复启动,对 RPA 应用尤为重要
4.8 全局快捷键(globalShortcut)
Electron 的 globalShortcut 模块允许你注册系统级的键盘快捷键,即使用户的应用没有聚焦也能响应。这对于需要快速唤醒或控制的应用场景非常有用。
注册与注销快捷键
javascript
// main.js
const { app, globalShortcut, BrowserWindow } = require('electron')
let mainWindow
app.whenReady().then(() => {
mainWindow = new BrowserWindow({ width: 1200, height: 800 })
mainWindow.loadFile('index.html')
// 注册 Ctrl+Shift+X 打开/隐藏主窗口
globalShortcut.register('CommandOrControl+Shift+X', () => {
if (mainWindow.isVisible()) {
mainWindow.hide()
} else {
mainWindow.show()
mainWindow.focus()
}
})
// 注册 Ctrl+Shift+R 开始录制(RPA 场景示例)
globalShortcut.register('CommandOrControl+Shift+R', () => {
mainWindow.webContents.send('shortcut:record')
})
})
app.on('will-quit', () => {
// 注销所有快捷键,避免影响其他应用
globalShortcut.unregisterAll()
})常用快捷键格式
| 平台 | 修饰键 | 示例 |
|---|---|---|
| macOS | Command, Option, Control, Shift | Command+Shift+X |
| Windows/Linux | Ctrl, Alt, Shift | Ctrl+Shift+X |
| 跨平台 | CommandOrControl | 自动适配 macOS/Windows |
检查快捷键是否可用
javascript
const ret = globalShortcut.register('CommandOrControl+X', () => {
console.log('快捷键被触发')
})
if (!ret) {
console.log('快捷键注册失败,可能已被其他应用占用')
}注意事项
globalShortcut注册的是系统全局快捷键,会覆盖其他应用的同名快捷键- 务必在
app.on('will-quit')中注销所有快捷键 - 建议在设置中允许用户自定义快捷键,避免冲突
- 某些系统快捷键(如
Ctrl+Alt+Del)无法被注册
RPA 场景中的应用
在自动化工具中,全局快捷键常用于:
- 快速录制:
Ctrl+Shift+R开始/停止录制用户操作 - 紧急停止:
Ctrl+Shift+Esc立即停止正在执行的自动化任务 - 快速唤醒:
Ctrl+Shift+X显示/隐藏主窗口
javascript
// 紧急停止快捷键示例
globalShortcut.register('CommandOrControl+Shift+Escape', () => {
mainWindow.webContents.send('automation:emergency-stop')
})下一章我们将学习系统交互与文件操作,让你的应用真正具备桌面级能力。