從零開始的electron開發-托盤與消息通知

托盤與消息通知

上一期簡單介紹了托盤的退出,這一期呢說一下托盤的消息處理。
先說說本期的目標:實現微信對話的消息通知及處理。vue

  • windows:當有新消息推送過來時,托盤閃動,任務欄閃爍,左下角推送消息(簡化處理)。
  • mac:當有新消息時,程序塢有未讀紅點消息數,托盤未讀數,右上角推送消息。

win
mac

消息的處理

首先說說消息的處理吧,當有一個新消息推送過來時,win下任務欄會閃動,當頁面聚焦或過一段時間,閃動取消,托盤閃動,當消息所有讀取,托盤還原。在mac下則是新消息推送過來程序塢跳動,當消息所有讀取,程序塢紅點清空,托盤數字清空。
簡單來講就是未讀消息數與win的托盤閃動綁定,mac則是未讀消息數的顯示綁定。任務欄或程序塢的閃動則是新消息的推送這一行爲的觸發。
不過呢具體到頁面對話還有三種狀況,以win爲例(mac同理):react

  1. 軟件沒聚焦點:任務欄閃動,托盤閃動。
  2. 軟件聚焦,但此消息的推送是正在對話的人發出的(對話人list的active):任務欄閃動,托盤不變。
  3. 軟件聚焦,但此消息的推送不是正在對話的人:任務欄不變,托盤閃動。

任務欄處理

這個比較簡單,經過傳入布爾值,就能夠設置了。git

win.flashFrame(flag)

托盤處理

win

托盤的閃動實際上就是一個定時器,把托盤的圖片和透明圖片來回替換,沒有透明圖片的話能夠經過nativeImage.createFromPath(null))來設置。github

this.flickerTimer = setInterval(() => {
  global.tray.setImage(this.count++ % 2 === 0 ? this.image : nativeImage.createFromPath(null))
}, 500)

當消息讀取完畢以後咱們關閉這個定時器,這裏須要注意一點是關閉定時器時咱們並不知道托盤的圖片是正常的仍是透明的,故需再設置一下正常圖片。web

this.count = 0
if (this.flickerTimer) {
  clearInterval(this.flickerTimer)
  this.flickerTimer = null
}
global.tray.setImage(this.image)

mac

mac呢其實也能夠這樣作,可是呢,通常來講設置紅點數就能夠。vue-cli

global.tray.setTitle(messageConfig.news === 0 ? '' : messageConfig.news+ '') // 右上角托盤的消息數
app.dock.setBadge(messageConfig.news === 0 ? '' : messageConfig.news+ '') // 程序塢紅點

須要注意的是這二者接收的都是string類型,故當消息數爲0時,設置爲'',且這兩個方法時mac獨有的。windows

Notification通知

這個主進程渲染進程均可以調用,基本上算是面向文檔開發了,參考官方文檔,一般狀況下,mac的消息推送和Notification同樣,win下這是移入托盤顯示一個消息列表,這裏簡化處理都用Notification推送消息了(懶),固然你也能夠用多頁本身創建一個相似的消息列表,後面講通訊的時候看有機會演示一下不。
若是是用主進程的Notification咱們會發現win10消息頂端是咱們的appid,而不是咱們的productName,這裏須要這樣處理一下:api

const config = {
  .....
  VUE_APP_APPID: env.VUE_APP_APPID
}
主進程
app.setAppUserModelId(config.VUE_APP_APPID)

實現思路

渲染進程經過輪詢或者長連接收消息,在渲染進程根據咱們的對話狀態進行消息的邏輯處理,把是否任務欄閃動,托盤閃動的結果推送到主進程,主進程接受後展現。
總的來講主進程方面是一個被動的狀態,負責展現,邏輯處理主要是在渲染進程,實際上咱們在開發的時候進程也不該有過於複雜的判斷,最好是渲染進程處理好以後,再發送給主進程微信

代碼邏輯

主進程

其餘的變化不大,不過這裏把Tray修改爲立class,主進程index.js調用改成setTray.init(win)
flash就是咱們的托盤與閃動處理了,它的參數時渲染進程傳遞的,flashFrame是任務欄閃動控制,messageConfig是推送消息的信息。當咱們點擊推送消息的Notification時,win-message-read通知渲染進程定位到點擊人的對話框。app

import { Tray, nativeImage, Menu, app, Notification } from 'electron'
import global from '../config/global'
const isMac = process.platform === 'darwin'
const path = require('path')
let notification

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)
  }
}
class createTray {
  constructor() {
    const iconType = isMac ? '16x16.png' : 'icon.ico'
    const icon = path.join(__static, `./icons/${iconType}`)
    this.image = nativeImage.createFromPath(icon)
    this.count = 0
    this.flickerTimer = null
    if (isMac) {
      this.image.setTemplateImage(true)
    }
  }
  init(win) {
    global.tray = new Tray(this.image)
    let contextMenu = Menu.buildFromTemplate([
      {
        label: '顯示vue-cli-electron',
        click: () => {
          winShow(win)
        }
      }, {
        label: '退出',
        click: () => {
          app.quit()
        }
      }
    ])
    if (!isMac) {
      global.tray.on('click', () => {
        if (this.count !== 0) {
          win.webContents.send('win-message-read') // 點擊閃動托盤時通知渲染進程
        }
        winShow(win)
      })
    }
    global.tray.setToolTip('vue-cli-electron')
    global.tray.setContextMenu(contextMenu)
  }
  flash({ flashFrame, messageConfig }) {
    global.sharedObject.win.flashFrame(flashFrame)
    if (isMac && messageConfig) { // mac設置未讀消息數
      global.tray.setTitle(messageConfig.news === 0 ? '' : messageConfig.news+ '')
      app.dock.setBadge(messageConfig.news === 0 ? '' : messageConfig.news+ '')
    }
    if (messageConfig.news !== 0) { // 總消息數
      if (!this.flickerTimer && !isMac) { // win托盤閃動
        this.flickerTimer = setInterval(() => {
          global.tray.setImage(this.count++ % 2 === 0 ? this.image : nativeImage.createFromPath(null))
        }, 500)
      }
      if (messageConfig.body) { // 消息Notification推送
        notification = new Notification(messageConfig)
        notification.once('click', () => {
          winShow(global.sharedObject.win)
          global.sharedObject.win.webContents.send('win-message-read', messageConfig.id)
          notification.close()
        })
        notification.show()
      }
    } else { // 取消托盤閃動,還原托盤
      this.count = 0
      if (this.flickerTimer) {
        clearInterval(this.flickerTimer)
        this.flickerTimer = null
      }
      global.tray.setImage(this.image)
    }
  }
}

