Windowsアプリのタスクトレイ常駐とトースト通知 ── NotifyIconの落とし穴とAppNotificationの選び方

· 更新日: · · タスクトレイ, NotifyIcon, トースト通知, AppNotification, Windows App SDK, WinForms, WPF, C#, .NET, 常駐アプリ, Windows開発, 技術相談

更新履歴(5件・最終更新 2026年08月02日)

この記事に加えた変更の記録です。アーカイブした更新前のバージョンは、DOI付きの固定URLから読めます。

記事の冒頭に「この記事の知識マップ」節を追加しました。本文で扱っている概念とその関係を、要約・図・詳細ページへのリンクにまとめたものです。本文の主張は変えていません。
外部レビュー(1283件)への対応として本文を更新しました。個々の変更内容は、この下の履歴を参照してください。
コールドスタート時の受け渡しに競合が残っていたのを直しました。フォームの有無を見てから預けるまでの間にフォームが表示されると、その間に走った引き取り処理は何も見つけられず、直後に預けた操作が流れないままになります。UIの準備完了フラグと預かり先を同じロックで扱う形にしました。
トーストのクリックがアプリの起動そのものだった場合に、操作が黙って捨てられる問題を直しました。ハンドラーが動く時点ではまだフォームが1つも無いため、`Application.OpenForms`を見て`null`なら何もしないと書くと、記事が主眼としているコールドスタートの経路が動きません。いったん預かってフォームの表示後に流す形にしました。
目的別にどの章を読めばよいかの案内を冒頭に追加しました。全体像だけ、トレイ常駐アプリを作る、通知APIを選ぶ、といった目的ごとに読む場所を示しています。
初版公開
この記事を引用する(DOI: 10.5281/zenodo.21590007)

この記事はZenodoにアーカイブされています。常に最新版へ解決されるDOIと、いま表示している版に固定されたDOIの両方を下に示します。

小村 豪(2026)「Windowsアプリのタスクトレイ常駐とトースト通知 ── NotifyIconの落とし穴とAppNotificationの選び方」合同会社小村ソフト. https://doi.org/10.5281/zenodo.21590007 https://staging.comcomponent.com/blog/windows-tray-icon-toast-notification-guide/

DOI(最新版)
10.5281/zenodo.21590007
DOI(この版)
10.5281/zenodo.21732986

「処理が終わったらユーザーにさりげなく知らせたい」「ウィンドウを閉じても監視は続けたい」── 業務アプリでは、タスクトレイ(通知領域)への常駐とトースト通知はセットで登場する定番の要件です。ところが実装してみると、閉じたはずのアプリが終了できなくなった、Explorerを再起動したらアイコンが消えた、開発中は出ていたトーストが配布先で表示されない、といった細かい問題が次々に出てきます。

さらにトースト通知は、UWP時代からAPIの世代交代が続いてきた領域です。検索して出てくるサンプルコードが、今となっては保守のみの古いAPIだったということも珍しくありません。この記事では、NotifyIconによるトレイ常駐の設計と、2026年時点でのトースト通知APIの選び方を、実務の落とし穴と一緒に整理します。

この記事の読み方

全11章と長いので、目的別の入口を示しておきます。頭から順に読む必要はありません。

目的 読むところ
全体像だけ知りたい 第1章(結論)と第11章(まとめ)
トレイ常駐アプリをこれから作る 第2〜5章 ── NotifyIconの基本、終了経路の設計、WPFでの併用、落とし穴3つ
トースト通知のAPIをどれにするか決めたい 第6章の比較表。判断だけならここだけで足ります
AppNotificationを実装する 第7章 ── 最小実装、csprojの設定、配布先へのランタイム導入
進捗バーなど凝った通知を作る 第8章。まず動かす段階では読み飛ばして構いません
「通知が出ない」を調査している 第9章 ── OS設定の確認場所と、昇格プロセスの制約
Windowsサービスからユーザーに通知したい 第10章

この記事で使う用語

用語 意味
通知領域(タスクトレイ) タスクバー右端のアイコンが並ぶ領域。Windowsの用語では「通知領域(notification area)」1
トースト通知 画面右下にバナーとして現れ、通知センターに残る通知
TFM Target Framework Moniker(ターゲットフレームワークモニカー)。csprojの <TargetFramework> に書く識別子で、net8.0-windows10.0.19041.0 のように.NETのバージョンと対象プラットフォームを表す
MSIX Windowsのアプリパッケージ形式。この形式で配布されたアプリを「パッケージ化された(packaged)アプリ」と呼ぶ
非パッケージ(unpackaged)アプリ MSIXでパッケージ化していない従来型のexe。WPF・WinForms・Win32のデスクトップアプリは既定でこちらです2
AUMID AppUserModelID。Windowsがアプリを一意に識別するためのIDで、トースト通知の送り主の識別に使われる
セッション0 Windows Vista以降、サービス専用に割り当てられるセッション。対話的なUIを持てない(第10章)3

1. まず結論

  • トレイ常駐の基本は今も System.Windows.Forms.NotifyIcon です。Icon を設定し Visible = true にしないと表示されません(既定は false)。ツールチップの Text は .NET 6 以降で最大127文字です(それ以前は63文字)。45
  • WPFには NotifyIcon 相当のコントロールがありません。公式の対照表にも「同等のコントロールなし」と明記されています。csprojで UseWPFUseWindowsForms を併用してWinFormsの NotifyIcon を使うのが最小構成です。67
  • 「×ボタンで閉じてもトレイに残す」は、FormClosinge.Cancel = true にして Hide() する設計です。必ず明示的な終了手段(トレイメニューの「終了」)を別に用意し、CloseReason を見てOSのシャットダウンまで妨げないようにします。8
  • トースト通知のAPIは、2026年時点の公式推奨がはっきりしています。WinForms / WPF / 非パッケージアプリのいずれも Windows App SDK の AppNotificationManager(NuGet: Microsoft.WindowsAppSDK)が推奨です。Windows.UI.Notifications は「保守のみ」、Community Toolkit の通知コンポーネントはアーカイブ済みです。910
  • Windows App SDK の AppNotification はパッケージIDなしの普通のexeでも使えますAppNotificationManager.Default.Register() がCOMサーバー登録やAUMID相当の処理を自動で行うため、旧世代で必要だったスタートメニューショートカットの用意は不要です。ただし管理者昇格したプロセスからの通知は未サポートです。111213
  • トーストは「必ずユーザーの目に入る」手段ではありません。応答不可(Focus Assist)中はバナー表示されず通知センターへ直行しますし、設定で通知自体を切ることもできます。「通知が表示されたこと」を業務フローの前提にしない設計が必要です。1415
  • Windowsサービスからトーストは出せません(セッション0にUIはありません)。ユーザーセッション側にトレイ常駐エージェントを置き、IPCで連携するのが定石です。3

