{T}

系统托盘

系统托盘(System Tray)允许应用在后台运行,通过托盘图标提供快速访问和状态显示。常用于音乐播放器、下载工具、即时通讯、系统监控等需要常驻后台的应用。

系统架构

code
┌─────────────────────────────────────────────────────────┐
│                     Tray 模块                           │
├─────────────────────────────────────────────────────────┤
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐ │
│  │  托盘图标     │  │  托盘菜单     │  │  托盘事件     │ │
│  │   (Image)    │  │   (Menu)     │  │  (Events)    │ │
│  └──────────────┘  └──────────────┘  └──────────────┘ │
├─────────────────────────────────────────────────────────┤
│                   核心功能模块                          │
│  • 应用常驻后台      • 状态显示        • 快速访问       │
│  • 消息通知          • 右键菜单        • 窗口管理       │
├─────────────────────────────────────────────────────────┤
│                   平台适配层                            │
│  macOS (右上角)    Windows (右下角)    Linux (系统托盘)│
└─────────────────────────────────────────────────────────┘

核心功能模块

1. 托盘图标管理

托盘图标是应用在系统托盘中的视觉标识,支持多种格式和动态变化。

javascript
const { Tray, nativeImage } = require('electron')
const path = require('path')

// 创建托盘图标
const createTrayIcon = () => {
  // 基础图标创建
  const iconPath = path.join(__dirname, 'assets/tray.png')
  let icon = nativeImage.createFromPath(iconPath)
  
  // 根据平台调整尺寸
  if (process.platform === 'darwin') {
    icon = icon.resize({ width: 16, height: 16 })
  } else if (process.platform === 'win32') {
    icon = icon.resize({ width: 16, height: 16 })
  }
  
  return icon
}

// macOS Template Image (自动适配深色/浅色模式)
const createTemplateIcon = () => {
  if (process.platform === 'darwin') {
    // 文件名以 Template 结尾
    return nativeImage.createFromPath(
      path.join(__dirname, 'assets/trayTemplate.png')
    )
  }
  return nativeImage.createFromPath(
    path.join(__dirname, 'assets/tray.png')
  )
}

2. 托盘菜单管理

托盘菜单提供应用的主要操作入口。

javascript
const { Menu } = require('electron')

// 创建基础托盘菜单
const createTrayMenu = (mainWindow, app) => {
  return Menu.buildFromTemplate([
    {
      label: '显示主窗口',
      click: () => {
        mainWindow.show()
        mainWindow.focus()
      }
    },
    {
      label: '设置',
      click: () => mainWindow.webContents.send('open-settings')
    },
    { type: 'separator' },
    {
      label: '退出',
      click: () => {
        app.isQuiting = true
        app.quit()
      }
    }
  ])
}

3. 窗口状态管理

托盘应用需要妥善管理窗口的显示和隐藏。

javascript
const manageWindowState = (mainWindow, app) => {
  // 最小化到托盘而不是关闭
  mainWindow.on('close', (event) => {
    if (!app.isQuiting) {
      event.preventDefault()
      mainWindow.hide()
    }
    return false
  })
  
  // macOS 点击 Dock 图标显示窗口
  app.on('activate', () => {
    if (mainWindow.isMinimized()) {
      mainWindow.restore()
    }
    mainWindow.show()
  })
}

Tray API 完整说明

构造函数

javascript
new Tray(image, guid)
参数类型说明
imageNativeImage | String托盘图标
guidString (Windows)Windows 下用于记住托盘图标位置

核心方法

方法参数返回值说明
destroy()-void销毁托盘图标
setImage(image)NativeImagevoid设置托盘图标
setPressedImage(image)NativeImagevoid设置按下状态的图标 (macOS)
setToolTip(toolTip)Stringvoid设置鼠标悬停提示
setTitle(title)Stringvoid设置托盘标题 (macOS)
getTitle()-String获取托盘标题
setContextMenu(menu)Menu | nullvoid设置右键菜单
getBounds()-Rectangle获取托盘图标位置和尺寸
isDestroyed()-Boolean托盘是否已销毁

事件列表

