Files

268 lines
11 KiB
C#

using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Text;
namespace FluidExplorer.Services.Shell;
/// <summary>
/// Windows 系统原版右键菜单(shell 的 IContextMenu):
/// 就是资源管理器"显示更多选项"里那一套(含第三方扩展、发送到、打开方式、Windows Terminal 等),
/// 直接用系统实现,不自己造菜单项。
/// </summary>
public static class ShellContextMenu
{
public static Task ShowAsync(IntPtr ownerHwnd, IReadOnlyList<string> 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<string> paths, int x, int y)
{
if (paths.Count == 0) return;
var pidls = new List<IntPtr>();
var allocated = new List<IntPtr>();
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<IntPtr> { 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<CMINVOKECOMMANDINFOEX>(),
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);
}
}
/// <summary>把菜单的 owner-draw / init 消息转发给 IContextMenu2/3(子菜单才能正常展开)。</summary>
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);
}