windows系统下如何实现类似微信新消息来临时出现的消息通知窗口?
实现思路
Electron的系统通知实例无法自定义样式,所以我们需要思考如何才能实现这样的自定义通知效果??小编的实现思路如下:
- 把新消息通知当作是一个独立的窗口,当鼠标经过托盘图标时,显示自定义系统通知窗口,因为Electron在windows系统下无法识别鼠标是否离开系统托盘,所以需要自己计算鼠标是否从托盘离开,当鼠标从系统托盘离开时就隐藏自定义系统菜单窗口
实现代码
1. 先创建一个html文件,作为自定义系统通知的载体。
文件名称:customNoticeHtml.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<div class="new-msg-list-div" id="new-msg-list-div">
<div class="content-div">
<div class="title" id="title">新消息</div>
<div class="list-div" id="list-div"></div>
</div>
<div class="footer" onclick="onIgnoreAll()">忽略全部</div>
</div>
</body>
</html>
<script type="text/javascript">
//获取标题dom
const title = document.getElementById('title');
//更新标题中的新消息总数量
function updateTitleNumber(totalNumber) {
title.innerHTML = '新消息('+totalNumber+')';
}
//获取存放列表的div
const listDiv = document.getElementById('list-div')
//清空列表dom
function clearListDivDom(){
while(listDiv.hasChildNodes()){
listDiv.removeChild(listDiv.firstChild);
}
}
//创建列表dom
function createListDom (listData){
clearListDivDom()//清空列表Dom
let totalNumber = 0;//消息总数
listData.forEach(item => {//遍历列表数据创建dom节点
const listItem = document.createElement('div');
listItem.className = "list-item"
listItem.onclick = () => this.onRead(item)
const content = document.createElement('div');
content.className = "content"
const avatorDiv = document.createElement('div');
avatorDiv.className = "avator-div"
if(item.chatType === 0){
const avator = document.createElement('img');
avator.src = item.avator
avator.className = "avator-1"
avatorDiv.append(avator)
}else if(item.chatType === 2){
itemAvator = JSON.parse(item.avator);
if(itemAvator.length > 9){
itemAvator = itemAvator.slice(0,9);
}
itemAvator.forEach(memberInfo => {
const avator = document.createElement('img');
avator.src = memberInfo.avator;
avator.className = `avator-${itemAvator.length===1?1:itemAvator.length<5?2:3}`;
avatorDiv.append(avator)
})
}
const nameDiv = document.createElement('div');
nameDiv.className = "name-div"
const name = document.createElement('span');
name.className = "name"
name.innerHTML = item.name
content.append(avatorDiv)
nameDiv.append(name)
content.append(nameDiv)
const msgNum = document.createElement('div');
msgNum.className = "msg-num";
msgNum.innerHTML = item.number
listItem.append(content)
listItem.append(msgNum)
listDiv.append(listItem)
totalNumber += item.number;//计算消息总数量
});
updateTitleNumber(totalNumber);//更新标题中的新消息总数量
}
// 测试数据
createListDom([
{
chatType: 0,
name:"小红",
avator:"https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fsafe-img.xhscdn.com%2Fbw%2F1e8de07f-2c9e-4ecb-8893-5a8194a09d8f%3FimageView2%2F2%2Fw%2F1080%2Fformat%2Fjpg&refer=http%3A%2F%2Fsafe-img.xhscdn.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1691724076&t=94e27960b3d78a23e2ce2b928c71f137",
number:1,
},
{
chatType: 2,
name:"开发小组群",
avator:JSON.stringify([{
avator:"https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fsafe-img.xhscdn.com%2Fbw1%2Fb4a87154-18b6-4163-ac80-f4dc4bf58d09%3FimageView2%2F2%2Fw%2F1080%2Fformat%2Fjpg&refer=http%3A%2F%2Fsafe-img.xhscdn.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1691724183&t=0527914495bf1d2c835608ab2434d9ee",
name:"小明",
user_uid:"185e0a6f09354b198ecefcd2fe951e7a",
},{
avator:"https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fsafe-img.xhscdn.com%2Fbw1%2F8075fa62-cf88-420a-88f7-9a4a4d714bb0%3FimageView2%2F2%2Fw%2F1080%2Fformat%2Fjpg&refer=http%3A%2F%2Fsafe-img.xhscdn.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1691724168&t=cb552345dca3226fc176f19936a1d901",
name:"小张",
user_uid:"3b794365c946443fb3ec1c2ee13d2984",
},{
avator:"https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fsafe-img.xhscdn.com%2Fbw1%2F9e18d14b-8a44-41b0-97d9-6aed05b70e7f%3FimageView2%2F2%2Fw%2F1080%2Fformat%2Fjpg&refer=http%3A%2F%2Fsafe-img.xhscdn.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1691724139&t=832f8c9b8de23e731497e4d001c3dc87",
name:"小美",
user_uid:"185e0a6f09354b198ecefcd2fe951e7a",
},{
avator:"https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fsafe-img.xhscdn.com%2Fbw1%2F5ea36c18-f346-4f99-a4e0-3017a434f1aa%3FimageView2%2F2%2Fw%2F1080%2Fformat%2Fjpg&refer=http%3A%2F%2Fsafe-img.xhscdn.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1691724120&t=8d5c76cd0185cfccecb6e013f11b5013",
name:"小包",
user_uid:"185e0a6f09354b198ecefcd2fe951e7a",
},
]),
number:5,
},
])
//读取了某个聊天对象的消息
function onRead(msg){
}
//点击 忽略全部 时触发
function onIgnoreAll(){
}
</script>
<style>
body{
margin: 0;
padding: 0;
}
.new-msg-list-div{
font-size: 12px;
cursor: context-menu;
}
.new-msg-list-div .content-div{
border-bottom: solid 1px #dedede;
}
.new-msg-list-div .content-div .title{
line-height: 34px;
font-weight: bold;
padding: 0 20px;
}
.new-msg-list-div .content-div .list-div{
max-height: 400px;
overflow-y: auto;
scrollbar-width:none;/*设置火狐浏览器不显示滚动条*/
}
.new-msg-list-div .content-div .list-div::-webkit-scrollbar{/*设置谷歌浏览器不显示滚动条*/
width: 0;
height: 0;
background-color: transparent;
}
.new-msg-list-div .content-div .list-div .list-item{
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 20px;
border-bottom: solid 1px #eee;
}
.new-msg-list-div .content-div .list-div .list-item:hover{
background-color: #e8e8e8;
}
.new-msg-list-div .content-div .list-div .list-item .content{
display: flex;
align-items: center;
}
.new-msg-list-div .content-div .list-div .list-item .content .avator-div{
width: 34px;
height: 34px;
background-color: #eee;
margin-right: 10px;
display: flex;
align-items: center;
flex-wrap: wrap-reverse;
justify-content: center;
overflow: hidden;
align-content: center;
border-radius: 0px;
flex-shrink: 0;
}
.new-msg-list-div .content-div .list-div .list-item .content .avator-div .avator-1{
width: 100%;
height: 100%;
border-radius: 2px;
font-size: 18px;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
}
.new-msg-list-div .content-div .list-div .list-item .content .avator-div .avator-2{
width: 46%;
height: 46%;
margin: 2%;
font-size: 8px;
line-height: 46%;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
}
.new-msg-list-div .content-div .list-div .list-item .content .avator-div .avator-2 span{
transform: scale(0.75);
display: inline-block;
}
.new-msg-list-div .content-div .list-div .list-item .content .avator-div .avator-3{
width: 30%;
height: 30%;
margin: 1%;
font-size: 7px;
line-height: 30%;
zoom: 0.7;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
}
.new-msg-list-div .content-div .list-div .list-item .content .avator-div .avator-3 span{
transform: scale(0.75);
display: inline-block;
}
.new-msg-list-div .content-div .list-div .list-item .content .name-div{
display: flex;
align-items: center;
}
.new-msg-list-div .content-div .list-div .list-item .content .name{
font-weight: bold;
max-width: 90px;
text-overflow: ellipsis;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
word-break: break-word;
}
.new-msg-list-div .content-div .list-div .list-item .msg-num{
color: #fff;
background-color: red;
padding: 0 8px;
border-radius: 100px;
}
.new-msg-list-div .footer{
line-height: 34px;
padding: 0 20px;
text-align: right;
color: #586cb1;
cursor: pointer;
}
</style>
2. Electron入口文件的代码如下
const { app, BrowserWindow, nativeImage, Tray, screen } = require('electron');
const path = require('path');
//入口文件
let indexHtml = 'http://localhost:' + 8080 + '/xiongxin/';
//熊信图标路径
const iconPath = path.join(__dirname,`../src/assets/logo/icon.png`);
//所有窗体
let windows = {
mainWindow: null, //主窗口
customNoticeWindow: null,//鼠标经过托盘图标时出现的自定义系统通知窗口
}
/**
* 创建自定义系统通知窗口
*/
function createCustomNoticeWindow(options) {
windows.customNoticeWindow = new BrowserWindow(Object.assign({
width: 220,
minHeight: 120,
height: 170,
frame: false,// 无边框
show: false,
modal: true,
parent: windows.mainWindow,//指定父窗口
icon: iconPath,// 窗口图标
disableAutoHideCursor: true,// 是否在打字时隐藏光标
resizable: false,//窗口大小是否可调整
movable: false,//窗口是否可移动
alwaysOnTop: true,// 窗口是否永远在别的窗口的上面
fullscreenable: false,//窗口是否可以进入全屏状态
webPreferences: {//网页功能设置。
preload: path.join(__dirname, 'preload.js'),//在页面运行其他脚本之前预先加载指定的脚本 无论页面是否集成Node, 此脚本都可以访问所有Node API 脚本路径为文件的绝对路径。
webSecurity: false,//禁用同源策略
plugins:true,//是否应该启用插件
},
},options))
windows.customNoticeWindow.loadURL(path.join(__dirname,"../electron/customNoticeHtml.html")); // 加载对应的菜单栏页面
//监听到主窗口关闭则清空
windows.customNoticeWindow.on('closed',() => {
windows.customNoticeWindow = null;
})
}
//系统托盘实例
let appTray = null;
/**
* 创建系统托盘
*/
function createAppTray(){
//系统托盘
appTray = new Tray(iconPath);
//系统托盘的提示文本
appTray.setToolTip('熊信');
//点击系统托盘打开窗口
appTray.on('click',() => {
windows.mainWindow.show();
});
//鼠标经过托盘图标时,出现自定义系统通知窗口
if(process.platform !== 'darwin'){ //非mac系统时出现
const appTrayBounds = appTray.getBounds();//获取系统托盘所在位置
createCustomNoticeWindow({x: appTrayBounds.x, y: appTrayBounds.y})//创建 鼠标经过托盘图标时出现的自定义系统通知窗口
let isLeaveTray = true;//存储鼠标是否离开托盘的状态
let isLeaveTimer = null;
appTray.on('mouse-move',() => {//系统托盘鼠标经过时触发
const appTrayBounds = appTray.getBounds();//获取系统托盘所在位置
let params = {}
if(isLeaveTray){
if(!params.x) {
params.x = appTrayBounds.x - (220/2);
}
if(!params.y) {
params.y = appTrayBounds.y - windows.customNoticeWindow.getBounds().height;
}
if(params.x < 0){
params.x = screen.getPrimaryDisplay().bounds.width - params.x
}
if(params.y < 0){
params.y = screen.getPrimaryDisplay().bounds.height - params.y
}
windows.customNoticeWindow.setBounds(params);
windows.customNoticeWindow.show()//显示自定义系统通知窗口
}
isLeaveTray = false;
//检查鼠标是否从托盘离开
clearInterval(isLeaveTimer)
isLeaveTimer = setInterval(() => {
let point = screen.getCursorScreenPoint();
// 判断鼠标是否再托盘内
if(!(appTrayBounds.x < point.x && appTrayBounds.y < point.y && point.x < (appTrayBounds.x + appTrayBounds.width) && point.y < (appTrayBounds.y + appTrayBounds.height))){
// 判断鼠标是否在弹出菜单内
let menuBounds = windows.customNoticeWindow?.getBounds()
if(menuBounds && menuBounds.x < point.x && menuBounds.y < point.y && point.x < (menuBounds.x + menuBounds.width) && point.y < (menuBounds.y + menuBounds.height)) {
console.log('鼠标在新消息菜单内');
return ;
}
// 触发 mouse-leave
clearInterval(isLeaveTimer);
windows.customNoticeWindow.hide(); // 隐藏自定义系统通知窗口
isLeaveTray = true;
console.log("鼠标离开系统托盘图标")
} else {
console.log('鼠标在系统托盘图标内');
}
}, 100)
});
}
}
//创建主窗口
const createMainWindow = () => {
//创建并控制浏览器窗口。
windows.mainWindow = new BrowserWindow({
width: 1200,
height: 900,
minWidth: 900,
minHeight: 600,
frame: false, // 无边框
icon: iconPath,// 窗口图标
webPreferences: {//网页功能设置。
preload: path.join(__dirname, 'preload.js'),//在页面运行其他脚本之前预先加载指定的脚本 无论页面是否集成Node, 此脚本都可以访问所有Node API 脚本路径为文件的绝对路径。
webSecurity: false,//禁用同源策略
nodeIntegration: true,
nodeIntegrationInWorker: true
}
});
//加载路径
windows.mainWindow.loadURL(indexHtml);
//创建系统托盘
createAppTray()
};
//当Electron 初始化完成时触发
app.whenReady().then(() => {
createMainWindow();//创建主窗口
app.on('activate', () => { // macOS 应用通常即使在没有打开任何窗口的情况下也继续运行,并且在没有窗口可用的情况下激活应用时会打开新的窗口。
if (BrowserWindow.getAllWindows().length === 0) createMainWindow();
});
});
//关闭所有窗口时退出应用 ,监听 app 模块的 'window-all-closed' 事件。如果用户不是在 macOS(darwin) 上运行程序,则调用 app.quit()。
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});