この記事の知識マップ

業務Windowsアプリのタスクトレイ常駐は今もNotifyIconが基本で、WPFには同等のコントロールがないためWinFormsを併用するのが最小構成です。Explorer再起動時のアイコン消失はTaskbarCreatedの再登録で吸収され、通知はCommunity Toolkit通知やWinRTのToastNotificationManagerが世代交代した末に、非パッケージアプリでも公式推奨となったWindows App SDKのAppNotificationへ集約されています。ただし非パッケージアプリはランタイムの配布が必須で、管理者昇格プロセスや応答不可(Focus Assist)では通知が届かないことがあり、Windowsサービスはセッション0で動くためトースト自体を出せず、ユーザーセッション側のトレイ常駐エージェントとIPCで連携する構成が定石になります。

タスクトレイ常駐とトースト通知の知識マップNotifyIconによるトレイ常駐とTaskbarCreatedでの再登録、WinRT・Community Toolkit・Windows App SDKというトースト通知APIの世代交代、非パッケージアプリのランタイム配布要件、応答不可や昇格プロセスによる通知の非到達、セッション0のWindowsサービスがトレイ常駐エージェントとIPC連携する構成の関係を示す図前提とする利用するの後継の後継前提とする推奨される対応前提とする前提とする両立しない両立しない両立しない前提とする利用する前提とする原因になり得る原因になり得る利用するで構成できるNotifyIconクラスAppNotification(Windows App SDK)WPFWindows FormsTaskbarCreatedメッセージWinRTのToastNotificationManagerCommunity Toolkit NotificationsAUMID(AppUserModelID)unpackaged appWindows App SDKランタイム(配布)COM(コンポーネントオブジェクトモデル)管理者権限セッション0Windowsサービストレイ常駐エージェント+IPCによる通知構成NotifyIcon.ShowBalloonTip応答不可(Focus Assist)通知が届かない・気づかれないケース

図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全18件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle

2. NotifyIconによるトレイ常駐の基本

タスクバー右端の通知領域にアイコンを出すコンポーネントが System.Windows.Forms.NotifyIcon です。バックグラウンドで動き続けるアプリが、状態表示と操作メニューの入口として使います。4

private NotifyIcon _trayIcon = null!;

private void InitializeTrayIcon()
{
    var menu = new ContextMenuStrip();
    menu.Items.Add("開く", null, (_, _) => ShowMainWindow());
    menu.Items.Add(new ToolStripSeparator());
    menu.Items.Add("終了", null, (_, _) => ExitApplication());

    _trayIcon = new NotifyIcon
    {
        Icon = Properties.Resources.AppIcon, // Iconは必須。未設定だと表示されない
        Text = "受注監視ツール ── 稼働中",     // ツールチップ(.NET 6+で最大127文字)
        ContextMenuStrip = menu,
        Visible = true                        // 既定はfalse。trueにして初めて表示される
    };
    _trayIcon.DoubleClick += (_, _) => ShowMainWindow();
}

押さえておくポイントは3つです。

  • IconVisible = true の両方が揃って初めて表示されます。Visible の既定値は false です。4
  • Text には長さ制限があります。.NET 6以降は最大127文字、.NET 5以前と.NET Frameworkは63文字で、超えると ArgumentException が発生します。状態をツールチップに詰め込みすぎる実装は、あるとき突然例外で落ちる原因になります。516
  • 右クリックメニューは ContextMenuStrip で関連付けます。トレイアイコンはユーザーから見える唯一の入口になることが多いので、「開く」と「終了」は最低限用意します。17

もう一つ、トレイアイコンは通知の入口であると同時に、常時見えている状態表示でもあります。IconText は実行中に差し替えられるので、「正常時は通常アイコン、異常時は警告アイコン」「ツールチップに最終処理時刻を出す」のように現在の状態を反映させておくと、ユーザーがトーストを見逃していても、アイコンを見れば今の状態が分かります。トーストが「その瞬間の出来事を知らせる」手段、トレイアイコンが「現在の状態を示し続ける」手段、という役割分担です。

3. 「閉じてもトレイに残る」設計と、正しい終了経路

常駐アプリの定番が「×ボタンで閉じてもプロセスは終了せず、トレイに残る」という動きです。実装自体は FormClosing のキャンセルで簡単に書けますが、雑に書くとアプリを終了する手段がなくなるWindowsのシャットダウンを妨げるという2つの事故につながります。

private bool _exitRequested;

protected override void OnFormClosing(FormClosingEventArgs e)
{
    // ユーザーが×を押しただけなら、閉じずに隠す
    if (!_exitRequested && e.CloseReason == CloseReason.UserClosing)
    {
        e.Cancel = true;
        Hide();
        return;
    }
    // 「終了」メニュー経由、またはOSシャットダウン等はそのまま閉じる
    base.OnFormClosing(e);
}

private void ExitApplication()
{
    _exitRequested = true;
    _trayIcon.Visible = false;
    _trayIcon.Dispose();
    Close();
}

FormClosing はフォームが閉じる前に発生し、e.Cancel = true で閉鎖を取り消せます。このとき必ず CloseReason を確認してください。UserClosing(ユーザーの操作)のときだけキャンセルし、WindowsShutDown などOS起因の閉鎖はキャンセルしないのが正しい振る舞いです。818

