Tauri (26)——托盘图标总对不上系统主题?一行 Template Image 搞定

写了一个桌面应用,发现托盘图标颜色跟系统主题不一致。很多人第一反应是手动检测明暗模式然后换图标,但其实 macOS 早就给了解决方案 ------ 只是你可能不知道而已。看下去,让你少走弯路。


起因:托盘图标颜色不对劲

事情是这样的。我最近在做 PinWall ------ 一个 macOS 桌面透明便签应用,基于 Tauri v2 构建。

有一次我打开应用,发现菜单栏上的托盘图标颜色不对。别的 app 的图标是白色的,我的 app 还是黑色的,一眼就能看到差异,强迫症直接犯病。

对,就是那种"明明别人都对,就我不对"的感觉。

很多人的第一反应:手动检测明暗模式

既然系统有明暗两种模式,那我检测到当前是哪种模式,然后换不同的图标不就行了?

我之前就是这么做的。思路很朴素,代码也很直观:

rust 复制代码
#[cfg(target_os = "macos")]
fn is_dark_mode() -> bool {
    match std::process::Command::new("defaults")
        .args(["read", "-g", "AppleAquaColorVariant"])
        .output()
    {
        Ok(out) if out.status.success() => {
            // AppleAquaColorVariant == 6 表示深色模式
            String::from_utf8_lossy(&out.stdout).trim() == "6"
        }
        _ => false,
    }
}

fn tray_icon_bytes() -> &'static [u8] {
    if cfg!(target_os = "macos") && is_dark_mode() {
        include_bytes!("../icons/tray/icon_white_32.png")
    } else {
        include_bytes!("../icons/tray/icon_32.png")
    }
}

启动时读一下 AppleAquaColorVariant,如果是 6 就用白色图标,否则用黑色图标。看起来没什么问题,对吧?

然后问题就来了。

AppleAquaColorVariant 读的是全局偏好设置,也就是系统设置里那个"深色/浅色"开关的值。它不管你有没有外接显示器,也不管每个显示器各自的模式是什么。它只会告诉你:"用户在全局设置里选了深色模式"。

所以当我本机是深色、外接是浅色的时候,is_dark_mode() 永远返回 true,托盘图标永远用白色------浅色显示器上看起来就不对了。

第二次尝试:监听系统外观变化

那我在程序里监听外观变化行不行?比如用户拔了外接显示器,或者切换了某个显示器的模式,我能收到通知然后换图标?

查了一圈文档,macOS 确实有 NSWorkspace.didChangeScreenParametersNotification 之类的通知,但问题是:

  1. 这些通知是 Cocoa / AppKit 层的 API
  2. 我用的是 Tauri(Rust 后端),要通过 objc 桥接才能调用
  3. 最关键的是------即使能监听到,我也没法知道每个显示器各自是深色还是浅色。macOS 没有暴露"当前哪个显示器是什么模式"这样的 API

折腾了半天,发现手动适配这条路走不通。手动检测永远会有边缘情况覆盖不到。

正确的解法:让 macOS 自己来处理

手动适配不行,那就换个思路------不手动适配,交给系统

macOS 原生提供了一种图标处理方式叫 Template Image 。当你把一个图标标记为 template 时,macOS 就不会把它当成一张普通的彩色图片来渲染,而是当成一个模板 (或者说"遮罩")。它会提取图片中的透明像素轮廓,然后根据当前环境自动着色:

  • 在深色背景下 → 渲染成白色
  • 在浅色背景下 → 渲染成黑色

而且最重要的是:这个着色是系统自动完成的,每个显示器各自算各自的。你不需要手动检测、不需要监听变化、不需要关心用户有几个显示器、每个显示器是什么模式。macOS 自己就搞定了。

具体怎么做呢?Tauri 提供了一个 API:

rust 复制代码
/// 加载托盘图标 ------ 白色模板图
fn tray_icon() -> Image<'static> {
    Image::from_bytes(include_bytes!("../icons/tray/icon_template_32.png")).unwrap()
}

pub fn setup_tray(app: &tauri::App) -> tauri::Result<()> {
    let menu = build_tray_menu(&app.handle(), lang)?;
    let icon = tray_icon();

    let _tray = TrayIconBuilder::with_id(TRAY_ID)
        .menu(&menu)
        .icon(icon)
        .icon_as_template(true)  // ← 关键:标记为 template
        .on_menu_event(|app, event| { /* ... */ })
        .build(app)?;

    Ok(())
}

就这么一行 .icon_as_template(true),解决了所有手动适配的麻烦。

图标长什么样?

图标本身也很简单------就是一个白色的 PNG,跟原来黑色图标的轮廓一模一样。因为 template 模式下,macOS 只看透明像素的轮廓,不在乎你是黑是白。

白色图标在深色环境下会被渲染成黑色,在浅色环境下会被渲染成白色,完美适配。

一些感悟

这件事让我意识到两个问题:

1. 很多"解决方案"其实已经是平台的一部分了

macOS 从很早以前就有 Template Image 这个概念了(iOS 的 UIImageTemplate 也是同理)。只是我以前一直做 Web 前端开发,对系统原生的这些特性了解不多。这次用 Tauri 开发桌面端,翻阅文档才发现,原来 Tauri 已经为我们封装好了这个底层 API。

很多时候我们遇到的问题,平台早就给了解决方案,只是我们不知道而已。

2. 手动适配永远不如系统原生方案可靠

我之前的方案之所以失败,根本原因是"手动检测"这个思路本身就是错的。系统的明暗模式不是一个全局布尔值,而是每个显示器独立的状态。除非你用系统提供的方式去处理,否则永远会有边缘情况覆盖不到。

总结

如果你也在用 Tauri 写 macOS 应用,托盘图标记得加上 .icon_as_template(true)。一行代码,省掉一堆麻烦。

项目地址:PinWall


如果这篇文章帮到了你,点个赞就是对我最大的鼓励 😄

相关推荐
kyriewen3 小时前
面试官问你:“AI 能写 80% 的代码了,公司为什么还需要你?”
前端·javascript·面试
甲维斯4 小时前
又升级咯!坦克大战2026,科技与复古并存!
前端·人工智能·游戏开发
搬砖的码农7 小时前
(08)为什么我的 Agent 一跑后台服务就卡死
前端·agent·ai编程
飘尘7 小时前
前端转全栈(Java 后端)必须要知道的:开发中的锁机制与分布式并发控制
前端·后端·全栈
doiito7 小时前
【Agent Harness】TPS的“自工程完结”教会了我一件事:别把Bug留给下一道工序
架构·rust
亲亲小宝宝鸭7 小时前
前端性能监控:web-vitals
前端·性能优化·监控
陆枫Larry7 小时前
可滚动页面背景填不满:`height: 100vh` vs `min-height: 100vh`
前端
Patrick_Wilson7 小时前
Squash Merge 的血缘陷阱:为什么删掉的代码又活了过来
前端·git·程序员