5.1 窗口的常用属性及应用场景

大部分桌面 GUI 应用都是由一个或多个窗口组成的,在前面的章节中我们已经创建了很多 Electron 窗口(也就是 BrowserWindow),但窗口的属性只用到了 webPreferences: { nodeIntegration: true }。其实,Electron 的 BrowserWindow 还有很多种可用属性。👇🏻 下面根据不同的应用场景,分组介绍一些常用的窗口属性。

控制窗口位置的属性

属性名称:

  • x
  • y
  • center
  • movable

属性说明:
通过 x,y 可以控制窗口在屏幕中的位置,如果没有设置这两个值,窗口默认会在屏幕正中展示

应用场景举例:
当多个相同大小的窗口被创建时,后创建的窗口会完全覆盖先创建的窗口。通过设置 x,y 属性,使后创建的窗口与先创建的窗口交错显示,比如:每创建一个窗口,使 x,y 分别比前一个窗口的 x,y 多 60 个像素。

控制窗口大小的属性

属性名称:

  • width
  • height
  • minWidth
  • minHeight
  • maxWidth
  • maxHeight
  • resizable
  • minimizable
  • maximizable

属性说明:
通过以上属性可以设置窗口的大小以及是否允许用户控制窗口的大小

应用场景举例:

  • 窗口尺寸不可变:设置好指定的 height、width 之后,将 resizable 设置为 false
  • 设置窗口的可变范围:minWidthminHeightmaxWidthmaxHeight

「补充」无法对初始化尺寸进行范围约束:
widthheight 设置的是窗口尺寸的初始值,如果不设置,那么默认宽度是 800 高度是 600。
需要注意的是:如果仅设置了 minWidthminHeightmaxWidthmaxHeight 这些来约束窗口的范围,还是不够的,它们对窗口初始化时的尺寸约束是无效的。
比如使用这 4 个属性,约束窗口的尺寸范围是 100*100 ~ 500*500,但是没有设置 widthheight,那么窗口启动的时候,尺寸依旧是 800*600,这并不在范围内,一旦尝试改变窗口的尺寸,那么窗口尺寸会突变为范围内的合法值。

  1. win = new BrowserWindow({
  2. minWidth: 100,
  3. minHeight: 100,
  4. maxWidth: 500,
  5. maxHeight: 500
  6. })
  7. win.loadFile('./index.html')

控制窗口边框、标题栏与菜单栏的属性

属性名称:

  • title
  • icon
  • frame
  • autoHideMenuBar
  • titleBarStyle

属性说明:
通过以上属性,可以设置窗口的边框、标题栏与菜单栏

应用场景举例:

  • 默认值
    • title 默认为网页的 title
    • icon 默认为可执行文件的图标
  • 自定义标题栏和边框:将 frame 设置成 false,能屏蔽掉系统标题栏和边框
  • 系统菜单:如果直接将系统菜单设置为 null 会出现问题,因为在 mac 系统下,系统菜单关系到“复制”“粘贴”“撤销”“重做”这样的常用快捷键命令

控制渲染进程访问 Node.js 环境能力的属性

属性名称:

  • webPreferences.nodeIntegration
  • webPreferences.nodeIntegrationInWorker
  • webPreferences.nodeIntegrationInSubFrames

属性说明:
控制窗口加载的网页是否继承 Node.js 环境

应用场景举例:

  • 安全问题:它们的默认值都是 false,如果你要把其中的任何一个设置为 true,需要确保网页不包含第三方提供的内容,否则第三方网页就有权利访问用户电脑的 Node.js 环境,这对于用户来说是非常危险的。Electron 官方也告诫开发者需要承担软件安全性的责任。

可以增强渲染进程能力的属性

属性名称:

  • webPreferences.preload
  • webPreferences.webSecurity
  • webPreferences.contextIsolation

属性说明:
允许开发者最大限度地控制渲染进程加载的页面

应用场景举例:

  • preload:使开发者可以给渲染进程加载的页面注入脚本。(即便渲染进程加载的是第三方页面,且开发者关闭了 nodeIntegration,注入的脚本还是有能力访问 Node.js 环境的)
  • webSecurity:控制网页的同源策略。(关闭同源策略后,开发者可以轻松地调用第三方网站的服务端接口,而不会出现跨域的问题)