WPFでも考え方は同じで、Window.Closinge.Cancel = true にして Hide() するパターンが公式ドキュメントにもそのまま載っています。19

なお、常駐アプリは性質上「二重に起動すると同じイベントを二重処理する」問題と隣り合わせです。多重起動防止は「Windowsアプリの多重起動防止 ── 名前付きMutexと二重起動時のアクティブ化」で整理しています。また、常駐アプリの中身(監視ループや定期処理)の作り方は「.NET Generic HostとBackgroundServiceをデスクトップアプリで使う理由」が土台になります。

4. WPFアプリでのトレイ常駐

WPFには NotifyIcon に相当するコントロールがありません。これは公式の「WinFormsコントロールと同等のWPFコントロール」対照表に「No equivalent control」と明記されている、仕様としての欠落です。6

現実的な選択肢は2つです。

選択肢 概要 判断の目安
WinFormsのNotifyIconを併用 csprojで UseWPF + UseWindowsForms を両方有効化 追加依存なし。アイコンとメニューだけなら十分
サードパーティ(H.NotifyIconなど) WPF/WinUI向けのトレイアイコン実装 XAMLで書きたい、リッチなポップアップが欲しい場合。Microsoft非公式である点は考慮

前者は、SDKスタイルのプロジェクトファイルで2つのプロパティを併記するだけです。7

<PropertyGroup>
  <TargetFramework>net8.0-windows</TargetFramework>
  <UseWPF>true</UseWPF>
  <UseWindowsForms>true</UseWindowsForms>
</PropertyGroup>

System.Windows.Forms.NotifyIcon をWPFの Application から普通にnewして使えます。メニューだけWinForms(ContextMenuStrip)になりますが、トレイメニュー程度であれば実用上の問題はまずありません。

5. トレイアイコンの落とし穴3つ

5.1. アイコンは既定で「隠れたアイコン」に入る ── 強制表示はできない

自分のアイコンをタスクバーに常時表示させたい、という要望は必ず出ますが、プログラムからは実現できません。公式ドキュメントに、アイコンは既定でオーバーフロー(隠れたアイコン)領域に追加され、通知領域への昇格(常時表示)を決められるのはユーザーだけ、と明記されています。1

Windows 11では「設定 → 個人用設定 → タスクバー → その他のシステムトレイアイコン」でユーザーがアプリごとにオン/オフします。常時表示が運用上重要なら、インストール手順書やヘルプに「この設定をオンにしてください」と書く、が正しい対応です。

手順書にはメニューのフルパスまで書いておくと、電話越しでも案内できます。文面の例は次のとおりです。

  1. タスクバーの何もないところを右クリックし、「タスクバーの設定」を選びます。
  2. 開いた画面で「その他のシステム トレイ アイコン」を開きます。
  3. 一覧から本アプリを探し、スイッチをオンにします。

※ アイコンが見当たらないときは、タスクバー右側の「^」(山形)ボタンを押すと、隠れているアイコンが一時的に表示されます。

5.2. Explorerが再起動するとアイコンが消える ── TaskbarCreated

Explorerがクラッシュや再起動でタスクバーを作り直すと、登録済みのトレイアイコンは消えます。シェルはタスクバー作成時に TaskbarCreated メッセージを全トップレベルウィンドウへブロードキャストするので、これを受けたらアイコンを再登録するのが Win32 レベルの決まりごとです。20

WinFormsの NotifyIcon はこのメッセージを内部で処理して自動的に再登録するため、通常は意識する必要はありません。自前で Shell_NotifyIcon を P/Invoke する場合(あるいはトレイアイコンを持つネイティブアプリを保守している場合)は、この再登録処理が入っているかを確認してください。「Explorerを再起動するとアイコンだけ消えて、プロセスは生きている」という症状はほぼこれです。

5.3. Disposeを忘れるとアイコンが残る

Win32的には、トレイアイコンは不要になったら明示的に削除(NIM_DELETE)する必要があり、WinFormsではそれを NotifyIcon.Dispose() が担います。1 終了経路で Visible = falseDispose() を確実に通すようにしてください。プロセスが異常終了するなどして削除されないままだと、マウスカーソルを重ねるまで幽霊アイコンが残り続ける、というのは常駐アプリ開発者なら一度は見る光景です。

第3章のコードのように、「終了」メニューの処理でトレイアイコンの後始末をしてから Close() する形にしておくのが安全です。

6. トースト通知 ── どのAPIを使うか

トースト通知は、画面右下にバナーとして表示され、通知センターに残る通知です。デスクトップアプリから出す方法はこの10年で世代交代が続いており、検索結果には古いサンプルが大量に残っています。2026年時点の整理は次のとおりです。9

API NuGet / 名前空間 位置付け(2026年時点) 判断
Windows App SDK AppNotification Microsoft.WindowsAppSDK / Microsoft.Windows.AppNotifications WinForms・WPF・非パッケージWin32に公式推奨 新規はこれ
Community Toolkit Notifications Microsoft.Toolkit.Uwp.Notifications アーカイブ済み。現行のCommunity Toolkitに含まれず、最終更新は2022年 既存コードの維持のみ。新規採用しない
WinRT直接利用 Windows.UI.Notifications(Windows TFM経由) 保守のみ。推奨対象はUWP 新規採用しない
NotifyIcon.ShowBalloonTip System.Windows.Forms バルーン通知のAPI。Windows 10ではトーストとして表示、Windows 11では一時表示で通知センターに残らない 簡易用途なら可。挙動がOSで変わる点に注意