事件名参数平台说明
clickevent, bounds全平台单击托盘图标
right-clickevent, boundsWin/Linux右键单击
double-clickevent, boundsmacOS/Win双击托盘图标
mouse-enterevent, bounds全平台鼠标进入托盘区域
mouse-leaveevent, bounds全平台鼠标离开托盘区域
mouse-moveevent, boundsWin/macOS鼠标在托盘上移动
balloon-click-Windows点击气泡通知
balloon-closed-Windows气泡通知关闭
drop-textevent, textmacOS文本拖拽到托盘
drop-filesevent, filesmacOS文件拖拽到托盘
drag-entereventmacOS拖拽进入托盘区域
drag-leaveeventmacOS拖拽离开托盘区域
drag-endeventmacOS拖拽结束

基本用法

创建托盘图标

javascript
const { app, Tray, Menu, nativeImage } = require('electron')
const path = require('path')

let tray = null

function createTray() {
  // 创建图标
  const icon = nativeImage.createFromPath(path.join(__dirname, 'icon.png'))
  
  // 创建托盘
  tray = new Tray(icon)
  
  // 设置托盘提示文字
  tray.setToolTip('My Electron App')
  
  // 设置托盘标题(macOS)
  tray.setTitle('App Title')
  
  // 点击托盘图标
  tray.on('click', () => {
    mainWindow.show()
  })
}

托盘菜单

右键菜单

javascript
const { Menu } = require('electron')

function createTray() {
  const icon = nativeImage.createFromPath(path.join(__dirname, 'icon.png'))
  tray = new Tray(icon)
  
  const contextMenu = Menu.buildFromTemplate([
    { label: '显示窗口', click: () => mainWindow.show() },
    { label: '设置', click: () => openSettings() },
    { type: 'separator' },
    { label: '退出', click: () => app.quit() }
  ])
  
  tray.setContextMenu(contextMenu)
}

动态菜单

javascript
let isPlaying = false

function updateTrayMenu() {
  const contextMenu = Menu.buildFromTemplate([
    { 
      label: isPlaying ? '暂停' : '播放', 
      click: () => {
        isPlaying = !isPlaying
        updateTrayMenu()
      }
    },
    { label: '上一曲', click: () => prevTrack() },
    { label: '下一曲', click: () => nextTrack() },
    { type: 'separator' },
    { 
      label: '退出', 
      click: () => app.quit() 
    }
  ])
  
  tray.setContextMenu(contextMenu)
}

带子菜单的托盘菜单

javascript
function createAdvancedTrayMenu() {
  const contextMenu = Menu.buildFromTemplate([
    { 
      label: '播放控制',
      submenu: [
        { label: '播放/暂停', click: () => togglePlay() },
        { label: '上一曲', click: () => prevTrack() },
        { label: '下一曲', click: () => nextTrack() }
      ]
    },
    {
      label: '播放模式',
      submenu: [
        { label: '顺序播放', type: 'radio', checked: true },
        { label: '随机播放', type: 'radio' },
        { label: '单曲循环', type: 'radio' }
      ]
    },
    { type: 'separator' },
    { 
      label: '音量',
      submenu: [
        { label: '静音', type: 'checkbox', click: () => toggleMute() },
        { label: '音量 +', click: () => volumeUp() },
        { label: '音量 -', click: () => volumeDown() }
      ]
    },
    { type: 'separator' },
    { label: '退出', click: () => app.quit() }
  ])
  
  tray.setContextMenu(contextMenu)
}

托盘图标

图标格式与尺寸

javascript
// PNG 图标(推荐)
const icon = nativeImage.createFromPath(path.join(__dirname, 'icon.png'))

// ICO 图标(Windows)
const icon = nativeImage.createFromPath(path.join(__dirname, 'icon.ico'))

// 根据平台创建不同尺寸的图标
const createPlatformIcon = () => {
  const icon = nativeImage.createFromPath('icon.png')
  
  switch (process.platform) {
    case 'darwin':
      // macOS: 16x16 或 22x22
      return icon.resize({ width: 16, height: 16 })
    case 'win32':
      // Windows: 16x16
      return icon.resize({ width: 16, height: 16 })
    default:
      // Linux: 取决于桌面环境
      return icon.resize({ width: 22, height: 22 })
  }
}

动态图标

javascript
// 更换图标(状态指示)
function setTrayIcon(status) {
  const icons = {
    normal: 'icons/tray-normal.png',
    active: 'icons/tray-active.png',
    error: 'icons/tray-error.png'
  }
  
  const icon = nativeImage.createFromPath(
    path.join(__dirname, icons[status])
  )
  tray.setImage(icon)
}

