在 Electron 中,如果你希望应用有一个完全自定义的标题栏,或者一个没有系统默认窗口装饰的界面,无边框窗口是必选项。但移除了系统提供的标题栏后,窗口的拖拽、缩放、关闭等基础交互就需要开发者自己来实现。本节会完整覆盖从创建无边框窗口到实现自定义标题栏、拖拽移动、边缘缩放、双击全屏等全套交互的实用方法。
10.3.1 创建无边框窗口
在主进程创建窗口时,将 frame 设为 false 即可得到一个无边框窗口:
// main.js (主进程)
const { BrowserWindow } = require('electron');
const win = new BrowserWindow({
width: 1000,
height: 700,
frame: false, // 移除系统标题栏与窗口边框
// 保留一个细微的阴影和圆角(仅 macOS 有效,Windows 上可以使用透明窗口 + CSS 模拟)
transparent: true, // 按需开启,如需自绘圆角/阴影
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
},
});
在 Windows 上,完全无框通常会让窗口失去阴影和调节大小的区域,因此很多应用会保留一个很细的 titleBarStyle: 'hidden' 或通过 CSS 自己画阴影。如果你不想完全透明,可以保持 transparent: false,并用 CSS 的 border-radius 或 box-shadow 模拟外观。
10.3.2 自定义标题栏与窗口控制
在渲染进程的 HTML/CSS 中构建自己的标题栏区域。标题栏通常包含:
- 应用图标和名称(可拖拽区域)
- 菜单按钮(可选)
- 最小化、最大化/还原、关闭按钮
<!-- index.html -->
<div id="title-bar">
<div class="drag-region">
<span class="title">我的应用</span>
</div>
<div class="window-controls">
<button id="minimize-btn" title="最小化">─</button>
<button id="maximize-btn" title="最大化">☐</button>
<button id="close-btn" title="关闭">✕</button>
</div>
</div>
<div id="app-content">
<!-- 你的界面内容 -->
</div>
/* styles.css */
body {
margin: 0;
overflow: hidden; /* 避免出现浏览器滚动条干扰自定义拖拽区 */
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
#title-bar {
display: flex;
align-items: center;
height: 32px;
background: #2c2c2c;
color: #fff;
user-select: none;
}
.drag-region {
flex: 1;
-webkit-app-region: drag; /* 关键:允许拖拽窗口 */
padding-left: 12px;
}
/* 按钮区不可拖拽,否则无法点击 */
.window-controls {
-webkit-app-region: no-drag;
display: flex;
}
.window-controls button {
width: 46px;
height: 32px;
border: none;
background: transparent;
color: #ccc;
font-size: 14px;
cursor: pointer;
transition: background 0.2s;
}
.window-controls button:hover {
background: rgba(255, 255, 255, 0.1);
}
#close-btn:hover {
background: #e81123;
color: #fff;
}
核心是 -webkit-app-region: drag,它告诉 Chromium 这个区域可以被拖拽来移动窗口。按钮所在的 .window-controls 被设置成 no-drag 来恢复正常的鼠标事件,否则按钮将无法点击。
10.3.3 通过预加载脚本暴露窗口控制 IPC
因为渲染进程是在沙箱中运行且没有直接访问 BrowserWindow 的能力,所以需要用 preload.js 暴露安全的 API,然后通过 IPC 让主进程执行最小化、最大化和关闭操作。
preload.js:
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('windowAPI', {
minimize: () => ipcRenderer.send('window-minimize'),
maximize: () => ipcRenderer.send('window-maximize'),
close: () => ipcRenderer.send('window-close'),
});
主进程 main.js 中接收事件:
const { ipcMain } = require('electron');
ipcMain.on('window-minimize', (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
win.minimize();
});
ipcMain.on('window-maximize', (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (win.isMaximized()) {
win.unmaximize();
} else {
win.maximize();
}
});
ipcMain.on('window-close', (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
win.close();
});
渲染进程调用:
// renderer.js
document.getElementById('minimize-btn').addEventListener('click', () => {
window.windowAPI.minimize();
});
document.getElementById('maximize-btn').addEventListener('click', () => {
window.windowAPI.maximize();
});
document.getElementById('close-btn').addEventListener('click', () => {
window.windowAPI.close();
});
10.3.4 监听窗口状态,同步最大化/还原按钮图标
当用户双击标题栏或者从系统任务栏最大化窗口时,我们需要更新按钮的样式。主进程可监听窗口的 maximize 和 unmaximize 事件并通知渲染进程。
主进程:
win.on('maximize', () => {
win.webContents.send('window-state-changed', 'maximized');
});
win.on('unmaximize', () => {
win.webContents.send('window-state-changed', 'normal');
});
preload.js 增加监听方法:
contextBridge.exposeInMainWorld('windowAPI', {
// ... 上面的三个方法
onWindowStateChanged: (callback) => {
ipcRenderer.on('window-state-changed', (event, state) => callback(state));
},
});
渲染进程:
window.windowAPI.onWindowStateChanged((state) => {
const btn = document.getElementById('maximize-btn');
if (state === 'maximized') {
btn.textContent = '❐'; // 还原图标
btn.title = '还原';
} else {
btn.textContent = '☐';
btn.title = '最大化';
}
});
10.3.5 窗口拖拽移动与双击最大化
因为我们已经在标题栏设置了 -webkit-app-region: drag,窗口拖拽已经自动生效。不过 Chrome 中的拖拽区域默认会取消 dblclick 事件,所以如果想实现双击标题栏最大化,需要额外处理。
方案一:在预加载脚本中暴露一个“最大化切换”功能,并让渲染进程双击时调用(同上)。但是 drag 区域的双击会被浏览器吞掉,所以可以采用以下 workaround:在拖拽区域的某个不可拖拽的子元素上监听双击。比如:
<div class="drag-region">
<span class="title" style="-webkit-app-region: no-drag; pointer-events: auto;">我的应用</span>
</div>
然后为 .title 添加双击事件:
document.querySelector('.title').addEventListener('dblclick', () => {
window.windowAPI.maximize();
});
如果不希望这样,还可以用更底层的方法:在 BrowserWindow 构造时使用 doubleClickable: true(实际上这个选项不存在),更好的思路是利用 webContents 的 before-input-event 或直接在主进程用系统钩子。通常上述方案已经满足需求。
10.3.6 窗口大小调整:边缘拖拽与自定义缩放
无边框窗口默认仍然保留了系统级的窗口边缘(大约几个像素)用于调整大小。可以通过设置 resizable: true(默认值)来保留。但如果你想要更明显的自定义调整手柄,比如在窗口内部某个位置放一个角标用于缩放,需要组合 CSS 和窗口消息。
要禁用系统默认的边缘并完全自绘缩放区域,可以在创建窗口时设置 resizable: false,然后通过在内容区域边缘放置透明的、不可拖拽但可交互的元素,并配合 setBounds 实现类似效果,但实现复杂度较高。多数无边框应用会保留系统原有的调整尺寸边缘,只是自定义标题栏。
如果想在某些区域禁用系统调整(例如标题栏靠近顶部边缘),可以把 -webkit-app-region: drag 铺满整个窗口顶部,这样鼠标移到上方边缘就无法触发垂直调整——这在 macOS 下尤其有用。
10.3.7 平台兼容性与细节微调
- Windows:移除系统框后,窗口阴影会丢失,可使用第三方库如
electron-window-controls或通过 CSSbox-shadow加上transparent: true模拟阴影。另外需要留意WS_CAPTION等样式,但 Electron 已处理大部分。 - macOS:无边框窗口的“红绿灯”按钮可以通过
titleBarStyle: 'customButtonsOnHover'等选项保留或隐藏。完全自定义时,通常隐藏它们。 - Linux:不同桌面环境对于无边框窗口的支持不一致,测试时建议覆盖 GNOME、KDE 等主流环境。
综合以上,一个完整的自定义窗口交互方案,核心在于正确使用 -webkit-app-region、通过 IPC 安全调用窗口操作,并监听状态变化保持 UI 同步。这些技术构成了自定义标题栏、窗口拖拽和基础窗口管理的基础,能够让你的 Electron 应用脱离千篇一律的原生标题栏,形成自己独特的视觉风格。