写了一个桌面应用,发现托盘图标颜色跟系统主题不一致。很多人第一反应是手动检测明暗模式然后换图标,但其实 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 之类的通知,但问题是:
- 这些通知是 Cocoa / AppKit 层的 API
- 我用的是 Tauri(Rust 后端),要通过 objc 桥接才能调用
- 最关键的是------即使能监听到,我也没法知道每个显示器各自是深色还是浅色。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
如果这篇文章帮到了你,点个赞就是对我最大的鼓励 😄