第 4 章:窗口管理与原生菜单
窗口是桌面应用的核心交互载体。本章将深入讲解 BrowserWindow API、窗口状态管理、无边框窗口、系统托盘、原生菜单和多窗口管理,让你的应用拥有专业的桌面体验。
4.1 BrowserWindow API 详解
BrowserWindow 是 Electron 中最重要的类,它负责创建和管理应用窗口。类比前端开发,它相当于浏览器中的 window 对象,但功能更强大。
类比理解
把 BrowserWindow 想象成你浏览器中的一个标签页,但这个标签页你可以完全控制:调整大小、隐藏边框、添加自定义按钮、控制它的显示/隐藏,甚至把它变成系统托盘图标。
常用配置选项
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
width | number | 800 | 窗口宽度(像素) |
height | number | 600 | 窗口高度(像素) |
x / y | number | 居中 | 窗口位置(屏幕坐标) |
minWidth / minHeight | number | 0 | 最小尺寸限制 |
maxWidth / maxHeight | number | 无限制 | 最大尺寸限制 |
resizable | boolean | true | 是否可调整大小 |
movable | boolean | true | 是否可移动 |
minimizable | boolean | true | 是否可最小化 |
maximizable | boolean | true | 是否可最大化 |
closable | boolean | true | 是否可关闭 |
fullscreen | boolean | false | 是否全屏显示 |
frame | boolean | true | 是否显示窗口边框和标题栏 |
transparent | boolean | false | 是否透明背景 |
alwaysOnTop | boolean | false | 是否始终置顶 |
show | boolean | true | 创建后是否立即显示 |
title | string | "Electron" | 窗口标题 |
icon | string | 系统默认 | 窗口图标路径 |
webPreferences | object | {} | 渲染进程配置(见第2章) |
完整窗口配置示例
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 维护窗口引用,支持单实例多窗口、模态对话框等多种策略
下一章我们将学习系统交互与文件操作,让你的应用真正具备桌面级能力。