:::info 以上涉及的很多配置对控制窗口表现都十分重要。部分本章未讨论的配置,会在其他章节讲述。 :::

锁定窗口的宽高比

  • win.setAspectRatio

提示:
在调用这个方法,输入宽高比的时候,假设要求是 1920 / 1080 的比例,常见的有下面两种写法:

  1. win.setAspectRatio(1.78) 先计算出 1920 / 1080 的结果,然后把结果作为参数传入
  2. win.setAspectRatio(1920 / 1080) 直接将表达式 1920 / 1080 作为参数传入(推荐)

5.2 窗口标题栏和边框

  • 实现三个窗口的控制按钮:最大化、最小化、关闭 22-08-20-1
  • 窗口最大化时,页面背景为红色,否则为蓝色 22-08-20-2
  • 记录窗口状态,窗口关闭,重新打开之后,依旧是关闭之前的位置和大小 22-08-21

5.2.1 自定义窗口的标题栏

窗口的边框和标题栏是桌面 GUI 应用非常重要的一个界面元素。开发者创建的窗口在默认情况下会使用操作系统提供的边框和标题栏,但操作系统默认的样式和外观都比较刻板,大多数优秀的商业应用都会选择自定义。自定义窗口标题栏,对提升产品品质有很大帮助。

  1. const win = new BrowserWindow({
  2. // ...
  3. frame: false
  4. // ...
  5. })

设置 frame: false 之后,启动应用,窗口的边框和标题栏都不见了,只显示窗口的内容区,此时我们无法拖拽移动窗口,也无法最大化、最小化、关闭窗口

frame: true(默认)
frame: false

自定义窗口的标题栏:
实现逻辑很简单,无非就是自己封装一个 titleBar.vue 组件,让它固定在顶部即可。

标题栏的位置:
标题栏的位置,想要固定在哪,自定义即可,也可以类似于「微信」那样,将相关操作列丢到左侧。因为标题栏都可以自行定义了,那么想要咋渲染,还不是咋们说的算。

image.png

image.png

窗口拖拽功能:

  • -webkit-app-region: drag; 这个样式标志着该元素所在的区域是一个窗口拖拽区域,用户可以拖拽元素来移动窗口的位置。
  • -webkit-app-region: no-drag; 如果我们在一个父元素上设置了 -webkit-app-region: drag; 样式,而又不希望父元素中的某个子元素(比如:标题栏组件中,用于控制窗口的按钮 - 最大化、最小化、关闭)拥有此拖拽窗口的功能,此时我们可以给该子元素设置 -webkit-app-region: no-drag; 样式,以此来屏蔽掉从父元素继承来的功能。

TitleBar 组件的具体代码实现,见 github - dahuyou_notes/0064 - 《Electron实战:入门、进阶与性能优化》/Chapter-05/5.2 窗口标题栏和边框/22-08-05,踩过的坑都记录在 readme.md 文件中了。

5.2.2 窗口的控制按钮

🤔 如何实现窗口控制呢?
首先,要获取到窗口实例 windowInstance,通过窗口实例身上的这些方法,来控制窗口:

  • 关闭 close
  • 最小化 minimize
  • 恢复 restore
  • 最大化 maximize

🤔 渲染进程该如何访问到当前窗口实例呢?
👇🏻 提供两种方案:

  1. 直接访问:通过 remote.getCurrentWindow() 即可获取到当前窗口的实例(书中描述的方案)
  2. 间接访问:通过进程间通信的方式来访问窗口实例,窗口实例位于主进程中,我们可以在渲染进程中向主进程发消息,让主进程去访问窗口实例(实际采用的方案)

通过进程间通信来实现窗口控制的 demo: image.png 补充:在使用进程间通信的方式来实现的过程中,踩了很多坑,代码量比使用 remote 的方案多,更多描述见 readme.md

🤔 既然都在写进程间通信的逻辑了,那么为何不直接让主进程将对应的窗口实例返回给渲染进程呢?
这也许涉及到序列化和反序列化的相关问题,虽然不知道具体的原因,不过尝试过该方案,是不可行的。

  1. // 渲染进程
  2. ipcRenderer.invoke('xxx', (winIns) => {
  3. // ...
  4. })
  5. // 主进程
  6. ipcMain.handle('xxx', async () => winIns)

