返回顶部

electron+vue——托盘图标及菜单实现

效果:

AF6DCF89-67ED-404d-AC0C-18BD770CDF03

 我的项目结构:

image

  background.js:

"use strict";
import { app, BrowserWindow, Menu, Tray, nativeImage } from "electron";
import * as path from "path";
const fs = require("fs");

let mainWindow = null; // 窗口
let tray = null; // 托盘

// 获取图标路径
function getIconPath() {
  let iconPath = "";
    const iconFile = process.platform === "win32" ? "icon.ico" : "icon.png"; // 图标文件名,linux下需要png格式,不然会不显示
  // 判断是否在打包模式
  if (app.isPackaged) {
    // 打包模式
    iconPath = path.join(process.resourcesPath, "icons", iconFile); // 路径根据自身情况调整
  } else {
    // 开发模式
    const projectRoot = path.join(__dirname, ".."); // __dirname 指向 dist_electron,但资源在项目根目录,所以..指向上一级
    iconPath = path.join(projectRoot, "icons", iconFile);
  }

  // 检查文件是否存在
  if (fs.existsSync(iconPath)) {
    console.log("图标文件存在:", iconPath);
    return iconPath;
  } else {
    console.error("图标文件不存在:", iconPath);
    // 返回一个默认图标路径或null
    return null;
  }
}

function createWindow() {
  // 创建浏览器窗口
  mainWindow = new BrowserWindow({
    // 其他
    icon: getIconPath(), // 本地启动图标
  });
}

// 创建系统托盘图标和菜单
function createTray() {
  // 创建图标
  const iconPath = getIconPath();
  const icon = nativeImage.createFromPath(iconPath);
  // 调整图标大小(可选)
  icon.resize({ width: 16, height: 16 });
  tray = new Tray(icon);

  // 创建上下文菜单
  const contextMenu = Menu.buildFromTemplate([
    {
      label: "显示",
      click: () => {
        mainWindow.show();
      },
    },
    {
      label: "隐藏",
      click: () => {
        mainWindow.hide();
      },
    },
    {
      label: "退出",
      click: () => {
        mainWindow.destroy();
      },
    },
  ]);

  tray.setContextMenu(contextMenu); // 设置托盘菜单
  tray.setToolTip("测试应用"); // 设置托盘图标的提示文本

  // 托盘图标右键事件(可选)
  tray.on("right-click", () => {
    tray.popUpContextMenu(); // 弹出托盘菜单
  });
}

// 主进程准备完毕
app.on("ready", async () => {
  createWindow();
  createTray();
});

vue.config.js:

const appName = "测试应用";
module.exports = {
  pluginOptions: {
    electronBuilder: {
      builderOptions: {
        artifactName: "${productName}.${ext}", // 打包生成的安装包名称,默认会有 "Setup ${version}" 后缀
        appId: `com.alpha.${appName}.app`, // 应用的唯一标识符
        productName: appName, // 应用名称
        win: {
          icon: "icons/icon.ico",
          target: [
            {
              target: "nsis",
              arch: ["x64", "ia32"], // 兼容32位
            },
          ],
        },
        linux: {
          icon: "icons/icon.png",
          target: [
            {
              target: "tar.gz",
              arch: ["x64"],
            },
          ],
          category: "Utility",
        },
        // 添加 nsis 配置避免其他错误
        nsis: {
          oneClick: false,
          perMachine: true,
          allowToChangeInstallationDirectory: true,
        },
        // 关键,确保打包后图标文件存在于运行时可访问的位置
        extraResources: [
          {
            from: "icons",
            to: "icons",
            filter: ["**/*"],
          },
        ],
      },
    },
  },
};

 

 动态设置菜单

1.新建preload.js(与background.js同级)

import { contextBridge, ipcRenderer } from "electron";

// 安全地暴露 API 给渲染进程
contextBridge.exposeInMainWorld("desktopHandler", {
  // 动态设置托盘菜单:传入一个数组模板,返回是否设置成功
  setTrayMenu: (templateArr) =>
    ipcRenderer.invoke("set-tray-menu", templateArr),
  // 监听托盘菜单点击事件,回调参数为 {id}
  onTrayMenuClick: (callback) =>
    ipcRenderer.on("tray-menu-click", (_, data) => callback(data)),
});

2.引入preload.js

background.js中:

function createWindow() {
  // 创建浏览器窗口
  mainWindow = new BrowserWindow({
    // 其他
    webPreferences: {
      nodeIntegration: true, // 是否启用 Node.js 集成
      contextIsolation: true, // 是否启用上下文隔离,启用时,需通过preload.js安全暴露 ipcRenderer
      preload: path.join(__dirname, "preload.js"),
    },
  });
}

vue.config.js中:

module.exports = {
  pluginOptions: {
    electronBuilder: {
      // 其他
      preload: "src/preload.js"
    },
  },
};

3.动态创建菜单

background.js中:

// 将渲染进程传来的菜单模板转换为 Electron 可用的模板,并在点击时发送事件回渲染进程
function buildMenuTemplate(arr) {
  return arr.map((item) => {
    // 点击时把id发回渲染进程
    item.click = () => {
      try {
        if (mainWindow && mainWindow.webContents) {
          mainWindow.webContents.send("tray-menu-click", {
            id: item.id,
          });
        }
      } catch (e) {
        console.warn("发送 tray-menu-click 失败", e);
      }
    };

    return item;
  });
}

// 动态设置托盘菜单模板(渲染进程调用)
function setTrayMenu(arr) {
  if (!tray) {
    console.warn("托盘尚未初始化,无法设置菜单");
    return false;
  }
  try {
    const built = buildMenuTemplate(arr);
    const contextMenu = Menu.buildFromTemplate(built);
    tray.setContextMenu(contextMenu);
    return true;
  } catch (e) {
    console.error("设置托盘菜单失败", e);
    return false;
  }
}

// 创建系统托盘图标和菜单
function createTray() {
  // 创建图标
  const iconPath = getIconPath();
  const icon = nativeImage.createFromPath(iconPath);
  // 调整图标大小(可选)
  icon.resize({ width: 16, height: 16 });
  tray = new Tray(icon);
  tray.setToolTip("联通5G专网安全盾"); // 设置托盘图标的提示文本
  // 托盘图标右键事件(可选)
  tray.on("right-click", () => {
    tray.popUpContextMenu(); // 弹出托盘菜单
  });
}


// 主进程准备完毕
app.on("ready", async () => {
  // 渲染进程调用以动态更新托盘菜单
  ipcMain.handle("set-tray-menu", async (_, template) => {
    return setTrayMenu(template);
  });

  ipcMain.handle("get-tray-menu", async () => {
    // 无法直接反序列化 Menu,因此只返回一个标记表示是否存在菜单
    return { hasTray: !!tray };
  });

  createWindow();
  createTray();
});

4.App.vue或者其他组件中调用

mounted() {
    const menu = [
      { id: "show", label: "显示" },
      { id: "hide", label: "隐藏" },
      { id: "exit", label: "退出" },
    ];
    window.desktopHandler
      .setTrayMenu(menu)
      .then((result) => {
        // 设置成功
      })
      .catch((error) => {
        // 设置失败
        console.error("设置托盘菜单失败:", error);
      });
    // 设置点击事件
    window.desktopHandler.onTrayMenuClick((data) => {
      switch (data.id) {
        case "show":
          console.log('show');
          break;
        case "hide":
          console.log('hide');
          break;
        case "exit":
          console.log('exit');
          break;
        default:
          break;
      }
    });
  },

 

posted @ 2025-12-04 15:33  前端-xyq  阅读(6)  评论(0)    收藏  举报