// 显示数字徽章(macOS)
function showBadge(count) {
  if (process.platform === 'darwin') {
    tray.setTitle(count > 0 ? `${count}` : '')
  }
}

// 闪烁提示(用于吸引注意)
let flashInterval = null

function startFlash() {
  const normalIcon = nativeImage.createFromPath('icon.png')
  const emptyIcon = nativeImage.createEmpty()
  
  flashInterval = setInterval(() => {
    const currentIcon = tray.getImage()
    tray.setImage(currentIcon.isEmpty() ? normalIcon : emptyIcon)
  }, 500)
}

function stopFlash() {
  if (flashInterval) {
    clearInterval(flashInterval)
    flashInterval = null
    tray.setImage(nativeImage.createFromPath('icon.png'))
  }
}

Template Image (macOS)

macOS 支持 Template Image,自动适配深色和浅色模式:

javascript
// 创建 Template Image
const createTemplateImage = () => {
  if (process.platform === 'darwin') {
    // 方式1: 文件名以 Template 结尾
    return nativeImage.createFromPath('trayTemplate.png')
    
    // 方式2: 使用模板选项
    const image = nativeImage.createFromPath('tray.png')
    return image.resize({ width: 16, height: 16 })
  }
  
  // 其他平台使用普通图标
  return nativeImage.createFromPath('tray.png')
}

// Template Image 设计规范
// - 使用黑色线条和透明背景
// - 不要使用抗锯齿
// - 保持简洁的设计风格

托盘事件

事件处理

javascript
// 点击事件
tray.on('click', (event, bounds) => {
  console.log('托盘被点击', bounds)
  console.log('托盘位置:', bounds.x, bounds.y)
  mainWindow.show()
})

// 右键点击(Windows/Linux)
tray.on('right-click', () => {
  console.log('右键点击')
  // 在 Windows 上,setContextMenu 已自动处理右键菜单
})

// 双击事件
tray.on('double-click', () => {
  mainWindow.show()
  mainWindow.focus()
})

// 鼠标悬停
tray.on('mouse-enter', () => {
  console.log('鼠标进入托盘区域')
})

// 鼠标离开
tray.on('mouse-leave', () => {
  console.log('鼠标离开托盘区域')
})

// 托盘图标被拖拽
tray.on('drag-start', (event) => {
  console.log('开始拖拽')
})

文件拖拽(macOS)

javascript
// 支持文件拖拽到托盘图标
tray.on('drop-files', (event, files) => {
  console.log('拖放的文件:', files)
  // 处理拖放的文件
  files.forEach(file => {
    openFile(file)
  })
})

// 拖拽事件流程
tray.on('drag-enter', () => {
  console.log('拖拽进入托盘区域')
  // 可以改变图标样式提示用户
})

tray.on('drag-leave', () => {
  console.log('拖拽离开托盘区域')
})

tray.on('drag-end', () => {
  console.log('拖拽结束')
})

完整示例:音乐播放器托盘

javascript
const { app, BrowserWindow, Tray, Menu, nativeImage, ipcMain } = require('electron')
const path = require('path')

let mainWindow
let tray
let isQuiting = false
let playerState = {
  isPlaying: false,
  currentTrack: '',
  volume: 50,
  isMuted: false,
  playMode: 'sequence' // sequence, random, repeat
}

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 1000,
    height: 700,
    show: false,  // 初始隐藏
    webPreferences: {
      preload: path.join(__dirname, 'preload.js')
    }
  })

  mainWindow.loadFile('index.html')

  // 最小化到托盘而不是关闭
  mainWindow.on('close', (event) => {
    if (!isQuiting) {
      event.preventDefault()
      mainWindow.hide()
    }
  })
}

function createTray() {
  const icon = nativeImage.createFromPath(
    path.join(__dirname, 'assets/tray.png')
  ).resize({ width: 16, height: 16 })
  
  tray = new Tray(icon)
  tray.setToolTip('音乐播放器')
  
  updateTrayMenu()
  
  // 点击显示窗口
  tray.on('click', () => {
    mainWindow.show()
    mainWindow.focus()
  })
  
  // 双击显示窗口
  tray.on('double-click', () => {
    mainWindow.show()
    mainWindow.focus()
  })
}