上面记录的这种方案,想要做的是,通过进程间通信的方式,将渲染进程所需要的窗口实例直接返回回去,这样就能避免使用 remote 模块了,但是,该方案是不可行的,winIns 没法传过去。

方案1

书中介绍的方式:
通过 remote 模块,remote.getCurrentWindow() 获取到当前窗口的实例,再去调用相应的方法来控制窗口。

  1. let { remote } = require('electron')
  2. export default {
  3. methods: {
  4. close() {
  5. remote.getCurrentWindow().close()
  6. },
  7. minisize() {
  8. remote.getCurrentWindow().minimize()
  9. },
  10. restore() {
  11. remote.getCurrentWindow().restore()
  12. },
  13. maxsize() {
  14. remote.getCurrentWindow().maxmize()
  15. }
  16. }
  17. };

方案2

实际的做法:
通过 IPC 通信的方式来实现,核心代码如下:
image.png
image.png
image.png

win.restore()

问题描述:
未进入最大化:image.png
进入了最大化:image.png
在进入最大化状态后,点击还原按钮image.png之后,无法还原到最大化之前的状态。

win.restore() 是用于还原窗口的,但是它只能控制窗口从最小化还原到最小化之前的状态,并不能控制窗口从最大化状态下还原到最大化之前的状态。

官方描述 image.png win.restore() 这个 api,盲猜是书中写错了。 点击它之后,并不能控制窗口退出最大化的状态,回退到最大化之前的状态。

5.2.3 窗口的最大化状态控制

maximize、unmaximize

  • win.on('maximize', () => {})
  • win.on('unmaximize', () => {})
  1. mounted() {
  2. let win = remote.getCurrentWindow()
  3. win.on('maximize', _ => {
  4. this.isMaxSize = true
  5. this.setState()
  6. })
  7. win.on('unmaximize', _ => {
  8. this.isMaxSize = false
  9. this.setState()
  10. })
  11. }

🤔 为什么不能直接将窗口最大化状态控制的逻辑写在 **restore****maxsize** 方法中呢?
因为不确定程序会不会因标题栏以外的其他地方的操作,导致窗口被最大化或还原,所以不能简单地在控制按钮的 restoremaxsize 方法中改变 isMaxSize 属性的值。

使用上述方式来监听窗口最大化状态的变化,可以有效避免这些问题,因为它只管当前窗口是否是最大化的,而不管是什么行为导致的最大化。

补充1 因为前端这玩意儿,用户的行为我们是很难预测的,就拿该案例来说,想要让某个窗口最大化,方式多得很。如果是 windows 用户

  • 按下 win + ↑,即可实现窗口的最大化
  • 双击窗口的标题栏,也可以实现窗口的最大化
  • 点击窗口右上角的最大化图标,也可以实现窗口的最大化(这种方式,其实就是调用 maxsize)
  • 某些第三方窗口管理软件,可以通过快捷键的方式来实现指定窗口的最大化
  • ……

由此可见,用户的行为我们是很难去全部都监听到的,对于这种情况,我们应该采取的做法是监听最终的状态

补充2 maximize 这玩意儿,在 mac 上测试时,发现一个问题:如果通过点击交通灯image.png进入窗口最大化的话,它是没法监听到的。这应该就是操作系统不同导致的差异了,mac 上页面的最大化,应该是有两种的。通过点击绿色的交通灯,进入的应该是全屏,而不叫最大化。

回调泄露问题

书中描述的这个回调泄露的问题,自行通过 demo 复现了一下,可以运行该 demo npx electron . 体验一下回调泄露问题 位置: image.png 22-08-20-3 的 readme.md 文件

此处在渲染进程中监听 winmaximizeunmaximize 事件,以及下一小节所讲的 moveresize 事件时,存在一个潜在问题。当用户按下 Ctrl + R(Mac 系统中为 Command + R)快捷键刷新页面后,再操作窗口触发相应的事件,主进程会报异常。

造成这个现象的原因是我们用 remote.getCurrentWindow 获取到的窗口对象,其实是一个远程对象,它实际上是存在于主进程中的。我们在渲染进程中为这个远程对象注册了两个事件处理程序(maximizeunmaximize),事件发生时,处理程序被调用,这个过程没有任何问题。但是一旦页面被刷新,注册事件的过程会被再次执行,每次刷新页面都会泄露一个回调。更糟的是,由于以前安全的回调的上下文已经被释放,因为在此事件发生时,泄露的回调函数找不到执行体,将在主进程中引发异常。

