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.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
}
相关推荐
袁煦丞3 小时前
开启SSH后,你的NAS竟成私有云“变形金刚”:cpolar内网穿透实验室第645个成功挑战
前端·程序员·远程工作
IT_陈寒3 小时前
SpringBoot 3.2新特性实战:这5个隐藏功能让我开发效率提升50%
前端·人工智能·后端
申阳4 小时前
2小时个人公司:一个全栈开发的精益创业之路
前端·后端·程序员
用户9873824581014 小时前
5. view component
前端
技术小丁4 小时前
零依赖!教你用原生 JS 把 JSON 数组秒变 CSV 文件
前端·javascript
古一|4 小时前
Vue路由两种模式深度解析+Vue+SpringBoot生产部署全流程(附Nginx配置)
javascript·vue.js·nginx
Crystal3284 小时前
原生 Vue + UniApp 的小程序或 App 项目里如何判断用户是否为首次下载应用
前端·vue.js
时间的情敌4 小时前
基于 Vue3 及TypeScript 项目后的总结
前端·vue.js·typescript
lpfasd1234 小时前
从 Electron 转向 Tauri:用 Rust 打造更轻、更快的桌面应用
javascript·rust·electron