...ing logging 4.0

はてなブログに移行しました。D言語の話とかいろいろ。

DFL: Win32デスクトップアプリのトースト通知のサンプルコード(改)

2025/09/23 全面的に更新

先日、D言語からWinRTを使うためのライブラリD/WinRTを見つけました。

しばらくメンテされていなかったため、そのままでは動かなくなっていましたが、 何回かプルリクしてみたらマージしてもらえたので今は動くようになっています。

試してみると、以外と簡単にDFLと一緒に使うことができました。 でも、D/WinRTライブラリを依存関係に加えるのはちょっと重たい。

そこで、WinRTについて少し分かってきたので、 DFLからWinRTのトースト通知(将来的にはアプリ通知と呼ばれるらしい)を表示するモジュールを追加しました。

実はdfl.notifyiconでもバルーン通知を表示させられるのですが、 DPI Awareなアプリの場合はバルーン通知が表示されないというWindowsの仕様があって、 いまいちだったので、dfl.notifyiconとは別にdfl.toastnotifierを追加しています。

dfl.notifyiconを拡張してトースト通知を選択的に出せるようにしようかとも思ったのですが、 イベントシステムが全く異なるので、別のモジュールにしました。

タスクトレイアイコンはdfl.notifyiconで、トースト通知はdfl.toastnotifierを使うのがいいでしょう。 dfl.notifyiconではアプリが終了すると通知が消えますが、 dfl.toastnotifierではアプリ終了後も通知を残せます。

純粋な Win32 デスクトップアプリでのトースト通知

そもそもトースト通知は、主にストアアプリ用に設計されたようで、 純粋な Win32 デスクトップアプリでトースト通知を実装するための情報は散逸していて、 だいぶ闇が深かったです。

何とかActivator COMサーバーとして動作するようになったので、 トーストやボタンをクリックしたことを検出することができます。 アプリ起動中でもアプリ終了後でもトーストイベントをハンドル可能です。

今回の実装で使わなかった技術を列挙しておきます。 大抵の記事では、いずれかが使われていて、そのまま適用できませんでした。

トースト通知が出るときと出ないとき

はっきり書かれたドキュメントを見つけることができなくて、かなり困らされましたが、 トースト通知にはレガシー版とモダン版があって、モダン版ではトースト通知が出るための条件があります。

詳細はAdCのネタに取っておいて簡単に書いておきます(コードを読んだら何をしているか分かるけど)。

ToastNotifierLegacy

トースト通知を出すときはXMLでコンテンツを表現するのですが、 当初はテンプレートの中から体裁を選んで文字と画像を設定するくらいしかできなかったようです。

enum ToastTemplateType
{
    TOAST_IMAGE_AND_TEXT_01 = 0,
    TOAST_IMAGE_AND_TEXT_02 = 1,
    TOAST_IMAGE_AND_TEXT_03 = 2,
    TOAST_IMAGE_AND_TEXT_04 = 3,
    TOAST_TEXT_01 = 4,
    TOAST_TEXT_02 = 5,
    TOAST_TEXT_03 = 6,
    TOAST_TEXT_04 = 7
}

learn.microsoft.com

ToastNotifierLegacyクラスを使うときは、テンプレートから1つ選んで、テキストと画像を設定して、 show()を呼び出せばトースト通知が表示されます。

選択したテンプレートによっては、テキストが1、2個しか表示されなかったり、 画像が表示されなかったりするので、適切なテンプレートを選択してください。

とても簡単ですが、あまり凝ったことはできないようになっています。

// NOTE: 自分のPC環境に合わせてパスを修正してください。
wstring appLogoImagePath = r"file:///C:/d/gitproj/dfl/examples/toastnotifier/image/d.bmp";

wstring aumid = "Dlang.Dfl.ToastNotifierExample";

ToastNotifierLegacy notifier = new ToastNotifierLegacy(aumid);

// notifier.toastTemplate = ToastTemplateType.TOAST_IMAGE_AND_TEXT_01;
// notifier.toastTemplate = ToastTemplateType.TOAST_IMAGE_AND_TEXT_02;
// notifier.toastTemplate = ToastTemplateType.TOAST_IMAGE_AND_TEXT_03;
notifier.toastTemplate = ToastTemplateType.TOAST_IMAGE_AND_TEXT_04;
// notifier.toastTemplate = ToastTemplateType.TOAST_TEXT_01;
// notifier.toastTemplate = ToastTemplateType.TOAST_TEXT_02;
// notifier.toastTemplate = ToastTemplateType.TOAST_TEXT_03;
// notifier.toastTemplate = ToastTemplateType.TOAST_TEXT_04;

