🧙

NativeAOT対応!AvaloniaでWindows シェルAPIを利用したコンテキストメニュー実装

2025/04/03に公開

元記事はこちら

はじめに

Windowsのエクスプローラーでファイルを右クリックしたときに表示されるコンテキストメニュー。このメニューを.NETアプリケーションで実装する方法を解説します。本記事では特に、以下のポイントに焦点を当てます。

  • NativeAOTビルドでのCOM操作
  • LibraryImportを使った構造体処理
  • Avaloniaアプリでの実装方法

「.NETアプリでファイルのコンテキストメニューを開きたいけど、どうすればいい?」という疑問に直接答える内容です。

私が開発中のアプリ「Filedini」にはファイルのコンテキストメニューを開く機能が実装されています。そのコードは公開しており、本記事ではその実装を解説します。

https://github.com/YoshihiroIto/Filedini-public/tree/main/Source/_70_ServiceImplements/Windows

NativeAOTビルドを採用する理由

.NETアプリ(WPFアプリなど)は起動時間が長くなりがちです。R2R(ReadyToRun)ビルドを活用しても、体感的にはまだ遅いと感じることがあります。

そこで、NativeAOT に対応した Avalonia でアプリを開発すれば、起動時間が劇的に短縮され、軽快に動作するアプリを作成することが可能になります。

一度この速さを体験すると、その快適さに驚かされ、もはや従来の方法には戻れなくなるかもしれません。場合によっては、何かを犠牲にしてでも NativeAOT を採用したくなるほどの魅力があります。

NativeAOT環境でのCOM操作

NativeAOT環境でCOMを扱う方法については、SHINTA様の記事が非常に参考になります。
https://zenn.dev/shinta0806/articles/native-aot-com

基礎技術としてしっかり押さえておきましょう。

コンテキストメニュー実例

https://github.com/YoshihiroIto/Filedini-public/blob/main/Source/_70_ServiceImplements/Windows/ShellContextMenu.cs

コンテキストメニューを表示する処理の流れは以下の通りです。詳細は ShellContextMenu.Show メソッドを参照してください。

  1. デスクトップフォルダを取得
  2. ファイルの親フォルダを取得
  3. 対象ファイルのIDLを作成
  4. コンテキストメニューインターフェースを取得
  5. メニューを表示
  6. ユーザーの選択に応じたコマンド実行

LibraryImportで引数に文字列を含む構造体を扱う方法

具体的な実装を見ていきます。

    private static unsafe void InvokeCommand(IContextMenu contextMenu, uint cmd, string folderName, int x, int y)
    {
        fixed (char* p = folderName)
        {
            var command = new CMINVOKECOMMANDINFOEX
            {
                cbSize = sizeof(CMINVOKECOMMANDINFOEX),
                lpVerb = (IntPtr)(cmd - CMD_FIRST),
                lpDirectory = p,
                lpVerbW = (IntPtr)(cmd - CMD_FIRST),
                lpDirectoryW = p,
                fMask = CMIC.UNICODE | CMIC.PTINVOKE |
                        (IsKeyPressControlKey ? CMIC.CONTROL_DOWN : 0) |
                        (IsKeyPressShiftKey ? CMIC.SHIFT_DOWN : 0),
                ptInvoke = new POINT(x, y),
                nShow = SW.SHOWNORMAL
            };

            contextMenu.InvokeCommand(ref command);
        }
    }

fixed を使って文字列のポインタを取得し、それを構造体に渡しています。

AvaloniaでWindowsのウィンドウハンドルを得る方法

Avaloniaはクロスプラットフォームのフレームワークですが、Windows固有の機能を利用するにはネイティブウィンドウハンドルが必要です。

public static void Show(TopLevel topLevel, FileInfo[] files, int x, int y)
{
    // ...
    var handleOwner = topLevel.TryGetPlatformHandle()?.Handle;
    if (handleOwner is null)
        return;
    // ...
}

AvaloniaのTopLevelクラスからTryGetPlatformHandle()メソッドを呼び出すことで、プラットフォーム固有のハンドルを取得できます。Windowsの場合、これはHWNDになります。

ウィンドウプロシージャをフックする方法

コンテキストメニューを適切に表示するためには、特定のウィンドウメッセージを処理する必要があります。

file class Hook : IDisposable
{
    private readonly TopLevel _topLevel;
    private readonly IContextMenu2? _contextMenu2;
    private readonly IContextMenu3? _contextMenu3;

    public Hook(TopLevel topLevel, IContextMenu2? contextMenu2, IContextMenu3? contextMenu3)
    {
        _topLevel = topLevel;
        _contextMenu2 = contextMenu2;
        _contextMenu3 = contextMenu3;

        Win32Properties.AddWndProcHookCallback(_topLevel, WndProc);
    }

    public void Dispose()
    {
        Win32Properties.RemoveWndProcHookCallback(_topLevel, WndProc);
    }

    private IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam, ref bool handled)
    {
        if (_contextMenu2 != null && (msg == (uint)WM.INITMENUPOPUP || msg == (uint)WM.MEASUREITEM || msg == (uint)WM.DRAWITEM))
        {
            if (_contextMenu2.HandleMenuMsg(msg, wParam, lParam) == S_OK)
            {
                handled = true;
                return IntPtr.Zero;
            }
        }

        if (_contextMenu3 != null && msg == (uint)WM.MENUCHAR)
        {
            if (_contextMenu3.HandleMenuMsg2(msg, wParam, lParam, IntPtr.Zero) == S_OK)
            {
                handled = true;
                return IntPtr.Zero;
            }
        }

        return IntPtr.Zero;
    }
}
  • WM.INITMENUPOPUP - ポップアップメニューの初期化時
  • WM.MEASUREITEM - メニュー項目のサイズ測定時
  • WM.DRAWITEM - メニュー項目の描画時
  • WM.MENUCHAR - メニューのキーボードショートカット処理時

AvaloniaのWin32Propertiesクラス

メッセージ処理新しくウィンドウを開かずに、既存のウィンドウ上で処理を行うだけで十分です。
すでにウィンドウハンドルが取得できているため、Win32 API を直接使用して処理することも可能ですが、Avalonia には Windows ネイティブのウィンドウプロシージャをフックするための Win32Properties クラスが用意されています。

// フックの追加
Win32Properties.AddWndProcHookCallback(_topLevel, WndProc);

// フックの解除
Win32Properties.RemoveWndProcHookCallback(_topLevel, WndProc);

まとめ

Windows Shell APIを使ったコンテキストメニュー実装を通して、以下の技術ポイントを紹介しました。

  1. NativeAOTビルドでのCOM操作方法
  2. LibraryImportによるなネイティブ連携
  3. AvaloniaアプリでのWindowsハンドル取得方法
  4. ウィンドウプロシージャフックの設定と管理

これらの技術を活用することで、クロスプラットフォーム対応のAvaloniaアプリケーションにおいても、Windowsのネイティブ機能を利用することが可能になります。

Avaloniaはマルチプラットフォーム対応のUIフレームワークですが、その特性上、実行環境のシェルに近い部分を扱う必要が生じることがあります。今回のコード実装は、クロスプラットフォームライブラリを用いてWindows固有の機能を実装する一例として、大変参考になるでしょう。

この機会に、ぜひAvaloniaに触れてみてください。

参照

Discussion