11
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rust で Windows のトースト通知を表示する

Last updated at Posted at 2024-03-08

動機

Rust で CLI アプリ作ってるときに Windows のトースト通知を表示したくなったので、どうやって表示するのか調べました。

環境

rustc 1.78.0
Windows 10
windows-rs 0.54.0

トースト通知とは

ウィンドウの右下に現れるアレです。

toast.png

手順

Microsoft 公式のドキュメントがあるので、おおよそこの流れに従います。

手順 1: レジストリにアプリを登録する

レジストリにAUMIDキーを登録します。今回はレジストリエディタで手動で追加しました。DisplayName文字列値を追加するだけで良いようです。

regedit.png

手順 2: COM アクティベーターを設定する

COM アクティベーターというのを設定するとトースト通知からデータを取得できるようですが、今回は使わないのでスキップ。

手順 3: トーストを送信する

公式ドキュメントの C++ コードを Rust に写経します。

// UTF-8 を UTF-16 の Vec<u16> に変換
let text = text.encode_utf16().collect::<Vec<_>>();

// トーストテンプレート生成
let content = XmlDocument::new()?;
content.LoadXml(h!("<toast><visual><binding template='ToastGeneric'><text></text></binding></visual></toast>"))?;

// テキストの挿入
content
    .SelectSingleNode(h!("//text[1]"))?
    .SetInnerText(&HSTRING::from_wide(&text)?)?;

// 通知の生成
let notification = ToastNotification::CreateToastNotification(&content)?;

// AUMID はレジストリに設定したものを使う
let toast =
    ToastNotificationManager::CreateToastNotifierWithId(h!("ToastExample.MyApp"))?;

// 通知の表示
toast.Show(&notification)?;

これで表示されるのかと思ったけどダメでした。通知の表示が終わる前にメインスレッドが終了するとダメみたいです。なので、もう少し工夫します。

手順 4: トースト表示を別スレッドで実行して終了を待つ

ということでトースト通知を表示する関数は下記のようになりました。

fn toast_show(text: String) -> Result<()> {
    let (tx, rx) = mpsc::channel();

    // スレッドを生成
    thread::spawn(move || -> Result<_> {
        let text = text.encode_utf16().collect::<Vec<_>>();

        let content = XmlDocument::new()?;
        content.LoadXml(h!("<toast><visual><binding template='ToastGeneric'><text></text></binding></visual></toast>"))?;
        content
            .SelectSingleNode(h!("//text[1]"))?
            .SetInnerText(&HSTRING::from_wide(&text)?)?;

        let notification = ToastNotification::CreateToastNotification(&content)?;

        // トースト通知が非表示なったときに呼ばれる
        let tx_clone = tx.clone();
        notification.Dismissed(&TypedEventHandler::new(move |_, _| {
            tx_clone.send(()).ok();
            Ok(())
        }))?;

        // トースト通知がクリックされたときに呼ばれる
        let tx_clone = tx.clone();
        notification.Activated(&TypedEventHandler::new(move |_, _| {
            tx_clone.send(()).ok();
            Ok(())
        }))?;

        // トースト通知の表示に失敗したときに呼ばれる?
        notification.Failed(&TypedEventHandler::new(move |_, _| {
            tx.send(()).ok();
            Ok(())
        }))?;

        let toast =
            ToastNotificationManager::CreateToastNotifierWithId(h!("ToastExample.MyApp"))?;
        toast.Show(&notification)?;
        Ok(())
    });

    // トースト通知の終了を待つ
    rx.recv()?;
    Ok(())
}

まとめ

コピペ用サンプルコード
Cargo.toml
[package]
name = "toast"
version = "0.1.0"
edition = "2021"

[dependencies]
anyhow = "1.0"

[dependencies.windows]
version = "0.54"
features = [
    "Data_Xml_Dom", 
    "UI_Notifications",
]
main.rs
use anyhow::Result;
use std::sync::mpsc;
use std::thread;
use windows::{
    core::{h, HSTRING},
    Data::Xml::Dom::XmlDocument,
    Foundation::TypedEventHandler,
    UI::Notifications::{ToastNotification, ToastNotificationManager},
};

fn toast_show(text: String) -> Result<()> {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || -> Result<_> {
        let text = text.encode_utf16().collect::<Vec<_>>();

        let content = XmlDocument::new()?;
        content.LoadXml(h!("<toast><visual><binding template='ToastGeneric'><text></text></binding></visual></toast>"))?;
        content
            .SelectSingleNode(h!("//text[1]"))?
            .SetInnerText(&HSTRING::from_wide(&text)?)?;

        let notification = ToastNotification::CreateToastNotification(&content)?;

        let tx_clone = tx.clone();
        notification.Dismissed(&TypedEventHandler::new(move |_, _| {
            tx_clone.send(()).ok();
            Ok(())
        }))?;

        let tx_clone = tx.clone();
        notification.Activated(&TypedEventHandler::new(move |_, _| {
            tx_clone.send(()).ok();
            Ok(())
        }))?;

        notification.Failed(&TypedEventHandler::new(move |_, _| {
            tx.send(()).ok();
            Ok(())
        }))?;

        let toast =
            ToastNotificationManager::CreateToastNotifierWithId(h!("ToastExample.MyApp"))?;
        toast.Show(&notification)?;
        Ok(())
    });

    rx.recv()?;
    Ok(())
}

fn main() -> Result<()> {
    toast_show("Hello, World!".into())?;
    Ok(())
}

winrt-toast というクレートがあるので、そちらを使いましょう。

11
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?