BrowserWindow 基础配置
Electron 中,每个窗口都是一个 BrowserWindow 实例:
const { BrowserWindow } = require('electron');
const path = require('path');
const mainWindow = new BrowserWindow({
width: 1200, // 窗口宽度(像素)
height: 800, // 窗口高度(像素)
minWidth: 800, // 最小宽度
minHeight: 600, // 最小高度
x: 100, // 窗口 x 坐标(屏幕左上角为原点)
y: 100, // 窗口 y 坐标
title: 'My App', // 窗口标题
backgroundColor: '#ffffff', // 背景色
show: false, // 创建后不显示,等 ready-to-show 再显示
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
}
});
ready-to-show 事件
为了避免窗口创建时的闪烁,推荐使用 ready-to-show:
mainWindow.once('ready-to-show', () => {
mainWindow.show();
});
窗口的显示与隐藏
基本控制
// 显示窗口
mainWindow.show();
// 隐藏窗口
mainWindow.hide();
// 关闭窗口
mainWindow.close();
// 最小化到任务栏
mainWindow.minimize();
// 最大化
mainWindow.maximize();
// 取消最大化
mainWindow.unmaximize();
// 切换最大化/非最大化状态
mainWindow.isMaximized() ? mainWindow.unmaximize() : mainWindow.maximize();
确保窗口在屏幕内
窗口可能在多显示器环境下被创建在不可见的区域,需要校验:
const { screen } = require('electron');
function ensureWindowOnScreen(win) {
const bounds = win.getBounds();
const displays = screen.getAllDisplays();
const isVisible = displays.some(display => {
const { x, y, width, height } = display.bounds;
return (
bounds.x >= x - bounds.width &&
bounds.x < x + width &&
bounds.y >= y - bounds.height &&
bounds.y < y + height
);
});
if (!isVisible) {
// 将窗口移回主屏幕
const primary = screen.getPrimaryDisplay();
win.setBounds({
x: primary.bounds.x + 50,
y: primary.bounds.y + 50,
width: bounds.width,
height: bounds.height
});
}
}
父子窗口关系
Modal 窗口(模态窗口)
模态窗口会锁定父窗口,常用于对话框:
const { BrowserWindow } = require('electron');
function openSettings(parentWindow) {
const settingsWindow = new BrowserWindow({
width: 600,
height: 400,
parent: parentWindow, // 指定父窗口
modal: true, // 开启模态
show: false
});
settingsWindow.loadFile('settings.html');
settingsWindow.once('ready-to-show', () => {
settingsWindow.show();
});
}
子窗口的行为特点
- 子窗口显示时,父窗口无法获得焦点
- 关闭子窗口不会影响父窗口
- 子窗口会跟随父窗口最小化/最大化
非模态子窗口
如果不需要锁定父窗口,只设置 parent 但不加 modal:
const childWindow = new BrowserWindow({
width: 400,
height: 300,
parent: mainWindow,
modal: false
});
无边框窗口
创建无边框窗口
const framelessWindow = new BrowserWindow({
width: 800,
height: 600,
frame: false, // 隐藏原生标题栏
transparent: false, // 可选:透明背景
titleBarStyle: 'hidden' // macOS 专用:隐藏标题栏但保留 Traffic Lights
});
自定义拖拽区域
在无边框窗口中,需要自己实现标题栏的拖拽:
/* CSS */
.titlebar {
-webkit-app-region: drag; /* 可拖拽区域 */
height: 40px;
background: #2c3e50;
}
.titlebar button {
-webkit-app-region: no-drag; /* 按钮不可拖拽 */
}
<!-- HTML -->
<div class="titlebar">
<button id="minimize">最小化</button>
<button id="close">关闭</button>
</div>
// renderer.js
document.getElementById('minimize').addEventListener('click', () => {
window.electronAPI.minimize();
});
document.getElementById('close').addEventListener('click', () => {
window.electronAPI.close();
});
macOS Traffic Lights
macOS 有原生的窗口控制按钮(红绿灯),可以用 titleBarStyle:
const win = new BrowserWindow({
width: 800,
height: 600,
titleBarStyle: 'hidden', // 隐藏标题栏内容
titleBarOverlay: { // Windows/Linux 也可以有 overlay
color: '#2c3e50',
height: 40
}
});
系统对话框
打开文件/目录对话框
const { dialog } = require('electron');
async function openFile() {
const result = await dialog.showOpenDialog(mainWindow, {
title: '选择文件',
defaultPath: '/Users/me/Documents',
filters: [
{ name: 'Images', extensions: ['jpg', 'png', 'gif'] },
{ name: 'All Files', extensions: ['*'] }
],
properties: ['openFile', 'multiSelections']
});
if (!result.canceled) {
console.log('Selected files:', result.filePaths);
return result.filePaths;
}
}
保存文件对话框
async function saveFile() {
const result = await dialog.showSaveDialog(mainWindow, {
title: '保存文件',
defaultPath: '/Users/me/Documents/report.pdf',
filters: [
{ name: 'PDF', extensions: ['pdf'] },
{ name: 'All Files', extensions: ['*'] }
]
});
if (!result.canceled) {
console.log('Save path:', result.filePath);
return result.filePath;
}
}
消息框
async function showMessage() {
const result = await dialog.showMessageBox(mainWindow, {
type: 'question', // 'none' | 'info' | 'error' | 'question'
buttons: ['取消', '保存', '另存为'],
defaultId: 1, // 默认按钮索引
cancelId: 0, // 取消按钮索引
title: '保存确认',
message: '是否保存当前文档?'
});
console.log('Button clicked:', result.response);
// 0 = 取消, 1 = 保存, 2 = 另存为
}
错误对话框
dialog.showErrorBox('错误', '文件保存失败,请重试!');
窗口状态管理
保存窗口状态
const fs = require('fs');
const { app } = require('electron');
function saveWindowState(win, fileName) {
const bounds = win.getBounds();
const state = {
x: bounds.x,
y: bounds.y,
width: bounds.width,
height: bounds.height,
isMaximized: win.isMaximized()
};
fs.writeFileSync(
getStatePath(fileName),
JSON.stringify(state)
);
}
function getStatePath(fileName) {
return path.join(app.getPath('userData'), `${fileName}-window-state.json`);
}
恢复窗口状态
function loadWindowState(fileName, defaults) {
try {
const statePath = getStatePath(fileName);
if (fs.existsSync(statePath)) {
const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
return { ...defaults, ...state };
}
} catch (err) {
console.error('Failed to load window state:', err);
}
return defaults;
}
应用到窗口创建
const defaultState = {
width: 1200,
height: 800,
x: undefined,
y: undefined
};
const state = loadWindowState('main', defaultState);
const mainWindow = new BrowserWindow({
...state,
webPreferences: { /* ... */ }
});
// 窗口关闭时保存状态
mainWindow.on('close', () => {
saveWindowState(mainWindow, 'main');
});
多显示器支持
获取所有显示器信息
const { screen } = require('electron');
// 获取所有显示器
const displays = screen.getAllDisplays();
console.log(displays);
// 获取主显示器
const primary = screen.getPrimaryDisplay();
console.log('Primary:', primary.bounds);
// 获取当前窗口所在的显示器
const currentDisplay = screen.getDisplayMatching(mainWindow.getBounds());
console.log('Current display:', currentDisplay.bounds);
在指定显示器上创建窗口
// 在第二个显示器上创建窗口
const displays = screen.getAllDisplays();
if (displays.length > 1) {
const secondDisplay = displays[1];
const windowOnSecondMonitor = new BrowserWindow({
x: secondDisplay.bounds.x + 50,
y: secondDisplay.bounds.y + 50,
width: 1200,
height: 800
});
}
监听显示器变化
screen.on('display-added', (event, display) => {
console.log('Display added:', display.bounds);
});
screen.on('display-removed', (event, display) => {
console.log('Display removed:', display.bounds);
});
屏幕截图与内容捕获
窗口截图
const { desktopCapturer } = require('electron');
async function captureWindow() {
const sources = await desktopCapturer.getSources({
types: ['window'],
thumbnailSize: { width: 0, height: 0 }
});
// 找到目标窗口
const targetWindow = sources.find(source => {
return source.name === 'My App';
});
if (targetWindow) {
// thumbnail 是一个 NativeImage
const thumbnail = targetWindow.thumbnail;
const buffer = thumbnail.toPNG();
// 保存到文件
fs.writeFileSync('screenshot.png', buffer);
}
}
全屏截图
async function captureScreen() {
const sources = await desktopCapturer.getSources({
types: ['screen'],
thumbnailSize: { width: 1920, height: 1080 }
});
if (sources.length > 0) {
const screenshot = sources[0].thumbnail;
const buffer = screenshot.toPNG();
fs.writeFileSync('fullscreen.png', buffer);
}
}
窗口交互最佳实践
1. 避免多次创建窗口
使用单例模式确保只有一个主窗口:
let mainWindow = null;
function createMainWindow() {
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
return mainWindow;
}
mainWindow = new BrowserWindow({ /* ... */ });
mainWindow.on('closed', () => {
mainWindow = null;
});
return mainWindow;
}
2. 优雅关闭确认
mainWindow.on('close', (event) => {
if (hasUnsavedChanges) {
event.preventDefault();
const choice = dialog.showMessageBoxSync(mainWindow, {
type: 'question',
buttons: ['取消', '不保存', '保存'],
defaultId: 2,
title: '确认关闭',
message: '有未保存的更改,是否保存?'
});
if (choice === 2) {
// 保存并关闭
saveAndClose();
} else if (choice === 1) {
// 不保存,强制关闭
hasUnsavedChanges = false;
mainWindow.close();
}
}
});
3. 跨窗口通信
父子窗口或多个窗口之间通信,可以使用 IPC:
// 主进程作为消息路由
ipcMain.on('window-action', (event, action, data) => {
if (action === 'refresh-list') {
// 通知所有窗口
BrowserWindow.getAllWindows().forEach(win => {
win.webContents.send('refresh-list', data);
});
}
});
这一章想说的
窗口管理是桌面应用的核心:
- BrowserWindow 配置:尺寸、最小/最大限制、位置控制
- 父子窗口:模态窗口锁定父窗口,非模态子窗口跟随父窗口
- 无边框窗口:自定义标题栏,需要自己处理拖拽和按钮事件
- 系统对话框:文件选择、保存、消息框
- 窗口状态持久化:保存位置和尺寸到文件,应用启动时恢复
良好的窗口管理能显著提升桌面应用的用户体验。