首页
/ Electron WindowStatePersistence 实战解析:窗口位置、尺寸与显示模式的跨重启持久化

Electron WindowStatePersistence 实战解析:窗口位置、尺寸与显示模式的跨重启持久化

2026-09-06 17:48:09作者:明树来

Electron 的 WindowStatePersistence 对象是 BaseWindow/BrowserWindow 构造选项 windowStatePersistence 的配置结构,用于声明应用重启后要恢复哪些窗口状态:位置与尺寸(bounds)以及显示模式(displayMode,即全屏、kiosk、最大化等)。读完本文,你将理解这两个字段的取值与默认行为,掌握开启持久化、选择性持久化和清除持久化状态的完整用法,并能从源码层面弄清 Electron 在何时保存、如何选屏、如何调整越界窗口的实现细节。

WindowStatePersistence 对象字段

依据 WindowStatePersistence Object 的定义,该对象包含两个可选布尔字段:

字段 类型 默认值 作用
bounds boolean(可选) true 是否在应用重启后持久化窗口的位置与尺寸
displayMode boolean(可选) true 是否在应用重启后持久化显示模式(全屏、kiosk、最大化等)

未显式指定时,两个字段都按 true 处理。这一点在 C++ 层构造函数中得到直接印证,见 shell/browser/native_window.cc

if (gin_helper::Dictionary persistence_options;
    options.Get(options::kWindowStatePersistence, &persistence_options)) {
  // Restore bounds by default
  restore_bounds_ = true;
  persistence_options.Get(options::kBounds, &restore_bounds_);
  // Restore display mode by default
  restore_display_mode_ = true;
  persistence_options.Get(options::kDisplayMode, &restore_display_mode_);
  window_state_persistence_enabled_ = true;
} else if (bool flag; options.Get(options::kWindowStatePersistence, &flag)) {
  restore_bounds_ = flag;
  restore_display_mode_ = flag;
  window_state_persistence_enabled_ = flag;
}

从源码结构看,windowStatePersistence 支持两种传入形式:

  • 对象:分别读取 boundsdisplayMode,任一未指定则该项默认开启,但整体持久化视为已启用;
  • 布尔值true 时两项全开,false 时整体关闭。

对应的选项常量定义在 shell/common/options_switches.h

启用持久化:name 是唯一前提

windowStatePersistenceBaseWindowConstructorOptions 中的完整描述为:

windowStatePersistence (WindowStatePersistence | boolean)(可选)——配置或启用窗口状态(位置、尺寸、最大化状态等)在应用重启间的持久化。若未提供窗口 name,则无效。在没有可用显示器时自动禁用。Experimental

注意三点关键约束:

  1. 必须提供唯一的 name,它是存储和检索窗口状态的标识符;
  2. 该特性标记为 Experimental,接口和行为可能随版本调整;
  3. 无可用显示器时特性自动失效(例如无头 CI 环境)。

基本用法:

const { app, BrowserWindow } = require('electron')

function createWindow () {
  const win = new BrowserWindow({
    name: 'main-window',
    width: 800,
    height: 600,
    windowStatePersistence: true
  })

  win.loadFile('index.html')
}

app.whenReady().then(createWindow)

在此配置下,Electron 会自动:

  1. 创建窗口时恢复其位置、尺寸与显示模式(若存在历史状态);
  2. 在状态变化(位置、尺寸或显示模式)时保存窗口状态;
  3. 成功恢复后发出 persisted-state-restored 事件;
  4. 自动将恢复的窗口状态适配多显示器布局与显示变更。

name 的唯一性不是建议而是硬性要求。在 shell/browser/api/electron_api_base_window.cc 中,IsWindowNameValid 会在创建窗口前遍历 WindowList,一旦发现重名窗口名,构造函数直接抛出类型错误("Window name 'xxx' is already in use. Window names must be unique.")。而当开启了持久化却没给 name 时,Electron 会静默降级:在 shell/browser/native_window.cc 中关闭持久化并打印警告:

} else if (window_state_persistence_enabled_ && window_name_.empty()) {
  window_state_persistence_enabled_ = false;
  LOG(WARNING) << "Window state persistence enabled but no window name "
                  "provided. Window state will not be persisted.";
}