notifier.headline = "Hello ToastNotifier with DFL!";
notifier.text = "ToastNotifierのサンプルコードです。";
notifier.subtext = "2025-09-15";
notifier.appLogoImage = appLogoImagePath;

notifier.show();

ToastNotifier

フルスペックのモダン版のToastNotifierでは、テンプレートを選択して体裁を決める制約はなく、 かなり自由にコンテンツを組み立てることができます。

learn.microsoft.com

ボタン、テキストボックス、ドロップダウンリストを表示したり、 テキストの上にヒーロー画像を表示したり、 アプリケーションアイコンを円でクリップしたりすることができます。

// 自分のPC環境に合わせて変更してください。
enum AppLogoImagePath = r"file:///C:/d/gitproj/dfl/examples/toastnotifier/image/d.bmp";
enum HeroImagePath = r"file:///C:/d/gitproj/dfl/examples/picturebox/image/dman-error.bmp";

wstring aumid = "Dlang.Dfl.ToastNotifierExample";
ToastNotifier notifier = new ToastNotifier(aumid);

notifier.headline = "Hello ToastNotifier with DFL!";
notifier.text = "ToastNotifierのサンプルコードです。";
notifier.subtext = "2025-09-15";
notifier.appLogoImagePath = AppLogoImagePath;
notifier.imagePath = HeroImagePath; // テキストの上に表示されるヒーロー画像のパス
notifier.hintCrop = true; // アプリケーション画像を円でクリップする

notifier.show();

DesktopNotificationManager

トースト通知のイベントを完全に処理するためには、

  • スタートメニューにAUMIDとCLSIDを設定したショートカットファイルを追加
  • レジストリにAUMIDとCLSIDを追記

をする必要があります。

この辺りの処理を担当するのが DesktopNotificationManager です。

AUMID (App User Model Id) は、所定のフォーマットは定められていますが、 実際のところ任意の文字列です。

CLSIDはレジストリに登録するActivator COMサーバーのCLSIDです。 お好きなGUIDジェネレータでUUIDを生成して、

CLSID clsid = clsidFromUUID("{****}");

でCLSIDに変換できます。

CLSID activatorClsid = clsidFromUUID("{****}");
// CLSID activatorClsid = DFL_CLSID_NOTIFICATION_ACTIVATOR; // 実験用ならこれでもOK

scope manager = new DesktopNotificationManager(args, AUMID, &activatorClsid);

final switch (manager.mode)
{
case DesktopNotificationMode.NORMAL:
    manager.installShellLink(); // スタートメニューにショートカットファイルを作成
    // 数秒待たされます。
    manager.registerAumidAndComServer(); // レジストリにAUMIDとCLSIDを書き込み

    auto activator = new CustomNotificationActivator;
    manager.registerActivator(activator); // COMサーバーを開始
    scope(exit)
        manager.unregisterActivator(); // COMサーバーを終了

    Application.run(new MainForm()); // メインウィンドウを表示
    break;

case DesktopNotificationMode.LAUNCH:
    // Activator COM サーバーから呼び出されたときの処理
    manager.registerActivator(new CustomNotificationActivator); // COMサーバーを開始
    scope(exit)
        manager.unregisterActivator(); // COMサーバーを終了

    Application.run(new Form()); // 適当なウィンドウを表示

    // 実行するとショートカットファイルとレジストリの登録が削除されます。
    // アプリ終了後のイベントは当然もらえなくなります。
    static if (0)
    {
        manager.unregisterAumidAndComServer();
        manager.uninstallShellLink();
    }
}

DesktopNotificationManager.installShellLink() を呼び出すと数秒間待たされますが、 新しいAUMIDを登録してからシェルが認識するまでに数秒必要であり、 認識されていないときにトースト通知を出そうとしても無視されるので、 そのための待ち時間になっています。

サンプルコード

完全版のサンプルコードです。

version行をコメントアウトするかしないかでToastNotifierとToastNotifierLegacyのどちらを使うか選択できます。

import dfl;

