Electron实现自定义系统通知的效果

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();
});
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
实现自定义菜单栏,可以使用 Electron 提供的 Menu 模块。以下是一个使用 Electron Vite Vue 实现自定义菜单栏的步骤: 1. 在 Vue 组件中引入 Electron 的 remote 模块,用于获取主进程的 Menu 对象。 ```javascript import { remote } from 'electron' const Menu = remote.Menu ``` 2. 在 Vue 组件的生命周期钩子函数中创建菜单项,可以使用 Menu.buildFromTemplate 方法创建菜单项的数组。 ```javascript created() { const template = [ { label: '文件', submenu: [ { label: '新建', accelerator: 'CmdOrCtrl+N', click: this.handleNew }, { label: '打开', accelerator: 'CmdOrCtrl+O', click: this.handleOpen }, { type: 'separator' }, { label: '保存', accelerator: 'CmdOrCtrl+S', click: this.handleSave }, { label: '另存为', accelerator: 'Shift+CmdOrCtrl+S', click: this.handleSaveAs }, { type: 'separator' }, { label: '退出', accelerator: 'CmdOrCtrl+Q', click: this.handleQuit } ] }, { label: '编辑', submenu: [ { label: '撤销', accelerator: 'CmdOrCtrl+Z', role: 'undo' }, { label: '重做', accelerator: 'Shift+CmdOrCtrl+Z', role: 'redo' }, { type: 'separator' }, { label: '剪切', accelerator: 'CmdOrCtrl+X', role: 'cut' }, { label: '复制', accelerator: 'CmdOrCtrl+C', role: 'copy' }, { label: '粘贴', accelerator: 'CmdOrCtrl+V', role: 'paste' }, { label: '全选', accelerator: 'CmdOrCtrl+A', role: 'selectAll' } ] } ] const menu = Menu.buildFromTemplate(template) Menu.setApplicationMenu(menu) } ``` 3. 在 Vue 组件中实现菜单项的点击事件。 ```javascript methods: { handleNew() { // 新建文件 }, handleOpen() { // 打开文件 }, handleSave() { // 保存文件 }, handleSaveAs() { // 另存为文件 }, handleQuit() { // 退出应用程序 } } ``` 这样就可以在 Electron Vite Vue 应用程序中实现自定义菜单栏了。需要注意的是,菜单项的点击事件可以调用主进程中的方法,例如使用 ipcRenderer 发送消息给主进程,让主进程执行相应的操作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值