选择性持久化

通过传入对象可以精细控制持久化的维度。例如只记住位置和尺寸,但每次都以普通窗口模式启动(即使上次是最大化或全屏退出):

const { app, BrowserWindow } = require('electron')

function createWindow () {
  const win = new BrowserWindow({
    name: 'main-window',
    width: 800,
    height: 600,
    windowStatePersistence: {
      bounds: true, // Save position and size (default: true)
      displayMode: false // Don't save maximized/fullscreen/kiosk state (default: true)
    }
  })

  win.loadFile('index.html')
}

app.whenReady().then(createWindow)

这里有一个源码层面的细节值得了解:displayMode: false 之所以能"只恢复普通窗口",是因为保存逻辑本身做了配合。在 NativeWindow::SaveWindowState 中,当窗口处于特殊显示模式(全屏、kiosk 或最大化)时,Electron 不会把当前的边界写入库,而是沿用此前保存的普通窗口边界:

// When the window is in a special display mode (fullscreen, kiosk, or
// maximized), save the previously stored window bounds instead of
// the current bounds.
if (!IsNormal() && existing_prefs) {
  // ... 从已有 prefs 中取回 left/top/right/bottom 重建 bounds
}

也就是说,"普通窗口边界"与"当前显示模式"在存储中是相互独立的,这保证了两个字段可以任意组合而不互相污染。

存储格式与保存时机

持久化数据落在浏览器进程的 local_state 偏好存储中,键为 kWindowStates,其下按窗口 name 建立子字典。每个窗口记录包含(见 shell/browser/native_window.cc):

  • 窗口边界:lefttoprightbottom
  • 显示模式:maximizedfullscreenkiosk 三个布尔值;
  • 保存时的显示器工作区:workAreaLeftworkAreaTopworkAreaRightworkAreaBottom(用于恢复时做工作区适配)。

保存不是每次事件都写盘,而是通过 200 毫秒防抖 合并高频的移动/缩放事件,见 NativeWindow::DebouncedSaveWindowState

void NativeWindow::DebouncedSaveWindowState() {
  save_window_state_timer_.Start(
      FROM_HERE, base::Milliseconds(200),
      base::BindOnce(&NativeWindow::SaveWindowState, base::Unretained(this)));
}

窗口销毁前还会调用 NativeWindow::FlushWindowState 立即触发挂起的保存(若防抖定时器在运行则直接 FireNow()),确保最后一次状态不丢失。

保存前还有若干保护性检查:

  • 恢复过程中(is_being_restored_)或全屏过渡期间跳过保存,避免把过渡态的临时边界写入库;
  • 窗口边界为 0×0 时记警告并跳过;
  • 通过 display::Screen::GetDisplayMatching 找到窗口所在显示器,若该显示器是伪造的(无物理显示器时 Chromium 会返回 1920×1080 的假显示)或尺寸为 0×0,则跳过保存——这正对应文档中"没有可用显示器时自动禁用"的描述。

恢复流程:如何挑选目标显示器