function updateTrayMenu() {
  const contextMenu = Menu.buildFromTemplate([
    {
      label: playerState.isPlaying ? '暂停' : '播放',
      click: () => {
        playerState.isPlaying = !playerState.isPlaying
        mainWindow.webContents.send('player-toggle')
        updateTrayMenu()
      }
    },
    { label: '上一曲', click: () => mainWindow.webContents.send('player-prev') },
    { label: '下一曲', click: () => mainWindow.webContents.send('player-next') },
    { type: 'separator' },
    {
      label: '播放模式',
      submenu: [
        {
          label: '顺序播放',
          type: 'radio',
          checked: playerState.playMode === 'sequence',
          click: () => {
            playerState.playMode = 'sequence'
            updateTrayMenu()
          }
        },
        {
          label: '随机播放',
          type: 'radio',
          checked: playerState.playMode === 'random',
          click: () => {
            playerState.playMode = 'random'
            updateTrayMenu()
          }
        },
        {
          label: '单曲循环',
          type: 'radio',
          checked: playerState.playMode === 'repeat',
          click: () => {
            playerState.playMode = 'repeat'
            updateTrayMenu()
          }
        }
      ]
    },
    { type: 'separator' },
    { label: '显示主窗口', click: () => mainWindow.show() },
    { label: '退出', click: () => {
      isQuiting = true
      app.quit()
    }}
  ])
  
  tray.setContextMenu(contextMenu)
  
  // 更新托盘标题 (macOS)
  if (process.platform === 'darwin' && playerState.isPlaying) {
    tray.setTitle('▶ ' + playerState.currentTrack)
  }
}

// IPC 通信:更新播放器状态
ipcMain.on('update-player-state', (event, state) => {
  playerState = { ...playerState, ...state }
  updateTrayMenu()
  
  // 更新托盘图标状态
  const iconPath = playerState.isPlaying 
    ? 'assets/tray-playing.png'
    : 'assets/tray.png'
  const icon = nativeImage.createFromPath(
    path.join(__dirname, iconPath)
  ).resize({ width: 16, height: 16 })
  tray.setImage(icon)
})

app.whenReady().then(() => {
  createWindow()
  createTray()
})

// macOS 点击 Dock 图标显示窗口
app.on('activate', () => {
  mainWindow.show()
})

// 真正退出前销毁托盘
app.on('before-quit', () => {
  isQuiting = true
  if (tray) tray.destroy()
})

平台差异

平台特性对照表

功能特性macOSWindowsLinux
托盘位置右上角菜单栏右下角任务栏系统托盘区域
图标尺寸16x16 / 22x2216x16取决于桌面环境
Template Image✅ 支持❌ 不支持❌ 不支持
setTitle✅ 显示文本❌ 不显示❌ 不显示
displayBalloon❌ 不支持✅ 气泡通知❌ 不支持
文件拖拽✅ 支持❌ 不支持⚠️ 部分支持
右键菜单⚠️ 需手动处理✅ setContextMenu✅ setContextMenu
高 DPI 支持✅ 自动处理✅ 自动处理⚠️ 视环境而定

平台适配代码

javascript
function createPlatformTray() {
  const isMac = process.platform === 'darwin'
  const isWin = process.platform === 'win32'
  
  // 图标适配
  let icon
  if (isMac) {
    // macOS 使用 Template Image
    icon = nativeImage.createFromPath(path.join(__dirname, 'trayTemplate.png'))
    if (icon.isEmpty()) {
      icon = nativeImage.createFromPath(path.join(__dirname, 'tray.png'))
        .resize({ width: 16, height: 16 })
    }
  } else if (isWin) {
    icon = nativeImage.createFromPath(path.join(__dirname, 'tray.ico'))
    if (icon.isEmpty()) {
      icon = nativeImage.createFromPath(path.join(__dirname, 'tray.png'))
        .resize({ width: 16, height: 16 })
    }
  } else {
    icon = nativeImage.createFromPath(path.join(__dirname, 'tray.png'))
      .resize({ width: 22, height: 22 })
  }
  
  tray = new Tray(icon)
  
  // macOS 特殊处理
  if (isMac) {
    tray.on('right-click', (event, bounds) => {
      // macOS 右键需要手动弹出菜单
      tray.popUpContextMenu()
    })
  }
  
  // Windows 气泡通知
  if (isWin) {
    tray.displayBalloon({
      icon: nativeImage.createFromPath(path.join(__dirname, 'tray.png')),
      title: '应用已启动',
      content: '应用已最小化到系统托盘',
      respectQuietTime: true
    })
  }
  
  return tray
}

