從零開始的electron開發-主進程-窗口關閉與托盤處理

窗口關閉與托盤處理

本期主要涉及窗口的關閉處理以及托盤的簡單處理。  
先說說本期的一個目標功能實現:以網易雲音樂爲例,在Windows環境下,咱們點擊右上角的關閉,這個時候會出現一個彈窗(之前沒有勾選不在提醒的話)詢問是直接退出仍是縮小到系統托盤,選擇後肯定纔會進行實際關閉處理,當勾選再也不提醒後點擊確認後下一次關閉再也不提示直接處理,若是是縮小到托盤,對托盤點擊退出纔會真正關閉。在Mac環境下,點擊右上角關閉直接縮小到程序塢,對程序塢右鍵退出或右上角托盤退出或左上角菜單退出,軟件纔會真正關閉。  
固然,每一個軟件都有不一樣的退出邏輯,這裏介紹如何實現上面功能的同時會對electron退出的各類事件進行說明,但願能幫助你找到想要的退出方式。  
這裏升級了一下版本,本版本爲electron:12.0.0html

網易雲關閉

關閉的概念

咱們在使用官方例子,打包安裝後會發現mac和win在關閉上有所不一樣,mac是直接縮小到程序塢,對程序塢右鍵退出才能關閉,win則是直接關閉軟件,這是爲何呢?  
這裏我先簡單說一下關閉的概念,不少人把軟件的關閉和窗口的關閉混淆在一塊兒了,我這裏把窗口和軟件區分開說一下:vue

窗口的關閉:

win:BrowserWindow實例
win.destroy():強制關閉這個窗口,會觸發win的closed事件,不會觸發close事件
win.close():關閉窗口,觸發win的close,closed事件

注意:窗口的關閉不必定會觸發軟件的關閉,可是一般狀況下咱們只有一個窗口,若是這個窗口關閉了,會觸發app的window-all-closed(當全部的窗口都被關閉時觸發)這個事件,在這個事件裏咱們能夠調用軟件的關閉app.quit(),故大多數狀況下,咱們把窗口關閉了,軟件也就退出了。
那麼形成這個差別的緣由也就浮出水面了:node

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不會被觸發

總結一下簡單來講軟件的關閉要知足兩個條件:react

  • 全部窗口都關閉了
  • 調用了app.quit()

因此軟件的關閉通常就是下面幾種狀況了web

  1. 全部窗口關閉觸發window-all-closed,在window-all-closed裏調用app.quit()
  2. 調用app.quit(),觸發全部窗口的close事件
  3. app.exit()

那麼要達成咱們的目標只有使用方法2了。vue-cli

進程通訊配置

進程通訊的話,放到後面再說,這裏只是介紹進程通訊的配置
若是我在渲染進程想使用electron的一些方法的話,使用以下app

const { ipcRenderer } = require('electron')
ipcRenderer.send('asynchronous-message', 'ping') // 向主進程發送消息

這樣使用沒問題,可是若是咱們有多個頁面都要使用那麼咱們每一個頁面都要require,比較麻煩,並且若是咱們想既打包electron,又想打包web一樣使用(能夠經過process.env.IS_ELECTRON處理不一樣場景),那麼引入的electron就無用了。electron的窗口的webPreferences提供了preload能夠注入js,咱們能夠在這裏把ipcRenderer掛載到window下面。electron

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')
}

這裏說明一下contextIsolation這個值,在12.0.0之前默認值爲false,本例子是12.0.0版本,默認值爲true,區別在於爲true的話,注入的preload.js可視爲一個獨立運行的環境,對於渲染進程是不可見的,簡單來講就是咱們把ipcRenderer掛載到window上,對應的渲染進程是獲取不到的,故這裏設置爲false。async

功能實現

如何實現呢?理一下思路,win的close事件有兩種觸發方式:ide

  1. 一個是咱們點擊關閉觸發,此時咱們並不想關閉窗口,那麼應該使用e.preventDefault()阻止窗口的關閉。
  2. 另外一個是咱們主動使用app.quit()觸發關閉,這時close事件裏就不作處理。

那麼經過一個變量flag的切換來實現,聲明一個全局變量willQuitApp,在onAppReady裏添加窗口的close事件,當咱們點擊關閉觸發close事件,此時e.preventDefault()禁止了窗口的關閉,咱們再經過主進程向渲染進程發出一個關閉的通知。  
咱們的流程爲:
主進程檢測關閉─>判斷是不是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
    }
  }
})

主進程接受渲染進程消息,initWindow裏win賦值後調用,這裏要注意的是Mac的處理,Mac在全屏狀態下若是隱藏的話,那麼會出現軟件白屏或黑屏狀況,咱們這裏要先退出全屏而後再隱藏掉。

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.show()前爲何要有個win.minimize(),這裏的處理呢是由於hide前若是咱們渲染進程有可見的改變(咱們這裏是讓關閉提示的彈窗關閉了),後面再show時會出現一個閃爍的問題,有興趣的同窗能夠把win.minimize()註釋一下再看一下效果。固然你也能夠用下面的處理方式:

win.on('show', () => {
  setTimeout(() => {
    win.setOpacity(1)
  }, 200)
})
win.on('hide', () => {
  win.setOpacity(0)
})

補充

Mac系統在處理上有一些邏輯和Windows是不同的,雖然並無一個硬性的規定要這樣處理,更多的是看我的喜愛與約定俗成。  
好比托盤的點擊處理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)
})

因爲咱們的窗口實際上就是chromium,故咱們能夠經過設置chromium的參數來實現,更多的參數請參考連接設置。

相關文章
相關標籤/搜索