异常信息:Attempting to call a function in a renderer window that has been closed or released.

尝试在渲染窗口中调用一个已经被关闭或销毁的函数。

有两种办法可以避免上述提到的这种异常:

  1. 不使用 remote 模块,改用进程间通信的方式,将注册事件的逻辑封装在主进程中
  2. 禁止页面刷新

方法1
这是更加常见的做法,也是更推荐的做法。将注册事件的逻辑封装在主进程中,每次刷新页面,并不会导致主进程中的代码重新执行,无论页面是否刷新,主进程中注册事件的逻辑,只会走一遍,所以并不会导致重复注册回调的行为出现。当窗口最大化状态变化后,需要设置渲染进程的 isMaxSize 属性来管理窗口的最大化状态,常用的方法是让主进程给渲染进程发送消息,再由渲染进程完成 isMaxSize 属性的设置工作。

方法2

  1. window.onkeydown = function (e) {
  2. if (e.keyCode === 82 && (e.ctrlKey || e.metaKey)) return false
  3. }

如果上述代码能成功屏蔽刷新快捷键,说明渲染进程内的页面先接收到了按键事件,并在事件中返回了 false,Electron 将不再处理该事件。

F5 虽然在浏览器中按 F5 也会触发页面的刷新事件,但在 Electron 中,并没有监听 F5 按键,所以开发者无需担心通过按 F5 而触发页面刷新的事儿发生。

HMR 该应用程序是基于 Vue 和 webpack 开发的,webpack 自带 hot-module-replacement(HMR 热更新)技术,并不需要用粗暴的 live-reload 技术来刷新页面,所以在开发者更新页面内容后,并不会像按下 Ctrl + R 那样,导致页面刷新,所以并不会对前端的代码调试工作造成影响。

🤔 HMR 和 live reload 有啥区别? 基于webpack的热重载live reload和热更新HMR - 掘金

5.2.4 防抖和限流

相关的窗口事件

  • move
  • moved
  • resize
  • resized
  1. /**
  2. * Emitted when the window is being moved to a new position.
  3. */
  4. on(event: 'move', listener: Function): this;
  5. once(event: 'move', listener: Function): this;
  6. addListener(event: 'move', listener: Function): this;
  7. removeListener(event: 'move', listener: Function): this;
  8. /**
  9. * Emitted once when the window is moved to a new position.
  10. *
  11. * __Note__: On macOS this event is an alias of `move`.
  12. *
  13. * @platform darwin,win32
  14. */
  15. on(event: 'moved', listener: Function): this;
  16. once(event: 'moved', listener: Function): this;
  17. addListener(event: 'moved', listener: Function): this;
  18. removeListener(event: 'moved', listener: Function): this;
  19. /**
  20. * Emitted after the window has been resized.
  21. */
  22. on(event: 'resize', listener: Function): this;
  23. once(event: 'resize', listener: Function): this;
  24. addListener(event: 'resize', listener: Function): this;
  25. removeListener(event: 'resize', listener: Function): this;
  26. /**
  27. * Emitted once when the window has finished being resized.
  28. *
  29. * This is usually emitted when the window has been resized manually. On macOS,
  30. * resizing the window with `setBounds`/`setSize` and setting the `animate`
  31. * parameter to `true` will also emit this event once resizing has finished.
  32. *
  33. * @platform darwin,win32
  34. */
  35. on(event: 'resized', listener: Function): this;
  36. once(event: 'resized', listener: Function): this;
  37. addListener(event: 'resized', listener: Function): this;
  38. removeListener(event: 'resized', listener: Function): this;

监听窗口移动和窗口大小改变的事件

为了让用户有更好的体验,我们希望系统能记住窗口的大小、位置和是否最大化的状态。所以,我们还需要监听窗口移动和窗口大小改变的事件。

  1. win.on('move', debounce(() => {
  2. // ...
  3. }))
  4. win.on('resize', debounce(() => {
  5. // ...
  6. }))

防抖

当窗口大小改变或窗口位置改变时,将会非常频繁地触发 moveresize 事件,所以还需要做一下防抖处理来优化一下。

  1. function debounce(fn, timeout = 300) {
  2. let timer = null
  3. return (...args) => {
  4. clearTimeout(timer)
  5. timer = setTimeout(() => { fn.apply(this, args) }, timeout)
  6. }
  7. }

此函数的原理为:在每次 resizemove 事件触发时,先清空 debounce 函数内的 timer 变量,再设置一个新的 timer 变量。如果事件被频繁地出发,旧的 timer 尚未执行就被清理掉了,而且 300 毫秒内只允许有一个 timer 等待执行。

闭包: debounce 函数返回了一个匿名函数,这个匿名函数被用来作为窗口 moveresize 事件的监听器。此处,这个匿名函数内部代码有访问 timerfn 的能力,即使 debounce 函数已经退出了,这个能力依然存在,这就是 JavaScript 语言的闭包特性。 其背后的原理是 js 的执行引擎不但记住了这个匿名函数,还记住了这个匿名函数所在的环境。

限流

  1. function throttle(fn, timeout = 300) {
  2. let timer = null
  3. return (...args) => {
  4. if (timer) return
  5. timer = setTimeout(() => {
  6. fn.apply(this, args)
  7. timer = null
  8. }, timeout)
  9. }
  10. }

此函数的原理为:当每次事件触发时,先判断与该事件关联的任务是否正等待执行。

  • 当前有任务等待执行,那么这次触发的事件无效,直接 return,并不会新建计时器。
  • 当前没有任务等待执行,那么就创建一个 timer,让任务等待执行。再有新事件触发时,如果发现有任务在等待执行,那么本次触发无效,会直接退出。

小结

  • 防抖函数的作用:当短期内,有大量的事件被触发时,只会执行最后一次事件关联的任务。
  • 限流函数的作用:当短期内,有大量的事件被触发时,只会执行第一次触发的事件。

无论是防抖函数还是限流函数,其主要作用都是防止在短期内频繁地调用某函数,导致进行大量无效的操作,损耗系统性能(甚至可能会产生有害的操作,影响软件的正确性)。

补充

  • resized Emitted once when the window has finished being resized.
  • moved Emitted once when the window is moved to a new position.

resizedmoved并不会频繁触发,只有在变化完成之后,才会触发,而 resizemove 在每次窗口尺寸发生变化时都会触发,触发地及其频繁。如果直接使用这两个事件来代替 resizemove 事件,那么就没必要使用 debounce 来处理了。

5.2.5 记录和恢复窗口状态

相关 API

  • winIns.getBounds()
  • winIns.setBounds()
  • winIns.isMaximized()
  • winIns.maximize()

localStorage

LocalStorage 是一种常用于网页数据本地化存储的技术,各大浏览器均已支持。因为 Electron 本身也是一种特殊的浏览器,所以我们也可以用 LocalStorage 来存储应用数据。

核心逻辑

  1. // 设置窗口状态
  2. function setWinState() {
  3. const winIns = remote.getCurrentWindow(),
  4. rect = winIns.getBounds(),
  5. isMaxSize = winIns.isMaximized()
  6. localStorage.setItem('winState', JSON.stringify({ rect, isMaxSize }))
  7. }
  8. // 读取窗口状态
  9. function getWinState() {
  10. const winIns = remote.getCurrentWindow(),
  11. winState = localStorage.getItem('winState')
  12. if (winState) {
  13. winState = JSON.parse(winState)
  14. if (winState.isMaxSize) winIns.maximize()
  15. else win.setBounds(winState.rect)
  16. }
  17. }

5.2.6 适时地显示窗口

待优化的问题

窗口初始位置和大小不正确
窗口一开始会显示在屏幕的正中间,然后才(winIns.getBounds(rect))移动到正确的位置,并调整大小

核心 API

  • const win = new BrowserWindow({ show: false }) 初始化窗口实例后,隐藏窗口
  • win.show() 在合适的时机,调用 show 方法,显示窗口
  1. win = new BrowserWindow({
  2. // ...
  3. show: false // 创建窗口时,不展示窗口
  4. })
  5. winIns.getBounds(rect) // 恢复窗口状态
  6. winIns.show() // 在恢复窗口状态之后,再展示窗口

maximize

调用 winIns.maximize() 语句时,如果窗口是隐藏状态,也会变成显示状态,因为它其实有 show 的作用。

ready-to-show

Electron 官方文档推荐开发者监听 BrowserWindow 的 ready-to-show 事件,但是作者不推荐这么做。

因为 ready-to-show 事件是在“当页面已经渲染完成并且窗口可以被显示时(但是还没有显示)”触发,但此时页面中的 JavaScript 代码可能还没完成工作。因此,你应该根据业务需求来适时地显示窗口,而不是把这个权利交给 ready-to-show 事件。

不建议在窗口显示前处理大量的阻塞业务,这可能会导致窗口迟迟加载不出来,用户体验下降。

5.3 不规则窗口

  • 创建一个不规则的窗口,可以是 圆形 ○ 也可以是 三角形 △,并且透明区域要具备点击穿透效果 22-08-22
  1. const {app, BrowserWindow} = require('electron')
  2. require('@electron/remote/main').initialize()
  3. let win
  4. app.whenReady().then(() => {
  5. createWindow()
  6. })
  7. function createWindow() {
  8. win = new BrowserWindow({
  9. width: 100,
  10. height: 100,
  11. transparent: true, // 使窗口透明
  12. frame: false, // 去除窗口边框
  13. resizable: false, // 透明的窗口不可调整大小
  14. maximizable: false, // 防止双击窗口可拖拽区(通常指标题栏)而出发窗口的最大化事件
  15. alwaysOnTop: true, // 让窗口置顶
  16. webPreferences: {
  17. nodeIntegration: true,
  18. contextIsolation: false
  19. }
  20. })
  21. win.loadFile('./index.html')
  22. require("@electron/remote/main").enable(win.webContents)
  23. win.webContents.openDevTools({ mode: 'detach' })
  24. }
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta http-equiv="X-UA-Compatible" content="IE=edge">
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7. <title>5.3 不规则窗口</title>
  8. <style>
  9. html, body {
  10. margin: 0;
  11. padding: 0;
  12. pointer-events: none;
  13. }
  14. .content {
  15. width: 100px;
  16. height: 100px;
  17. line-height: 100px;
  18. text-align: center;
  19. box-sizing: border-box;
  20. border-radius: 50%;
  21. border: 1px solid #ddd;
  22. background-color: #fff;
  23. overflow: hidden;
  24. pointer-events: auto;
  25. /* 可拖动 */
  26. -webkit-app-region: drag;
  27. }
  28. </style>
  29. </head>
  30. <body>
  31. <div class="content">
  32. 窗口
  33. </div>
  34. <script>
  35. const { getCurrentWindow } = require('@electron/remote'),
  36. win = getCurrentWindow()
  37. window.addEventListener('mousemove', e => {
  38. // console.log('e.target => ', e.target)
  39. // console.log('document.documentElement => ', document.documentElement)
  40. if (e.target === document.documentElement) win.setIgnoreMouseEvents(true, { forward: true })
  41. else win.setIgnoreMouseEvents(false)
  42. })
  43. win.setIgnoreMouseEvents(true, { forward: true }) // 这行代码不影响功能
  44. </script>
  45. </body>
  46. </html>

