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サーバーとして動作するようになったので、 トーストやボタンをクリックしたことを検出することができます。 アプリ起動中でもアプリ終了後でもトーストイベントをハンドル可能です。
今回の実装で使わなかった技術を列挙しておきます。 大抵の記事では、いずれかが使われていて、そのまま適用できませんでした。
- ストアアプリ
- C#
- WPF
- .NET
- Microsoft.Windows.AppNotifications.Builder API
- UWP Community Toolkit (Microsoft.Toolkit.Uwp.Notifications)
- NuGet
- XAML
トースト通知が出るときと出ないとき
はっきり書かれたドキュメントを見つけることができなくて、かなり困らされましたが、 トースト通知にはレガシー版とモダン版があって、モダン版ではトースト通知が出るための条件があります。
詳細は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 }
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では、テンプレートを選択して体裁を決める制約はなく、 かなり自由にコンテンツを組み立てることができます。
ボタン、テキストボックス、ドロップダウンリストを表示したり、 テキストの上にヒーロー画像を表示したり、 アプリケーションアイコンを円でクリップしたりすることができます。
// 自分の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&userId=49183"w); button1.buttonStyle = ToastButtonStyle.SUCCESS; // buttonStyle は useButtonStyle が true のとき有効 _notifier.buttons.add(button1); // Button2 _notifier.buttons.add(new ToastButton("Cancel"w, "action=CancelButton&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¶m2=dfl¶m3=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 のダウンロード
DUB のパッケージ
参考文献
- Microsoft
- アプリ通知のコンテンツ - Windows apps | Microsoft Learn
- 通知の設計の基本 - Windows apps | Microsoft Learn
- トースト スキーマ - Windows UWP applications | Microsoft Learn
- Microsoft.Toolkit.Uwp.Notifications Namespace | Microsoft Learn
- How to enable desktop toast notifications through an AppUserModelID (Windows) | Microsoft Learn
- クイックスタート: Windows アプリ SDK のアプリ通知 - Windows apps | Microsoft Learn
- Blog
- GitHub