概要
本記事では、TreePaste の開発時に発生した「タスクトレイアイコンの右クリックで表示した ContextMenu が、フォーカス移動後も閉じない」問題を扱う。
解消には ContextMenu.StaysOpen = false の設定に加えて、表示直前に Win32Api.SetForegroundWindow() を呼び出し、メニュー表示の起点をフォアグラウンド状態へ揃える構成を採用する。
前提・対象環境
- フレームワーク/言語: .NET 10 / C# 14
- 対象 UI: WPF +
System.Windows.Forms.NotifyIcon - アーキテクチャ: コードビハインド
- 対象プロジェクト: TreePaste
- 検証環境: .NET 10 / Windows 11
本記事の根拠について
本記事の中心的な主張、すなわち「タスクトレイ起点で開いた ContextMenu が StaysOpen = false だけでは閉じず、SetForegroundWindow の併用で閉じるようになる」は、TreePaste の実際の開発で遭遇し、この構成に変更して解消した事象に基づく。
この挙動は自動テストで再現しにくい。
トレイアイコンへの実際のマウス入力と、別プロセス間のフォアグラウンド遷移が同時に必要になるためである。
実際、後述のフォアグラウンド制限により、直前にユーザー入力を受けていないプロセスからは前面化そのものができない。
そのうえで、次の 3 点は上記の環境で実際に動かして確認した。
| 確認した内容 | 結果 |
|---|---|
ContextMenu.StaysOpen の既定値 |
true(明示的に false にする必要がある) |
自ウィンドウが既に前面のときの SetForegroundWindow |
true を返し、前面のまま |
別ウィンドウが前面のときの SetForegroundWindow |
false を返し、前面化されない |
ContextMenu が閉じるかどうかそのものは、上記の理由から自動化して確認できていない。
問題
タスクトレイアイコンを右クリックして ContextMenu を表示すると、メニュー項目を選択しない限り表示が残り続ける挙動が発生した。
本来は他ウィンドウへフォーカスが移った時点でメニューが閉じることが期待されるが、実際には閉じず、操作体験を阻害していた。
原因・背景
WPF 側の ContextMenu は、StaysOpen の設定と表示時のフォアグラウンド状態に依存して閉じ方が変わる。
タスクトレイ起点の右クリックは通常の Window 配下の右クリックと異なり、アクティブウィンドウ遷移の文脈がずれる場合がある。
その状態で ContextMenu を開くと、フォーカス喪失を契機としたクローズ判定が期待どおりに働かないことがある。
解決方法
採用した解決策は次の 2 点である。
ContextMenu.StaysOpen = falseを明示する。ContextMenuを開く直前にWin32Api.SetForegroundWindow()を呼び、メニュー表示時のウィンドウ状態をフォアグラウンドへ合わせる。
この 2 点を併用すると、右クリックで開いたメニューがフォーカス移動時に閉じる挙動へ安定する。
StaysOpen だけを設定した場合で、フォアグラウンドが別アプリのままメニューが開くため、フォーカス喪失の判定が働かない。下段は表示前に SetForegroundWindow を呼ぶ経路である。Windows にはフォアグラウンド化の制限があり、この API は常に成功するとは限らない(上の P/Invoke 宣言では失敗時に false が返る)。トレイアイコンのクリック直後は、フォアグラウンド化が許可される条件の 1 つを満たすが、それでも Windows が拒否して false を返すことはある。TreePaste ではこの構成で切り替わることを確認しているが、成功を前提にせず、戻り値を見て失敗時の経路も用意しておく。実装例
最初に、タスクトレイ右クリック時の処理で SetForegroundWindow を呼び出してから ContextMenu を開く。
以下は TreePaste の MainWindow.xaml.cs で使用している実装である。
_notifyIcon.MouseClick += (_, e) =>
{
if (e.Button == System.Windows.Forms.MouseButtons.Right)
{
Dispatcher.Invoke(() =>
{
var helper = new WindowInteropHelper(this);
Win32Api.SetForegroundWindow(helper.Handle);
_trayContextMenu.Placement = System.Windows.Controls.Primitives.PlacementMode.MousePoint;
_trayContextMenu.IsOpen = true;
});
}
};
この順序により、メニューを開く直前のウィンドウ状態を明示的に整えられる。
IsOpen = true だけを先に実行する構成より、フォーカス遷移時の挙動が安定しやすい。
次に、ContextMenu 側で StaysOpen = false を明示する。
以下は TreePaste の CreateTrayContextMenu() で返却している構成である。
return new System.Windows.Controls.ContextMenu
{
Items = { showItem, githubItem, separator, exitItem },
StaysOpen = false
};
ContextMenu.StaysOpen の既定値は true であり、明示的に false にしない限り外側のクリックでは閉じない(実測で確認)。
StaysOpen = false は「外側をクリックした際に閉じる」ための基本設定である。
タスクトレイ起点の表示では、この設定単独では不十分な場合があり、前段の SetForegroundWindow との組み合わせが有効である。
SetForegroundWindow は Win32Api で次のように定義して使用する。
[DllImport("user32.dll")]
public static extern bool SetForegroundWindow(IntPtr hWnd);
Win32 API の P/Invoke 定義を追加しておくことで、WPF アプリ側から表示制御の補助が可能になる。
注意点
SetForegroundWindowは OS のフォアグラウンド制御制約を受けるため、常に完全な制御を保証するものではない。実測では、直前にユーザー入力を受けていないプロセスから別ウィンドウを前面化しようとするとfalseが返り、前面は切り替わらなかった。トレイアイコンのクリック直後は許可条件の 1 つを満たすが、それで成功が保証されるわけではない。タイマーやバックグラウンド処理から呼ぶ場合はより失敗しやすい。いずれの場合も戻り値を確認し、falseのときはメニューが閉じない可能性があるものとして扱う。NotifyIconと WPFContextMenuを混在させる実装では、UI スレッド上でメニュー操作を行うためDispatcher.Invokeを維持する。StaysOpen = falseを設定しても、表示元の状態が不整合なままでは期待どおりに閉じないケースがある。- タスクトレイ常駐はウィンドウの有無と寿命が一致しないため、
ShutdownModeをOnExplicitShutdownにしたうえで終了メニューからShutdown()を呼ぶ(呼び忘れるとウィンドウが無いままプロセスが残る。切り分けはWPF でウィンドウを閉じてもプロセスが終了しない原因の切り分けと ShutdownMode・フォアグラウンドスレッドの扱いで扱っている)。
代替案・比較
| 方法 | メリット | デメリット | 適するケース |
|---|---|---|---|
StaysOpen = false のみ |
実装が最小で簡潔 | タスクトレイ起点では閉じない事例が残る | 通常のウィンドウ内右クリック中心のメニュー |
SetForegroundWindow のみ |
表示時のアクティブ状態を補正できる | ContextMenu 設定不足時の閉じ漏れを防ぎ切れない |
既存メニュー設定を維持したい場合 |
| 2 つを併用(採用) | 表示時とクローズ条件の双方を補完できる | Win32 API の依存が増える | タスクトレイメニューを安定動作させたい場合 |
まとめ
TreePaste で発生したタスクトレイ ContextMenu の閉じ残りは、StaysOpen = false と SetForegroundWindow() の併用で解消できる。
この構成により、右クリックで表示したメニューがフォーカス移動時に閉じる標準的な操作感へ揃えられる。
タスクトレイ起点のメニューを WPF で扱う場合は、メニュー設定だけでなく、表示直前のフォアグラウンド制御を合わせて設計することが有効である。