5.3.1 创建不规则窗口

  1. win = new BrowserWindow({ transparent: true }) 创建窗口实例的时候,加上 transparent 属性,让整个窗口边透明
  2. .content { width: 100px; height: 100px; border-radius: 50%; background-color: #fff } 对于窗口中想要展示的内容,自定义好形状(使用 css 的相关知识),并添加一个可见的背景色

5.3.2 点击穿透透明区域

监听鼠标的移动事件,时刻判断鼠标当前所在的位置,如果是透明区域,则穿透,否则不穿透。

  1. win.setIgnoreMouseEvents(true, { forward: true }) 穿透
  2. win.setIgnoreMouseEvents(false) 不穿透

判断当前位置是否是透明区域

判断当前所在位置存在于透明区域的逻辑:e.target === document.documentElement
上述判断成立的前提是:

  • html, body { pointer-events: none; }
  • .content { pointer-events: auto; }

html, body 加上 pointer-events: none; 声明之后,如果鼠标在透明区域移动,就会触发窗口对象的 mousemove 事件,此时获取到的 e.target 就是 document.documentElement

没有设置 **pointer-events** 在透明区域移动

electron 第五章 窗口 - 图16

设置 **pointer-events** 在透明区域移动

electron 第五章 窗口 - 图17

5.4 窗口控制

  • 离开窗口(刷新、关闭窗口)提供提示信息 22-08-23-122-08-23-2
  • mac 系统下,关闭应用所有窗口,不退出应用程序,而是驻留在 Dock 栏上 22-08-31-2

5.4.1 阻止窗口关闭

想象一个场景,用户在使用应用时完成了大量的操作,但还没来得及保存,此时,他误触了窗口的关闭按钮。如果开发者没有做 防范机制,那么用户的大量工作将毁于一旦。

这时候应用一般需要 阻止窗口关闭,并提醒用户工作尚未保存,确认是否需要退出应用。

在开发网页时,我们可以使用如下代码来阻止窗口关闭:

  1. window.onbeforeunload = function () {
  2. return false
  3. }

设置完上述代码之后,用户如果关闭当前网页,那么会在关闭之前弹出一个提示框,如下图所示:
image.png

  • 如果用户点击了取消按钮,那么窗口将不会关闭
  • 如果用户点击了离开按钮,那么窗口将被关闭

在 Electron 应用中,我们也可以使用 onbeforeunload 来阻止窗口关闭,但并不会弹出如上图所示的提示窗口,而且开发者也不能再 onbeforeunload 事件内使用 alertconfirm 等技术来完成用户提示。

因为开发者没法在 Electron 的 onbeforeunload 事件中使用原生的 alertconfirm 来完成用户提示,所以我们得自己手写 Dialog 提示框。如下图所示:

image.png

  • 点击确定,关闭页面,渲染进程将被销毁 win.destroy()
  • 点击取消,取消关闭页面

onbeforeunload 事件什么情况下会触发

  1. 刷新页面 Command + R / Ctrl + R
  2. 关闭页面 Command + W / Ctrl + W

销毁窗口

✅ 正确的做法
调用 win.destroy() 来销毁窗口。

❌ 错误的做法
注意,不能使用 win.close() 来销毁窗口。因为它相当于:

  • 点击窗口左上角的关闭按钮
  • Command + W / Ctrl + W

如果使用 win.close() 来关闭窗口,那么又会触发 onbeforeunload 事件,该事件中 return false 阻止了窗口的关闭,最终会导致窗口无法被关闭。相当于啥也没干。

阻止窗口关闭的其它方案

也可以使用 Electron 窗口内置的 close 事件来阻止窗口的关闭。我们在应用主进程中增加如下代码,即可屏蔽关闭事件:

  1. win.on('close', (e) => {
  2. // 此处通知渲染进程,告知渲染进程此时触发了窗口的关闭行为
  3. e.preventDefault()
  4. })

close 事件发生时由主进程发送消息,通知渲染进程显示提示性信息,待用户做出选择后,再由渲染进程发送消息给主进程,主进程接到消息后,销毁窗口。

不能在渲染进程中监听窗口的关闭事件

就算我们屏蔽了刷新快捷键,也不能在渲染进程中监听窗口的 close 事件,因为渲染进程持有的 win 对象是一个在主进程中的远程对象。事件发生时,主进程中的 win 对象调用渲染进程的事件处理程序,这个过程是异步的,此时在渲染进程中执行 event.preventDefault() 已经毫无效用了。同理,我们也不应该期望主进程能及时获得事件处理程序的返回值,所以用 return false 也没有作用。

5.4.2 多窗口竞争资源

electron 第五章 窗口 - 图20

image.png

image.png

5.4.3 模态窗口和父子窗口

在一个业务较多的 GUI 应用中,我们经常会用到模态窗口来控制用户的行为,比如用户在窗口 A 操作至某一业务环节时,需要打开窗口 B,在窗口 B 内完成一项重要的操作,在关闭窗口 B 后,才能回到窗口 A 继续操作。此时,窗口 B 就是窗口 A 的模态窗口。

1. 模态窗口

一旦模态窗口打开,用户就只能操作该窗口,而不能再操作其父窗口。此时,父窗口处于禁用状态,只有等待子窗口关闭后,才能操作其父窗口。

  1. const remote = require('electron').remote
  2. this.win = new remote.BrowserWindow({
  3. parent: remote.getCurrentWindow(),
  4. modal: true, // 新建的窗口为 parent 的模态窗口
  5. webPreferences: {
  6. nodeIntegration: true
  7. }
  8. })

2. 父子窗口

子窗口总是现在在父窗口顶部。与模态窗口不同,子窗口不会禁用父窗口。子窗口创建成功后,虽然始终在父窗口上面,但父窗口仍然可以接收点击事件、完成用户输入等。

创建:和创建模态窗口类似,唯一的不同在于 modal,如果在创建窗口时,没填 modalmodal: false 那么创建的窗口为 remote.getCurrentWindow() 的子窗口。

  1. const remote = require('electron').remote
  2. this.win = new remote.BrowserWindow({
  3. parent: remote.getCurrentWindow(),
  4. // modal: flase, // modal 为 false 或者不写 modal 配置项
  5. webPreferences: {
  6. nodeIntegration: true
  7. }
  8. })

5.4.4 Mac 系统下的关注点

  1. const {app, BrowserWindow, nativeTheme} = require('electron')
  2. let win
  3. app.whenReady().then(() => {
  4. createWindow()
  5. })
  6. function createWindow() {
  7. win = new BrowserWindow({
  8. webPreferences: {
  9. nodeIntegration: true,
  10. contextIsolation: false
  11. }
  12. })
  13. win.loadFile('./index.html')
  14. win.webContents.openDevTools()
  15. win.on('close', () => win = null)
  16. // 查看当前是否是深色模式
  17. console.log('当前是深色模式嘛?', nativeTheme.shouldUseDarkColors)
  18. }
  19. // mac 下的特殊处理,所有窗口关闭后,不退出应用,应用图标依旧驻留在 Dock 栏上
  20. app.on('window-all-closed', () => {
  21. console.log('所有窗口都被关闭了')
  22. if (process.platform !== 'darwin') {
  23. app.quit()
  24. }
  25. })
  26. app.on('activate', () => {
  27. console.log('激活应用程序')
  28. if (win === null) createWindow()
  29. })
  30. // 当前的操作系统信息
  31. console.log('process.platform => ', process.platform)
  32. console.log("require('os').platform() => ", require('os').platform())
  33. // 在同一环境下,使用这两种方式,获取到的结果都是一样的
  34. // 查看当前的 Electron 版本号
  35. console.log('查看当前的 Electron 版本号 => ', process.versions.electron)

1. window-all-closed

所有窗口关闭时,如果是 mac 系统,则不退出应用

  1. app.on('window-all-closed', () => {
  2. if (process.platform !== 'darwin') {
  3. app.quit()
  4. }
  5. })

注意,window-all-closed 监听的是窗口的关闭,但是没法监听应用的退出,窗口的关闭和应用的退出是不一样的。

  • 关闭窗口
    • cmd + w
    • 点击窗口标题栏的关闭图标
  • 退出应用
    • cmd + q
    • 右键应用程序后选择退出

2. process.platform

  • darwin 表示当前操作系统为 mac 系统
  • linux 表示当前操作系统为 Linux 系统
  • win32 表示当前操作系统为 Windows 系统(不管是不是 64 位的)

在 Electron 中常接触到的就是上面这 3 个值

3. require(‘os’).platform()

除了通过 process.platform 获取操作系统信息外,还可以通过 require('os').platform 获取。在同一环境下,通过它俩获取到的结果都是一样的。

4. process 对象

process 对象身上常用的成员:

  • process.argv 属性,表示当前经常启动时的命令行参数
  • process.env 属性,包含用户环境信息,开发者经常为此属性附加内容,以判断开发的应用程序运行在什么环境下
  • process.kill() 方法,可以尝试结束某个进程
  • process.nextTick() 方法,可以添加一个回调到当前 JavaScript 事件队列末尾

🤔 如何获取当前使用的 Electron 的版本号?
process.versions.electron

5. activate

当应用程序被激活的时候会触发该事件

  1. app.on('activate', () => {
  2. if (win === null) createWindow()
  3. })

🤔 什么情况下叫应用程序被激活?
当点击 Dock 栏的应用图标时,会触发该 activate 事件
electron 第五章 窗口 - 图23

6. nativeTheme.shouldUseDarkColors

  • true 表示当前为深色模式
  • false 表示当前不是深色模式

5.5 本章小结

  • 了解 Electron 窗口的常见属性
  • 实现自定义窗口
  • 认识 Electron 对窗口都有哪些控制能力