Windows 气泡通知

Windows 平台独有的气泡通知功能,用于向用户显示提醒信息。

javascript
// 显示气泡通知
function showBalloon(title, content) {
  if (process.platform !== 'win32') {
    console.log('气泡通知仅支持 Windows 平台')
    return
  }
  
  tray.displayBalloon({
    icon: nativeImage.createFromPath(path.join(__dirname, 'icon.png')),
    title: title,
    content: content,
    respectQuietTime: true  // 尊重免打扰模式
  })
}

// 气泡通知事件
tray.on('balloon-click', () => {
  mainWindow.show()
  mainWindow.focus()
})

tray.on('balloon-closed', () => {
  console.log('气泡通知已关闭')
})

// 示例:下载完成通知
function onDownloadComplete(fileName) {
  showBalloon('下载完成', `${fileName} 已下载完成`)
}

错误处理

图标加载错误

javascript
function createTraySafely() {
  try {
    let icon = nativeImage.createFromPath(path.join(__dirname, 'tray.png'))
    
    // 检查图标是否加载成功
    if (icon.isEmpty()) {
      console.warn('托盘图标加载失败,使用默认图标')
      // 创建默认图标
      icon = nativeImage.createEmpty()
    }
    
    tray = new Tray(icon)
    
    return tray
  } catch (error) {
    console.error('创建托盘失败:', error)
    return null
  }
}

菜单更新错误

javascript
function updateTrayMenuSafely() {
  if (!tray || tray.isDestroyed()) {
    console.warn('托盘不存在或已销毁')
    return
  }
  
  try {
    const contextMenu = Menu.buildFromTemplate([
      { label: '菜单项', click: () => {} }
    ])
    tray.setContextMenu(contextMenu)
  } catch (error) {
    console.error('更新托盘菜单失败:', error)
  }
}

托盘销毁处理

javascript
// 应用退出前销毁托盘
app.on('before-quit', () => {
  if (tray && !tray.isDestroyed()) {
    tray.destroy()
    tray = null
  }
})

// 安全更新托盘
function safeTrayUpdate(updateFn) {
  if (!tray || tray.isDestroyed()) {
    return
  }
  
  try {
    updateFn()
  } catch (error) {
    console.error('托盘更新失败:', error)
  }
}

性能优化

1. 图标优化

javascript
// 缓存图标实例
const iconCache = new Map()

function getCachedIcon(iconPath) {
  if (!iconCache.has(iconPath)) {
    const icon = nativeImage.createFromPath(iconPath)
      .resize({ width: 16, height: 16 })
    iconCache.set(iconPath, icon)
  }
  return iconCache.get(iconPath)
}

// 预加载图标
function preloadIcons() {
  const icons = ['tray.png', 'tray-playing.png', 'tray-paused.png']
  icons.forEach(icon => {
    getCachedIcon(path.join(__dirname, 'assets', icon))
  })
}

2. 菜单更新优化

javascript
// 避免频繁重建菜单
let lastMenuState = ''

function updateMenuIfNeeded(newState) {
  const stateKey = JSON.stringify(newState)
  
  if (lastMenuState !== stateKey) {
    updateTrayMenu(newState)
    lastMenuState = stateKey
  }
}

// 使用防抖更新
const debounce = require('lodash.debounce')

const debouncedMenuUpdate = debounce((state) => {
  updateTrayMenu(state)
}, 100)

3. 事件处理优化

javascript
// 避免频繁的事件触发
let lastClickTime = 0

tray.on('click', (event, bounds) => {
  const now = Date.now()
  if (now - lastClickTime < 300) {
    // 忽略快速连续点击
    return
  }
  lastClickTime = now
  
  mainWindow.show()
  mainWindow.focus()
})

安全注意事项

1. 托盘图标安全

javascript
// 验证图标路径
function validateIconPath(iconPath) {
  const allowedDir = path.join(__dirname, 'assets')
  const resolvedPath = path.resolve(iconPath)
  
  if (!resolvedPath.startsWith(allowedDir)) {
    throw new Error('Invalid icon path')
  }
  
  return resolvedPath
}