補足が2つあります。

  • ShowBalloonTip は「昔のAPIだが実は生きている」枠です。Windows 10ではバルーンはトーストとして描画され、閉じるまで通知センターに残ります。一方Windows 11では従来のバルーンに近い一時的な表示になり、消えた後は通知センターに残りません。表示時間の指定(timeout)も現在は無視され、OS側のアクセシビリティ設定に従います。トレイ常駐アプリが自分の状態変化を軽く知らせる程度なら十分ですが、「通知センターに履歴を残したい」ならWindows 11では役に立たない、と覚えておいてください。21
  • 素の Windows.UI.Notifications をデスクトップアプリから使う古典的な方法には、AUMID(AppUserModelID)付きショートカットをスタートメニューに登録しておかないとトーストが表示されないという有名な制約があります。22 この面倒さを解消したのが次章の AppNotification です。

表の「簡易用途なら可」がどこまでを指すのかは、機能で線を引けます。次の5つをすべて満たすなら ShowBalloonTip で十分で、Windows App SDKへの依存を増やす必要はありません。

  • そもそもトレイアイコンを持っているアプリである(バルーンはトレイアイコンに付随する通知なので、これが前提です)
  • 通知に操作ボタンや入力欄が要らない。クリックしたらアプリのウィンドウを前に出す、程度で足りる
  • 通知センターに履歴として残す必要がない。Windows 11では残らないため、「後から見返す」用途には使えません21
  • 進捗バーや、表示済み通知の内容更新が要らない
  • 通知は「気づけたら早く対応できる」補助であり、見逃しても業務が回る

逆に、次のいずれか1つでも当てはまるなら、素直に第7章の AppNotification を使ってください。

要件 理由
通知にボタン(「ログを開く」など)や入力欄を置きたい バルーンはタイトル・本文・アイコンだけで、操作要素を持てません
Windows 11でも通知センターに残したい バルーンはWindows 11では残りません21
長い処理の進捗を1枚の通知で更新したい 進捗バーと UpdateAsync はAppNotification側の機能です(第8章)
アプリが起動していない状態からの復帰も扱いたい バルーンはアプリが起動しトレイアイコンを出していないと存在しません。AppNotificationは通知のクリックでプロセスを起動できます(第7章)
古い通知を後からコードで消したい・差し替えたい Tag / Group による管理はAppNotification側の機能です(第8章)

判断に迷ったときの目安はひとつで、「その通知に対してユーザーに何かを操作させたいか」です。読ませるだけならバルーン、押させるならAppNotificationです。

7. Windows App SDK AppNotificationの最小実装

Windows App SDK のアプリ通知APIは、パッケージ化していない普通のexeでも、パッケージIDなしで動作します11 ただし「NuGetを入れれば終わり」ではなく、開発側と配布側の両方に前提があります。順に見ていきます。

7.1. プロジェクト側の準備 ── NuGetとcsproj

公式の前提は、.NET 6以降のWPFまたはWinFormsプロジェクトであること、そのプロジェクトがWinRT APIを呼べるように構成されていること、そしてWindows App SDKを使うように構成されていることの3点です。12

やることは2つです。まずNuGetパッケージ Microsoft.WindowsAppSDK を追加します(Visual Studioなら「ツール → NuGet パッケージ マネージャー → ソリューションの NuGet パッケージの管理」から)。このパッケージにはWindows App SDKの各コンポーネントのサブパッケージが含まれており、公式もまずメインのパッケージを入れることを推奨しています。2

次にcsprojを編集します。第4章のWPF+WinFormsの例に、Windows App SDKの分を足すと次の形になります。

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <!-- WinRT APIを呼ぶため、TFMにWindowsのバージョンまで含める -->
    <TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
    <UseWPF>true</UseWPF>
    <UseWindowsForms>true</UseWindowsForms>
    <!-- 非パッケージ(MSIXでない)アプリで、起動時にランタイムを自動初期化する -->
    <WindowsPackageType>None</WindowsPackageType>
  </PropertyGroup>

  <ItemGroup>
    <!-- バージョンはNuGetパッケージマネージャーで選んだものが入る -->
    <PackageReference Include="Microsoft.WindowsAppSDK" Version="(選択した安定版)" />
  </ItemGroup>
</Project>

<WindowsPackageType>None</WindowsPackageType> が要になります。非パッケージアプリは、Windows App SDKの機能を使う前にランタイムを初期化しなければならず、このプロパティを書いておくと起動時の自動初期化(auto-initialization)が有効になります。ビルドすると MddBootstrapAutoInitializer.csWindowsAppSDK-VersionInfo.cs がプロジェクトに追加されます。細かい制御が必要な場合だけ、自動初期化の代わりにブートストラップAPIを直接呼びます。2

7.2. 配布先へのランタイム導入

ここが見落とされがちです。非パッケージアプリの場合、Windows App SDKランタイムを利用者のPCへ届ける責任は配布する側にあります。23 exeを配るだけでは動きません。

前提として、非パッケージアプリでは Visual C++ 再頒布可能パッケージも必要です。23 導入方法は2つあります。

方法 内容 向いているケース
インストーラーを実行する 公式配布の WindowsAppRuntimeInstall.exe を実行する。x86 / x64 / Arm64 それぞれに別のインストーラーがあり、その名前のアーキテクチャのパッケージだけを配置する23 通常はこちら
MSIXパッケージを直接入れる 自社のセットアップやMSIにMSIXパッケージを同梱して配置する23 配置を細かく制御したい場合

インストーラーはUIを持たないため、自社インストーラーから連鎖(チェーン)して呼び出す形になります。無人実行のオプションが用意されています。23

:: 画面表示・テキスト出力を一切出さずに導入する
WindowsAppRuntimeInstall.exe --quiet

:: 使用中のWindows App SDKプロセスを終了させたうえで強制更新する(1.1以降)
WindowsAppRuntimeInstall.exe --force

導入時に押さえておきたい点が3つあります。23

  • 管理者権限の有無で結果が変わります。昇格して実行され、かつ実行ユーザーが管理者権限を持つ場合はシステム全体(将来追加されるユーザーも含む)に登録されます。そうでない場合は実行したユーザーだけへの導入になります。戻り値 0x80070005 はまさにこの「昇格していない/管理者権限がない」ケースです。
  • 戻り値で成否を判定します。0x0 が成功、0x80073d06 は1つ以上のパッケージの導入失敗です。自社インストーラーから連鎖する場合は、この戻り値を必ず見てください。
  • 開発機に入れるランタイムは、参照しているNuGetパッケージのバージョン・リリースチャネルに合わせます。2 「開発機では出るのに配布先で出ない」の原因がここにあることは珍しくありません。