恢复入口是 NativeWindow::RestoreWindowState。其流程如下:

  1. kWindowStates 中读取当前窗口 name 对应的记录,缺少任一必需字段(边界或工作区)时按"值损坏"处理并放弃恢复;
  2. 遍历所有显示器,对每个候选显示器的 work_area 执行 AdjustToFit 后计算与保存边界的 曼哈顿距离,位移最小的显示器被选为目标显示——从源码结构看,这一策略让窗口在"拔掉了原显示器"的多屏场景下落到最接近的屏幕上;
  3. 若目标显示器同样是伪造/无效显示器,则放弃恢复;
  4. 先置 is_being_restored_ = true 抑制恢复期间的反向保存,再按需调用 RestoreBounds
  5. displayMode 已启用,则在窗口 Show() 之后异步执行显示模式切换,优先级为 kiosk > fullscreen > maximized;由于 macOS 上这些过渡是异步的,is_being_restored_ 标志会保留到对应的 NotifyWindowEnterFullScreen/NotifyWindowMaximize 回调,防止过渡中的中间边界覆盖已保存状态;
  6. 最后调用 NotifyWindowStateRestored(),最终体现为 BaseWindowpersisted-state-restored 事件(见 docs/api/base-window.md,该事件仅在启用了 windowStatePersistence 时发出)。

越界窗口的边界修正

即使找到了目标显示器,保存的坐标也可能越界(分辨率变化、DPI 变化、显示器移除等)。NativeWindow::RestoreBounds 参考了 Chromium 的窗口定位逻辑(window_sizer.cc)做修正:

  • 强制窗口不小于最小可见尺寸(kMinVisibleHeight × kMinVisibleWidth),标题栏不允许跑到工作区上方;
  • 当保存时的工作区与当前工作区不同、且当前工作区没有完整包含窗口时,执行 AdjustToFit
  • macOS 策略更激进:部分离屏即整体吸附回工作区;其他平台更保守:只要保证最小可见区域在工作区内即可,仅做坐标钳制。

清除持久化状态

BaseWindow(以及继承自它的 BrowserWindow)提供静态方法 clearPersistedState 用于按窗口名清除已保存的状态,实现见 shell/browser/api/electron_api_base_window.cc:它从 kWindowStates 字典中移除对应 name 的条目;若窗口名为空或名字不存在,只记录警告而不抛错。

const { BrowserWindow } = require('electron')

// Clear saved state for a specific window
BrowserWindow.clearPersistedState('main-window')

// Now when you create a window with this name,
// it will use the default constructor options
const win = new BrowserWindow({
  name: 'main-window',
  width: 800,
  height: 600,
  windowStatePersistence: true
})

清除后再次创建同名窗口,将回退到构造选项中指定的默认位置与尺寸。

测试验证

该特性有一套独立的集成测试,位于 spec/api-browser-window-spec.tswindowStatePersistence 测试组,从 L8059 起,仅在 hasCapturableScreen() 成立时运行——再次印证对真实显示器的依赖)。配套的子进程 fixture 覆盖了各类触发场景,位于 spec/fixtures/api/window-state-save/ 目录:

  • move-save / resize-save:移动与缩放窗口后状态被保存;
  • maximize-save / fullscreen-save / kiosk-save:各显示模式可被恢复(含"恢复最大化状态""恢复全屏状态"等用例);
  • close-save:关闭窗口时最终状态被刷写;
  • minimize-save / work-area-primary / main-thread-busy:最小化、工作区适配等边缘场景;
  • schema-check:校验存储数据的结构。

测试中还包含明确的反例验证:windowStatePersistence: false 时不恢复任何状态;启用了持久化且存在历史状态时才发出 persisted-state-restored 事件,未启用时不发出。

小结与使用建议

  • WindowStatePersistence 的完整语义只有两个布尔字段:boundsdisplayMode,缺省均为 true;传 windowStatePersistence: true/false 等价于两者同开/同关。
  • 启用前提是窗口有唯一的 name;重名会在构造时直接抛错,无 name 则特性被静默禁用(伴随日志警告)。
  • 特性为 Experimental:接口与存储细节可能演进,且依赖真实可用的显示器,无头/虚拟显示环境下会自动跳过保存与恢复。
  • 想理解行为边界,优先阅读 shell/browser/native_window.ccSaveWindowStateRestoreWindowStateRestoreBounds 三个函数与 docs/tutorial/window-state-persistence.md 教程文档,二者与本结构定义 docs/api/structures/window-state-persistence.md 互为印证。
登录后查看全文
热门项目推荐
相关项目推荐