· 4 min read

How to implement the File Context Menu

The context menu that appears when you right-click a file in Windows Explorer—how can you implement this in a .NET application? This article explains the process

The context menu that appears when you right-click a file in Windows Explorer—how can you implement this in a .NET application? This article explains the process

Introduction

The context menu that appears when you right-click a file in Windows Explorer—how can you implement this in a .NET application? This article explains the process, focusing on the following key points:

  • COM operations in a NativeAOT build
  • Handling structures with LibraryImport
  • Implementation in an Avalonia application

If you’ve ever wondered, “How can I open a file’s context menu in a .NET app?” this article provides a direct answer.

My application, Filedini, which I’m currently developing, includes a feature to open file context menus. The code for this is publicly available, and this article explains its implementation.

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

Why Use NativeAOT Builds?

.NET applications (such as WPF apps) often suffer from long startup times. Even with R2R (ReadyToRun) builds, performance may still feel sluggish.

Developing an application with Avalonia that supports NativeAOT can significantly reduce startup time, leading to a much smoother user experience.

Once you experience this speed, you may find it hard to go back to traditional methods. In some cases, the benefits of NativeAOT are compelling enough to justify trade-offs.

COM Operations in a NativeAOT Environment

For handling COM in a NativeAOT environment, this article by SHINTA provides valuable insights: https://zenn.dev/shinta0806/articles/native-aot-com

Understanding these foundational techniques is crucial.

Context Menu Implementation Example

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

The process of displaying a context menu follows these steps. For details, see the ShellContextMenu.Show method:

  1. Retrieve the desktop folder
  2. Get the parent folder of the target file
  3. Create an IDL for the target file
  4. Obtain the context menu interface
  5. Display the menu
  6. Execute commands based on user selection

Handling Structures Containing Strings with LibraryImport

Here is a specific implementation example:

    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);
        }
    }

Here, fixed is used to obtain a pointer to the string, which is then passed to the structure.

Obtaining a Window Handle in Avalonia

Avalonia is a cross-platform framework, but to use Windows-specific features, you need a native window handle.

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

By calling TryGetPlatformHandle() on Avalonia’s TopLevel class, you can retrieve the platform-specific handle. On Windows, this corresponds to an HWND.

Hooking the Window Procedure

To properly display the context menu, you need to handle specific window messages.

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 - When initializing a popup menu
  • WM.MEASUREITEM - When measuring menu item size
  • WM.DRAWITEM - When drawing menu items
  • WM.MENUCHAR - When handling keyboard shortcuts in the menu

Avalonia’s Win32Properties Class

Rather than opening a new window, it’s sufficient to process messages within an existing window. Since the window handle is already available, you can use Win32 API directly, but Avalonia provides the Win32Properties class for hooking into the Windows procedure.

// Add hook
Win32Properties.AddWndProcHookCallback(_topLevel, WndProc);

// Remove hook
Win32Properties.RemoveWndProcHookCallback(_topLevel, WndProc);

Conclusion

Through this implementation of the Windows Shell API for context menus, we covered the following technical points:

  1. COM operations in a NativeAOT build
  2. Native interop using LibraryImport
  3. Obtaining a Windows handle in an Avalonia app
  4. Setting up and managing window procedure hooks

By leveraging these techniques, you can incorporate Windows-native features into cross-platform Avalonia applications.

While Avalonia is a multi-platform UI framework, certain use cases require working closely with the OS shell. This implementation serves as an excellent example of using cross-platform libraries while integrating Windows-specific functionality.

Take this opportunity to explore Avalonia for your development projects!

References

Back to Blog

Related Posts

View All Posts »