Electron托盘与消息通知

上一期简单介绍了托盘的退出,这一期呢说一下托盘的消息处理。

先说说本期的目标:实现微信对话的消息通知及处理。

  • windows:当有新消息推送过来时,托盘闪动,任务栏闪烁,左下角推送消息(简化处理)。

  • mac:当有新消息时,程序坞有未读红点消息数,托盘未读数,右上角推送消息。

win

mac

消息的处理

首先说说消息的处理吧,当有一个新消息推送过来时,win下任务栏会闪动,当页面聚焦或过一段时间,闪动取消,托盘闪动,当消息全部读取,托盘还原。在mac下则是新消息推送过来程序坞跳动,当消息全部读取,程序坞红点清空,托盘数字清空。

简单来说就是未读消息数与win的托盘闪动绑定,mac则是未读消息数的显示绑定。任务栏或程序坞的闪动则是新消息的推送这一行为的触发。

不过呢具体到页面对话还有三种情况,以win为例(mac同理):

  1. 软件没聚焦点:任务栏闪动,托盘闪动。

  2. 软件聚焦,但此消息的推送是正在对话的人发出的(对话人list的active):任务栏闪动,托盘不变。

  3. 软件聚焦,但此消息的推送不是正在对话的人:任务栏不变,托盘闪动。

任务栏处理

这个比较简单,通过传入布尔值,就可以设置了。

win.flashFrame(flag)

托盘处理

win

托盘的闪动实际上就是一个定时器,把托盘的图片和透明图片来回替换,没有透明图片的话可以通过nativeImage.createFromPath(null))来设置。

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

当消息读取完毕之后我们关闭这个定时器,这里需要注意一点是关闭定时器时我们并不知道托盘的图片是正常的还是透明的,故需再设置一下正常图片。

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

mac

mac呢其实也可以这样做,但是呢,一般来说设置红点数就可以。

global.tray.setTitle(messageConfig.news === 0 ? '' : messageConfig.news+ '') // 右上角托盘的消息数
app.dock.setBadge(messageConfig.news === 0 ? '' : messageConfig.news+ '') // 程序坞红点

需要注意的是这两者接收的都是string类型,故当消息数为0时,设置为”,且这两个方法时mac独有的。

Notification通知

这个主进程渲染进程都可以调用,基本上算是面向文档开发了,参考官方文档,通常情况下,mac的消息推送和Notification一样,win下这是移入托盘显示一个消息列表,这里简化处理都用Notification推送消息了(懒),当然你也可以用多页自己建立一个类似的消息列表,后面讲通信的时候看有机会演示一下不。

如果是用主进程的Notification我们会发现win10消息顶端是我们的appid,而不是我们的productName,这里需要这样处理一下:

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通知渲染进程定位到点击人的对话框。

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。

本文地址:https://xuxin123.com/electron/tary-message

本文github地址:链接

您可能还喜欢...

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注