Electron托盘与消息通知

山明水秀
• 阅读 10117

上一期简单介绍了托盘的退出,这一期呢说一下托盘的消息处理。
先说说本期的目标:实现微信对话的消息通知及处理。

  • windows:当有新消息推送过来时,托盘闪动,任务栏闪烁,左下角推送消息(简化处理)。
  • mac:当有新消息时,程序坞有未读红点消息数,托盘未读数,右上角推送消息。

Electron托盘与消息通知
Electron托盘与消息通知

消息的处理

首先说说消息的处理吧,当有一个新消息推送过来时,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)
  2. 软件聚焦,但此消息的推送是正在对话的人发出的(对话人list的active):任务栏闪动,托盘不变。

    pushNews传入0,固定消息为第一人发出的,那么我们在定时器发生前点击第一个人使其处于active。
    setTimeout(() => {
      pushNews(0)
    }, 5000)
  3. 软件聚焦,但此消息的推送不是正在对话的人:任务栏不变,托盘闪动。

    同上,我们在定时器发生前点击除第一个人以外的其他人使其处于active。

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

点赞
收藏
评论区
推荐文章
Wesley13 Wesley13
3年前
3分钟了解华为推送服务优势,第一项就让你心动!
消息推送(Pushnotification)指产品运营人员通过自身或三方的“推送服务”向用户主动地推送消息。简单来说,我们在移动设备(例如:手机)的通知中心或锁屏界面看到的消息都属于消息推送。作为消息推送的服务提供商之一,华为推送具有怎样的特点和优势?!在这里插入图片描述(https://imgblog.csdnimg.cn/202012221
Stella981 Stella981
3年前
Android 服务器推送技术
在开发Android和iPhone应用程序时,我们往往需要从服务器不定的向手机客户端即时推送各种通知消息,iPhone上已经有了比较简单的和完美的推送通知解决方案,可是Android平台上实现起来却相对比较麻烦,最近利用几天的时间对Android的推送通知服务进行初步的研究。在Android手机平台上,Google提供了C2DM(CloudtoDevi
Stella981 Stella981
3年前
Android 必备进阶之百度推送
写在前边今天给大家推送一篇关于百度推送的文章。我们手机上常用的App都会时不时的推送消息在我们的消息栏显示,常用的是QQ消息推送、微信消息推送、支付宝转账消息推送等。以后再做大大小小的项目都会用到推送,今天就总结了一篇用百度云做推送消息,以后做项目会经常用到的,有时间就学习一下吧!!(https://oscimg.oschin
Wesley13 Wesley13
3年前
Uber准备放弃自动驾驶,转手卖给前谷歌无人车CTO,估值曾被孙正义炒到72.5亿美元
!(https://oscimg.oschina.net/oscnet/0fe7cb00a0cf4872b022342d1e21d47e.png)杨净发自凹非寺量子位报道|公众号QbitAI最新消息,Uber要出售无人驾驶部门(ATG)了。据TechCrunch报道,Uber有意向出售,而也有人愿意买。
Stella981 Stella981
3年前
JVM 字节码指令表
字节码助记符指令含义0x00nop什么都不做0x01aconst\_null将null推送至栈顶0x02iconst\_m1将int型1推送至栈顶0x03iconst\_0将int型0推送至栈顶0x04iconst\_1将int型1推送至栈顶0x05ic
Stella981 Stella981
3年前
Noark入门之协议映射
0x00消息控制器消息控制器,主要作用就是为每个模块提供消息处理的入口.这里的消息不仅仅是协议,还有内部指令,事件等等逻辑入口,这也是为了响应线程模型作出的一种支撑,只要入口在此消息控制器内,那必然走期望的线程调度。@Controller用于标识一个类为当前模块的消息控制器入口.@Controller(threadGroup
Stella981 Stella981
3年前
Easypush微信消息推送——打破传统的消息推送方式
通过使用EasyPush实现信息推送官网:https://easypush.baigekeji.com/Easypush从1.1.0(发行版)开始,进行行业模块分析,致力于高效下服务消息推送,将原先常见的推送开发模式统一封装,实现多种推送方式,目前仍在不断研发,在提服务提醒领域更加智能化(不定时更新)
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
融云IM即时通讯 融云IM即时通讯
8个月前
融云IM干货丨IM服务消息推送,推送通知失败时,SDK会提供哪些错误信息?
当推送通知失败时,SDK可能会提供以下错误信息:推送服务未开启或配置错误:确保已经在IM控制台开启了推送服务,并且正确配置了推送证书或密钥。设备未正确注册推送服务:检查设备是否成功注册到了推送服务,获取到了正确的设备令牌。应用权限问题:确保应用有发送通知的
融云IM即时通讯 融云IM即时通讯
8个月前
融云IM干货丨如果用户不在线,推送通知会怎样处理?
如果用户不在线,融云的推送通知会按照以下方式处理:离线消息推送:当用户不在线时,融云会将收到的单聊消息、群聊消息、系统消息、超级群消息通过第三方推送厂商或融云自建的推送服务通知客户端。这意味着即使用户的应用没有运行,他们也能通过系统通知栏接收到消息提醒。服
美凌格栋栋酱 美凌格栋栋酱
5个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(