WinUI 3/.NET 8で、Explorerの右クリックメニューをWin32から取得してMenuFlyoutなどに再現したい――そのときハマりがちなのが「&が混ざる」「言語が揺れる」「サブメニューが欠ける(Win10/11で症状が違う)」問題です。本記事では原因と実装上の落とし穴、安定させるための具体策を整理します。
やりたいこと:Explorerの右クリックメニューを“自前UI”で再構成する
Win32 API(GetMenuItemInfo など)とShellの IContextMenu(必要に応じて IContextMenu2/IContextMenu3)を使うと、フォルダーやファイルの右クリックメニュー(コンテキストメニュー)を アプリ側で列挙できます。列挙した項目を元に、WinUI 3の MenuFlyout や独自のコマンドパレットへ変換できれば、アプリのUI統一やショートカット導線の追加ができて便利です。
一方で、Explorerの右クリックメニューは「ただの静的なリスト」ではありません。Shell拡張(サードパーティ含む)が混ざり、開くタイミングで項目が追加されたり、サブメニューが動的に組み立てられたり、描画(owner-draw)時に初めて内容が確定するものもあります。そのため、取得したメタ情報だけで“完全再現”を目指すと、OS差・初回のみ不完全・言語の揺れといった症状が起きやすくなります。
前提:IContextMenuで見ているのは「クラシックメニュー」の世界
Windows 11では右クリックメニューが新しいデザインに変わりましたが、IContextMenu と HMENU を中心にした仕組みは、基本的に「クラシックなWin32メニュー」を構築します。つまり、アプリが列挙できるのは多くの場合、Explorerで「その他のオプションを表示(Show more options)」に近いメニュー体系です。
この前提を押さえると、Win10/Win11で見え方が違うこと自体は自然です。アプリ側で“同じに見せる”には、どこまで忠実に追従するか(再現する/しない)を設計として先に決めるのが近道です。
メニュー文字列に &(アンパサンド)が混ざるのは正常
GetMenuItemInfo などで取得した項目名に & が混ざっていても、これは不具合ではなく仕様です。Win32メニューでは & は アクセラレータキー(Alt+文字で選択するためのショートカット) を表します。たとえば「&Open」となっていれば、Altキー操作でその項目にフォーカスが当たる想定です。
| 取得した文字列の例 | 意味 | 自前UIに出すときの扱い |
|---|---|---|
| &Open | Oがアクセラレータキー | &を除去して「Open」表示にしてよい |
| Save && Close | &&はリテラルの&(表示用) | 「Save & Close」として表示したい |
| Copy\tCtrl+C | タブ以降はショートカット表記のことがある | 必要ならタブ以降を切り落とす |
自前UI(WinUIのMenuFlyoutなど)でそのまま表示すると見た目が崩れるため、表示用には & を除去するのが一般的です。ただし && は「表示上の&」を意味するので、単純に全置換で消すと情報が欠落します。おすすめは「&& を一時退避してから単独の & を除去する」やり方です。
public static string NormalizeMenuText(string raw)
{
if (string.IsNullOrWhiteSpace(raw)) return string.Empty;
// Win32メニュー: & はアクセラレータ、&& はリテラルの &
const char placeholder = '\u0001';
var s = raw.Replace("&&", placeholder.ToString());
s = s.Replace("&", string.Empty);
s = s.Replace(placeholder.ToString(), "&");
// 右側にショートカット表記が付く場合がある(例: "Copy\tCtrl+C")
var tab = s.IndexOf('\t');
if (tab >= 0) s = s.Substring(0, tab);
return s.Trim();
}
ローカライズ(表示言語)が揺れる理由:Shell拡張の実装と環境差がそのまま出る
「端末によって“Include in library”が翻訳されたりされなかったりする」問題は、アプリの取り方が悪いというより、メニューを提供している側(Windows本体やShell拡張)がどのリソースを返すかに依存するケースが多いです。特に次のような要因が重なると揺れが出ます。
- Windowsの表示言語・地域設定、言語パックの有無
- Shell拡張(サードパーティ含む)が独自にリソースを持ち、MUI対応が不完全
- 同じコマンドでも、状況により “静的ラベル” と “動的ラベル” を切り替える実装
- 初回はキャッシュ未生成で英語のフォールバック文字列が出る(2回目以降にローカライズされる)
つまり、文字列だけで項目を同定しようとすると、環境差に振り回されやすくなります。可能なら verb(コマンド名) や コマンドID、あるいは IExplorerCommand 系の識別子を使いたいところですが、すべての項目でそれが取れるとは限りません(後述の「Include in library」が典型です)。
Windows 10/11でサブメニューが欠ける主因:IContextMenu2/IContextMenu3へのメッセージ転送不足
質問で挙がっている「ライブラリに含める(Include in library / ライブラリに追加)」のサブメニューが不完全になる問題は、典型的には IContextMenu2(またはIContextMenu3)が必要とするWin32メッセージをアプリ側が渡せていないことが原因です。
Shell拡張の一部は、サブメニューを開く瞬間に WM_INITMENUPOPUP を受け取ってから、初めてサブ項目を“動的に”追加します。Explorerは当然そのメッセージを正しく配送しますが、アプリが独自に HMENU を作って列挙するだけだと、このライフサイクルが抜け落ちます。結果として、
- Windows 10:常にサブ項目が欠ける(動的挿入が走らない)
- Windows 11:初回だけ欠けるが、2回目以降はキャッシュ等で偶然見える
といった「OS差/初回差」に見える症状になります。逆に言えば、WM_INITMENUPOPUP 等を正しく中継できる構成にすると、Windows 10でもサブ項目が揃うケースが増え、切り分けが一気に進みます。
| 症状 | 起きやすい背景 | 疑うべきポイント |
|---|---|---|
| サブメニューが空、または一部しか出ない | 動的サブメニュー(ライブラリ、送る、共有、圧縮ツール等) | WM_INITMENUPOPUP を IContextMenu2/3 へ転送しているか |
| 初回だけ欠ける/2回目以降は正常 | 初回に初期化が必要、キャッシュが効く | メッセージ転送不足、OLE初期化不足、COMアパートメント不一致 |
| 項目は列挙できるが実行時に変な挙動 | Invoke時の構造体が不足、hwndやfMaskが不適切 | CMINVOKECOMMANDINFOEX とUnicode指定、呼び出しスレッド(STA) |
解決の王道:ウィンドウをサブクラス化し、必要なメッセージを HandleMenuMsg へ中継する
IContextMenu だけで完結する項目は多いのですが、動的サブメニューやowner-draw項目を含む場合、IContextMenu2/IContextMenu3 を取得してメッセージを転送するのが安定策です。ポイントは次の2つです。
- メニューを表示(またはサブメニューを開く)タイミングで発生するWin32メッセージを捕まえる
- 捕まえたメッセージを
IContextMenu2.HandleMenuMsg(またはIContextMenu3.HandleMenuMsg2)へ転送する
WinUI 3でも、トップレベルウィンドウはWin32の HWND を持っています。WindowNative.GetWindowHandle などでハンドルを取得し、SetWindowSubclass(または独自WndProc差し替え)でメッセージをフックします。
転送が効く代表的なメッセージ
| メッセージ | 何が起きる | 転送先 |
|---|---|---|
WM_INITMENUPOPUP | サブメニューを開く直前。動的に項目を追加する拡張が多い | IContextMenu2.HandleMenuMsg / IContextMenu3.HandleMenuMsg2 |
WM_DRAWITEM | owner-draw項目の描画 | 同上(必要な拡張のみ) |
WM_MEASUREITEM | owner-draw項目のサイズ計測 | 同上(必要な拡張のみ) |
WM_MENUCHAR | キーボード操作(アクセラレータ) | 同上(必要な拡張のみ) |
自前UIで描画する場合、WM_DRAWITEM/WM_MEASUREITEM は「Win32メニューを実際に描かないなら不要では?」と思いがちですが、拡張によっては初期化に絡むことがあるため、まずは転送対象に入れておくと切り分けが楽です。
C#(.NET 8)での実装イメージ
以下は「WinUI 3のウィンドウをサブクラス化して、メニュー関連メッセージをIContextMenu2/3へ転送する」骨格例です。実際にはCOMの取得・解放や例外処理が必要ですが、考え方の把握に役立つはずです。P/InvokeやCOM定義は手書きより、CsWin32(Microsoft.Windows.CsWin32)で生成すると保守性が上がります。
// 代表的なメッセージ定数
private const int WM_INITMENUPOPUP = 0x0117;
private const int WM_DRAWITEM = 0x002B;
private const int WM_MEASUREITEM = 0x002C;
private const int WM_MENUCHAR = 0x0120;
// 現在表示中のコンテキストメニュー(表示中のみ保持する)
private static IContextMenu2? _cm2;
private static IContextMenu3? _cm3;
// comctl32!SetWindowSubclass / DefSubclassProc を使う想定
private static readonly SUBCLASSPROC _subclassProc = SubclassProc;
private static IntPtr SubclassProc(
IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam,
IntPtr uIdSubclass, IntPtr dwRefData)
{
switch ((int)msg)
{
case WM_INITMENUPOPUP:
case WM_DRAWITEM:
case WM_MEASUREITEM:
case WM_MENUCHAR:
// IContextMenu3 を優先(戻り値が必要なメッセージがある)
if (_cm3 != null)
{
_cm3.HandleMenuMsg2((uint)msg, wParam, lParam, out var result);
return result;
}
_cm2?.HandleMenuMsg((uint)msg, wParam, lParam);
break;
}
return DefSubclassProc(hWnd, msg, wParam, lParam);
}
重要なのは、メニューを構築している期間だけ _cm2/_cm3 を保持し、メニュー処理が終わったら必ず解放することです。COMオブジェクトを静的に抱え続けると、Explorer由来の拡張がアプリ終了まで残り、メモリリークや予期しない再入を招きます。
OLE/COM初期化を軽視しない:OleInitialize(またはSTAのCoInitializeEx)
IContextMenu を含むShell COMは、スレッドモデルやOLE初期化に敏感です。WinUI 3のUIスレッドは多くの場合STAで動作しますが、バックグラウンドスレッドで列挙・実行をしたり、ライブラリ側でスレッドが変わったりすると、急に挙動が不安定になります。
実務では「コンテキストメニュー取得・列挙・実行は専用のSTAスレッドで行い、そのスレッドで OleInitialize / OleUninitialize を対にする」構成が安定しやすいです。UIスレッドで完結させる場合でも、COM初期化の前提を明示しておくと、将来の修正で崩れにくくなります。
| 項目 | 推奨 | 理由 |
|---|---|---|
| 実行スレッド | STAスレッドで統一 | Shell COMや一部拡張がSTA前提。MTAだと初回のみ不完全などが出やすい |
| 初期化 | OleInitialize を使用 | クリップボード/ドラッグ&ドロップ等、OLE依存をまとめて満たしやすい |
| 解放 | OleUninitialize を必ず実行 | スレッド終了時にCOM/OLE状態を正しく片付ける |
列挙モデルを作るときのコツ:メニューは「表示用テキスト」だけではない
Win32のメニュー項目は、表示文字列だけでなく、状態・種類・ID・サブメニュー有無などが混在します。自前UIへ変換する場合、最低限次の情報をモデルに持たせると後工程が楽になります。
- 表示名(正規化済み)
- 種類(通常項目/セパレーター/サブメニュー)
- 有効・無効、チェック状態
- コマンドID(
QueryContextMenuで割り当てられたID) - 可能なら verb(
GetCommandStringで取得)
GetMenuItemInfoで見るポイント | 例 | WinUIに変換するとき |
|---|---|---|
fType | セパレーター(MFT_SEPARATOR) | WinUIなら区切り線として追加、または無視 |
fState | 無効(MFS_DISABLED) | MenuFlyoutItemの IsEnabled を false |
hSubMenu | サブメニューあり | MenuFlyoutSubItemへ変換し、子要素も列挙 |
wID | コマンドID | クリック時にInvokeするため保持 |
“自前UIで表示する”場合に起きやすい罠:サブメニューは開くまで確定しない
Explorerのメニューは、ユーザーがサブメニューにホバーした瞬間に中身を生成するものが多くあります。WinUIのMenuFlyoutは「開いた時点で子が揃っている」前提になりやすいため、次のどちらかの戦略を取ると破綻しにくいです。
- 戦略A:サブメニューを開く直前(FlyoutのOpening等)に、Win32側で
WM_INITMENUPOPUP相当の処理を走らせてから再列挙し、子項目を差し替える - 戦略B:「動的サブメニューが多い領域だけ」は無理に再現せず、そこだけはネイティブメニュー(TrackPopupMenuEx)を出す、または別画面で専用UIを用意する
特にLibrariesのような“特殊フォルダー連携”は、戦略B(専用APIで作る)が結果的に最短です。
InvokeCommandが「成功したように見えるのに変」なときのチェックリスト
IContextMenu.InvokeCommand は、戻り値や例外だけでは「正しく実行されたか」を判断しにくいことがあります。実行時に妙なメッセージが出たり、UIがちらついたり、想定と違う動作になる場合は、次を順番に疑うと原因に辿り着きやすいです。
| チェック項目 | ありがちなミス | 対策 |
|---|---|---|
| 呼び出しスレッド | MTA/ThreadPoolから呼んでいる | STAスレッドに寄せる。必要なら専用STAスレッドを用意 |
| 構造体 | CMINVOKECOMMANDINFOだけで呼ぶ(Unicode不足) | CMINVOKECOMMANDINFOEX を使い、CMIC_MASK_UNICODE を付ける |
| hwnd | hwndを0で渡す | 可能なら実ウィンドウのHWNDを渡す(UIが必要な拡張がある) |
| verb/ID | IDのオフセット計算を誤る | QueryContextMenuで指定したidFirstからの差分で渡す |
| 作業ディレクトリ | lpDirectoryが空で拡張が誤動作 | 対象パスや親フォルダを渡す(必要な場合) |
「verbが取れる項目はverbでInvokeする」「verbが取れない項目はIDでInvokeする」など、二段構えにしておくとローカライズの揺れにも強くなります。ただし今回の主題である “Include in library” は verbが空になりがちで、識別が難しい側に寄ります。
Libraries(「ライブラリに含める」)は別枠:コンテキストメニュー列挙に頼らずShellのライブラリAPIで組み立てる
「Include in library / ライブラリに追加」は、見た目は普通のサブメニューですが、実態はライブラリ管理機能に直結する特殊なコマンドです。右クリックメニュー経由でサブ項目(ライブラリ一覧)を列挙しようとすると、環境差やタイミング差を受けやすく、再現性が落ちます。
安定性を最優先するなら、次のように “ライブラリ自体の一覧” を専用APIで取得し、アプリ側でサブメニューを自前構築するのが堅実です。
SHGetKnownFolderPath+FOLDERID_Librariesでライブラリの場所を取得し、配下を列挙するIShellLibrary等のShellインターフェースでライブラリを列挙・操作する
このやり方なら、OS差でサブメニューが欠ける問題から切り離せます。さらに、UI側では「ライブラリ一覧の読み込み」「追加/削除の結果反映」などを制御できるため、Explorerと同じ体験に近づけやすくなります。
「Include in library」項目だけ除外したい:ローカライズ文字列を取得して一致でフィルタする
実装上どうしても「この項目だけは自前UIに出さない(またはWin32メニューから消したい)」というニーズがあります。しかし現実には、問題の項目は verbが空 であったり、拡張側が汎用的な識別子を返さないことがあり、メニュー項目の同定が難しいケースが出ます。
そこで実用的な手段として、「OSの言語に応じた表示文字列を取得し、正規化した上で文字列一致でフィルタする」方法があります。たとえば shell32.dll のメニューリソースから “Include in library” 相当のラベルを読み出し、GetMenuItemInfo(MIIM_STRING) の結果と突き合わせます(例として、メニューリソースID 262 のような値が話題になりますが、環境やバージョンで変わり得るため“例”として扱うのが安全です)。
- 比較前に
&(アクセラレータ)や余計な空白、末尾の省略記号などを正規化する - 一致判定に加えて「サブメニューを持つ」「verbが空」などの条件を組み合わせ、誤除外を避ける
- Win32メニューを編集するなら
RemoveMenu/DeleteMenuで除去し、自前UIなら「その項目だけ作らない」
ポイントは「ローカライズ済み文字列を自分で作らない」ことです。アプリ側で多言語の辞書を持つとメンテが破綻しやすいので、できる限りWindowsが持つ現在言語のリソースを読み取り、同じ目線で比較します。
実務で効く“落としどころ”:100%再現より、壊れない設計に寄せる
最後に、WinUI 3/.NET 8でコンテキストメニューを再現する際の現実的な落としどころをまとめます。Explorerの右クリックメニューは、OS標準機能に加えてサードパーティ拡張も混ざる「動く対象」です。完全再現に固執すると、将来のWindows更新や拡張アップデートで簡単に壊れます。
| 目的 | おすすめの方針 | 理由 |
|---|---|---|
| UIを統一したい(よく使う数項目だけ) | 必要なコマンドだけ自前実装+残りは「その他」へ逃がす | 安定性とユーザー体験の両立がしやすい |
| Explorerと同じ機能を全部出したい | ネイティブメニュー表示(TrackPopupMenuEx)を基本にする | Shell拡張互換を最優先できる |
| どうしても列挙して自前UIにしたい | IContextMenu2/3 メッセージ転送+STA/OLE初期化を徹底 | 動的サブメニューの欠落を大きく減らせる |
| Librariesだけ扱いが特殊で困る | ライブラリAPIで専用に構築する | OS差の影響を切り離して再現性を上げられる |
まとめ
Win32で取得したExplorer右クリックメニューをWinUI 3/.NET 8の自前UIへ落とし込むとき、&混入やローカライズ揺れは仕様として受け止めつつ、致命的になりやすいのは 動的サブメニューを支えるメッセージ処理です。IContextMenu2/IContextMenu3 を使い、WM_INITMENUPOPUP などを正しく転送する構成にすることで、Windows 10/11の不整合や「初回だけ不完全」といった現象を大きく抑えられます。
また「Include in library」のような特殊領域は、右クリックメニューの列挙にこだわるより、ShellのライブラリAPIで一覧を取り、自前でサブメニューを構築した方が堅牢です。どうしても除外したい場合は、OS言語のリソース文字列を取り出して正規化・一致でフィルタするのが実務的な解になります。

コメント