Initial commit: FluidExplorer:从零实现的 WinUI 3 文件资源管理器替代品(Mica、命令栏、面包屑)
This commit is contained in:
@@ -0,0 +1,267 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user