效果:
1、菜单位置会跟随应用logo位置变化
2、样式支持自定义

实现:
1、electron 创建托盘图标
复制代码
const {app, BrowserWindow, Tray, Menu, nativeImage, Notification, session, screen, const {app, BrowserWindow, Tray, Menu, nativeImage, Notification, session, screen, ipcMain } = require('electron');
let tray; // 系统托盘图标
let customWindow = null; // 自定义窗口实例:右键系统托盘图标展示的菜单
/**
* 创建系统托盘
*/
function createTray() {
try {
// 托盘图标路径(建议使用 .ico 或 .png,不同系统有适配要求)
const iconPath = path.join(__dirname, './assets/logo.png');
tray = new Tray(iconPath);
// 托盘悬停提示文本
tray.setToolTip('强总即时通讯');
// 监听右键点击事件
tray.on('right-click', () => {
toggleCustomWindow(); // 切换自定义窗口显示/隐藏
});
// 可选:监听左键点击(如需要)
tray.on('click', () => {
// 左键点击打开主窗口
let openPageList = ['home', 'login']
const windows = BrowserWindow.getAllWindows();
if (windows) {
for (let window of windows) {
if (openPageList.includes(window.appPageId)) {
window.show()
return
}
}
}
});
} catch (e) {
console.log("创建系统托盘 err", e.message)
}
}
/**
* 创建/显示/隐藏系统托盘窗口
*/
function toggleCustomWindow() {
try { // 如果窗口已存在,直接切换显示状态
if (customWindow) {
if (customWindow.isVisible()) {
customWindow.hide();
} else {
customWindow.show();
positionWindowNearTray(); // 重新定位窗口
}
return;
}
// 创建系统托盘窗口(无边框、置顶、小尺寸)
customWindow = new BrowserWindow({
width: 120, // 自定义窗口宽度
height: 160, // 自定义窗口高度
frame: false, // 无边框(去掉标题栏)
resizable: false, // 不可缩放
alwaysOnTop: true, // 始终置顶
skipTaskbar: true, // 不在任务栏显示
webPreferences: {
contextIsolation: true,
preload: path.join(__dirname, './views/tray/preload.js'), // 弹窗专属预加载脚本
devTools: true, // 关闭弹窗的开发者工具(生产环境建议关闭)
sandbox: false,
nodeIntegration: false,
session: null,
zoomFactor: 1.0,
webSecurity: false,
}
});
if(config.developmentMode) {
customWindow.loadURL(config.htmlRoot + 'tray.html').then()
} else {
customWindow.loadFile(config.htmlRoot + 'tray.html').then()
}
// 窗口关闭时清理实例
customWindow.on('closed', () => {
customWindow = null;
});
// 点击窗口外区域关闭窗口
customWindow.on('blur', () => {
if (!customWindow.isDestroyed()) {
customWindow.hide();
}
});
// 首次显示时定位窗口
positionWindowNearTray();
} catch (e) {
console.log("创建/显示/隐藏自定义窗口 err", e.message)
}
}
/**
* 计算窗口位置(显示在托盘图标附近)
*/
function positionWindowNearTray() {
try {
if (!tray || !customWindow) return;
// 获取托盘图标的边界信息(位置和尺寸)
const trayBounds = tray.getBounds();
// 获取屏幕尺寸
const primaryDisplay = screen.getPrimaryDisplay();
const {workArea} = primaryDisplay; // 工作区(排除任务栏)
// 获取窗口尺寸
const windowBounds = customWindow.getBounds();
// 计算窗口位置(默认显示在托盘上方,水平居中对齐)
let x = Math.round(trayBounds.x + (trayBounds.width / 2) - (windowBounds.width / 2));
let y = Math.round(trayBounds.y - windowBounds.height); // 托盘上方
// 确保窗口不超出屏幕左边界
if (x < workArea.x) {
x = workArea.x;
}
// 确保窗口不超出屏幕右边界
if (x + windowBounds.width > workArea.x + workArea.width) {
x = workArea.x + workArea.width - windowBounds.width;
}
// 确保窗口不超出屏幕上边界(如果托盘在顶部)
if (y < workArea.y) {
y = trayBounds.y + trayBounds.height; // 显示在托盘下方
}
// 设置窗口位置
customWindow.setPosition(x, y);
} catch (e) {
console.log("计算窗口位置(显示在托盘图标附近) err", e.message)
}
}
app.on('ready', () => {
initLogger(); // 先初始化日志,再执行其他逻辑(如创建窗口)
createTray();
}
2、添加静态文件(html 文件)
不使用vue的原因:
1、目前没找到实现窗口定位的方法,自动会在屏幕中间,不符合要求
2、一个系统托盘,没必要使用vue,会导致打包后的文件体积过大
复制代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
// 样式文件放在自定义位置,引用就可以
<link rel="stylesheet" href="./src/views/tray/index.scss">
<title>系统托盘</title>
</head>
<body>
<div id="app">
<div class="item about">
<img src="./src/views/tray/assets/about.svg" alt="" srcset="">
<span class="label">关于</span>
</div>
<div class="item update">
<img src="./src/views/tray/assets/checkForUpdates.svg" alt="" srcset="">
<span class="label">检查更新</span>
</div>
<!-- <div class="item help">-->
<!-- <img src="./src/views/tray/assets/help.jpg" alt="" srcset="">-->
<!-- <span class="label">帮助</span>-->
<!-- </div>-->
<div class="item quit">
<img src="./src/views/tray/assets/quit.jpg" alt="" srcset="">
<span class="label">退出</span>
</div>
</div>
// 脚本放到自定义位置,关联就可以
<script type="module" src="./src/views/tray/index.js"></script>
</body>
</html>
3、添加js脚本
复制代码
// 获取所有按钮的父容器(或直接获取所有.item元素)
const actionBox = document.getElementById('app');
console.log("actionBox", actionBox)
// 绑定统一的点击事件
actionBox.addEventListener('click', function (e) {
// 找到被点击的最外层.item元素(可能点击的是img或span,需要向上查找)
const clickedItem = e.target.closest('.item');
if (!clickedItem) return; // 不是按钮区域则退出
// 通过类名区分按钮类型
if (clickedItem.classList.contains('help')) {
// 处理帮助按钮逻辑
window.close()
handleHelpAction();
} else if (clickedItem.classList.contains('quit')) {
// 处理退出按钮逻辑
handleQuitAction();
} else if (clickedItem.classList.contains('update')) {
// 检查更新
handleUpdateAction();
} else if (clickedItem.classList.contains('about')) {
// 关于
handleAboutAction();
}
});
// 帮助按钮具体逻辑
function handleHelpAction() {
window.trayApi.help();
}
// 退出按钮具体逻辑
function handleQuitAction() {
console.log("监听系统托盘:退出")
window.trayApi.quit();
}
// 检查更新
function handleUpdateAction() {
window.trayApi.update();
}
// 关于
function handleAboutAction() {
window.trayApi.about();
}
4、electron 配置主进程与渲染进程的交互
复制代码
// tray.index
const {showQuitConfirm, trayOpen, TRAYLIST} = require("./trayOperate");
// 监听系统托盘:退出
ipcMain.on(`tray:${TRAYLIST['quit']}`, (event) => {
console.log('监听系统托盘:退出')
// app.quit();
showQuitConfirm()
});
// 监听系统托盘:帮助
ipcMain.on(`tray:${TRAYLIST['help']}`, async (event) => {
console.log("帮助")
trayOpen('help')
});
// 监听系统托盘:关于
ipcMain.on(`tray:${TRAYLIST['about']}`, async (event) => {
trayOpen('about')
});
ipcMain.on(`tray:${TRAYLIST['update']}`, async (event) => {
trayOpen('update')
});
// tray/preload.js
/**
* 系统托盘窗口 预加载模块
*/
console.log('load: ', __filename);
const {contextBridge, ipcRenderer} = require('electron/renderer')
// api 类别
const api = 'tray'
contextBridge.exposeInMainWorld(api + 'Api', {
/**
* 退出
*/
quit: () => {
ipcRenderer.send('tray:quit');
},
/**
* 帮助
*/
help: () => {
ipcRenderer.send('tray:help');
},
/**
* 检查更新
*/
update: () => {
ipcRenderer.send('tray:update');
},
/**
* 关于
*/
about: () => {
ipcRenderer.send('tray:about');
},
})
// 引入通用api
const exposeWindowApi = require('../base/base_window_preload')
exposeWindowApi()
// trayOperate.js
const {BrowserWindow, Tray, screen} = require("electron");
const {ResponseBuilder} = require("../../util/responseBuilder");
const {config} = require("../../config");
const HelpWindow = require("../help");
const path = require('path');
const TRAYLIST = {
help: 'help',
about: 'about',
update: 'update',
quit: 'quit',
login: "login",
home: "home",
}
/**
* 系统托盘退出/任务栏退出弹窗提示
*/
function showQuitConfirm(win1 = "home"){
console.log("系统托盘退出/任务栏退出弹窗提示", win1)
const windows = BrowserWindow.getAllWindows();
// 先找到目标窗口(只找第一个匹配的)
for (let window of windows) {
console.log("目标窗口", window.appPageId)
if (window.appPageId === TRAYLIST['login']) {
app.quit();
return;
}
if (window.appPageId === TRAYLIST[win1]) {
window.show();
window.webContents.send('Api:onTrayQuit', new ResponseBuilder().setTemp().build());
break; // 找到后立即退出循环,避免重复
}
}
}
function trayOpen(winName){
const windows = BrowserWindow.getAllWindows();
for (let window of windows) {
if (window.appPageId === TRAYLIST[winName]) {
window.show() // 窗口置顶
return
}
}
const url = config.htmlRoot + TRAYLIST[winName] + '.html';
const win = new HelpWindow(url)
win.create()
}
module.exports = {
showQuitConfirm,
trayOpen,
TRAYLIST
}