当前位置: 首页 > news >正文

Electron_Vue3 自定义系统托盘及退出二次确认

效果:

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.indexconst {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.jsconst {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
}

http://www.dtcms.com/a/540367.html

相关文章:

  • 为什么 Electron 项目推荐使用 Monorepo 架构 [特殊字符][特殊字符][特殊字符]
  • BLIP2 工业实战(一):从零实现 LAVIS 跌倒检测 (微调与“踩坑”指南)
  • NPM下载和安装图文教程(附安装包)
  • 2025 年台湾 5 大 CDP 平台推荐比较
  • 【数据结构】栈(Stack)详解——数据结构的“后进先出”
  • Java 大视界 -- Java 大数据在智能金融理财产品风险评估与个性化配置中的应用
  • Bootstrap4 安装使用指南
  • 怎么建设购物网站免费入驻的网站设计平台
  • vue2 将接口返回数据导出为 excel 文件
  • Java 使用 Spire.XLS 库合并 Excel 文件实践
  • Vultr × Caddy 多站点反向代理 + 负载均衡网关系统实战
  • 【数据结构】(C++数据结构)查找算法与排序算法详解
  • @pytest.fixture函数怎么传变量参数
  • Excel高性能异步导出完整方案!
  • 网站正在建设 敬请期待免费的cms模板
  • 输电线路绝缘子缺陷检测图像数据集VOC+YOLO格式1578张3类别
  • 跨文化理解的困境与AI大模型作为“超级第三方“的桥梁作用
  • JDK版本管理工具JVMS
  • 【JUnit实战3_18】第十章:用 Maven 3 运行 JUnit 测试(上)
  • SQLite 核心知识点讲解
  • JAiRouter v1.1.0 发布:把“API 调没调通”从 10 分钟压缩到 10 秒
  • 自建网站如何赚钱c2c模式为消费者提供了便利和实惠
  • Lua-编译,执行和错误
  • Lua与LuaJIT的安装与使用
  • 数独生成题目lua脚本
  • 影响网站加载速度wordpress获得当前文章的相关文章
  • Hive 技术深度解析与 P7 数据分析架构师多行业全场景实战课程合集(视频教程)
  • 嘉兴高端网站建设公司网络安全等级保护
  • HOW - localstorage 超时管理方案
  • java如何判断上传文件的类型,不要用后缀名判断