Electron 应用开发实践指南 实战篇:自定义窗口的拖拽和缩放

Electron 应用开发实践指南 - muwoo - 掘金小册

前言

默认情况,在构建 Electron BrwoserWindow 的时候,会使用系统自带的原生窗口样式,比如在 MacOS 下的样式:

image.png

在有些情况下,操作系统的原生窗口并不能符合我们的一些视觉和交互需求。所以,在使用 electron 创建桌面应用的时候,有时候我们希望能完全掌控窗口的样式,而隐藏掉系统提供的窗口边框和标题栏等。这个时候就需要用到自定义窗口。

无边框窗口的拖拽

无边框窗口是不带外壳(包括窗口边框、工具栏等),只含有网页内容的窗口。要创建无边框窗口,需在 BrowserWindow 的构造中将 frame 参数设置为 false

 

js

复制代码

// main.js const { BrowserWindow } = require('electron') const win = new BrowserWindow({ frame: false })

image.png

默认情况下,无边框窗口是不可以拖拽的。所以接下来,我们介绍几种让无边框窗口支持拖拽的方式。

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 无法做到全屏拖拽移动窗口,那么有没有更好的办法呢?其实另一种方案就是自定拖拽移动,具体怎么做呢?

  1. Electron 需要拖拽的窗口的内容区域监听 mousedown 事件,如果是鼠标左键按下,则开启可拖拽开关 draggable = true。然后记录鼠标按下去的位置 mouseXmouseY
  2. 接下来就启动一个 requestAnimationFrame 函数来把 mouseX 和 mouseY 传递给主进程并不断和主进程通信。
  3. 主进程那边没收到一次通信请求就使用 screen.getCursorScreenPoint() 来获取最新的鼠标位置 x、y。并计算鼠标的位移数值。最后通过 window.setBounds 来重新设置窗口的位置
  4. 监听鼠标的 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", } });

但是这样的无边框窗口仅能实现通用的样式,而且样式也比较奇怪:控制区域图标大小、间距无法修改,也没法内置其他的操作图标进去。

image.png

所以这个时候,在 windows 中,如果想要实现下面这样的效果(有自定义的标题、icon 图标),那么就不得不重新实现一个自定义的标题栏。

image.png

这种实现也很简单,首先就是构造一个不带控制栏的窗口:

 

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 的交互方式都是将红绿灯统一放在窗口的左上角:

image.png

所以对于 macOS 下,自定义菜单栏的交互就是下面这样:

image.png

这里,我们不需要再手动实现关闭、缩小、放大等系统功能了,只需要调整一下红绿灯的位置就可以了:

 

js

复制代码

new BrowserWindow({ // ... // 设置 macOS 下红绿灯的位置 trafficLightPosition: { x: 12, y: 21 }, // ... })

总结

本小节,我们完成了对 Electron 常用的 无边框窗口 和 自定义窗口标题栏 的介绍,它们相对于默认的系统窗口而言,需要处理一些小的交互问题。希望通过本小节的介绍,可以让你清楚地了解这些问题背后的原因和解决问题的方式。

 

  • 16
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

人工智能_SYBH

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值