export default new createTray()

因爲消息呢是由渲染進程推送過來的,services/ipcMain.js添加對應的監聽及flash的調用

ipcMain.handle('win-message', (_, data) => {
  setTray.flash(data)
})

渲染進程(Vue3)

<div class="tary">
  <div class="btn"><a-button type="primary" @click="pushNews()">推送消息</a-button></div>
  <section class="box">
    <a-list class="list" :data-source="list">
      <template #renderItem="{ item, index }">
        <a-list-item
          class="item"
          :class="{ active: item.id === activeId }"
          @click="openList(index)"
        >
          <a-badge :count="item.news">
            <a-list-item-meta
              :description="item.newsList[item.newsList.length - 1]"
            >
              <template #title>
                <span>{{ item.name }}</span>
              </template>
              <template #avatar>
                <a-avatar
                  src="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png"
                />
              </template>
            </a-list-item-meta>
          </a-badge>
        </a-list-item>
      </template>
    </a-list>
    <div class="messageList">
      <ul class="messageBox" v-if="activeId">
        <li v-for="(item, index) in messageList" :key="index">
          {{ item }}
        </li>
      </ul>
    </div>
  </section>
</div>

import { defineComponent, reactive, toRefs, onMounted, onUnmounted, computed } from 'vue'
import Mock from 'mockjs'


export default defineComponent({
  setup() {
    聲明一個對話列表list,`activeId`是當前正在對話的人的id,`lastId`爲最後消息推送人的id。
    const state = reactive({
      activeId: '',
      list: [{
        name: Mock.mock('@cname'),
        id: Mock.mock('@id'),
        news: 0,
        newsList: []
      }, {
        name: Mock.mock('@cname'),
        id: Mock.mock('@id'),
        news: 0,
        newsList: []
      }, {
        name: Mock.mock('@cname'),
        id: Mock.mock('@id'),
        news: 0,
        newsList: []
      }]
    })
    onMounted(() => {
    接受主進程傳遞的消息讀取,若是有對應id,定位到對應id,無則定位到第一個未讀消息
    window.ipcRenderer.on('win-message-read', (_, data) => {
      const index = data ? state.list.findIndex(s => s.id === data) : state.list.findIndex(s => s.news !== 0)
      ~index && openList(index)
    })
    })
    onUnmounted(() => {
      window.ipcRenderer.removeListener('win-message-read')
    })
    news 是總消息數。
    const news = computed(() => state.list.reduce((pre, cur) => pre + cur.news, 0))
    const messageList = computed(() => state.list.find(s => s.id === state.activeId)['newsList'])

    function setMessage(obj) { // 向主進程推送消息
      window.ipcRenderer.invoke('win-message', obj)
    }
    function openList(index) { // 點擊對話框
      state.activeId = state.list[index].id
      state.list[index].news = 0
      setMessage({
        flashFrame: false,
        messageConfig: {
          news: news.value
        }
      })
    }
    function pushNews(index) { // 模擬消息的推送,index爲固定人的消息發送
      let flashFrame = true
      const hasFocus = document.hasFocus()
      const puahIndex = index != null ? index : getRandomIntInclusive(0, 2)
      const item = state.list[puahIndex]
      if (state.activeId !== item.id) { // 頁面對話的狀況處理
        item.news += 1
        if (hasFocus) {
          flashFrame = false
        }
      } else {
        if (hasFocus) {
          flashFrame = false
        }
      }
      item.newsList.push(Mock.mock('@csentence(20)'))
      setMessage({
        flashFrame,
        messageConfig: {
          title: item.name,
          id: item.id,
          body: item.newsList[item.newsList.length - 1],
          news: news.value
        }
      })
    }
    function getRandomIntInclusive(min, max) {
      min = Math.ceil(min)
      max = Math.floor(max)
      return Math.floor(Math.random() * (max - min + 1)) + min
    }
    return {
      ...toRefs(state),
      messageList,
      openList,
      pushNews
    }
  }
})

檢驗

  1. 軟件沒聚焦點:任務欄閃動,托盤閃動。
咱們加個定時器,而後把軟件關閉到托盤。
setTimeout(() => {
  pushNews()
}, 5000)
  1. 軟件聚焦,但此消息的推送是正在對話的人發出的(對話人list的active):任務欄閃動,托盤不變。
pushNews傳入0,固定消息爲第一人發出的,那麼咱們在定時器發生前點擊第一我的使其處於active。
setTimeout(() => {
  pushNews(0)
}, 5000)
  1. 軟件聚焦,但此消息的推送不是正在對話的人:任務欄不變,托盤閃動。
同上,咱們在定時器發生前點擊除第一我的之外的其餘人使其處於active。

本文地址:連接
本文github地址:連接

相關文章
相關標籤/搜索