如何用electron高度自定义制一个系统菜单栏?
背景
最近在做一个实时聊天的PC客户端,遇到这样一个任务,在客户端接收到其他用户消息的时候要闪烁系统托盘图标,并且在鼠标滑到系统托盘的时候显示未读消息的菜单栏(对,就是类似QQ的消息提示,例如下图);这里补充一下,我们是选用electron作为我们的开发框架,对于我们来说,electron可以使用前端语言(HTML+CSS+JS),并且可以跨平台的框架,就是我们的最佳选择。
解题思路
1、正常人的思路,都是先看看electron有没有集成好的api, 我也不例外,所以找了一圈,找到了一个系统托盘Tray模块,大概的api模板是这样的:
const { app, Menu, Tray } = require('electron') let tray = null; let openVoice = true; app.whenReady().then(() => { tray = new Tray('/path/to/my/icon'); const contextMenu = Menu.buildFromTemplate([ { label: openVoice ? '关闭声音' : '开启声音', icon: openVoice ? "trayMenu/notVoice.png": "trayMenu/voice.png", click: (event: any) => { openVoice = !openVoice; }, }, { label: '退出', icon: "trayMenu/exit.png", click: () => { app.exit(0); } } ]); tray.setToolTip('This is my application.') tray.setContextMenu(contextMenu) })
实际的效果大概就是这样:
大体的就是支持标题、icon、子菜单、点击回调等比较通用的东西,貌似离我们理想的样子差距有点大,不信邪的我就想想试试能不能支持渲染HTML,于是乎就往contextMenu加多了一个子项:
{ label: '<p style="backgroud: #red">测试</p>', icon: "trayMenu/exit.png", click: () => { app.exit(0); } }
结果也是比较感人,真是完全按我写的输出,看来此路不通;
2、自己定制一个系统菜单栏
按我们上一点的做法也基本看出系统托盘Tray除了创建一个托盘图标外,基本对我们来说没什么用;于是,想到了用一个浏览器窗口(BrowserWindow)作为菜单栏,这样菜单栏要做成什么样子,就完全掌握在我们手上了,不过首先要解决以下几个问题:
1.如何监控鼠标滑动到我们的托盘图标上
我们先创建对应的菜单栏窗口,默认是隐藏
menuWin = new BrowserWindow({ modal: true, autoHideMenuBar: true, disableAutoHideCursor: true, frame: false, show: false, }); menuWin.loadURL(config.frontUrl + '/#/newMessage'); // 加载对应的菜单栏页面
找了一下tray的api,只发现一个mouse-move的事件们去处理这个问题,但这个事件只是滑入托盘的时候才触发事件,也没有相应的划出托盘的事件,所以我们还要做一个机制来判断鼠标是否滑出托盘,具体看下面代码;
import { app, Tray, Menu, nativeImage, screen, BrowserWindow,BrowserView,shell } from "electron"; let isLeave = true; // 存储鼠标是否离开托盘的状态 tray.on('mouse-move', (event: any,point: any) => { if( isLeave == true ) { // 从其他地方第一次移入菜单时,开始显示菜单页,然后在菜单内移动时不重复再显示菜单 menuWin.show(); } isLeave = false; checkTrayLeave(); }); /** * 检查鼠标是否从托盘离开 */ checkTrayLeave() { clearInterval(this.leaveInter) this.leaveInter = setInterval(() => { let trayBounds = tray.getBounds(); let point = screen.getCursorScreenPoint(); // 判断是否再托盘内 if(!(trayBounds.x < point.x && trayBounds.y < point.y && point.x < (trayBounds.x + trayBounds.width) && point.y < (trayBounds.y + trayBounds.height))){ // 触发 mouse-leave clearInterval(this.leaveInter); menuWin.hide(); // 隐藏菜单栏 isLeave = true; } else { console.log('isOn'); } }, 100) },
2.怎么控制窗口的位置
根据上一步的代码可以看出,tray.getBounds()可以用来获取托盘图标的位置信息,我们先假设我们菜单栏的高度、宽度均为200
let trayBounds = this.appTray.getBounds(); if(!params.x) { params.x = trayBounds.x - ( 200 / 2) } if(!params.y) { params.y = trayBounds.y - params.height; } this.menuWin.setBounds(params);
做到这一步的时候基本上鼠标在我们的托盘图标商滑入滑出的时候,控制菜单栏的显示和隐藏;但是会发现一个问题,当我们鼠标滑动到菜单栏的窗口时就会隐藏掉菜单栏,这样子根本做不了什么操作。所以还要在我们第一步鉴定托盘位置时将整个菜单栏的窗口划入到我们系统托盘滑动的正常范围内:
/** * 检查鼠标是否从托盘离开 */ checkTrayLeave() { clearInterval(this.leaveInter) this.leaveInter = setInterval(() => { let trayBounds = tray.getBounds(); let point = screen.getCursorScreenPoint(); // 判断是否再托盘内 if(!(trayBounds.x < point.x && trayBounds.y < point.y && point.x < (trayBounds.x + trayBounds.width) && point.y < (trayBounds.y + trayBounds.height))){ // 判断是否在弹出菜单内 let menuBounds = this.menuWin.getBounds() if(menuBounds.x < point.x && menuBounds.y < point.y && point.x < (menuBounds.x + menuBounds.width) && point.y < (menuBounds.y + menuBounds.height)) { console.log('isOnMenupage'); return ; } // 触发 mouse-leave clearInterval(this.leaveInter); menuWin.hide(); // 隐藏菜单栏 isLeave = true; } else { console.log('isOn'); } }, 100) },
总结
基本上整个自定义系统菜单栏的方案大概就是这样的流程,但这个方案也仅限于window和mac系统上,Linux上的兼容确实做不了啊,大部分api都不支持Linux系统。