MSIXでパッケージ化して配布する場合は、ランタイムの配布はパッケージの依存関係として扱えますが、代わりに Package.appxmanifest へCOMアクティベーターの記載が必要になります(7.3の最後を参照)。

7.3. 最小実装

using System;
using System.Windows.Forms;
using Microsoft.Windows.AppNotifications;
using Microsoft.Windows.AppNotifications.Builder;

internal static class ToastActivation
{
    private static readonly object Gate = new();
    private static Action? _pending;
    private static bool _uiReady;

    // 起動時に一度だけ。ハンドラー登録 → Register() の順序が重要
    public static void Initialize()
    {
        AppNotificationManager.Default.NotificationInvoked += (_, args) =>
        {
            // トーストのクリックやボタン押下がここに届く(引数はargs.Argumentsで判別)
            if (!args.Arguments.TryGetValue("action", out var action) || action != "openLog")
            {
                return;
            }

            // NotificationInvoked はバックグラウンドスレッドで発生する。
            // UIに触る処理は必ずUIスレッドへ渡すこと
            Dispatch(ShowLogWindow);
        };
        AppNotificationManager.Default.Register();
    }

    // MainForm の OnShown から呼ぶ。フォームができる前に届いた分をここで流す
    public static void MarkUiReady()
    {
        Action? pending;
        lock (Gate)
        {
            _uiReady = true;
            pending = _pending;
            _pending = null;
        }
        // OnShown は UI スレッドなので、そのまま実行してよい
        pending?.Invoke();
    }

    private static void Dispatch(Action action)
    {
        lock (Gate)
        {
            if (!_uiReady)
            {
                // トーストのクリックがアプリの起動そのものだった場合、ここへ来た
                // 時点ではまだフォームが1つも無い。預かっておいて MarkUiReady が流す
                _pending = action;
                return;
            }
        }
        MainWindow()?.Invoke(action);
        // WPFなら: Application.Current.Dispatcher.Invoke(action);
    }

    private static Form? MainWindow() =>
        Application.OpenForms.Count > 0 ? Application.OpenForms[0] : null;

    private static void ShowLogWindow() { /* ログウィンドウを開く */ }
}
// MainForm 側。起動のきっかけがトーストのクリックだった場合はここで拾う
protected override void OnShown(EventArgs e)
{
    base.OnShown(e);
    ToastActivation.MarkUiReady();
}
ToastActivation.Initialize();

// 通知を出す
var notification = new AppNotificationBuilder()
    .AddText("受注取込が完了しました")
    .AddText("新規12件、エラー1件")
    .AddButton(new AppNotificationButton("ログを開く")
        .AddArgument("action", "openLog"))
    .BuildNotification();
AppNotificationManager.Default.Show(notification);

実務上の注意点は次のとおりです。

  • NotificationInvoked の購読は Register() より前に行います。順序を誤ると、トーストのクリックでアプリが起動されたときにイベントを取りこぼします。12
  • クリックでアプリが起動された場合、ハンドラーが動く時点ではまだフォームが1つもありません。Application.OpenForms を見て null なら何もしない、という書き方をすると、この経路の操作が黙って捨てられます。上のように一度預かって、フォームが表示されてから流してください。順序を正しくしてイベントを受け取れても、ここで捨てていては同じことです。
  • NotificationInvoked はバックグラウンドスレッドで発生します。公式サンプルにも「バックグラウンドスレッドで発生するので、UI更新はUIスレッドへディスパッチすること」と明記されています。ハンドラーからウィンドウを開いたりコントロールを触ったりする処理をそのまま書くと、スレッド関連の例外や不可解な固まり方に遭遇します。WPFなら Application.Current.Dispatcher.Invoke(...)、WinFormsなら Control.Invoke(...) でUIスレッドへ渡してください。12
  • アプリ終了前には AppNotificationManager.Default.Unregister() を呼びます。公式ドキュメントに、終了前のUnregisterでリソースを後始末し、以後の通知クリックでアプリが正しく起動されるようにすべきと明記されています。第3章の「終了」メニューの処理に、NotifyIcon.Dispose() と並べて組み込んでください(通知機能を今後一切使わないアプリをアンインストールする場合などは、登録情報ごと消す UnregisterAll() が別に用意されています)。24
  • 非パッケージアプリでは Register() がCOMサーバー登録・AUMID相当の処理を自動で行います。旧世代(素のWinRT)で必要だったショートカット設置やレジストリ登録は不要です。MSIXでパッケージ化する場合は、逆にPackage.appxmanifestへのCOMアクティベーター記載が必要になります。12
  • Windows App SDKランタイムを同梱するself-contained配布を選ぶ場合は注意が必要です。AppNotificationManager はWindows App SDKのSingletonパッケージに依存しており、self-containedアプリから呼び出す際の考慮事項が公式に案内されています。配布方式を決める前に確認してください。25
  • 管理者に昇格したプロセスからの通知は未サポートです。Show は例外を出さず静かに失敗するため、原因に気づきにくい問題です。昇格が必要なアプリは、通知だけ非昇格プロセスに任せる分離を検討してください(この分離の考え方は「Windowsアプリで「管理者権限が必要な処理だけ」を分離する具体的な書き方」で扱っています)。13
  • トーストがクリックされたとき、アプリが起動していなければプロセスが新たに起動されます。トレイ常駐アプリでは多重起動防止(第3章参照)と組み合わせて、既存インスタンスへ処理を渡す設計にしておく必要があります。

8. 一歩進んだ使い方 ── 進捗バー付きトーストと通知の後始末

8.1. 進捗をトーストの中で更新する ── UpdateAsync

