Electron WindowStatePersistence 实战解析:窗口位置、尺寸与显示模式的跨重启持久化
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 支持两种传入形式:
- 传 对象:分别读取
bounds与displayMode,任一未指定则该项默认开启,但整体持久化视为已启用; - 传 布尔值:
true时两项全开,false时整体关闭。
对应的选项常量定义在 shell/common/options_switches.h。
启用持久化:name 是唯一前提
windowStatePersistence 在 BaseWindowConstructorOptions 中的完整描述为:
windowStatePersistence(WindowStatePersistence|boolean)(可选)——配置或启用窗口状态(位置、尺寸、最大化状态等)在应用重启间的持久化。若未提供窗口name,则无效。在没有可用显示器时自动禁用。Experimental
注意三点关键约束:
- 必须提供唯一的
name,它是存储和检索窗口状态的标识符; - 该特性标记为 Experimental,接口和行为可能随版本调整;
- 无可用显示器时特性自动失效(例如无头 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 会自动:
- 创建窗口时恢复其位置、尺寸与显示模式(若存在历史状态);
- 在状态变化(位置、尺寸或显示模式)时保存窗口状态;
- 成功恢复后发出
persisted-state-restored事件; - 自动将恢复的窗口状态适配多显示器布局与显示变更。
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):
- 窗口边界:
left、top、right、bottom; - 显示模式:
maximized、fullscreen、kiosk三个布尔值; - 保存时的显示器工作区:
workAreaLeft、workAreaTop、workAreaRight、workAreaBottom(用于恢复时做工作区适配)。
保存不是每次事件都写盘,而是通过 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。其流程如下:
- 从
kWindowStates中读取当前窗口name对应的记录,缺少任一必需字段(边界或工作区)时按"值损坏"处理并放弃恢复; - 遍历所有显示器,对每个候选显示器的
work_area执行AdjustToFit后计算与保存边界的 曼哈顿距离,位移最小的显示器被选为目标显示——从源码结构看,这一策略让窗口在"拔掉了原显示器"的多屏场景下落到最接近的屏幕上; - 若目标显示器同样是伪造/无效显示器,则放弃恢复;
- 先置
is_being_restored_ = true抑制恢复期间的反向保存,再按需调用RestoreBounds; - 若
displayMode已启用,则在窗口Show()之后异步执行显示模式切换,优先级为 kiosk > fullscreen > maximized;由于 macOS 上这些过渡是异步的,is_being_restored_标志会保留到对应的NotifyWindowEnterFullScreen/NotifyWindowMaximize回调,防止过渡中的中间边界覆盖已保存状态; - 最后调用
NotifyWindowStateRestored(),最终体现为BaseWindow的persisted-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.ts(windowStatePersistence 测试组,从 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的完整语义只有两个布尔字段:bounds与displayMode,缺省均为true;传windowStatePersistence: true/false等价于两者同开/同关。- 启用前提是窗口有唯一的
name;重名会在构造时直接抛错,无name则特性被静默禁用(伴随日志警告)。 - 特性为 Experimental:接口与存储细节可能演进,且依赖真实可用的显示器,无头/虚拟显示环境下会自动跳过保存与恢复。
- 想理解行为边界,优先阅读 shell/browser/native_window.cc 中
SaveWindowState、RestoreWindowState、RestoreBounds三个函数与 docs/tutorial/window-state-persistence.md 教程文档,二者与本结构定义 docs/api/structures/window-state-persistence.md 互为印证。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00