首页 / Electron 入门教程 / 窗口事件与多窗口管理

Electron 入门教程

窗口事件与多窗口管理

本教程共 45 篇 · 第 8 篇 · 更新于 2026-08-03

ElectronBrowserWindow多窗口窗口事件窗口管理

8. 窗口事件与多窗口管理

本节目标

  • 学会在同一个应用里创建并管理多个 BrowserWindow
  • 理解父子窗口(含模态窗口)的创建与行为差异。
  • 掌握 ready-to-showclosedfocusblur 等常用事件。
  • 明白为什么要用一个集合持有窗口引用,避免被垃圾回收。
  • 了解跨平台在窗口行为上的细微差别。

桌面应用很少有「单窗口」的。设置面板、关于框、独立的编辑器窗口,都需要你同时管理多个 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 设为 falseready-to-show 将不会触发。

8-4 父子窗口与模态窗口

给新窗口传一个 parent 选项,它就变成父窗口的子窗口。子窗口会始终浮在父窗口之上,最小化父窗口时子窗口也会跟着最小化。

如果同时传 parentmodal: 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,看看这座桥到底是怎么搭起来的。