画面の明るさ(輝度)を制御する

brightness クレートで内蔵ディスプレイと DDC/CI 対応の外部モニターの明るさを取得・設定する。外部モニターで効かないときの確認点、macOS でビルドを通す書き方も示す。

ハードウェア連携 対象: Tauri 2.x 更新日: 読了目安: 約8分 hw-018
目次
  1. 前提条件
  2. 1. バックエンドから実装する (Rust)
  3. 2. フロントエンドから呼び出す (TypeScript)
  4. 動作確認
  5. よくあるエラーと対処法
  6. OS ごとの違いと注意点
  7. 関連レシピ

「QR コードを表示している間だけ画面を明るくする」「バッテリー駆動になったら暗くする」のように、アプリから画面の明るさを変えたい場面があります。Tauri 自体に輝度の API は無いので、Rust の brightness クレートを使います。対応するのは Windows と Linux で、ノート PC の内蔵ディスプレイと、DDC/CI(モニターを PC から操作する仕組み)に対応した外部モニターが対象です。変わるのは画面全体の明るさなので、アプリの表示だけを暗くしたいなら CSS の filter: brightness(0.7) で足ります。

前提条件

brightness 0.8 は macOS では「unsupported platform」というエラーでビルドが止まります。macOS にも配るアプリでは、src-tauri/Cargo.toml の対象 OS を限定した欄に入れます。非同期の一覧を扱うため futures も使います。

[target.'cfg(any(windows, target_os = "linux"))'.dependencies]
brightness = "0.8"
futures = "0.3"

自作コマンドの invoke だけなので、capability の追加は不要です。画面の種類と OS による条件は次のとおりです。

画面WindowsLinux
ノート PC の内蔵そのまま使える。決まった段階に丸められる/sys/class/backlight に出ていれば使える
外部モニターモニターの DDC/CI が有効なら使えるddcci-backlight ドライバーが必要

1. バックエンドから実装する (Rust)

押さえる点は 3 つです。

  • 一覧に出ても変えられるとは限らない: Windows では DDC/CI に対応していないモニターも一覧に出て、get() の段階で失敗します。読めた画面だけを操作対象にします。
  • 1 台の失敗で打ち切らない: 一覧は Result の列で届きます。最初のエラーで止めず、全部集めてから振り分けます。
  • 原因まで出す: エラーの to_string() は「どの画面で失敗したか」だけなので、source() をたどって原因をつなげます。

macOS 用には同じ名前の関数を持つ空のモジュールを用意し、コマンドの登録はどの OS でも同じにします。

use serde::Serialize;

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct ScreenBrightness {
    id: String,           // 変更するときに渡す識別名
    name: String,         // 表示用(Windows はモニターの製品名)
    percent: Option<u32>, // 読めなかった画面は None
    error: Option<String>,
}

#[cfg(any(windows, target_os = "linux"))]
mod screen {
    use super::ScreenBrightness;
    use brightness::{Brightness, BrightnessDevice};
    use futures::StreamExt;
    use std::error::Error as _;

    fn describe(e: &brightness::Error) -> String {
        let mut msg = e.to_string();
        let mut cause = e.source();
        while let Some(c) = cause {
            msg = format!("{msg}: {c}");
            cause = c.source();
        }
        msg
    }

    /// 1 台の失敗で打ち切らないよう、Result のまま全部集める
    async fn devices() -> Vec<Result<BrightnessDevice, brightness::Error>> {
        brightness::brightness_devices().collect().await
    }

    pub async fn list() -> Vec<ScreenBrightness> {
        let mut out = Vec::new();
        for dev in devices().await.into_iter().flatten() {
            let Ok(id) = dev.device_name().await else { continue };
            let name = dev.friendly_device_name().await.unwrap_or_else(|_| id.clone());
            let (percent, error) = match dev.get().await {
                Ok(p) => (Some(p), None),
                Err(e) => (None, Some(describe(&e))),
            };
            out.push(ScreenBrightness { id, name, percent, error });
        }
        out
    }

    pub async fn set(id: &str, percent: u32) -> Result<(), String> {
        for mut dev in devices().await.into_iter().flatten() {
            if dev.device_name().await.ok().as_deref() == Some(id) {
                return dev.set(percent).await.map_err(|e| describe(&e));
            }
        }
        Err(format!("画面が見つかりません: {id}"))
    }
}

/// brightness が対応していない OS(macOS など)では「変えられる画面なし」として扱う
#[cfg(not(any(windows, target_os = "linux")))]
mod screen {
    use super::ScreenBrightness;

    pub async fn list() -> Vec<ScreenBrightness> {
        Vec::new()
    }

    pub async fn set(_id: &str, _percent: u32) -> Result<(), String> {
        Err("この OS では明るさを変更できません".into())
    }
}

#[tauri::command]
async fn list_screen_brightness() -> Vec<ScreenBrightness> {
    screen::list().await
}

#[tauri::command]
async fn set_screen_brightness(id: String, percent: u32) -> Result<(), String> {
    // 機種によっては 0% でバックライトが消えて何も見えなくなるので、下限を設ける
    screen::set(&id, percent.clamp(5, 100)).await
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![list_screen_brightness, set_screen_brightness])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