import std.conv : to;
import std.string : join;

version(Have_dfl) // For DUB.
{
}
else
{
    pragma(lib, "dfl.lib");
}

version = ToastNotifier;// コメントアウトすると ToastNotifierLegacy が使われます。

// あなたの AUMID に変更してください。
enum AUMID = "Dlang.Dfl.ToastNotifierExample"w;

// あなたのPC環境に合わせて修正してください。
enum APP_LOGO_IMAGE_PATH = r"file:///C:/d/gitproj/dfl/examples/toastnotifier/image/d.bmp";
enum IMAGE_PATH = r"file:///C:/d/gitproj/dfl/examples/picturebox/image/dman-error.bmp";

class MainForm : Form
{
    private Button _button;

    version (ToastNotifier)
        private ToastNotifier _notifier;
    else
        private ToastNotifierLegacy _notifier;

    this()
    {
        this.text = "ToastNotifier example";
        this.size = Size(300, 250);

        version (ToastNotifier)
        {
            _notifier = new ToastNotifier(AUMID);

            // _notifier.launch = "action=Test&userId=49183";
            _notifier.setLaunch("action=Test&userId=49183"); // エスケープされたXMLに自動変換します。

            _notifier.imagePath = IMAGE_PATH;
            
            _notifier.hintCrop = true;

            _notifier.imageStyle = ToastNotifierImageStyle.HERO;
            // _notifier.imageStyle = ToastNotifierImageStyle.INLINE;
            

            // Input1
            _notifier.inputs.add(new ToastTextBox("input1"w, "Message"w, "Place holder content"w));
            // Input2
            ToastSelectionBox input2 = new ToastSelectionBox("input2"w, "Fruits"w, "Place holder content"w, "select3"w);
            input2.items.add(new ToastSelectionBoxItem("select1"w, "Apple"w));
            input2.items.add(new ToastSelectionBoxItem("select2"w, "Orange"w));
            input2.items.add(new ToastSelectionBoxItem("select3"w, "Pine"w));
            _notifier.inputs.add(input2);
            // Input3
            _notifier.inputs.add(new ToastTextBox("input3"w, "to"w, "Place holder content"w));
            // Input4
            _notifier.inputs.add(new ToastTextBox("input4"w, "cc"w, "Place holder content"w));
            // Input5
            _notifier.inputs.add(new ToastTextBox("input5"w, "bcc"w, "Place holder content"w));
            //
            assert(_notifier.inputs.length <= 5, "Toast textboxes length must be 0 to 5.");

            _notifier.useButtonStyle = true;
            // Button1
            ToastButton button1 = new ToastButton("Ok"w, "action=OkButton&amp;userId=49183"w);
            button1.buttonStyle = ToastButtonStyle.SUCCESS; // buttonStyle は useButtonStyle が true のとき有効
            _notifier.buttons.add(button1);
            // Button2
            _notifier.buttons.add(new ToastButton("Cancel"w, "action=CancelButton&amp;userId=49183"w));
            // Button3
            ToastButton button3 = new ToastButton("Open Google"w, "https://www.google.com/"w);
            button3.activationType = ToastActivationType.PROTOCOL;
            button3.buttonStyle = ToastButtonStyle.CRITICAL;
            _notifier.buttons.add(button3);
            // Button4
            _notifier.buttons.add(new ToastButton("Option"w, "action=Option"w));
            // Button5
            _notifier.buttons.add(new ToastButton("Close"w, "action=Close"w));
            //
            assert(_notifier.buttons.length <= 5, "Toast buttons length must be 0 to 5.");
        }
        else
        {
            _notifier = new ToastNotifierLegacy(AUMID);
            // _notifier.toastTemplate = ToastTemplateType.TOAST_IMAGE_AND_TEXT_01;
            // _notifier.toastTemplate = ToastTemplateType.TOAST_IMAGE_AND_TEXT_02;
            // _notifier.toastTemplate = ToastTemplateType.TOAST_IMAGE_AND_TEXT_03;
            _notifier.toastTemplate = ToastTemplateType.TOAST_IMAGE_AND_TEXT_04;
            // _notifier.toastTemplate = ToastTemplateType.TOAST_TEXT_01;
            // _notifier.toastTemplate = ToastTemplateType.TOAST_TEXT_02;
            // _notifier.toastTemplate = ToastTemplateType.TOAST_TEXT_03;
            // _notifier.toastTemplate = ToastTemplateType.TOAST_TEXT_04;
        }
        _notifier.headline = "Hello ToastNotifier with DFL!";
        _notifier.text = "ToastNotifierのサンプルコードです。";
        _notifier.subtext = "2025-09-22";
        _notifier.appLogoImagePath = APP_LOGO_IMAGE_PATH;

        _button = new Button;
        _button.text = "Show toast";
        _button.location = Point(50, 50);
        _button.size = Size(150,60);
        _button.parent = this;
        _button.click ~= (Control c, EventArgs e) {
            _notifier.show();
        };
    }
}

