Skip to content

Instantly share code, notes, and snippets.

@jctaoo
Created August 16, 2025 06:36
Show Gist options
  • Select an option

  • Save jctaoo/0ae4a7f99d85a8552fddd05a6e353a33 to your computer and use it in GitHub Desktop.

Select an option

Save jctaoo/0ae4a7f99d85a8552fddd05a6e353a33 to your computer and use it in GitHub Desktop.
electron 记录并持久化窗口位置,支持 devtools
import { BrowserWindow, screen, WebContents } from "electron";
import Store from "electron-store";
import log from "electron-log";
import { WINDOW_PATH } from "@common/path.js";
export type WindowState = {
x?: number;
y?: number;
width: number;
height: number;
isMaximized: boolean;
isFullScreen: boolean;
};
type WindowStateStoreType = {
/** store window-id to window-state */
[key: string]: WindowState;
};
class WindowStateManager {
// electron-store
private readonly store: Store<WindowStateStoreType>;
private webContentsIdToWindowId: Map<WebContents["id"], string> = new Map();
constructor() {
this.store = new Store<WindowStateStoreType>({
name: "window-state",
defaults: {},
});
}
/**
* register window to window state manager
* @param window
* @returns a function to unregister window and a function to setup dev tools window
*/
registerWindow(window: BrowserWindow, path: keyof typeof WINDOW_PATH): [() => void, () => void] {
const windowId = this.generateWindowId(window, path);
this.webContentsIdToWindowId.set(window.webContents.id, windowId);
log.info(`[WindowStateManager] Registering window ${windowId}`);
// 恢复窗口状态
const savedState = this.getWindowStateFromStore(windowId);
if (savedState) {
log.info(`[WindowStateManager] Restoring saved state for window ${windowId}`);
this.applyWindowStateToWindow(window, savedState);
}
const unRegister = this.setupWindowListeners(window, (windowId, state) =>
this.saveWindowStateToStore(windowId, state),
);
return [unRegister, this.buildSetupDevToolsWindow(window, savedState)];
}
private buildSetupDevToolsWindow(window: BrowserWindow, currentState?: WindowState): () => void {
let devToolsWindow: BrowserWindow | null = null;
return () => {
// check window is unregistered or not
const windowId = this.webContentsIdToWindowId.get(window.webContents.id);
if (!windowId) {
log.warn(
`[WindowStateManager] Window ${window.webContents.id} not found in webContentsIdToWindowId when building setup dev tools window`,
);
return;
}
const devToolsWindowId = `${windowId}_devTools`;
const devToolsWindowState = this.getWindowStateFromStore(devToolsWindowId);
devToolsWindow = new BrowserWindow();
devToolsWindow.setMenu(null);
this.webContentsIdToWindowId.set(devToolsWindow.webContents.id, devToolsWindowId);
if (window.webContents.isDevToolsOpened()) {
log.warn(
`[WindowStateManager] Dev tools window is already opened for window ${windowId} when building setup dev tools window`,
);
return;
}
window.webContents.setDevToolsWebContents(devToolsWindow.webContents);
window.webContents.openDevTools({ mode: "detach" });
// on devtools loaded, change title
devToolsWindow.once("ready-to-show", () => {
devToolsWindow?.setTitle(`DevTools for ${windowId}`);
});
// listen close dev tools window
window.webContents.once("devtools-closed", () => {
if (!devToolsWindow?.isDestroyed()) {
devToolsWindow?.close();
}
});
window.once("closed", () => {
if (!devToolsWindow?.isDestroyed()) {
devToolsWindow?.close();
}
});
if (devToolsWindowState) {
// restore dev tools window state
log.info(
`[WindowStateManager] Restoring dev tools window state for window ${windowId}:`,
JSON.stringify(devToolsWindowState),
);
this.applyWindowStateToWindow(devToolsWindow, devToolsWindowState);
}
const unregisterDevTools = this.setupWindowListeners(devToolsWindow, (windowId, state) => {
this.saveWindowStateToStore(windowId, state);
});
devToolsWindow.once("close", () => {
log.info(`[WindowStateManager] Dev tools window (${devToolsWindowId}) closed for window ${windowId}`);
unregisterDevTools();
devToolsWindow = null;
});
};
}
/**
* setup window listeners
* @param window the window to setup listeners
* @returns a function to unregister window
*/
private setupWindowListeners(
window: BrowserWindow,
storeFn: (windowId: string, state: WindowState) => void,
): () => void {
// 保存当前窗口状态
const saveCurrentState = () => {
if (window.isDestroyed()) return;
const bounds = window.getBounds();
const isMaximized = window.isMaximized();
const isFullScreen = window.isFullScreen();
const currentState: WindowState = {
x: bounds.x,
y: bounds.y,
width: bounds.width,
height: bounds.height,
isMaximized,
isFullScreen,
};
const windowId = this.webContentsIdToWindowId.get(window.webContents.id);
if (windowId) {
storeFn(windowId, currentState);
} else {
log.warn(`[WindowStateManager] Window ${window.webContents.id} not found in webContentsIdToWindowId`);
}
};
// 防抖保存函数,用户停止操作后延迟保存
let saveTimeout: NodeJS.Timeout | null = null;
const debouncedSave = () => {
if (saveTimeout) {
clearTimeout(saveTimeout);
}
saveTimeout = setTimeout(saveCurrentState, 250);
};
// 监听窗口事件
const onResize = () => debouncedSave();
const onMove = () => debouncedSave();
const onMaximize = () => saveCurrentState();
const onUnmaximize = () => saveCurrentState();
const onEnterFullScreen = () => saveCurrentState();
const onLeaveFullScreen = () => saveCurrentState();
const onClosed = () => {
if (saveTimeout) {
clearTimeout(saveTimeout);
}
saveCurrentState();
};
// 注册事件监听器
window.on("resize", onResize);
window.on("move", onMove);
window.on("maximize", onMaximize);
window.on("unmaximize", onUnmaximize);
window.on("enter-full-screen", onEnterFullScreen);
window.on("leave-full-screen", onLeaveFullScreen);
window.on("closed", onClosed);
// 返回注销函数
const unRegister = () => {
if (saveTimeout) {
clearTimeout(saveTimeout);
}
// 移除事件监听器
if (!window.isDestroyed()) {
window.removeListener("resize", onResize);
window.removeListener("move", onMove);
window.removeListener("maximize", onMaximize);
window.removeListener("unmaximize", onUnmaximize);
window.removeListener("enter-full-screen", onEnterFullScreen);
window.removeListener("leave-full-screen", onLeaveFullScreen);
window.removeListener("closed", onClosed);
}
const windowId = this.webContentsIdToWindowId.get(window.webContents.id);
if (windowId) {
this.webContentsIdToWindowId.delete(window.webContents.id);
}
log.info(`[WindowStateManager] Unregistered window ${windowId}`);
};
return unRegister;
}
generateWindowId(window: BrowserWindow, path: keyof typeof WINDOW_PATH): string {
return `window_${path}`;
}
private getWindowStateFromStore(windowId: string): WindowState | undefined {
return this.store.get(windowId);
}
private validateWindowState(windowState: WindowState): WindowState {
const displays = screen.getAllDisplays();
const primaryDisplay = screen.getPrimaryDisplay();
// 验证最小尺寸
const minWidth = 400;
const minHeight = 300;
let validatedState = { ...windowState };
// 验证并修正宽高
validatedState.width = Math.max(minWidth, windowState.width);
validatedState.height = Math.max(minHeight, windowState.height);
// 验证位置是否在任何显示器范围内
if (windowState.x !== undefined && windowState.y !== undefined) {
let isInValidDisplay = false;
for (const display of displays) {
const { x, y, width, height } = display.bounds;
const windowRight = windowState.x + validatedState.width;
const windowBottom = windowState.y + validatedState.height;
const displayRight = x + width;
const displayBottom = y + height;
// 检查窗口是否至少部分在显示器内
if (windowState.x < displayRight && windowRight > x && windowState.y < displayBottom && windowBottom > y) {
isInValidDisplay = true;
break;
}
}
// 如果不在任何显示器内,重置到主显示器中心
if (!isInValidDisplay) {
const { x, y, width, height } = primaryDisplay.bounds;
validatedState.x = x + Math.floor((width - validatedState.width) / 2);
validatedState.y = y + Math.floor((height - validatedState.height) / 2);
log.warn("[WindowStateManager] Window position out of bounds, resetting to primary display center");
}
} else {
// 如果没有位置信息,使用主显示器中心
const { x, y, width, height } = primaryDisplay.bounds;
validatedState.x = x + Math.floor((width - validatedState.width) / 2);
validatedState.y = y + Math.floor((height - validatedState.height) / 2);
}
return validatedState;
}
private saveWindowStateToStore(windowId: string, windowState: WindowState): void {
try {
this.store.set(windowId, windowState);
log.info(`[WindowStateManager] Saved window state for ${windowId}:`, JSON.stringify(windowState));
} catch (error) {
log.error(`[WindowStateManager] Failed to save window state for ${windowId}:`, error);
}
}
private applyWindowStateToWindow(window: BrowserWindow, windowState: WindowState): void {
try {
const validatedState = this.validateWindowState(windowState);
// 首先设置窗口大小和位置(在非最大化/全屏状态下)
if (!validatedState.isMaximized && !validatedState.isFullScreen) {
if (validatedState.x !== undefined && validatedState.y !== undefined) {
window.setBounds({
x: validatedState.x,
y: validatedState.y,
width: validatedState.width,
height: validatedState.height,
});
} else {
window.setSize(validatedState.width, validatedState.height);
}
} else {
// 如果窗口是最大化或全屏状态,先设置正常大小(用于恢复时)
window.setSize(validatedState.width, validatedState.height);
if (validatedState.x !== undefined && validatedState.y !== undefined) {
window.setPosition(validatedState.x, validatedState.y);
}
}
// 然后应用特殊状态
if (validatedState.isFullScreen) {
window.setFullScreen(true);
} else if (validatedState.isMaximized) {
window.maximize();
}
log.debug(`[WindowStateManager] Applied window state:`, validatedState);
} catch (error) {
log.error("[WindowStateManager] Failed to apply window state:", error);
}
}
}
export default WindowStateManager;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment