从零开始的 electron 开发-主进程-窗口关闭与托盘处理
窗口关闭与托盘处理
本期主要涉及窗口的关闭处理以及托盘的简单处理。
先说说本期的一个目标功能实现:以网易云音乐为例,在Windows环境下,我们点击右上角的关闭,这个时候会出现一个弹窗(以前没有勾选不在提醒的话)询问是直接退出还是缩小到系统托盘,选择后确定才会进行实际关闭处理,当勾选不再提醒后点击确认后下一次关闭不再提示直接处理,如果是缩小到托盘,对托盘点击退出才会真正关闭。在Mac环境下,点击右上角关闭直接缩小到程序坞,对程序坞右键退出或右上角托盘退出或左上角菜单退出,软件才会真正关闭。
当然,每个软件都有不同的退出逻辑,这里介绍如何实现上面功能的同时会对electron退出的各种事件进行说明,希望能帮助你找到想要的退出方式。
这里升级了一下版本,本版本为electron:12.0.0
关闭的概念
我们在使用官方例子,打包安装后会发现mac和win在关闭上有所不同,mac是直接缩小到程序坞,对程序坞右键退出才能关闭,win则是直接关闭软件,这是为什么呢?
这里我先简单说一下关闭的概念,很多人把软件的关闭和窗口的关闭混淆在一起了,我这里把窗口和软件区分开说一下:
窗口的关闭:
win:BrowserWindow实例
win.destroy():强制关闭这个窗口,会触发win的closed事件,不会触发close事件
win.close():关闭窗口,触发win的close,closed事件
注意:窗口的关闭不一定会触发软件的关闭,但是通常情况下我们只有一个窗口,如果这个窗口关闭了,会触发app的window-all-closed(当所有的窗口都被关闭时触发)这个事件,在这个事件里我们可以调用软件的关闭app.quit(),故大多数情况下,我们把窗口关闭了,软件也就退出了。
那么造成这个差异的原因也就浮出水面了:
app.on('window-all-closed', () => {
if (!isMac) {
app.quit()
}
})
软件的关闭:
app.quit():调用会先触发app的before-quit事件,然后再触发所有窗口的关闭事件,窗口全部关闭了(调用app.quit()关闭窗口是不会触发window-all-closed的,会触发will-quit),触发app的quit事件。但是如果在quit事件前使用event.preventDefault()阻止了默认行为(win的close事件,app的before-quit和will-quit),软件还是不会关闭。
app.exit():很好理解,最粗暴的强制关闭所有窗口,触发app的quit事件,故win的close事件,app的before-quit和will-quit不会被触发
总结一下简单来说软件的关闭要满足两个条件:
所有窗口都关闭了 调用了app.quit()
所有窗口关闭触发window-all-closed,在window-all-closed里调用app.quit() 调用app.quit(),触发所有窗口的close事件 app.exit()
进程通信配置
如果我在渲染进程想使用electron的一些方法的话,使用如下
const { ipcRenderer } = require('electron')
ipcRenderer.send('asynchronous-message', 'ping') // 向主进程发送消息
vue.config.js:
electronBuilder: {
nodeIntegration: true, // 这里设置实际上是设置process.env.ELECTRON_NODE_INTEGRATION的值
preload: 'src/renderer/preload/ipcRenderer.js',
......
}
ipcRenderer.js:
import { ipcRenderer } from 'electron'
window.ipcRenderer = ipcRenderer
主进程:
win = createWindow({
....
webPreferences: {
contextIsolation: false,
nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION,
preload: path.join(__dirname, 'preload.js'),
scrollBounce: isMac
}
}, '', 'index.html')
渲染进程:
if (process.env.IS_ELECTRON) {
window.ipcRenderer.send('asynchronous-message', 'ping')
}
功能实现
一个是我们点击关闭触发,此时我们并不想关闭窗口,那么应该使用e.preventDefault()阻止窗口的关闭。 另一个是我们主动使用app.quit()触发关闭,这时close事件里就不做处理。
我们的流程为:
主进程检测关闭─>判断是否是app.quit()触发
──> 否,通知渲染进程关闭消息,渲染进程接收后根据用户操作或本地存储通知主进程将软件关闭或缩小到托盘
──> 是,关闭软件
let willQuitApp = false
onAppReady:
win.on('close', (e) => {
console.log('close', willQuitApp)
if (!willQuitApp) {
win.webContents.send('win-close-tips', { isMac })
e.preventDefault()
}
})
我们主动使用`app.quit()`触发关闭时把willQuitApp设置为true,然后会触发win的close事件,让窗口关闭掉,达成方法2。
app.on('activate', () => win.show()) // mac点击程序坞显示窗口
app.on('before-quit', () => {
console.log('before-quit')
willQuitApp = true
})
<a-modal
v-model:visible="visible"
:destroyOnClose="true"
title="关闭提示"
ok-text="确认"
cancel-text="取消"
@ok="hideModal"
>
<a-radio-group v-model:value="closeValue">
<a-radio :style="radioStyle" :value="1">最小化到托盘</a-radio>
<a-radio :style="radioStyle" :value="2">退出vue-cli-electron</a-radio>
<a-checkbox v-model:checked="closeChecked">不再提醒</a-checkbox>
</a-radio-group>
</a-modal>
import { defineComponent, reactive, ref, onMounted, onUnmounted } from 'vue'
import { LgetItem, LsetItem } from '@/utils/storage'
export default defineComponent({
setup() {
const closeChecked = ref(false)
const closeValue = ref(1)
const visible = ref(false)
const radioStyle = reactive({
display: 'block',
height: '30px',
lineHeight: '30px',
})
onMounted(() => {
window.ipcRenderer.on('win-close-tips', (event, data) => { // 接受主进程的关闭通知
const closeChecked = LgetItem('closeChecked')
const isMac = data.isMac
if (closeChecked || isMac) { // mac和win的区分处理
event.sender.invoke('win-close', LgetItem('closeValue')) // 当是mac或者勾选了不再提示时向主进程发送消息
} else {
visible.value = true
event.sender.invoke('win-focus', closeValue.value) // 显示关闭弹窗并聚焦
}
})
})
onUnmounted(() => {
window.ipcRenderer.removeListener('win-close-tips')
})
async function hideModal() {
if (closeChecked.value) {
LsetItem('closeChecked', true)
LsetItem('closeValue', closeValue.value)
}
await window.ipcRenderer.invoke('win-close', closeValue.value) // 向主进程推送我们选择的结果
visible.value = false
}
return {
closeChecked,
closeValue,
radioStyle,
visible,
hideModal
}
}
})
import { ipcMain, app } from 'electron'
import global from '../config/global'
export default function () {
const win = global.sharedObject.win
const isMac = process.platform === 'darwin'
ipcMain.handle('win-close', (event, data) => {
if (isMac) {
if (win.isFullScreen()) { // 全屏状态下特殊处理
win.once('leave-full-screen', function () {
win.setSkipTaskbar(true)
win.hide()
})
win.setFullScreen(false)
} else {
win.setSkipTaskbar(true)
win.hide()
}
} else {
if (data === 1) { // win缩小到托盘
win.setSkipTaskbar(true) // 使窗口不显示在任务栏中
win.hide() // 隐藏窗口
} else {
app.quit() // win退出
}
}
})
ipcMain.handle('win-focus', () => { // 聚焦窗口
if (win.isMinimized()) {
win.restore()
win.focus()
}
})
}
托盘设置
托盘的右键点击退出直接退出,所以直接调用app.quit()触发退出流程
initWindow里win赋值后调用setTray(win)
import { Tray, nativeImage, Menu, app } from 'electron'
const isMac = process.platform === 'darwin'
const path = require('path')
let tray = null
export default function (win) {
const iconType = isMac ? '16x16.png' : 'icon.ico'
const icon = path.join(__static, `./icons/${iconType}`)
const image = nativeImage.createFromPath(icon)
if (isMac) {
image.setTemplateImage(true)
}
tray = new Tray(image)
let contextMenu = Menu.buildFromTemplate([
{
label: '显示vue-cli-electron',
click: () => {
winShow(win)
}
}, {
label: '退出',
click: () => {
app.quit()
}
}
])
if (!isMac) {
tray.on('click', () => {
winShow(win)
})
}
tray.setToolTip('vue-cli-electron')
tray.setContextMenu(contextMenu)
}
function winShow(win) {
if (win.isVisible()) {
if (win.isMinimized()) {
win.restore()
win.focus()
} else {
win.focus()
}
} else {
!isMac && win.minimize()
win.show()
win.setSkipTaskbar(false)
}
}
win.on('show', () => {
setTimeout(() => {
win.setOpacity(1)
}, 200)
})
win.on('hide', () => {
win.setOpacity(0)
})
补充
比如托盘的点击处理win上左击直接打开软件,右击打开菜单,而mac上左击除了触发click外还会打开菜单,如果和win上一样处理的话有些不太适宜。
这里再补充一个mac上的,mac软件在全屏时,大多数软件都是把缩小这个按钮给禁用了的,那么electron怎么实现这个呢:
win.on('enter-full-screen', () => {
isMac && app.commandLine.appendSwitch('disable-pinch', true)
})
win.on('leave-full-screen', () => {
isMac && app.commandLine.appendSwitch('disable-pinch', false)
})
评论