「受注データ120件を取り込み中」のような長い処理の進捗は、通知を何度も出し直すのではなく、1枚のトーストの中の進捗バーを書き換えるのが正しい作り方です。AppNotificationには、そのためのデータバインドと更新の仕組みがあります。26

作りは3段階です。まず進捗バーの各値を Bind 系メソッドでプレースホルダーにし、Tag(と必要なら Group)で通知を識別できるようにして表示します。その後は AppNotificationProgressDataUpdateAsync に渡すだけで、表示済みのトーストの中身だけが書き換わります。

const string tag = "import-progress";
const string group = "batch";

// 初回表示: 値はバインドにして、初期値をProgressに設定する
var notification = new AppNotificationBuilder()
    .AddText("受注データを取り込んでいます")
    .AddProgressBar(new AppNotificationProgressBar()
        .BindStatus().BindValue().BindValueStringOverride())
    .SetTag(tag)
    .SetGroup(group)
    .BuildNotification();

notification.Progress = new AppNotificationProgressData(1) // シーケンス番号
{
    Status = "取込中...", Value = 0, ValueStringOverride = "0/120件"
};
AppNotificationManager.Default.Show(notification);

// 進捗が進むたびに: シーケンス番号を増やして更新する
var data = new AppNotificationProgressData(2)
{
    Status = "取込中...", Value = 45 / 120.0, ValueStringOverride = "45/120件"
};
await AppNotificationManager.Default.UpdateAsync(data, tag, group);

シーケンス番号は更新の順序を示す非ゼロの整数で、複数の更新が前後して届いた場合は最も大きい番号が表示されます。非同期処理から並行して更新を投げても古い値で巻き戻らないための仕組みなので、単調増加させてください。27

8.2. 「更新」と「差し替え」の使い分け

同じ Tag で新しい通知を Show し直す「差し替え」でも見た目は更新できますが、挙動が異なります。26

  差し替え(同じTagで再Show) 更新(UpdateAsync)
通知センター内の位置 先頭に移動する その場に留まる
変更できる内容 レイアウトを含めてすべて バインドした進捗バーと先頭テキストのみ
ポップアップ 再表示できる 再表示されない(静かに書き換わる)
ユーザーが既に閉じていた場合 常に送られる 失敗する

公式ドキュメントの推奨も明確で、途中経過は更新、完了は差し替えです。進捗が50%から65%になったことをいちいちポップアップで知らせる必要はありませんが、「取込完了(エラー3件)」はユーザーが進捗通知を閉じてしまっていても届いてほしいからです。完了時には進捗バーを外してボタン(「エラー内容を見る」など)を付けた別レイアウトに差し替える、という形が実務の定番になります。

8.3. 通知センターに残った古い通知を片付ける

トーストはユーザーが消すまで通知センターに溜まります。常駐アプリが毎日何件も通知を出すと、通知センターが自アプリの古い通知だらけになり、かえって重要な通知が埋もれます。

Tag / Group を付けておけば、自アプリの通知をコードから削除できます(タグ指定・グループ指定・全削除)。有効期限の設定もできるので、「進捗系は完了時に消す」「情報系は有効期限を短くする」といった片付けのルールを、通知を出す設計と同時に決めておくのがおすすめです。28

9. 通知が「届かない」ケースを設計に織り込む

トースト通知は確実な伝達手段ではありません。表示されない・気づかれない経路が正常系として存在します。

ケース 挙動
応答不可(Focus Assist / Do not disturb)が有効 バナーは表示されず、通知は通知センターへ直行する
アプリ単位・システム全体で通知がオフ 表示されない
管理者昇格プロセスからのAppNotification 未サポート。表示されない
フルスクリーンアプリ実行中など OSの判断で抑制されることがある

応答不可は、Windows 10では「集中モード(Focus assist)」、Windows 11では「設定 → システム → 通知」の「応答不可」トグルで、ユーザーやグループポリシーが制御します。OSアップグレード直後の1時間は自動的に有効になる仕様さえあります。14 集中モードの動作は「通知を出さない」ではなく「バナー表示を抑止して通知センターへ送る」なので、ユーザーが後から通知センターを開けば見えます。15

確認する場所は、メニューのフルパスで持っておくと現場で早いです。「通知が出ない」は電話やチャット越しに案内することが多いためです。

確認したいこと どこを見るか(Windows 11)
応答不可が有効になっていないか 設定 → システム → 通知 → 「応答不可」のトグル
自動的に応答不可になる条件が設定されていないか 同じ「通知」の画面内にある、自動で応答不可にする条件の設定
自アプリの通知がオフにされていないか 同じ「通知」の画面を下にスクロールし、アプリごとの一覧から自アプリを探す
バナーが出ないだけなのか、通知自体が届いていないのか タスクバー右端の日付と時刻をクリックして通知センターを開き、通知が積まれているか見る
トレイアイコンが隠れているだけではないか 設定 → 個人用設定 → タスクバー → その他のシステム トレイ アイコン(第5.1章)

Windows 10の場合、応答不可に相当するのは「設定 → システム → 集中モード」です。通知センターに届いているかを先に確認するのが切り分けの近道で、届いているならOS側のバナー抑止(応答不可など)、届いていないならアプリ側の送信そのものを疑う、と分岐できます。

設計への含意はシンプルです。

  • トーストは「気づいてもらえたら早く対応できる」補助チャネルとして使い、業務上必須の情報はアプリ内の画面・一覧で必ず確認できるようにします。「トーストが出たはず」を業務フローの前提にしないでください。
  • どうしても割り込みが必要な通知(緊急停止の警告など)は、AppNotificationScenario.Urgent(重要な通知)でユーザー許可のもと応答不可を突破する仕組みがありますが29、乱用すればユーザーはアプリの通知ごとオフにします。本当に緊急のものだけに絞るべきです。

外部装置の異常などをどう画面に出すかという、より広い話は「外部機器の状態の確認と表示のベストプラクティス」も参考にしてください。

10. Windowsサービスから通知したいとき

