録音中は赤いアイコンにする、未読があれば印を付ける、サブウィンドウごとに別のアイコンにする、といった場面では、ウィンドウのアイコンを実行中に差し替えます。JS の setIcon() か Rust の set_icon() を使い(プラグイン不要)、画像はファイルのパス・PNG のバイト列・画素データのどれでも渡せます。ただしパスとバイト列には Cargo の feature が必要で、macOS にはウィンドウのアイコン自体がありません。実行ファイルやインストーラーのアイコンを作る方法は アプリアイコンを画像から生成する で扱います。
前提条件
| API | 権限 | core:default |
|---|---|---|
setIcon() | core:window:allow-set-icon | 含まれない |
defaultWindowIcon()(既定のアイコンを取得) | core:app:allow-default-window-icon | 含まれない |
Image.new() / fromBytes() / fromPath() | core:image:default | 含まれる |
setOverlayIcon()(Windows のタスクバーの印) | core:window:allow-set-overlay-icon | 含まれない |
setBadgeCount()(Dock などの数字) | core:window:allow-set-badge-count | 含まれない |
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"core:window:allow-set-icon",
"core:app:allow-default-window-icon"
]
}
PNG のパスやバイト列を渡すには、src-tauri/Cargo.toml の tauri に image-png(ICO なら image-ico)の feature を足します。画素データ(RGBA)を渡す Image.new() と、後述の Rust の include_image! は feature 無しで使えます。
[dependencies]
tauri = { version = "2", features = ["image-png"] }
既定のアイコンと表示される場所
設定ファイルにはウィンドウごとのアイコンの項目が無く、全ウィンドウ共通の既定のアイコンが bundle.icon から選ばれてビルド時に埋め込まれます(Windows は最初の .ico、それ以外は最初の .png)。setIcon() で変わるのは呼んだウィンドウだけで、後から開くウィンドウは既定のアイコンで開きます。
| 表示される場所 | 変える方法 |
|---|---|
| タイトルバー左上など、ウィンドウ自身のアイコン | setIcon() / set_icon() |
| タスクバーのボタンに重ねる小さな印(Windows) | setOverlayIcon() / set_overlay_icon() |
| Dock などに出る数字のバッジ(macOS・Linux) | setBadgeCount() / set_badge_count() |
| 実行ファイル・インストーラー・Dock のアプリアイコン | bundle.icon(build-002) |
| システムトレイのアイコン | トレイの setIcon()(menu-009) |
1. フロントエンドから実装する (TypeScript)
用意した PNG に切り替える
画像をフロントエンドの素材として置き(Vite なら public/icons/)、fetch() で読んで渡すと、パスの解決も bundle.resources の設定も要りません。切り替えのたびに画像を送り直さないよう、Image.fromBytes() で一度だけ読み込んで使い回します。
import { getCurrentWindow } from '@tauri-apps/api/window';
import { Image } from '@tauri-apps/api/image';
type IconState = 'idle' | 'recording' | 'error';
const win = getCurrentWindow();
const cache = new Map<IconState, Image>();
async function loadIcon(state: IconState): Promise<Image> {
const cached = cache.get(state);
if (cached) return cached;
const res = await fetch(`/icons/${state}.png`); // public/icons/ に置いた PNG
if (!res.ok) throw new Error(`icon ${state}: HTTP ${res.status}`);
const image = await Image.fromBytes(await res.arrayBuffer()); // Cargo の image-png が必要
cache.set(state, image);
return image;
}
export async function showState(state: IconState) {
await win.setIcon(await loadIcon(state)); // core:window:allow-set-icon が必要
console.log('[icon]', state);
}
配布物に同梱 したファイルのパスを渡すなら、resolveResource() で絶対パスにします。相対パスは起動したときの作業フォルダーが基準になるので、環境によって見つかりません。
canvas で描いた画像にする(feature 不要)
未読件数のように中身が変わる印は、既定のアイコンに canvas で描き足し、画素データを Image.new() で渡します。Image は Rust 側に置かれた画像を指すので、使い終わったら close() で解放します。
import { getCurrentWindow } from '@tauri-apps/api/window';
import { Image } from '@tauri-apps/api/image';
import { defaultWindowIcon } from '@tauri-apps/api/app';
const SIZE = 64;
let base: ImageBitmap | null = null;
// 既定のアイコンを一度だけ読み、canvas に描ける形にしておく
async function loadBase(): Promise<ImageBitmap | null> {
if (base) return base;
const icon = await defaultWindowIcon(); // core:app:allow-default-window-icon が必要
if (!icon) return null;
const { width, height } = await icon.size();
const pixels = new ImageData(Uint8ClampedArray.from(await icon.rgba()), width, height);
await icon.close();
base = await createImageBitmap(pixels);
return base;
}
export async function showUnread(count: number) {
const bitmap = await loadBase();
const ctx = document.createElement('canvas').getContext('2d');
if (!bitmap || !ctx) return;
ctx.canvas.width = ctx.canvas.height = SIZE;
ctx.drawImage(bitmap, 0, 0, SIZE, SIZE);
if (count > 0) {
ctx.fillStyle = '#e53935'; // 右下に赤い丸と件数を描く
ctx.beginPath();
ctx.arc(SIZE - 18, SIZE - 18, 17, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#fff';
ctx.font = 'bold 22px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(count > 9 ? '9+' : String(count), SIZE - 18, SIZE - 17);
}
const rgba = new Uint8Array(ctx.getImageData(0, 0, SIZE, SIZE).data);
const icon = await Image.new(rgba, SIZE, SIZE); // 画素データなので feature は不要
await getCurrentWindow().setIcon(icon);
await icon.close(); // ウィンドウに設定した後なら解放してよい
}
2. バックエンドから実装する (Rust)
Rust では set_icon() に tauri::image::Image を渡します。include_image! は PNG / ICO をコンパイル時に画素データへ変換して埋め込むので feature は要りません(パスは src-tauri/ からの相対)。設定ファイルでは指定できないウィンドウごとのアイコンも、ビルダーの icon() で付けられます。タスクバーや Dock の印は OS で API が違い、Windows は set_overlay_icon() で小さな画像を重ね、macOS と Linux は set_badge_count() で数字を出します。
use tauri::image::Image;
use tauri::{include_image, Manager, WebviewUrl, WebviewWindowBuilder};
/// RGBA の画像の右下に赤い丸を描く(ratio は半径の割合)
fn paint_dot(rgba: &mut [u8], width: u32, height: u32, ratio: f32) {
let r = width.min(height) as f32 * ratio;
let (cx, cy) = (width as f32 - r, height as f32 - r);
for y in 0..height {
for x in 0..width {
let (dx, dy) = (x as f32 + 0.5 - cx, y as f32 + 0.5 - cy);
if dx * dx + dy * dy <= r * r {
let i = ((y * width + x) * 4) as usize;
rgba[i..i + 4].copy_from_slice(&[229, 57, 53, 255]);
}
}
}
}
/// 呼び出し元ウィンドウのアイコンに印を付ける・外す
#[tauri::command]
fn set_alert_icon(window: tauri::WebviewWindow, alert: bool) -> Result<(), String> {
let base = window
.app_handle()
.default_window_icon()
.cloned()
.ok_or("default window icon not found")?;
if !alert {
return window.set_icon(base).map_err(|e| e.to_string());
}
let (w, h) = (base.width(), base.height());
let mut rgba = base.rgba().to_vec();
paint_dot(&mut rgba, w, h, 0.25);
window.set_icon(Image::new_owned(rgba, w, h)).map_err(|e| e.to_string())
}
/// タスクバー・Dock に未読を出す(0 で消す)
#[tauri::command]
fn set_unread_badge(window: tauri::WebviewWindow, count: u32) -> Result<(), String> {
#[cfg(target_os = "windows")]
{
// 数字のバッジが無いので、赤い丸だけの画像を重ねる
let overlay = (count > 0).then(|| {
let mut rgba = vec![0u8; 32 * 32 * 4];
paint_dot(&mut rgba, 32, 32, 0.5);
Image::new_owned(rgba, 32, 32)
});
window.set_overlay_icon(overlay).map_err(|e| e.to_string())
}
#[cfg(not(target_os = "windows"))]
{
let badge = (count > 0).then(|| i64::from(count));
window.set_badge_count(badge).map_err(|e| e.to_string())
}
}
/// 既定とは別のアイコンでサブウィンドウを開く
#[tauri::command]
async fn open_editor(app: tauri::AppHandle) -> Result<(), String> {
WebviewWindowBuilder::new(&app, "editor", WebviewUrl::App("editor.html".into()))
.title("Editor")
.icon(include_image!("icons/32x32.png")) // 実際は差し替え用の PNG を置いて指定する
.map_err(|e| e.to_string())?
.build()
.map_err(|e| e.to_string())?;
Ok(())
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![set_alert_icon, set_unread_badge, open_editor])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Rust のコマンド経由なら、JS 側に core:window:allow-set-icon などの権限は要りません。include_image! は画素のまま埋め込むので、小さな画像にします。
import { invoke } from '@tauri-apps/api/core';
await invoke('set_alert_icon', { alert: true }); // ウィンドウのアイコンに赤い丸
await invoke('set_unread_badge', { count: 3 }); // Windows は赤い丸、macOS・Linux は「3」
await invoke('open_editor');
動作確認
npm run tauri dev で起動して showState('recording') を呼ぶと、Windows ではタイトルバー左上のアイコンが recording.png に変わり、コンソールに次のように出ます。
[icon] recording
showUnread(3) では既定のアイコンに「3」の印が付き、showUnread(0) で戻ります。set_unread_badge は、Windows ではタスクバーのボタンに赤い丸が重なり、macOS では Dock に「3」が付きます。
よくあるエラーと対処法
- 「from_bytes is only supported if the
image-icoorimage-pngCargo features are enabled」: feature を付けずにImage.fromBytes()を呼んでいます。setIcon()にパスを直接渡した場合は「expected RGBA image data, found a file path」になります。どちらもimage-pngを足してtauri devを再起動します。 - 「window.set_icon not allowed. Permissions associated with this command: core:window:allow-set-icon」: 権限の追加漏れです。リリースビルドでは「Command plugin:window|set_icon not allowed by ACL」と出ます。
include_image!がコンパイル時に「icon <パス> is not RGBA」で止まる: PNG が RGBA 形式ではありません(パレット形式など)。透過付きの 32bit PNG で保存し直すか、tauri iconで作ったものを使います。- 「Command set_overlay_icon not found」: Windows 以外で
setOverlayIcon()を呼んでいます(権限を付けていてもこうなります)。他の OS ではsetBadgeCount()を使います。
OS ごとの違いと注意点
- Windows: 数字のバッジ(
setBadgeCount())は非対応なので、setOverlayIcon()で小さな画像を重ねます。重ねる画像はウィンドウごとに設定できます。 - macOS: ウィンドウにアイコンを出す場所が無く、
setIcon()は成功しても見た目は変わりません。Dock に出るのはアプリのアイコンで、tauri devの間もbundle.iconのものが使われ、実行中に差し替える API はありません。状態はsetBadgeCount()や、文字を出せるsetBadgeLabel()(macOS 専用)で伝えます。バッジはアプリ全体で 1 つです。 - Linux: ウィンドウのアイコンがどこに出るかはデスクトップ環境によります。
- iOS / Android:
setIcon()などはデスクトップ専用です。 - 気付いてほしい通知には タスクバーでウィンドウを点滅させる も組み合わせます。