// 安全加载图标
function safeLoadIcon(iconName) {
  try {
    const iconPath = validateIconPath(
      path.join(__dirname, 'assets', iconName)
    )
    return nativeImage.createFromPath(iconPath)
  } catch (error) {
    console.error('图标加载失败:', error)
    return nativeImage.createEmpty()
  }
}

2. IPC 通信安全

javascript
// 主进程:验证消息来源
ipcMain.on('update-tray', (event, data) => {
  // 验证发送者
  const webContents = event.sender
  const mainWindow = BrowserWindow.getAllWindows()[0]
  
  if (webContents !== mainWindow.webContents) {
    console.warn('拒绝来自未知源的托盘更新请求')
    return
  }
  
  // 验证数据格式
  if (typeof data !== 'object' || data === null) {
    console.warn('无效的托盘更新数据')
    return
  }
  
  updateTrayMenu(data)
})

// 渲染进程:使用 contextBridge 安全暴露
// preload.js
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('tray', {
  updateMenu: (data) => ipcRenderer.send('update-tray', data)
})

3. 防止托盘劫持

javascript
// 限制托盘实例数量
let trayInstance = null

function createTray() {
  if (trayInstance && !trayInstance.isDestroyed()) {
    console.warn('托盘实例已存在')
    return trayInstance
  }
  
  trayInstance = new Tray(createIcon())
  return trayInstance
}

// 应用退出时清理
app.on('will-quit', () => {
  if (trayInstance) {
    trayInstance.destroy()
    trayInstance = null
  }
})

实际应用场景

场景 1:下载管理器

javascript
// 托盘菜单实时显示下载进度
let downloadProgress = 0
let isDownloading = false

function updateDownloadProgress(progress) {
  downloadProgress = progress
  
  // 更新托盘标题(macOS)
  if (process.platform === 'darwin') {
    tray.setTitle(`${Math.round(progress)}%`)
  }
  
  // 更新菜单
  updateDownloadMenu()
}

function updateDownloadMenu() {
  const contextMenu = Menu.buildFromTemplate([
    {
      label: isDownloading 
        ? `下载中... ${Math.round(downloadProgress)}%` 
        : '无下载任务',
      enabled: false
    },
    { type: 'separator' },
    {
      label: isDownloading ? '暂停下载' : '继续下载',
      enabled: true,
      click: () => toggleDownload()
    },
    { type: 'separator' },
    { label: '退出', click: () => app.quit() }
  ])
  
  tray.setContextMenu(contextMenu)
}

场景 2:系统监控工具

javascript
// 托盘实时显示系统状态
function updateSystemStatus(cpu, memory) {
  // 更新托盘图标颜色指示状态
  let iconColor = 'green'
  if (cpu > 80 || memory > 80) {
    iconColor = 'red'
  } else if (cpu > 50 || memory > 50) {
    iconColor = 'yellow'
  }
  
  const icon = nativeImage.createFromPath(
    path.join(__dirname, `assets/tray-${iconColor}.png`)
  )
  tray.setImage(icon)
  
  // 更新提示文本
  tray.setToolTip(`CPU: ${cpu}% | 内存: ${memory}%`)
  
  // 更新托盘标题(macOS)
  if (process.platform === 'darwin') {
    tray.setTitle(`${Math.round(cpu)}%`)
  }
}

// 定时更新
setInterval(() => {
  const cpuUsage = getCpuUsage()
  const memoryUsage = getMemoryUsage()
  updateSystemStatus(cpuUsage, memoryUsage)
}, 1000)

场景 3:即时通讯工具

javascript
// 托盘显示未读消息数量
let unreadCount = 0

function updateUnreadCount(count) {
  unreadCount = count
  
  // 更新托盘标题(macOS)
  if (process.platform === 'darwin') {
    tray.setTitle(count > 0 ? `${count}` : '')
  }
  
  // 更新托盘图标(Windows/Linux)
  if (count > 0) {
    const icon = nativeImage.createFromPath(
      path.join(__dirname, 'assets/tray-unread.png')
    )
    tray.setImage(icon)
    
    // Windows 气泡通知
    if (process.platform === 'win32') {
      tray.displayBalloon({
        title: '新消息',
        content: `您有 ${count} 条未读消息`,
        respectQuietTime: true
      })
    }
  } else {
    const icon = nativeImage.createFromPath(
      path.join(__dirname, 'assets/tray.png')
    )
    tray.setImage(icon)
  }
}

常见问题解答