「常駐サービスが異常を検知したら、ログオン中のユーザーにトーストで知らせたい」という要件は自然に出てきますが、サービスから直接トーストは出せません。Windows Vista以降、サービスはセッション0という専用セッションで動作し、ユーザーと対話するUIを持てないためです。3

定石は、UIをユーザーセッション側のプロセスに分離する構成です。

[セッション0]                      [ユーザーセッション]
 Windowsサービス   ←─ IPC ─→   トレイ常駐エージェント
 (監視・業務処理)   名前付きパイプ等   (NotifyIcon + AppNotification)

複数ユーザーが同時ログオンする環境(RDP・共有端末)では、エージェントはセッションごとに1つずつ動くことになります。「誰に通知すべきか」(全員か、特定ユーザーだけか)を要件として先に決めておくと、設計がぶれません。

11. まとめ

  • トレイ常駐は NotifyIcon が基本。Icon + Visible = true で表示し、Text は127文字制限(.NET 6+)、終了経路では Dispose() を確実に通します。アイコンとツールチップは「現在の状態」を示す常時表示としても使います。
  • 「×で閉じてもトレイに残す」は FormClosing のキャンセルで実装しますが、CloseReason.UserClosing のときだけキャンセルし、明示的な「終了」メニューを必ず用意します。
  • アイコンの常時表示はユーザーにしか決められません。Explorer再起動時の消失は TaskbarCreated の再登録で対処します(WinFormsは自動)。
  • トースト通知の新規実装は Windows App SDK の AppNotificationManager 一択です。非パッケージexeでもパッケージIDなしで動き、Register() が面倒な登録を自動化します。Community Toolkit通知はアーカイブ済み、素のWinRTは保守のみです。ボタンも履歴も進捗も要らない軽い通知なら、ShowBalloonTip で済ませる判断も残ります(第6章)。
  • 非パッケージアプリでは、Windows App SDKランタイムを配布先へ届けるのは配布側の責任です。csprojに <WindowsPackageType>None</WindowsPackageType> を書き、インストーラーから WindowsAppRuntimeInstall.exe を連鎖させるところまでを配布設計に含めてください(第7.2章)。
  • 長い処理の途中経過は UpdateAsync で1枚のトーストの進捗バーを静かに書き換え、完了は差し替えでポップアップさせるのが定石です。Tag/Group を付けて、古い通知の削除まで設計に含めます。
  • トーストは応答不可や通知オフで届かないことがある補助チャネルです。必須情報はアプリ内で確認できる設計にし、昇格プロセスからの通知が未サポートである点にも注意してください。
  • サービスからの通知は、ユーザーセッション側のトレイ常駐エージェント+IPCという分離構成で実現します。

関連記事

関連する相談領域

合同会社小村ソフトでは、トレイ常駐型の監視ツールや通知エージェントの設計・実装、Windowsサービスと組み合わせた常駐システムの構成検討、既存常駐アプリの不具合調査を扱っています。

