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


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

相关推荐
mldong6 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排7 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60618 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角9 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!9 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰9 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi11 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen11 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马12 小时前
AI源码分析:拆解模型底层实现逻辑
前端
IT_陈寒12 小时前
React子组件莫名其妙重渲染?你可能漏了这个Hook
前端·人工智能·后端