1. 如何让窗口关闭时最小化到托盘?

javascript
let mainWindow
let isQuiting = false

mainWindow.on('close', (event) => {
  if (!isQuiting) {
    event.preventDefault()
    mainWindow.hide()
  }
})

// 退出菜单项
{ 
  label: '退出', 
  click: () => {
    isQuiting = true
    app.quit()
  }
}

2. 如何在 macOS 上正确显示托盘图标?

javascript
// macOS 使用 Template Image
if (process.platform === 'darwin') {
  // 方式1: 文件名以 Template 结尾
  const icon = nativeImage.createFromPath('trayTemplate.png')
  
  // 方式2: 创建合适尺寸
  const icon = nativeImage.createFromPath('tray.png')
    .resize({ width: 16, height: 16 })
  
  tray = new Tray(icon)
}

3. 托盘图标不显示怎么办?

检查以下几点:

  1. 图标路径是否正确
  2. 图标是否加载成功(检查 icon.isEmpty()
  3. 是否在 app.whenReady() 后创建托盘
  4. 图标尺寸是否符合平台要求
javascript
function createTray() {
  const iconPath = path.join(__dirname, 'assets/tray.png')
  const icon = nativeImage.createFromPath(iconPath)
  
  if (icon.isEmpty()) {
    console.error('图标加载失败:', iconPath)
    return
  }
  
  tray = new Tray(icon)
}

app.whenReady().then(createTray)

4. 如何实现托盘图标的右键菜单在 macOS 上正常工作?

javascript
function createTray() {
  const tray = new Tray(icon)
  
  const contextMenu = Menu.buildFromTemplate([
    { label: '菜单项', click: () => {} }
  ])
  
  // macOS 需要监听右键事件
  if (process.platform === 'darwin') {
    tray.on('right-click', () => {
      tray.popUpContextMenu(contextMenu)
    })
  } else {
    // Windows/Linux 可以直接设置
    tray.setContextMenu(contextMenu)
  }
}

5. 如何获取托盘图标的位置?

javascript
const bounds = tray.getBounds()
console.log('托盘位置:', bounds.x, bounds.y)
console.log('托盘尺寸:', bounds.width, bounds.height)

// 在托盘附近显示窗口
function showWindowNearTray() {
  const { x, y, width, height } = tray.getBounds()
  const windowWidth = 300
  const windowHeight = 400
  
  mainWindow.setPosition(
    x - windowWidth / 2 + width / 2,
    y - windowHeight
  )
  mainWindow.show()
}

6. 如何防止托盘应用多开?

javascript
const gotTheLock = app.requestSingleInstanceLock()

if (!gotTheLock) {
  app.quit()
} else {
  app.on('second-instance', () => {
    // 当运行第二个实例时,聚焦到已有窗口
    if (mainWindow) {
      mainWindow.show()
      mainWindow.focus()
    }
  })
  
  app.whenReady().then(() => {
    createWindow()
    createTray()
  })
}

最佳实践

1. 图标准备

  • macOS: 准备 16x16 和 22x22 的 Template Image
  • Windows: 准备 16x16 的 ICO 或 PNG 图标
  • Linux: 准备 22x22 的 PNG 图标
  • 提供 dark 和 light 两套图标(非 macOS)

2. 用户体验

  • 提供清晰的托盘提示文本(setToolTip
  • 确保点击托盘能恢复窗口
  • 提供明确的退出菜单项
  • 最小化到托盘而不是关闭窗口

3. 状态同步

  • 菜单状态应与窗口状态同步
  • 使用动态菜单反映当前状态
  • 避免过度的菜单更新频率

4. 跨平台兼容

javascript
// 使用条件判断处理平台差异
const isMac = process.platform === 'darwin'
const isWin = process.platform === 'win32'

// macOS 特定功能
if (isMac) {
  tray.setTitle('状态文本')
}

// Windows 特定功能
if (isWin) {
  tray.displayBalloon({ title: '通知', content: '内容' })
}

5. 资源管理

javascript
// 应用退出时清理资源
app.on('before-quit', () => {
  if (tray) {
    tray.destroy()
  }
})

// 清理定时器和监听器
let updateInterval

function cleanup() {
  if (updateInterval) {
    clearInterval(updateInterval)
  }
  if (tray) {
    tray.removeAllListeners()
    tray.destroy()
  }
}

参考链接