参考リンク

  1. Microsoft Learn, Notification Area (Windows Shell). アイコンが既定でオーバーフロー領域に追加されること、通知領域への昇格を決められるのはユーザーだけであること、不要になったアイコンはNIM_DELETEで削除する必要があることについて。  2 3

  2. Microsoft Learn, Use the Windows App SDK in an existing project. WPF・WinForms・Win32のデスクトップアプリが既定で非パッケージであること、NuGetパッケージ Microsoft.WindowsAppSDK を導入しメインのパッケージを入れることが推奨されること、非パッケージアプリはWindows App SDKの機能を使う前にランタイムの初期化が必要で <WindowsPackageType>None</WindowsPackageType> により起動時の自動初期化が有効になること、ビルド時に MddBootstrapAutoInitializer.csWindowsAppSDK-VersionInfo.cs が追加されること、開発機に導入するランタイムをNuGetパッケージのバージョン・リリースチャネルに合わせるべきことについて。  2 3 4

  3. Microsoft Learn, Service Changes for Windows Vista / Interactive Services. セッション0がサービス専用でありユーザーとの対話をサポートしないこと、ユーザーへの表示にはCreateProcessAsUserでユーザーセッションにプロセスを作りIPCで連携する方法が推奨されることについて。  2 3

  4. Microsoft Learn, NotifyIcon Class / NotifyIcon.Visible Property. 通知領域にアイコンを表示するコンポーネントであること、Iconプロパティの設定が必要なこと、Visibleの既定値がfalseであることについて。  2 3

  5. Microsoft Learn, NotifyIcon.Text Property. ツールチップテキストの上限と、超過時にArgumentExceptionが発生することについて。  2

  6. Microsoft Learn, Windows Forms Controls and Equivalent WPF Controls. WPFにNotifyIconと同等のコントロールが存在しないことについて。  2

  7. Microsoft Learn, MSBuild properties for .NET desktop projects. UseWPFとUseWindowsFormsを併用できることについて。  2

  8. Microsoft Learn, Form.FormClosing Event. フォームが閉じる前に発生し、Cancelプロパティで閉鎖を取り消せることについて。  2

  9. Microsoft Learn, Windows notifications overview. アプリ種別ごとの推奨通知APIの対応表。WinForms/WPF/非パッケージWin32へのWindows App SDK推奨と、Windows.UI.NotificationsのMaintenance only位置付けについて。  2

  10. Microsoft Learn, Windows Community Toolkit — Notifications (archive). Notificationsコンポーネントがアーカイブされ、Windows App SDKのアプリ通知に置き換えられたことについて。 

  11. Microsoft Learn, Use the Windows App SDK in an existing project. Windows App SDKのアプリ通知APIがパッケージ・非パッケージ両対応で、パッケージIDを必要としないことについて。  2

  12. Microsoft Learn, Quickstart: App notifications in the Windows App SDK. AppNotificationManager/AppNotificationBuilderの使い方、非パッケージアプリでのRegister()による自動登録、NotificationInvokedをRegister()前に購読すべきこと、パッケージアプリでのマニフェスト記載について。  2 3 4 5

  13. Microsoft Learn, App notifications overview. 管理者昇格したアプリからの通知送信が未サポートであることについて。  2

  14. Microsoft Learn, Notifications don’t appear or aren’t displayed. 応答不可(Quiet Hours / Focus Assist)による通知抑止、OSアップグレード後1時間の自動有効化、通知が表示されない場合の確認手順について。  2

  15. Microsoft Learn, Focus assist. 集中モードが通知のバナー表示を抑止して通知センターへ直接送る仕組みであることについて。  2

  16. Microsoft Learn, NotifyIcon.Text maximum text length increased. .NET 6でTextの最大長が63文字から127文字に拡張された破壊的変更について。 

  17. Microsoft Learn, How to associate a shortcut menu with a Windows Forms NotifyIcon component. ContextMenuStripによる右クリックメニューの関連付けについて。 

  18. Microsoft Learn, FormClosingEventArgs Class. CloseReasonでフォームが閉じられる理由を判別できることについて。 

  19. Microsoft Learn, How to close a window or dialog box (WPF). WPFのClosingイベントでe.Cancel = trueとしてHide()する非表示化パターンについて。 

  20. Microsoft Learn, The Taskbar. タスクバー作成時にTaskbarCreatedメッセージがブロードキャストされ、受信したらトレイアイコンを再登録すべきであることについて。 

  21. Microsoft Learn, Shell_NotifyIconW function. バルーン通知がWindows 10ではトースト(バナー)として表示され通知センターに残ること、Windows 11では一時的な表示となり通知センターに残らないこと、表示時間がアクセシビリティ設定に従うことについて。  2 3

  22. Microsoft Learn, Send a local toast notification from a C# app (Win32/クラシックAPI). 素のToastNotificationManagerを使う場合、AUMID付きショートカットのスタートメニュー登録が必須であることについて。 

  23. Microsoft Learn, Windows App SDK deployment guide for framework-dependent apps packaged with external location or unpackaged. 非パッケージアプリの開発者が利用者へWindows App SDKランタイムを配布する責任を負うこと、Visual C++再頒布可能パッケージが必要であること、インストーラー方式とMSIXパッケージ直接配置の2方式があること、インストーラーがx86・x64・Arm64ごとに分かれておりその名前のアーキテクチャのパッケージのみを配置すること、--quiet--force のオプション、昇格かつ管理者権限の場合はシステム全体に、そうでない場合は実行ユーザーのみに導入されること、戻り値 0x00x80073d060x80070005 の意味、自社セットアップからShellExecuteで連鎖できることについて。  2 3 4 5 6

  24. Microsoft Learn, AppNotificationManager.Register Method. アプリ終了前にUnregisterを呼んでリソースを後始末し、以後の通知起動が正しく行われるようにすべきこと、通知機能を今後使わない場合はUnregisterAllで登録情報を消去すべきことについて。 

  25. Microsoft Learn, AppNotificationManager.UpdateAsync Method / Windows App SDK deployment architecture. AppNotificationManagerクラスがWindows App SDKのSingletonパッケージに依存しており、self-contained配布のアプリから呼び出す場合に考慮事項があることについて。 

  26. Microsoft Learn, App notification progress bar and data binding. Bindメソッドによるデータバインド、AppNotificationProgressDataとUpdateAsyncによる更新、更新と差し替えの違い(通知センター内の位置・変更できる範囲・ポップアップ再表示・ユーザーが閉じた後の挙動)、完了時には差し替えを検討すべきことについて。  2

  27. Microsoft Learn, AppNotificationProgressData(UInt32) Constructor. シーケンス番号が更新の順序を指定する非ゼロの整数であり、複数の更新データを受信した場合は最も大きい番号のデータが表示されることについて。 

  28. Microsoft Learn, Remove app notifications. タグ・グループの付与、表示済み通知の削除、有効期限の設定について。 

  29. Microsoft Learn, App notification content. scenario=”urgent”の重要な通知がユーザー許可のもとで応答不可設定より優先されることについて。 

同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。

このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。

この記事は次のサービスページにつながります。近い入口からご覧ください。

よくある質問

この記事のテーマについて、相談時によくある質問をまとめています。

WPFでタスクトレイ常駐アプリを作るにはどうすればよいですか?
WPFにはNotifyIcon相当のコントロールが存在しないため、csprojでUseWPFとUseWindowsFormsを併用してWinFormsのNotifyIconを使うのが最小構成です。XAMLフレンドリーな書き味が欲しい場合はH.NotifyIconなどのサードパーティライブラリという選択肢もありますが、Microsoft公式ではない点を踏まえて採用判断してください。
トースト通知のAPIはどれを使うべきですか?
2026年時点の新規実装であれば、WinForms/WPF/非パッケージアプリのいずれもWindows App SDKのAppNotificationManagerが公式推奨です。Microsoft.Toolkit.Uwp.Notificationsはアーカイブ済みで現行のCommunity Toolkitに含まれず、WinRTのToastNotificationManagerは保守のみの位置付けです。既存コードが後者2つで動いているなら急いで書き換える必要はありませんが、新規に選ぶ理由はありません。
トースト通知が表示されないときは何を確認すればよいですか?
まずOS側の設定です。応答不可(Focus Assist)が有効だと通知はバナー表示されず通知センターへ直行しますし、設定アプリでアプリ単位・システム全体の通知がオフになっている場合も表示されません。実装側では、管理者昇格したプロセスからのAppNotificationは未サポートで表示に失敗する点、素のWindows.UI.NotificationsをAUMIDなしのデスクトップアプリから呼んでも表示されない点が代表的な原因です。
Windowsサービスからトースト通知を出せますか?
出せません。Windows Vista以降、サービスはセッション0で動作し、ユーザーと対話するUIを持てないためです。ユーザーに通知したい場合は、ユーザーセッション側にトレイ常駐のエージェントプロセスを用意し、サービスとは名前付きパイプなどのIPCで連携して、通知の表示はエージェントが行う構成が定石です。

著者プロフィール

記事の著者プロフィールページです。

小村 豪

合同会社小村ソフト 代表

Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。

ブログ一覧に戻る