using System.Runtime.InteropServices; using System.Runtime.InteropServices.ComTypes; using System.Text; namespace FluidExplorer.Services.Shell; /// /// Windows 系统原版右键菜单(shell 的 IContextMenu): /// 就是资源管理器"显示更多选项"里那一套(含第三方扩展、发送到、打开方式、Windows Terminal 等), /// 直接用系统实现,不自己造菜单项。 /// public static class ShellContextMenu { public static Task ShowAsync(IntPtr ownerHwnd, IReadOnlyList paths, Windows.Graphics.PointInt32 screenPoint) { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var dispatcher = Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread(); void Run() { try { ShowCore(ownerHwnd, paths, screenPoint.X, screenPoint.Y); tcs.TrySetResult(); } catch (Exception ex) { tcs.TrySetException(ex); } } if (dispatcher is null || dispatcher.HasThreadAccess) Run(); else dispatcher.TryEnqueue(Run); return tcs.Task; } private static void ShowCore(IntPtr hwnd, IReadOnlyList paths, int x, int y) { if (paths.Count == 0) return; var pidls = new List(); var allocated = new List(); IShellFolder? parentFolder = null; try { // 取第一条的父文件夹,作为构建 IContextMenu 的宿主(多选时要求同一父目录) var firstHr = SHParseDisplayName(paths[0], IntPtr.Zero, out var firstPidl, 0, out _); if (firstHr != 0 || firstPidl == IntPtr.Zero) return; allocated.Add(firstPidl); var bindHr = SHBindToParent(firstPidl, typeof(IShellFolder).GUID, out var folderObj, out var childPidl); if (bindHr != 0 || folderObj is null) return; parentFolder = (IShellFolder)folderObj; var childPidls = new List { childPidl }; for (var i = 1; i < paths.Count; i++) { if (SHParseDisplayName(paths[i], IntPtr.Zero, out var pidl, 0, out _) != 0 || pidl == IntPtr.Zero) continue; allocated.Add(pidl); var hr = SHBindToParent(pidl, typeof(IShellFolder).GUID, out var folder, out var child); if (hr != 0 || folder is null) continue; if (folder != parentFolder) continue; // 不同目录的多选:忽略额外项(由上层逐个处理) childPidls.Add(child); } var iid = typeof(IContextMenu).GUID; var uiObjectHr = parentFolder.GetUIObjectOf(hwnd, (uint)childPidls.Count, childPidls.ToArray(), ref iid, IntPtr.Zero, out var contextMenuObj); if (uiObjectHr != 0 || contextMenuObj is null) return; var contextMenu = (IContextMenu)contextMenuObj; var contextMenu2 = contextMenuObj as IContextMenu2; var contextMenu3 = contextMenuObj as IContextMenu3; var hMenu = CreatePopupMenu(); if (hMenu == IntPtr.Zero) return; try { const uint CMF_NORMAL = 0x00000000; const uint CMF_EXTENDEDVERBS = 0x00000100; var queryHr = contextMenu.QueryContextMenu(hMenu, 0, 1, 0x7FFF, CMF_NORMAL | CMF_EXTENDEDVERBS); if (queryHr < 0) return; // 系统菜单里"打开方式/发送到"等子菜单需要转发 owner-draw 消息 using var hook = contextMenu2 is null && contextMenu3 is null ? null : new MenuMessageHook(hwnd, contextMenu2, contextMenu3); const uint TPM_RETURNCMD = 0x0100; const uint TPM_RIGHTBUTTON = 0x0002; var command = TrackPopupMenuEx(hMenu, TPM_RETURNCMD | TPM_RIGHTBUTTON, x, y, hwnd, IntPtr.Zero); if (command <= 0) return; var info = new CMINVOKECOMMANDINFOEX { cbSize = Marshal.SizeOf(), fMask = 0x00004000 /*CMIC_MASK_UNICODE*/, hwnd = hwnd, lpVerb = (IntPtr)(command - 1), lpVerbW = (IntPtr)(command - 1), nShow = 1 }; var invokeHr = contextMenu.InvokeCommand(ref info); _ = invokeHr; } finally { DestroyMenu(hMenu); } } finally { foreach (var pidl in allocated) Marshal.FreeCoTaskMem(pidl); if (parentFolder is not null) Marshal.ReleaseComObject(parentFolder); } } /// 把菜单的 owner-draw / init 消息转发给 IContextMenu2/3(子菜单才能正常展开)。 private sealed class MenuMessageHook : IDisposable { private readonly IntPtr _hwnd; private readonly IContextMenu2? _menu2; private readonly IContextMenu3? _menu3; private readonly SubclassProc _proc; private readonly IntPtr _oldProc; public MenuMessageHook(IntPtr hwnd, IContextMenu2? menu2, IContextMenu3? menu3) { _hwnd = hwnd; _menu2 = menu2; _menu3 = menu3; _proc = HookProc; _oldProc = SetWindowLongPtr(hwnd, GWLP_WNDPROC, Marshal.GetFunctionPointerForDelegate(_proc)); } private IntPtr HookProc(IntPtr hwnd, uint msg, IntPtr wParam, IntPtr lParam) { switch (msg) { case WM_INITMENUPOPUP: case WM_DRAWITEM: case WM_MEASUREITEM: case WM_MENUCHAR: if (_menu3 is not null) { var handled = IntPtr.Zero; if (_menu3.HandleMenuMsg2(msg, wParam, lParam, out handled) == 0 && handled != IntPtr.Zero) return handled; } else if (_menu2 is not null) { if (_menu2.HandleMenuMsg(msg, wParam, lParam) == 0) return IntPtr.Zero; } break; } return CallWindowProc(_oldProc, hwnd, msg, wParam, lParam); } public void Dispose() { if (_oldProc != IntPtr.Zero) SetWindowLongPtr(_hwnd, GWLP_WNDPROC, _oldProc); } } private const int GWLP_WNDPROC = -4; private const uint WM_INITMENUPOPUP = 0x0117; private const uint WM_DRAWITEM = 0x002B; private const uint WM_MEASUREITEM = 0x002C; private const uint WM_MENUCHAR = 0x0120; private delegate IntPtr SubclassProc(IntPtr hwnd, uint msg, IntPtr wParam, IntPtr lParam); [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] private struct CMINVOKECOMMANDINFOEX { public int cbSize; public uint fMask; public IntPtr hwnd; public IntPtr lpVerb; public IntPtr lpParameters; public IntPtr lpDirectory; public int nShow; public uint dwHotKey; public IntPtr hIcon; public IntPtr lpTitle; public IntPtr lpVerbW; public IntPtr lpParametersW; public IntPtr lpDirectoryW; public IntPtr lpTitleW; public POINT ptInvoke; } [StructLayout(LayoutKind.Sequential)] private struct POINT { public int X; public int Y; } [ComImport] [Guid("000214E6-0000-0000-C000-000000000046")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] private interface IShellFolder { [PreserveSig] int ParseDisplayName(IntPtr hwnd, IntPtr pbc, [MarshalAs(UnmanagedType.LPWStr)] string pszDisplayName, out uint pchEaten, out IntPtr ppidl, ref uint pdwAttributes); [PreserveSig] int EnumObjects(IntPtr hwnd, uint grfFlags, out IntPtr ppenumIDList); [PreserveSig] int BindToObject(IntPtr pidl, IntPtr pbc, ref Guid riid, out IntPtr ppv); [PreserveSig] int BindToStorage(IntPtr pidl, IntPtr pbc, ref Guid riid, out IntPtr ppv); [PreserveSig] int CompareIDs(IntPtr lParam, IntPtr pidl1, IntPtr pidl2); [PreserveSig] int CreateViewObject(IntPtr hwndOwner, ref Guid riid, out IntPtr ppv); [PreserveSig] int GetAttributesOf(uint cidl, IntPtr[] apidl, ref uint rgfInOut); [PreserveSig] int GetUIObjectOf(IntPtr hwndOwner, uint cidl, IntPtr[] apidl, ref Guid riid, IntPtr rgfReserved, out object ppv); [PreserveSig] int GetDisplayNameOf(IntPtr pidl, uint uFlags, out IntPtr pName); [PreserveSig] int SetNameOf(IntPtr hwnd, IntPtr pidl, [MarshalAs(UnmanagedType.LPWStr)] string pszName, uint uFlags, out IntPtr ppidlOut); } [ComImport] [Guid("000214E4-0000-0000-C000-000000000046")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] private interface IContextMenu { [PreserveSig] int QueryContextMenu(IntPtr hmenu, uint indexMenu, uint idCmdFirst, uint idCmdLast, uint uFlags); [PreserveSig] int InvokeCommand(ref CMINVOKECOMMANDINFOEX pici); [PreserveSig] int GetCommandString(IntPtr idCmd, uint uType, IntPtr pReserved, StringBuilder pszName, uint cchMax); } [ComImport] [Guid("000214F4-0000-0000-C000-000000000046")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] private interface IContextMenu2 { [PreserveSig] int QueryContextMenu(IntPtr hmenu, uint indexMenu, uint idCmdFirst, uint idCmdLast, uint uFlags); [PreserveSig] int InvokeCommand(ref CMINVOKECOMMANDINFOEX pici); [PreserveSig] int GetCommandString(IntPtr idCmd, uint uType, IntPtr pReserved, StringBuilder pszName, uint cchMax); [PreserveSig] int HandleMenuMsg(uint uMsg, IntPtr wParam, IntPtr lParam); } [ComImport] [Guid("BCFCE0A0-EC17-11D0-8D10-00A0C90F2719")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] private interface IContextMenu3 { [PreserveSig] int QueryContextMenu(IntPtr hmenu, uint indexMenu, uint idCmdFirst, uint idCmdLast, uint uFlags); [PreserveSig] int InvokeCommand(ref CMINVOKECOMMANDINFOEX pici); [PreserveSig] int GetCommandString(IntPtr idCmd, uint uType, IntPtr pReserved, StringBuilder pszName, uint cchMax); [PreserveSig] int HandleMenuMsg(uint uMsg, IntPtr wParam, IntPtr lParam); [PreserveSig] int HandleMenuMsg2(uint uMsg, IntPtr wParam, IntPtr lParam, out IntPtr plResult); } [DllImport("shell32.dll", CharSet = CharSet.Unicode)] private static extern int SHParseDisplayName(string pszName, IntPtr pbc, out IntPtr ppidl, uint sfgaoIn, out uint psfgaoOut); [DllImport("shell32.dll", CharSet = CharSet.Unicode)] private static extern int SHBindToParent(IntPtr pidl, Guid riid, out object ppv, out IntPtr ppidlLast); [DllImport("user32.dll")] private static extern IntPtr CreatePopupMenu(); [DllImport("user32.dll")] private static extern bool DestroyMenu(IntPtr hMenu); [DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern int TrackPopupMenuEx(IntPtr hMenu, uint fuFlags, int x, int y, IntPtr hwnd, IntPtr lptpm); [DllImport("user32.dll", EntryPoint = "SetWindowLongPtrW")] private static extern IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong); [DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); }