<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 13029字,需瀏覽 27分鐘

           ·

          2021-03-17 13:34

          作者:陌路凡歌
          來源:SegmentFault 思否社區(qū)




          窗口關閉與托盤處理


          本期主要涉及窗口的關閉處理以及托盤的簡單處理。


          先說說本期的一個目標功能實現(xiàn):以網(wǎng)易云音樂為例,在Windows環(huán)境下,我們點擊右上角的關閉,這個時候會出現(xiàn)一個彈窗(以前沒有勾選不在提醒的話)詢問是直接退出還是縮小到系統(tǒng)托盤,選擇后確定才會進行實際關閉處理,當勾選不再提醒后點擊確認后下一次關閉不再提示直接處理,如果是縮小到托盤,對托盤點擊退出才會真正關閉。在Mac環(huán)境下,點擊右上角關閉直接縮小到程序塢,對程序塢右鍵退出或右上角托盤退出或左上角菜單退出,軟件才會真正關閉。


          當然,每個軟件都有不同的退出邏輯,這里介紹如何實現(xiàn)上面功能的同時會對electron退出的各種事件進行說明,希望能幫助你找到想要的退出方式。


          這里升級了一下版本,本版本為electron:12.0.0





          關閉的概念


          我們在使用官方例子,打包安裝后會發(fā)現(xiàn)mac和win在關閉上有所不同,mac是直接縮小到程序塢,對程序塢右鍵退出才能關閉,win則是直接關閉軟件,這是為什么呢?


          這里我先簡單說一下關閉的概念,很多人把軟件的關閉和窗口的關閉混淆在一起了,我這里把窗口和軟件區(qū)分開說一下:


          窗口的關閉:


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


          注意:窗口的關閉不一定會觸發(fā)軟件的關閉,但是通常情況下我們只有一個窗口,如果這個窗口關閉了,會觸發(fā)app的window-all-closed(當所有的窗口都被關閉時觸發(fā))這個事件,在這個事件里我們可以調(diào)用軟件的關閉app.quit(),故大多數(shù)情況下,我們把窗口關閉了,軟件也就退出了。


          那么造成這個差異的原因也就浮出水面了:


          app.on('window-all-closed', () => {
            if (!isMac) {
              app.quit()
            }
          })


          軟件的關閉:


          app.quit():調(diào)用會先觸發(fā)app的before-quit事件,然后再觸發(fā)所有窗口的關閉事件,窗口全部關閉了(調(diào)用app.quit()關閉窗口是不會觸發(fā)window-all-closed的,會觸發(fā)will-quit),觸發(fā)app的quit事件。但是如果在quit事件前使用event.preventDefault()阻止了默認行為(win的close事件,app的before-quit和will-quit),軟件還是不會關閉。
          app.exit():很好理解,最粗暴的強制關閉所有窗口,觸發(fā)app的quit事件,故win的close事件,app的before-quit和will-quit不會被觸發(fā)


          總結(jié)一下簡單來說軟件的關閉要滿足兩個條件:


          • 所有窗口都關閉了
          • 調(diào)用了app.quit()

          所以軟件的關閉一般就是下面幾種情況了

          1. 所有窗口關閉觸發(fā)window-all-closed,在window-all-closed里調(diào)用app.quit()
          2. 調(diào)用app.quit(),觸發(fā)所有窗口的close事件
          3. app.exit()

          那么要達成我們的目標只有使用方法2了。



          進程通信配置


          進程通信的話,放到后面再說,這里只是介紹進程通信的配置

          如果我在渲染進程想使用electron的一些方法的話,使用如下

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

          這樣使用沒問題,但是如果我們有多個頁面都要使用那么我們每個頁面都要require,比較麻煩,而且如果我們想既打包electron,又想打包web同樣使用(可以通過process.env.IS_ELECTRON處理不同場景),那么引入的electron就無用了。electron的窗口的webPreferences提供了preload可以注入js,我們可以在這里把ipcRenderer掛載到window下面。

          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,區(qū)別在于為true的話,注入的preload.js可視為一個獨立運行的環(huán)境,對于渲染進程是不可見的,簡單來說就是我們把ipcRenderer掛載到window上,對應的渲染進程是獲取不到的,故這里設置為false。

          功能實現(xiàn)


          如何實現(xiàn)呢?理一下思路,win的close事件有兩種觸發(fā)方式:

          1. 一個是我們點擊關閉觸發(fā),此時我們并不想關閉窗口,那么應該使用e.preventDefault()阻止窗口的關閉。
          2. 另一個是我們主動使用app.quit()觸發(fā)關閉,這時close事件里就不做處理。

          那么通過一個變量flag的切換來實現(xiàn),聲明一個全局變量willQuitApp,在onAppReady里添加窗口的close事件,當我們點擊關閉觸發(fā)close事件,此時e.preventDefault()禁止了窗口的關閉,我們再通過主進程向渲染進程發(fā)出一個關閉的通知。

          我們的流程為:

          主進程檢測關閉─>判斷是否是app.quit()觸發(fā)
          ──> 否,通知渲染進程關閉消息,渲染進程接收后根據(jù)用戶操作或本地存儲通知主進程將軟件關閉或縮小到托盤
          ──> 是,關閉軟件

          主進程:

          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()`觸發(fā)關閉時把willQuitApp設置為true,然后會觸發(fā)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的區(qū)分處理
                    event.sender.invoke('win-close', LgetItem('closeValue')) // 當是mac或者勾選了不再提示時向主進程發(fā)送消息
                  } 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) // 向主進程推送我們選擇的結(jié)果
                visible.value = false
              }
              return {
                closeChecked,
                closeValue,
                radioStyle,
                visible,
                hideModal
              }
            }
          })

          主進程接受渲染進程消息,initWindow里win賦值后調(diào)用,這里要注意的是Mac的處理,Mac在全屏狀態(tài)下如果隱藏的話,那么會出現(xiàn)軟件白屏或黑屏情況,我們這里要先退出全屏然后再隱藏掉。

          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()) { // 全屏狀態(tài)下特殊處理
                  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()
              }
            })
          }


          托盤設置


          這里的托盤設置只是為了完成軟件的退出功能,故只是簡單介紹,其余的功能后面的篇章會詳細介紹的。

          托盤的右鍵點擊退出直接退出,所以直接調(diào)用app.quit()觸發(fā)退出流程

          initWindow里win賦值后調(diào)用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時會出現(xiàn)一個閃爍的問題,有興趣的同學可以把win.minimize()注釋一下再看一下效果。當然你也可以用下面的處理方式:

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



          補充


          Mac系統(tǒng)在處理上有一些邏輯和Windows是不一樣的,雖然并沒有一個硬性的規(guī)定要這樣處理,更多的是看個人喜好與約定俗成。

          比如托盤的點擊處理win上左擊直接打開軟件,右擊打開菜單,而mac上左擊除了觸發(fā)click外還會打開菜單,如果和win上一樣處理的話有些不太適宜。

          這里再補充一個mac上的,mac軟件在全屏時,大多數(shù)軟件都是把縮小這個按鈕給禁用了的,那么electron怎么實現(xiàn)這個呢:

          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的參數(shù)來實現(xiàn),更多的參數(shù)請參考鏈接設置。



          點擊左下角閱讀原文,到 SegmentFault 思否社區(qū) 和文章作者展開更多互動和交流,掃描下方”二維碼“或在“公眾號后臺回復“ 入群 ”即可加入我們的技術(shù)交流群,收獲更多的技術(shù)文章~

          - END -


          瀏覽 39
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  三级色图 | 国产日韩视频在线观看 | а√天堂中文在线资源8 | 三级网站在线观看视频 | 一级a一级a爱片免费免会永久 |