どちらも async コマンドなので、外部モニターとの通信を待つ間も画面は固まりません(重い処理を別スレッド・非同期で実行する)。

2. フロントエンドから呼び出す (TypeScript)

画面ごとにスライダーを並べます。外部モニターとの通信は、読み取りだけでも 1 回に数十ミリ秒かかります。スライダーの input のたびに送ると処理がたまるので、動きが止まってから送ります。

import { invoke } from '@tauri-apps/api/core';

type ScreenBrightness = { id: string; name: string; percent: number | null; error: string | null };

export async function renderBrightness(root: HTMLElement) {
  const screens = await invoke<ScreenBrightness[]>('list_screen_brightness');
  root.replaceChildren();
  if (screens.length === 0) {
    root.textContent = '明るさを変更できる画面がありません';
    return;
  }
  for (const s of screens) {
    const row = document.createElement('label');
    row.append(`${s.name} `);
    if (s.percent === null) {
      // DDC/CI に対応していないモニターなど。操作させず、理由はツールチップに出す
      row.append('(変更できません)');
      row.title = s.error ?? '';
      root.append(row);
      continue;
    }
    const slider = document.createElement('input');
    slider.type = 'range';
    slider.min = '5';
    slider.max = '100';
    slider.value = String(s.percent);
    let timer: number | undefined;
    slider.addEventListener('input', () => {
      window.clearTimeout(timer);
      timer = window.setTimeout(() => {
        invoke('set_screen_brightness', { id: s.id, percent: Number(slider.value) })
          .catch((e) => console.error('明るさを変えられませんでした:', e));
      }, 200);
    });
    row.append(slider);
    root.append(row);
  }
}

await renderBrightness(document.querySelector<HTMLElement>('#brightness') ?? document.body);

一時的に明るくする用途では、変える前の percent を控えておき、終わったら戻します。Windows の id は \\.\DISPLAY1\Monitor0 の形で、モニター情報 の currentMonitor() が返す name(\\.\DISPLAY1)で始まるので、ウィンドウが表示されている画面を選べます。

動作確認

npm run tauri dev で起動し、一覧を 1 行ずつ出します。

import { invoke } from '@tauri-apps/api/core';

type ScreenBrightness = { id: string; name: string; percent: number | null; error: string | null };

for (const s of await invoke<ScreenBrightness[]>('list_screen_brightness')) {
  console.log(`${s.id} | ${s.name} | ${s.percent ?? '-'}% | ${s.error ?? ''}`);
}

DDC/CI に対応した外部モニター 1 台の Windows では次のように出ます。DDC/CI で変わるのはモニター本体の設定なので、スライダーを動かすと、モニターのメニューにある明るさの値も同じように変わります。

\\.\DISPLAY1\Monitor0 | Generic Monitor | 50% |

よくあるエラーと対処法

  • macOS で「unsupported platform」: brightness を通常の [dependencies] に入れています。前提条件のように対象 OS を限定します。
  • 外部モニターの percent が null: エラーは「Failed to get brightness device \\.\DISPLAY2\Monitor0 information: Failed to get monitor brightness (DDCCI)」で始まります。モニターのメニューで DDC/CI を有効にし、ドッキングステーションや変換アダプター、KVM スイッチを通さずに PC へ直接つないで試します。テレビや一部のモニターは DDC/CI に対応していないことがあります。
  • Linux で一覧が空: /sys/class/backlight に画面がありません。デスクトップ PC の外部モニターは、ddcci-backlight ドライバーを入れると現れます。
  • Linux で「Setting brightness failed for device intel_backlight」: 設定はログイン中のセッションを通して行われ、使えないときは /sys/class/backlight/<名前>/brightness への書き込みに切り替わります。SSH 越しなどセッションの外から動かしていないか確かめ、必要なら udev ルールで書き込み権限を与えます。
  • リモートデスクトップ接続中に一覧が空: Windows では、リモート接続の仮想の画面は対象になりません。

OS ごとの違いと注意点

  • Windows: 内蔵ディスプレイは決まった段階の中から近い値に丸められます。明るさの自動調整が有効だと、設定した値が OS に上書きされることがあります。
  • Linux: 値は整数の百分率に切り捨てて返ります。ノート PC で acpi_video0 と intel_backlight のように同じ画面が 2 つ出て、効くのが片方だけのことがあります。
  • macOS: brightness は対応していません。例では一覧が空になるので、OS の種類を取得 して明るさの項目ごと隠すと親切です。
  • 共通: 変更は画面全体に効き、アプリを終了しても戻りません。バッテリー駆動への切り替えで明るさを変えるなら バッテリー状態と電源接続を取得・監視する と組み合わせます。

関連レシピ

参考リンク(公式ドキュメント)

Web Ninja

この記事を書いた人

Web Ninja ウェブエンジニア (Web Engineer)

会社員ネットワークエンジニアから独立してかれこれ 25 年以上 Web エンジニアとして活動中。普段は JavaScript と Node.js を自在に操り、時には C++ や Perl といった古流の技も嗜みます。近年は Tauri × Rust という新たな武器を手に、デスクトップアプリ開発の最前線を駆け抜けています。「作りたい」を「作れる」に変えるための、実践的な「技」をお届けします。

お問い合わせ: tauri.ninja@gmail.com

内容の誤り・動かないコードを報告する