用户对桌面应用有一个基本预期:上次关闭时窗口在屏幕的哪个位置、多大尺寸、是否最大化,下次打开时应该原样恢复。这个看似微小的细节,直接影响着应用的“专业感”。好在 Electron 提供了所有必要的钩子,实现起来并不复杂。
10.4.1 核心思路
窗口状态的持久化本质上是一个“保存—读取—应用”的循环:
- 在窗口关闭或移动/缩放时,记录当前的
x、y、width、height以及isMaximized等状态,存入本地存储(如 JSON 文件)。 - 应用启动时,先读取这些值,再传给
BrowserWindow的构造函数。 - 考虑到多显示器、任务栏位置变化等异常情况,还需要加一些边界校验,避免窗口“飞出”屏幕。
因为主进程可以直接使用 Node.js 的 fs 模块,最简单的做法就是用 electron-store(一个基于 JSON 文件的持久化库)来管理这些状态。当然,手动读写 JSON 文件也完全可行。
10.4.2 使用 electron-store 方案
安装依赖:
npm install electron-store
在主进程文件中配置 store 和窗口状态逻辑:
// main.js
const { app, BrowserWindow, screen } = require('electron');
const Store = require('electron-store');
const path = require('path');
const store = new Store({
name: 'window-state', // 存储文件名
defaults: {
windowBounds: { width: 1024, height: 768 }, // 默认尺寸
isMaximized: false,
},
});
let mainWindow;
function createWindow() {
// 读取上次保存的状态
const { windowBounds, isMaximized } = store.get();
// 新建窗口,应用位置和尺寸
mainWindow = new BrowserWindow({
width: windowBounds.width,
height: windowBounds.height,
x: windowBounds.x,
y: windowBounds.y,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
},
});
// 如果上次关闭时是最大化状态,就最大化
if (isMaximized) {
mainWindow.maximize();
}
mainWindow.loadFile('index.html');
// 监听窗口的移动和缩放,实时保存非最大化时的位置和尺寸
mainWindow.on('resize', saveWindowState);
mainWindow.on('move', saveWindowState);
mainWindow.on('maximize', () => store.set('isMaximized', true));
mainWindow.on('unmaximize', () => store.set('isMaximized', false));
// 窗口关闭前做最后一次保存
mainWindow.on('close', () => {
saveWindowState();
});
}
function saveWindowState() {
if (!mainWindow || mainWindow.isDestroyed()) return;
// 只有在非最大化时才记录位置和尺寸,否则存的是最大化状态下的畸变值
if (!mainWindow.isMaximized()) {
const bounds = mainWindow.getBounds();
store.set('windowBounds', bounds);
}
store.set('isMaximized', mainWindow.isMaximized());
}
app.whenReady().then(createWindow);
10.4.3 边界情况处理
在实际使用中,简单地记录和恢复坐标可能会遇到一些麻烦:
- 多显示器切换:用户可能上次在外接显示器上关闭了应用,下次在没有外接屏的笔记本上打开,窗口就跑到了不存在的区域。此时需要用
screen模块校验坐标是否还可见。 - 任务栏/ Dock 遮挡:记录的位置可能与输入法工具栏、系统任务栏重叠,导致窗口部分“隐藏”。可以适当校验窗口是否完全在可用区域内。
- 极端尺寸:如果用户把窗口缩得非常小,下次启动时可能连标题栏按钮都显示不全。最好加一个最小宽度/高度的限制。
以下是增强版的 createWindow,添加了位置校验:
function createWindow() {
const { windowBounds, isMaximized } = store.get();
// 校验坐标是否至少部分可见
const bounds = ensureWindowIsVisible(windowBounds);
mainWindow = new BrowserWindow({
width: bounds.width,
height: bounds.height,
x: bounds.x,
y: bounds.y,
minWidth: 400, // 防止尺寸过小
minHeight: 300,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
},
});
if (isMaximized && bounds.width >= 400 && bounds.height >= 300) {
mainWindow.maximize();
}
// ... 其余相同
}
function ensureWindowIsVisible(bounds) {
// 获取所有显示器的合并矩形区域
const displays = screen.getAllDisplays();
const visibleArea = displays.reduce((acc, display) => {
const { x, y, width, height } = display.bounds;
acc.minX = Math.min(acc.minX, x);
acc.minY = Math.min(acc.minY, y);
acc.maxX = Math.max(acc.maxX, x + width);
acc.maxY = Math.max(acc.maxY, y + height);
return acc;
}, { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity });
// 检查窗口的左上角是否在任何一个显示器内
let { x, y } = bounds;
if (x < visibleArea.minX) x = visibleArea.minX;
if (y < visibleArea.minY) y = visibleArea.minY;
// 如果窗口完全在屏幕外,则将其移到第一个显示器中心
if (x > visibleArea.maxX || y > visibleArea.maxY) {
const primaryDisplay = screen.getPrimaryDisplay();
x = Math.round((primaryDisplay.bounds.width - bounds.width) / 2);
y = Math.round((primaryDisplay.bounds.height - bounds.height) / 2);
}
return { ...bounds, x, y };
}
10.4.4 配合托盘或“关闭到托盘”功能
如果你的应用支持点击关闭按钮后最小化到系统托盘而不是真正退出,那么窗口状态保存的逻辑需要稍作调整。此时 mainWindow.on('close') 会被触发,但你需要阻止默认行为并隐藏窗口,同时记录当前状态。只有当应用真正退出时(比如通过托盘菜单选择“退出”)才允许关闭进程。
mainWindow.on('close', (event) => {
if (!app.isQuitting) {
event.preventDefault();
saveWindowState();
mainWindow.hide();
} else {
// 真正退出时再保存一次
saveWindowState();
}
});
在用户点击托盘退出按钮时,设置 app.isQuitting = true,然后调用 app.quit() 触发真正的关闭流程。
10.4.5 纯手工方案(不用第三方库)
如果你不想引入 electron-store,也可以自己实现一个轻量版的 JSON 存储。原理一样,只是替换了读写方法:
const fs = require('fs');
const path = require('path');
const app = require('electron').app;
const stateFile = path.join(app.getPath('userData'), 'window-state.json');
function loadState() {
try {
if (fs.existsSync(stateFile)) {
return JSON.parse(fs.readFileSync(stateFile, 'utf8'));
}
} catch (e) {
// 忽略错误,使用默认值
}
return { width: 1024, height: 768 };
}
function saveState(bounds, isMaximized) {
try {
fs.writeFileSync(stateFile, JSON.stringify({ ...bounds, isMaximized }, null, 2));
} catch (e) {
// 写入失败不影响应用运行
}
}
然后只需将之前的 store.get 和 store.set 替换为 loadState 和 saveState 即可。
窗口状态持久化虽然代码不多,但它直接提升了应用的品质感。从用户角度看,一个能记住自己偏好的应用才像一个“真正的桌面软件”。在章节配套的示例项目中,你可以在 main 目录下找到完整的主进程代码,它会与这一节讲解的逻辑高度一致。