// 自前の Activator COM サーバー
// 実際に使うときはこのクラスのために新しいCLSIDを生成した方がよい。
class CustomNotificationActivator : NotificationActivator
{
    override void onActivated(NotificationActivator activator, ToastActivatedEventArgs args)
    {
        // ここにユーザーコードを書く。
        wstring[wstring] argList = parseArguments(args.arguments);

        wstring inputList;
        foreach (key, value; args.userInputs)
            inputList ~= "[" ~ key ~ ":" ~ value ~ "]\n";
        
        msgBox(
            "<activated>\n" ~
            "- args  : " ~ argList.to!string ~ "\n" ~
            "- inputs: " ~ inputList.to!string);
    }
}

wstring[wstring] parseArguments(wstring args)
{
    import std.string;
    import std.array;
    wstring[wstring] ret;
    foreach (wstring e; args.split("&"w))
    {
        wstring[] set = e.split("=");
        wstring key = set[0];
        wstring value = set[1];
        ret[key] = value;
    }
    return ret;
}
unittest
{
    wstring[wstring] result = parseArguments("param1=hello&param2=dfl&param3=world"w);
    assert(result["param1"w] == "hello"w);
    assert(result["param2"w] == "dfl"w);
    assert(result["param3"w] == "world"w);
}

void main(string[] args)
{
    // DFLによって定義されたCLSIDを除き、既存のCLSIDを設定しないでください。
    // レジストリが壊れます。
    //
    // 自前のActivator COMサーバーを使いたい場合は、新たに作成したCLSIDに変更してください。
    //
    // もしDFLによって定義されたCLSIDを使う場合は、
    // DFLを使うアプリ間でCOMサーバーが共有されるため、
    // トースト通知から起動されるアプリは1つだけになります。
    CLSID activatorClsid = DFL_CLSID_NOTIFICATION_ACTIVATOR;

    scope manager = new DesktopNotificationManager(args, AUMID, &activatorClsid);
    final switch (manager.mode)
    {
    case DesktopNotificationMode.NORMAL:
        manager.installShellLink(); // スタートメニューにショートカットファイルを追加
        // 数秒待たされます。
        manager.registerAumidAndComServer(); // レジストリに AUMID と CLSID を登録

        static if (1)
        {
            // 方法1
            auto activator = new CustomNotificationActivator;
        }
        else
        {
            // 方法2
            auto activator = new NotificationActivator;
            activator.activated ~= (NotificationActivator na, ToastActivatedEventArgs ea) {
                // 何かする
            };
        }
        manager.registerActivator(activator);
        scope(exit)
            manager.unregisterActivator();

        Application.enableVisualStyles();

        import dfl.internal.dpiaware;
        SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);

        Application.run(new MainForm()); // メインフォームを表示
        break;

    case DesktopNotificationMode.LAUNCH:
        // Activator COM サーバーによって起動されたときの処理
        manager.registerActivator(new CustomNotificationActivator);
        scope(exit)
            manager.unregisterActivator();

        // onActivate() が呼び出されるためにはここでメッセージループに入る必要がある。

        msgBox("<Embedding>\n" ~ args.join("\n")); // msgBox() を呼んでもメッセージループに入る。

        Application.run(new Form()); // 適当なウィンドウを表示。もちろんメッセージループに入る。

        // ショートカットとレジストリを削除するときに呼び出してください。
        // なお、削除するとアプリ終了後にトーストクリックしても当然何も起きません。
        static if (0)
        {
            manager.unregisterAumidAndComServer();
            manager.uninstallShellLink();
        }
    }
}   

DFL のダウンロード

github.com

DUB のパッケージ

code.dlang.org

参考文献