字节笔记本
2026年2月22日
Electron 无边框窗口与自定义标题栏开发指南
默认情况,在构建 Electron BrowserWindow 的时候,会使用系统自带的原生窗口样式,比如在 MacOS 下的样式:
在有些情况下,操作系统的原生窗口并不能符合我们的一些视觉和交互需求。所以,在使用 electron 创建桌面应用的时候,有时候我们希望能完全掌控窗口的样式,而隐藏掉系统提供的窗口边框和标题栏等。这个时候就需要用到自定义窗口。
无边框窗口的拖拽
无边框窗口是不带外壳(包括窗口边框、工具栏等),只含有网页内容的窗口。要创建无边框窗口,需在 BrowserWindow 的构造中将 frame 参数设置为 false:
// main.js
const { BrowserWindow } = require('electron')
const win = new BrowserWindow({ frame: false })默认情况下,无边框窗口是不可以拖拽的。所以接下来,我们介绍几种让无边框窗口支持拖拽的方式。
1. 使用 -webkit-app-region: drag
应用程序需要在 CSS 中指定 -webkit-app-region: drag 来告诉 Electron 哪些区域是可拖拽的(如操作系统的标准标题栏),当前只支持矩形形状区域。
<body style="-webkit-app-region: drag"></body>注意:在某部分 windows 上使用 -webkit-app-region: drag 来设置拖拽,那么请记住需要在可拖拽区域内部使用 -webkit-app-region: no-drag 来将其中部分需要交互的区域排除。不然那些需要交互的元素几乎无法响应所有的鼠标事件,包括点击、拖拽等。
<body style="-webkit-app-region: drag">
<button style="-webkit-app-region: no-drag;">click</button>
</body>所以,如果你需要整个窗口所有区域都支持拖拽,那臣妾就做不到了~
2. 自定义拖拽事件
既然 -webkit-app-region: drag 无法做到全屏拖拽移动窗口,那么有没有更好的办法呢?其实另一种方案就是自定拖拽移动,具体怎么做呢?
- Electron 需要拖拽的窗口的内容区域监听
mousedown事件,如果是鼠标左键按下,则开启可拖拽开关draggable = true。然后记录鼠标按下去的位置mouseX、mouseY。 - 接下来就启动一个
requestAnimationFrame函数来把mouseX和mouseY传递给主进程并不断和主进程通信。 - 主进程那边每收到一次通信请求就使用
screen.getCursorScreenPoint()来获取最新的鼠标位置x、y。并计算鼠标的位移数值。最后通过window.setBounds来重新设置窗口的位置 - 监听鼠标的
mouseup事件,如果触发,则设置draggable=false。防止意外拖拽的产生。
对应到代码的实现:
// renderer/dragWindow.js
import { ipcRenderer } from 'electron';
const useDrag = () => {
let animationId;
let mouseX;
let mouseY;
let clientWidth = 0;
let clientHeight = 0;
let draggable = true;
const onMouseDown = (e) => {
// 右击不移动,只有左击的时候触发
if (e.button === 2) return;
draggable = true;
// 记录位置
mouseX = e.clientX;
mouseY = e.clientY;
// 记录窗口大小
if (Math.abs(document.body.clientWidth - clientWidth) > 5) {
clientWidth = document.body.clientWidth;
}
if (Math.abs(document.body.clientHeight - clientHeight) > 5) {
clientHeight = document.body.clientHeight;
}
// 注册 mouseup 事件
document.addEventListener('mouseup', onMouseUp);
// 启动通信
animationId = requestAnimationFrame(moveWindow);
};
const onMouseUp = () => {
// 释放锁
draggable = false;
// 移除 mouseup 事件
document.removeEventListener('mouseup', onMouseUp);
// 清除定时器
cancelAnimationFrame(animationId);
};
const moveWindow = () => {
// 传给主进程位置信息
ipcRenderer.send('msg-trigger', {
type: 'windowMoving',
data: { mouseX, mouseY, width: clientWidth, height: clientHeight },
});
if (draggable) animationId = requestAnimationFrame(moveWindow);
};
return {
onMouseDown,
};
};
export default useDrag;// main.js
public windowMoving({ data: { mouseX, mouseY, width, height } }, window, e) {
// 获取当前鼠标的绝对位置。
const { x, y } = screen.getCursorScreenPoint();
// 获取当前需要移动的窗口
const originWindow = this.getCurrentWindow(window, e);
if (!originWindow) return;
// 重新设置窗口位置
originWindow.setBounds({ x: x - mouseX, y: y - mouseY, width, height });
}但这么做也有一些问题,首先就是渲染进程需要主进程直接进行通信,通信需要一定时间,所以窗口的移动必然慢于鼠标的移动,会造成一定程度的卡顿。其次,只能通过 document.removeEventListener('mouseup') 的方法来注销对鼠标移动事件的监听,这个跟第一点接到一起就可能出现这样一种情况:鼠标移动得太快,界面没来得及跟得上,导致鼠标在界面外部释放,未能触发 mouseup 事件,后面就会出现鼠标不管移动到哪里,界面都会跟着,特别烦人!😠
3. 使用 electron-drag 库
相对于我们方案 2 提到问题,主要是因为我们需要监听鼠标的 mousedown 和 mouseup 事件必须要和 DOM 绑定。所以如何实现系统级别的 mousedown 和 mouseup 就成了关键所在。
electron-drag 模块使用 osx-mouse 或 win-mouse 模块来跟踪整个屏幕上的鼠标位置,从而实现了一致的窗口拖动,同时受影响的元素仍能够接收 DOM 事件。使用方式也非常方便:
// app.vue
import drag from 'electron-drag-latest';
const undrag = drag('#app');
// 如果不需要拖拽,调用 undrag 函数
// undrag()完整代码见:github.com/muwoo/electron-drag-latest
但需要注意的是,electron-drag 仅支持 macOS 和 windows 操作系统,其他平台都不支持。因此,我们可以在不支持的平台上使用第二种方案来实现。
注意:electron-drag 因为依赖了
osx-mouse或win-mouse模块,而这两个模块都是需要进行 C++ 额外本地编译的,所以你可能还需要用electron-rebuild进行重新编译。
自定义窗口标题栏
前面说到无边框窗口是一种不带外壳(包括窗口边框、工具栏等)、只含有网页内容的窗口。但是有的时候,我们还是希望要包含工具栏和标题。这样不仅可以方便用户进行窗口最大化、最小化和关闭的操作,我们还可以融合一些自定义的操作能力进入标题栏。
这种情况下,我们就需要实现一种自定义标题栏了,但这种自定义标题栏,在 Electron 中,Windows 和 macOS 的实现和样式是不一样的。接下来将详细介绍。
1. Windows 下自定义标题栏
在 Electron 中,我们可以通过 frame = false 设置无边框窗口。再通过 titleBarStyle = 'hidden' 和 titleBarOverlay 的方式来创建一个带有操作栏的无边框窗口:
new BrowserWindow({
width: 800,
height: 600,
titleBarStyle: 'hidden',
// 在windows上,设置默认显示窗口控制工具
titleBarOverlay: { color: "#fff", symbolColor: "black", }
});但是这样的无边框窗口仅能实现通用的样式,而且样式也比较奇怪:控制区域图标大小、间距无法修改,也没法内置其他的操作图标进去。
所以这个时候,在 windows 中,如果想要实现下面这样的效果(有自定义的标题、icon 图标),那么就不得不重新实现一个自定义的标题栏。
这种实现也很简单,首先就是构造一个不带控制栏的窗口:
new BrowserWindow({
autoHideMenuBar: true,
// 无边框窗口
frame: true,
// 无标题
titleBarStyle: 'hidden',
show: false,
});然后在渲染进程中,自己**"画一个标题栏"**:
<div class="info">
<img :src="plugInfo.logo"/>
<span>rubick 系统菜单</span>
</div>
<div class="handle-container">
<div class="handle">
<div class="devtool" @click="openDevTool" title="开发者工具"></div>
</div>
<div class="window-handle" v-if="process.platform !== 'darwin'">
<div class="minimize" @click="minimize"></div>
<div class="maximize" @click="maximize"></div>
<div class="close" @click="close"></div>
</div>
</div>然后定义一下 icon 的样式:
.minimize {
background: center / 20px no-repeat url("./assets/minimize.svg");
}
.maximize {
background: center / 20px no-repeat url("./assets/maximize.svg");
}
.unmaximize {
background: center / 20px no-repeat url("./assets/unmaximize.svg");
}
.close {
background: center / 20px no-repeat url("./assets/close.svg");
}
.close:hover {
background-color: #e53935;
background-image: url("./assets/close-hover.svg");
}最后,渲染进程中通过 ipcRenderer 向主进程中发送操作事件:
// 最小化
const minimize = () => {
ipcRenderer.send('detach:service', { type: 'minimize' });
};
// 最大化
const maximize = () => {
ipcRenderer.send('detach:service', { type: 'maximize' });
};
// 关闭窗口
const close = () => {
ipcRenderer.send('detach:service', { type: 'close' });
};主进程对操作事件进行响应:
ipcMain.on('detach:service', async (event, arg: { type: string }) => {
operation[arg.type]();
});
const operation = {
minimize: () => {
win.focus();
win.minimize();
},
maximize: () => {
win.isMaximized() ? win.unmaximize() : win.maximize();
},
close: () => {
win.close();
},
};关于自定义窗口标题栏的完整代码可以看这里:github.com/rubickCenter/rubick
2. macOS 下自定义菜单栏
在 macOS 中,要实现上面的自定义菜单栏还是比较方便的,因为 macOS 的操作栏主要是红绿灯效果,而且 macOS 的交互方式都是将红绿灯统一放在窗口的左上角:
所以对于 macOS 下,自定义菜单栏的交互就是下面这样:
这里,我们不需要再手动实现关闭、缩小、放大等系统功能了,只需要调整一下红绿灯的位置就可以了:
new BrowserWindow({
// ...
// 设置 macOS 下红绿灯的位置
trafficLightPosition: { x: 12, y: 21 },
// ...
})总结
本小节,我们完成了对 Electron 常用的 无边框窗口 和 自定义窗口标题栏 的介绍,它们相对于默认的系统窗口而言,需要处理一些小的交互问题。希望通过本小节的介绍,可以让你清楚地了解这些问题背后的原因和解决问题的方式。
来源:掘金小册 - Electron 应用开发实践指南 作者:muwoo