窗口事件与多窗口管理
本教程共 45 篇 · 第 8 篇 · 更新于 2026-08-03
8. 窗口事件与多窗口管理
本节目标
- 学会在同一个应用里创建并管理多个
BrowserWindow。 - 理解父子窗口(含模态窗口)的创建与行为差异。
- 掌握
ready-to-show、closed、focus、blur等常用事件。 - 明白为什么要用一个集合持有窗口引用,避免被垃圾回收。
- 了解跨平台在窗口行为上的细微差别。
桌面应用很少有「单窗口」的。设置面板、关于框、独立的编辑器窗口,都需要你同时管理多个 BrowserWindow。本章带你搞清楚:怎么开第二个窗口、父子窗口怎么用、窗口的显示与焦点怎么监听,以及最容易被忽略的「窗口引用被回收」问题。
8-1 多窗口的本质
每个 BrowserWindow 实例背后,都对应一个独立的渲染进程与一套 WebContents。窗口和窗口之间默认互不相通,它们各自加载自己的页面、跑各自的脚本。
打开第二个窗口,和打开第一个窗口的代码几乎一样。你只要再次 new BrowserWindow(...) 并 loadFile 即可。难点不在「创建」,而在「管理」:当窗口关闭时,你的引用还在不在?用户又关又开,会不会泄漏?
// 主进程 main.js
const { app, BrowserWindow } = require('electron/main')
const path = require('node:path')
function createWindow () {
const win = new BrowserWindow({
width: 900,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
win.loadFile('index.html')
return win
}
app.whenReady().then(() => {
createWindow()
})
上面这段代码只开一个窗口。要开第二个,再调一次 createWindow() 即可。但要注意:如果只是局部变量,窗口关掉后引用就可能被回收。
8-2 用一个窗口集合持有引用
JavaScript 的垃圾回收依赖「还有没有地方引用这个对象」。如果 BrowserWindow 实例没有变量持有,关闭后 Electron 内部会释放它,这时你再去调用它的方法就会报错。
最稳妥的做法,是用一个 Map 或数组把窗口管起来,关掉时再删掉。
// 主进程 main.js
const { app, BrowserWindow } = require('electron/main')
const path = require('node:path')
// 用 Map 管理所有窗口,key 可以自定义
const windows = new Map()
function createEditorWindow (id) {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
win.loadFile('editor.html')
// 关闭时从集合里移除,避免悬挂引用
win.once('closed', () => {
windows.delete(id)
})
windows.set(id, win)
return win
}
这里有一个细节:监听 closed 事件时,回调里不要再使用这个窗口对象,文档明确要求「收到该事件后应当移除引用,并避免继续使用它」。
8-3 ready-to-show 消除白屏
默认情况下,new BrowserWindow 后页面会立刻显示。但页面还没渲染完,用户会先看到一块白屏,再「闪」出内容。
ready-to-show 事件在页面首次渲染完成、且窗口尚未显示时触发。配合 show: false 构造选项,可以先把窗口藏起来,等它就绪再显示,从而避免闪烁。
// 主进程 main.js —— 放在 createWindow 函数里
const win = new BrowserWindow({
show: false, // 先不显示
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
win.once('ready-to-show', () => {
win.show()
})
win.loadFile('index.html')
提示:复杂应用的 ready-to-show 可能来得太晚,让人觉得启动慢。这时建议直接显示窗口,并设一个接近应用背景色的 backgroundColor,体验会更自然。还要注意:如果你把构造项的 paintWhenInitiallyHidden 设为 false,ready-to-show 将不会触发。
8-4 父子窗口与模态窗口
给新窗口传一个 parent 选项,它就变成父窗口的子窗口。子窗口会始终浮在父窗口之上,最小化父窗口时子窗口也会跟着最小化。
如果同时传 parent 和 modal: true,就得到「模态窗口」:它会禁用父窗口的交互,常用于确认框、对话框。
// 主进程 main.js
const { BrowserWindow } = require('electron/main')
function openModal (parentWin) {
const child = new BrowserWindow({
parent: parentWin, // 指定父窗口
modal: true, // 模态:禁用父窗口
show: false,
width: 400,
height: 300
})
child.loadFile('modal.html')
child.once('ready-to-show', () => {
child.show()
})
}
跨平台差异需要留意:在 macOS 上,模态窗口会显示为「附着」在父窗口上的表单(sheet);在 Linux 上,模态窗口的类型会变成 dialog,且很多桌面环境不支持隐藏模态窗口。
8-5 焦点与可见性事件
窗口的显示、隐藏、获得焦点、失去焦点,都能通过事件监听到。这在「只保留一个活跃窗口」「失焦时暂停动画」等场景很有用。
常用事件包括:focus(获得焦点)、blur(失去焦点)、show(显示)、hide(隐藏)、minimize(最小化)、restore(从最小化恢复)、maximize / unmaximize。
// 主进程 main.js —— win 是前面创建好的窗口实例
win.on('focus', () => {
console.log('窗口获得焦点')
})
win.on('blur', () => {
console.log('窗口失去焦点')
})
win.on('show', () => {
console.log('窗口已显示')
})
win.on('hide', () => {
console.log('窗口已隐藏')
})
想主动操作窗口,可以用 win.focus()、win.show()、win.hide()、win.minimize()、win.close() 等方法。需要找窗口时,BrowserWindow.getAllWindows() 返回全部窗口,BrowserWindow.getFocusedWindow() 返回当前聚焦的窗口,BrowserWindow.fromId(id) 按 id 取窗口。
8-6 多窗口间的协作思路
多窗口之间不能直接共享 JavaScript 对象。窗口 A 想通知窗口 B,标准做法是经过主进程中转:窗口 A 用 IPC 把消息发给主进程,主进程再 webContents.send 给窗口 B。
本书第三篇会专门讲 IPC。这里先记住一个原则:窗口只是「视图」,状态尽量放在主进程,窗口只负责展示。这样多窗口才不会各说各话。
// 主进程:把消息从一个窗口转发给另一个
const { ipcMain, BrowserWindow } = require('electron/main')
ipcMain.on('relay-to-other', (event, payload) => {
const sender = BrowserWindow.fromWebContents(event.sender)
const others = BrowserWindow.getAllWindows().filter(w => w !== sender)
others.forEach(w => w.webContents.send('from-other-window', payload))
})
8-7 父子关系的动态调整与遍历
除了在构造时指定 parent,你还可以在窗口创建后动态改变父子关系。用 win.setParentWindow(parent) 可以把一个已有窗口挂到新的父窗口下;传 null 则解除父子关系。
// 主进程 main.js —— mainWin 是已经建好的主窗口
const child = new BrowserWindow({ width: 400, height: 300 })
child.setParentWindow(mainWin) // 挂到主窗口之下
// child.setParentWindow(null) // 解除父子关系
想知道某个窗口有哪些子窗口,可以遍历 BrowserWindow.getAllWindows(),对比每个窗口的 getParentWindow() 返回值。注意只有被设为 modal 的子窗口才会真正禁用父窗口交互,普通的父子窗口只是层级上的附着。
关闭父窗口时,子窗口默认会跟随关闭。因此清理引用时,建议先处理子窗口再处理父窗口,避免访问已销毁的父对象。多窗口管理的本质,就是维护好这样一棵树或一张表,让创建、显示、关闭都有迹可循。
8-8 常见误区
- 误区一:用局部变量存窗口,关掉后还去调用它。请用集合管理,并在
closed时移除。 - 误区二:为了「不闪」把所有窗口都
show: false。复杂应用反而应该直接显示并设置backgroundColor。 - 误区三:想让两个窗口直接共享一个对象。进程之间只能传可被结构化克隆的数据,不能传窗口、DOM 或函数。
- 误区四:新开的窗口忘了配
webPreferences.preload。它不会从主窗口继承,每个窗口都要自己指定,漏了就会出现「这个窗口调不到接口」的怪事。 - 误区五:在
closed回调里继续操作那个窗口对象。此时原生窗口已销毁,任何方法调用都会抛错,回调里只该做清理。
窗口一多,最好给每个窗口起个明确的角色名(主窗口、设置窗口、预览窗口),用它做 Map 的键。这样查日志、做权限判断时,都能一眼认出是谁在发消息。
8-9 本章小结
管理多窗口的核心是「持有引用、及时清理、用事件驱动」。父子窗口适合做对话框,模态窗口能强制用户先处理。焦点与可见性事件帮你做出更聪明的交互,而窗口间通信则应交由主进程统一调度。
下一章我们把视线移到 preload,看看这座桥到底是怎么搭起来的。