Electron 应用开发实践指南 - muwoo - 掘金小册
前言
默认情况,在构建 Electron BrwoserWindow
的时候,会使用系统自带的原生窗口样式,比如在 MacOS
下的样式:
在有些情况下,操作系统的原生窗口并不能符合我们的一些视觉和交互需求。所以,在使用 electron
创建桌面应用的时候,有时候我们希望能完全掌控窗口的样式,而隐藏掉系统提供的窗口边框和标题栏等。这个时候就需要用到自定义窗口。
无边框窗口的拖拽
无边框窗口是不带外壳(包括窗口边框、工具栏等),只含有网页内容的窗口。要创建无边框窗口,需在 BrowserWindow
的构造中将 frame
参数设置为 false
:
js
复制代码
// main.js const { BrowserWindow } = require('electron') const win = new BrowserWindow({ frame: false })
默认情况下,无边框窗口是不可以拖拽的。所以接下来,我们介绍几种让无边框窗口支持拖拽的方式。
1. 使用 -webkit-app-region: drag
应用程序需要在 CSS 中指定 -webkit-app-region: drag
来告诉 Electron 哪些区域是可拖拽的(如操作系统的标准标题栏),当前只支持矩形形状区域。
html
复制代码
<body style="-webkit-app-region: drag"></body>
注意:在某部分 windows 上使用 -webkit-app-region: drag
来设置拖拽,那么请记住需要在可拖拽区域内部使用 -webkit-app-region: no-drag
来将其中部分需要交互的区域排除。不然那些需要交互的元素几乎无法响应所有的鼠标事件,包括点击、拖拽等。
html
复制代码
<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
。防止意外拖拽的产生。
对应到代码的实现:
js
复制代码
// 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;
js
复制代码
// 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 事件。使用方式也非常方便:
js
复制代码
// app.vue import drag from 'electron-drag-latest'; const undrag = drag('#app'); // 如果不需要拖拽,调用 undrag 函数 // undrag()
完整代码见:github.com/muwoo/elect…
但需要注意的是,electron-drag
仅支持 macOS
和 windows
操作系统,其他平台都不支持。因此,我们可以在不支持的平台上使用第二种方案来实现。
注意:electron-drag 因为依赖了
osx-mouse
或win-mouse
模块,而这两个模块都是需要进行 C++ 额外本地编译的,所以你可能还需要用electron-rebuild
进行重新编译。
自定义窗口标题栏
前面说到无边框窗口是一种不带外壳(包括窗口边框、工具栏等)、只含有网页内容的窗口。但是有的时候,我们还是希望要包含工具栏和标题。这样不仅可以方便用户进行窗口最大化、最小化和关闭的操作,我们还可以融合一些自定义的操作能力进入标题栏。
这种情况下,我们就需要实现一种自定义标题栏了,但这种自定义标题栏,在 Electron
中,Windows
和 macOS
的实现和样式是不一样的。接下来将详细介绍。
1. Windows 下自定义标题栏
在 Electron 中,我们可以通过 frame = false
设置无边框窗口。再通过 titleBarStyle = 'hidden'
和 titleBarOverlay
的方式来创建一个带有操作栏的无边框窗口:
js
复制代码
new BrowserWindow({ width: 800, height: 600, titleBarStyle: 'hidden', // 在windows上,设置默认显示窗口控制工具 titleBarOverlay: { color: "#fff", symbolColor: "black", } });
但是这样的无边框窗口仅能实现通用的样式,而且样式也比较奇怪:控制区域图标大小、间距无法修改,也没法内置其他的操作图标进去。
所以这个时候,在 windows 中,如果想要实现下面这样的效果(有自定义的标题、icon 图标),那么就不得不重新实现一个自定义的标题栏。
这种实现也很简单,首先就是构造一个不带控制栏的窗口:
js
复制代码
new BrowserWindow({ autoHideMenuBar: true, // 无边框窗口 frame: true, // 无标题 titleBarStyle: 'hidden', show: false, });
然后在渲染进程中,自己 “画一个标题栏”:
html
复制代码
<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
的样式:
css
复制代码
.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
向主进程中发送操作事件:
js
复制代码
// 最小化 const minimize = () => { ipcRenderer.send('detach:service', { type: 'minimize' }); }; // 最大化 const maximize = () => { ipcRenderer.send('detach:service', { type: 'maximize' }); }; // 关闭窗口 const close = () => { ipcRenderer.send('detach:service', { type: 'close' }); };
主进程对操作事件进行响应:
js
复制代码
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/rubickCente…
2. macOS 下自定义菜单栏
在 macOS
中,要实现上面的自定义菜单栏还是比较方便的,因为 macOS
的操作栏主要是红绿灯效果,而且 macOS
的交互方式都是将红绿灯统一放在窗口的左上角:
所以对于 macOS
下,自定义菜单栏的交互就是下面这样:
这里,我们不需要再手动实现关闭、缩小、放大等系统功能了,只需要调整一下红绿灯的位置就可以了:
js
复制代码
new BrowserWindow({ // ... // 设置 macOS 下红绿灯的位置 trafficLightPosition: { x: 12, y: 21 }, // ... })
总结
本小节,我们完成了对 Electron
常用的 无边框窗口
和 自定义窗口标题栏
的介绍,它们相对于默认的系统窗口而言,需要处理一些小的交互问题。希望通过本小节的介绍,可以让你清楚地了解这些问题背后的原因和解决问题的方式。