在写项目的时候想要实现一个类似qq那样的有消息时图标闪烁,鼠标移入图标时预览消息内容,百度了很多,但是大多数博客都是使用原生的Notification,于是就自己动手写一个。把这个封装成了一个类,可以使用原生的消息提醒也可以使用自定义的消息提醒。
原理解析
实现鼠标移入托盘图标显示自定义内容,就是再开辟一个新的窗口,把其定位到图标的位置上方,然后开启一个定时器,隔一段时间去查询一下鼠标是否再图标上或新窗口的位置,如果不在就销毁创建的窗口。electron在加载的时候是可以指定加载的hash路径的,所以我们就可以直接指定hash路径,来达到显示自定义内容的目的。(注意:这里为什么使用hash路径,其主要原因是打包后history路径有问题,会显示白屏)。 图标闪动就是开启一个定时器,隔一段时间去更换托盘的图标照片。
另外需要额外注意的是,我们上边实现鼠标移入显示一个新窗口,监听的事件是托盘的mouse-move(鼠标移入)事件,为什么需要开启一个定时器去查询鼠标位置呢,一方面electron的托盘mouse-leave(鼠标移出)事件并不支持window电脑,另一方面是鼠标在新窗口中,我们仍要把它当作在托盘中,不能销毁新创建的窗口
说了上边的注意事项,我们可以发现,其难点就是实现查询鼠标位置,去销毁窗口,还有就是使用mouse-move事件去实现mouse-move和mouse-leave两个事件
代码
import { is } from '@electron-toolkit/utils'
import {
BrowserWindow,
BrowserWindowConstructorOptions,
Tray,
screen,
Notification,
NotificationConstructorOptions
} from 'electron'
import { join } from 'path'
export type NotifiType = 'Nomal' | 'Custom'
//自定义通知配置
interface OptionsType extends BrowserWindowConstructorOptions {
// 显示的hash路径
hash?: string
// 是否图标闪动
isFlash?: boolean
// 是否自动关闭刷新
removeClose?: boolean
}
// electron自带通知配置
interface NomalOptionsType extends NotificationConstructorOptions {}
interface flashOptions {
time: number //闪动的时间间隔
}
// 闪动图标,关闭函数
interface flashReturn {
close: () => void
}
/*
需要传递Tray
内置提醒,配置对象,点击执行函数
自定义提醒(创建一个新窗口),是否闪动图标,新窗口配置对象,新窗口显示的路径
*/
// 默认显示图标
const DEFAULTIMGURL = '../../resources/icon.png'
// 突变闪动时切换的图标
const FLICKERIMGURL = '../../resources/empty.png'
// 默认自定义通知配置
const defaultOptions: OptionsType = {
width: 300,
height: 100,
show: false,
frame: false,
alwaysOnTop: true,
isFlash: true
}
// 默认系统通知配置
const defaultNomalOptions: NomalOptionsType = {
title: '测试',
icon: join(__dirname, DEFAULTIMGURL),
body: '测试body'
}
// 用于记录监听函数,用于实现移除监听事件,避免重复监听
let listenerFn: any = null
export class NotifiCoustom {
// 是否离开的定时器
protected leaveInter: any
// 闪动图标的定时器
protected flashInter: any
protected trayBounds: any
protected point: any
protected isLeave: boolean = true
protected tray: Tray
protected messagePreview: any
protected options: OptionsType | NomalOptionsType = defaultOptions
constructor(tray: Tray) {
this.tray = tray
}
// 显示
show({
type,
options,
clickFn
}: {
type: NotifiType
options?: OptionsType
clickFn?: (event: any) => void
}) {
if (type === 'Nomal') {
this.options = options ? options : defaultNomalOptions
this.nomalNotification(clickFn)
} else {
this.options = options ? options : defaultOptions
// 执行自定义(自定义图标闪动是固定的500ms)
this.customNotification()
}
}
// 闪动图标
flashIcon(options: flashOptions): flashReturn {
let count = 0
if (this.flashInter) {
clearInterval(this.flashInter)
}
this.flashInter = setInterval(() => {
count += 1
if (count % 2 === 0) {
this.tray.setImage(join(__dirname, DEFAULTIMGURL))
} else {
this.tray.setImage(join(__dirname, FLICKERIMGURL))
}
}, options.time)
return { close: this.close.bind(this) }
}
// 关闭闪动
close() {
clearInterval(this.flashInter)
this.tray.setImage(join(__dirname, DEFAULTIMGURL))
}
// 移除监听事件
removeListener() {
this.tray.removeListener('mouse-move', listenerFn)
listenerFn = null
}
private nomalNotification(clickFn?: (event: any) => void) {
new Notification(this.options)
.on('click', (event) => {
clickFn && clickFn(event)
})
.show()
}
private customNotification() {
this.customInit()
}
private listener() {
// 鼠标移入停止闪烁
this.close()
if (this.isLeave) {
this.messagePreview = new BrowserWindow(this.options)
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
this.messagePreview.loadURL(
`${process.env['ELECTRON_RENDERER_URL']}/#${(this.options as OptionsType).hash}`
)
} else {
this.messagePreview.loadFile(join(__dirname, '../renderer/index.html'), {
hash: (this.options as OptionsType).hash
})
}
const position = this.tray.getBounds()
this.messagePreview.setPosition(position.x, position.y - 100)
this.messagePreview.show()
//触发mouse-enter
this.isLeave = false
this.checkTrayLeave()
}
}
private customInit() {
if ((this.options as OptionsType).isFlash) {
this.flashIcon({ time: 500 })
}
// 监听过了,不再监听
if (listenerFn) {
this.close()
return
}
listenerFn = this.listener.bind(this)
this.tray.on('mouse-move', listenerFn)
}
private checkTrayLeave() {
clearInterval(this.leaveInter)
this.leaveInter = setInterval(() => {
this.trayBounds = this.tray.getBounds()
this.point = screen.getCursorScreenPoint()
// 鼠标移出托盘
if (
!(
this.trayBounds.x < this.point.x &&
this.trayBounds.y < this.point.y &&
this.point.x < this.trayBounds.x + this.trayBounds.width &&
this.point.y < this.trayBounds.y + this.trayBounds.height
)
) {
// 鼠标移除自定义窗口的位置
if (
this.point.x - this.trayBounds.x < 0 ||
this.point.x - this.trayBounds.x > (this.options as any).width ||
this.point.y < this.trayBounds.y - (this.options as any).height
) {
this.isLeave = true
if ((this.options as OptionsType).removeClose) {
this.removeListener()
}
this.messagePreview.close()
clearInterval(this.leaveInter)
}
}
}, 100)
}
}