背景
最近在做一個實時聊天的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系統。