Initial commit: FluidExplorer:从零实现的 WinUI 3 文件资源管理器替代品(Mica、命令栏、面包屑)

This commit is contained in:
WpyQwq
2026-09-19 11:54:03 +08:00
commit 5780fde61a
60 changed files with 15035 additions and 0 deletions
+116
View File
@@ -0,0 +1,116 @@
using System.Runtime.InteropServices;
using System.Text;
namespace FluidExplorer.Services.Shell;
/// <summary>
/// 用外壳自己的 API(SHGetKnownFolderPath)解析系统文件夹,
/// 保证和资源管理器指向同一批真实位置(含 OneDrive 重定向后的"桌面/文档"等)。
/// </summary>
public static class KnownFolders
{
public static string Profile { get; } = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
public static string Desktop { get; } = Get(FolderId.Desktop);
public static string Documents { get; } = Get(FolderId.Documents);
public static string Downloads { get; } = Get(FolderId.Downloads);
public static string Pictures { get; } = Get(FolderId.Pictures);
public static string Music { get; } = Get(FolderId.Music);
public static string Videos { get; } = Get(FolderId.Videos);
public static string RecycleBin { get; } = @"shell:RecycleBinFolder";
/// <summary>此电脑 / 回收站等虚拟外壳对象的解析名,交给外壳取图标与打开。</summary>
public const string ThisPcParsingName = "::{20D04FE0-3AEA-1069-A2D8-08002B30309D}";
public const string RecycleBinParsingName = "::{645FF040-5081-101B-9F08-00AA002F954E}";
public const string NetworkParsingName = "::{F02C1A0D-BE21-4350-88B0-7367FC96EF3C}";
public const string HomeParsingName = "::{F874310E-B6B7-47DC-BC84-B9E6B38F5903}"; // 主页
public const string GalleryParsingName = "::{E88865EA-0E1C-4E20-9AA6-EDCD0212C87C}"; // 图库
private static string Get(Guid id)
{
try
{
var hr = SHGetKnownFolderPath(ref id, 0, IntPtr.Zero, out var ptr);
if (hr != 0 || ptr == IntPtr.Zero) return string.Empty;
try { return Marshal.PtrToStringUni(ptr) ?? string.Empty; }
finally { Marshal.FreeCoTaskMem(ptr); }
}
catch
{
return string.Empty;
}
}
[DllImport("shell32.dll", CharSet = CharSet.Unicode, ExactSpelling = true)]
private static extern int SHGetKnownFolderPath(ref Guid rfid, uint dwFlags, IntPtr hToken, out IntPtr ppszPath);
private static class FolderId
{
public static Guid Desktop = new("B4BFCC3A-DB2C-424C-B029-7FE99A87C641");
public static Guid Documents = new("FDD39AD0-238F-46AF-ADB4-6C85480369C7");
public static Guid Downloads = new("374DE290-123F-4565-9164-39C4925E467B");
public static Guid Pictures = new("33E28130-4E1E-4676-835A-98395C3BC3BB");
public static Guid Music = new("4BD8D571-6D19-48D3-BE97-422220080E43");
public static Guid Videos = new("18989B1D-99B5-455B-841C-AB7C74E4DDFC");
}
}
/// <summary>
/// 卷信息(用于侧边栏"此电脑"和状态栏):显示名、总容量、可用空间、就绪状态。
/// </summary>
public sealed class DriveItem
{
public required string RootPath { get; init; }
public required string DisplayName { get; init; }
public required string VolumeLabel { get; init; }
public required string FileSystem { get; init; }
public long TotalBytes { get; init; }
public long FreeBytes { get; init; }
public int DriveType { get; init; }
public bool IsReady { get; init; }
public double UsedRatio => TotalBytes <= 0 ? 0 : 1.0 - (double)FreeBytes / TotalBytes;
public string CapacityText => !IsReady || TotalBytes <= 0
? "不可用"
: $"{FluidExplorer.Models.FileEntry.FormatSize(FreeBytes)} 可用,共 {FluidExplorer.Models.FileEntry.FormatSize(TotalBytes)}";
public string Glyph => DriveType switch
{
2 => "\uE88E", // 可移动磁盘
3 => "\uEDA2", // 本地磁盘
4 => "\uE8CE", // 网络驱动器
5 => "\uE958", // 光驱
_ => "\uEDA2"
};
public static IReadOnlyList<DriveItem> Enumerate()
{
var list = new List<DriveItem>();
foreach (var drive in DriveInfo.GetDrives())
{
try
{
var label = drive.IsReady ? drive.VolumeLabel : string.Empty;
var display = string.IsNullOrWhiteSpace(label)
? (drive.Name.TrimEnd('\\') is { Length: > 0 } letter ? $"本地磁盘 ({letter})" : drive.Name)
: $"{label} ({drive.Name.TrimEnd('\\')})";
list.Add(new DriveItem
{
RootPath = drive.Name,
DisplayName = display,
VolumeLabel = label,
FileSystem = drive.IsReady ? drive.DriveFormat : string.Empty,
TotalBytes = drive.IsReady ? drive.TotalSize : 0,
FreeBytes = drive.IsReady ? drive.TotalFreeSpace : 0,
DriveType = (int)drive.DriveType,
IsReady = drive.IsReady
});
}
catch
{
// 未就绪的驱动器(空读卡器等)直接跳过
}
}
return list;
}
}
+30
View File
@@ -0,0 +1,30 @@
using System.Runtime.InteropServices;
namespace FluidExplorer.Services.Shell;
/// <summary>
/// 资源管理器"名称"列用的排序:调用系统 shlwapi 的 StrCmpLogicalW(自然排序,数字按数值比较)。
/// 这样 "文件2" 会排在 "文件10" 前面,和原版体验一致。
/// </summary>
public sealed class NaturalStringComparer : IComparer<string>
{
public static NaturalStringComparer Instance { get; } = new();
public int Compare(string? x, string? y)
{
if (ReferenceEquals(x, y)) return 0;
if (x is null) return -1;
if (y is null) return 1;
try
{
return StrCmpLogicalW(x, y);
}
catch
{
return string.Compare(x, y, StringComparison.CurrentCultureIgnoreCase);
}
}
[DllImport("shlwapi.dll", CharSet = CharSet.Unicode, ExactSpelling = true)]
private static extern int StrCmpLogicalW(string psz1, string psz2);
}
+109
View File
@@ -0,0 +1,109 @@
using System.Buffers.Binary;
using System.Text;
namespace FluidExplorer.Services.Shell;
/// <summary>回收站里的一条记录(从 $I 元数据文件解析,含原始路径)。</summary>
public sealed record RecycleBinEntry(
string RecyclePath,
string OriginalPath,
long Size,
DateTime DeletedUtc,
string VolumeRoot)
{
public string Name => Services.FileSystem.PathHelper.GetName(OriginalPath);
public string OriginalDirectory => Services.FileSystem.PathHelper.GetParent(OriginalPath);
public bool IsDirectory => Size == 0 && OriginalPath.Length > 0 && !Path.HasExtension(OriginalPath);
}
/// <summary>
/// 直接读取各卷的 $Recycle.Bin\&lt;SID&gt;\$I* 元数据文件,得到回收站内容与原始路径。
/// 这样"还原/彻底删除"都能由本程序完成(不依赖系统弹窗),也支持一步撤销。
/// </summary>
public static class RecycleBinView
{
public static async Task<IReadOnlyList<RecycleBinEntry>> EnumerateAsync(CancellationToken cancellationToken)
=> await Task.Run(() => Enumerate(cancellationToken), cancellationToken).ConfigureAwait(false);
public static IReadOnlyList<RecycleBinEntry> Enumerate(CancellationToken cancellationToken = default)
{
var results = new List<RecycleBinEntry>();
foreach (var drive in DriveInfo.GetDrives())
{
cancellationToken.ThrowIfCancellationRequested();
try
{
if (!drive.IsReady || drive.DriveType != DriveType.Fixed) continue;
var binRoot = Path.Combine(drive.Name, "$Recycle.Bin");
if (!Directory.Exists(binRoot)) continue;
foreach (var sidDir in Directory.EnumerateDirectories(binRoot))
{
cancellationToken.ThrowIfCancellationRequested();
try
{
foreach (var metaFile in Directory.EnumerateFiles(sidDir, "$I*"))
{
cancellationToken.ThrowIfCancellationRequested();
var entry = TryParse(metaFile, drive.Name);
if (entry is not null) results.Add(entry);
}
}
catch
{
// 其他用户的回收站目录通常无权限,跳过
}
}
}
catch
{
// 卷不可读时跳过
}
}
return results;
}
/// <summary>解析单个 $I 元数据文件。</summary>
public static RecycleBinEntry? TryParse(string metaFilePath, string volumeRoot)
{
try
{
var bytes = File.ReadAllBytes(metaFilePath);
if (bytes.Length < 24) return null;
var version = BinaryPrimitives.ReadInt64LittleEndian(bytes.AsSpan(0, 8));
var size = BinaryPrimitives.ReadInt64LittleEndian(bytes.AsSpan(8, 8));
var fileTime = BinaryPrimitives.ReadInt64LittleEndian(bytes.AsSpan(16, 8));
var deleted = fileTime > 0 ? DateTime.FromFileTimeUtc(fileTime) : DateTime.MinValue;
string originalPath;
if (version >= 2 && bytes.Length >= 28)
{
var length = BinaryPrimitives.ReadInt32LittleEndian(bytes.AsSpan(24, 4));
if (length <= 0 || 28 + length * 2 > bytes.Length) return null;
originalPath = Encoding.Unicode.GetString(bytes, 28, length * 2).TrimEnd('\0');
}
else if (version == 1)
{
// 旧格式:路径固定在 0x2C 偏移处的 260 个宽字符
if (bytes.Length < 0x2C + 520) return null;
originalPath = Encoding.Unicode.GetString(bytes, 0x2C, 520).TrimEnd('\0');
}
else
{
return null;
}
if (string.IsNullOrWhiteSpace(originalPath)) return null;
var fileName = Path.GetFileName(metaFilePath);
var recyclePath = Path.Combine(Path.GetDirectoryName(metaFilePath)!, "$R" + fileName[2..]);
return new RecycleBinEntry(recyclePath, originalPath, Math.Max(0, size), deleted, volumeRoot);
}
catch
{
return null;
}
}
}
+223
View File
@@ -0,0 +1,223 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
namespace FluidExplorer.Services.Shell;
/// <summary>
/// 交给 Windows 外壳去做的动作(打开、属性、打开方式、在资源管理器中显示…)。
/// 全部走系统原版行为,保证和资源管理器完全一致。
/// </summary>
public static class ShellActions
{
/// <summary>用默认程序/默认动作打开(= 双击)。</summary>
public static bool Open(string path)
{
try
{
Process.Start(new ProcessStartInfo(path) { UseShellExecute = true });
return true;
}
catch
{
return false;
}
}
/// <summary>打开"打开方式"选择器。</summary>
public static bool OpenWith(string path)
{
try
{
Process.Start(new ProcessStartInfo("rundll32.exe", $"shell32.dll,OpenAs_RunDLL {path}") { UseShellExecute = true });
return true;
}
catch
{
return false;
}
}
/// <summary>调用系统原版"属性"对话框(含只读/隐藏复选框、磁盘清理等)。</summary>
public static bool ShowProperties(IntPtr ownerHwnd, IReadOnlyList<string> paths)
{
if (paths.Count == 0) return false;
var info = new SHELLEXECUTEINFO
{
cbSize = Marshal.SizeOf<SHELLEXECUTEINFO>(),
fMask = SEE_MASK_INVOKEIDLIST | SEE_MASK_FLAG_NO_UI,
hwnd = ownerHwnd,
lpVerb = "properties",
lpFile = paths[0],
nShow = 5
};
if (paths.Count == 1) return ShellExecuteEx(ref info);
// 多选时用外壳的多文件属性对话框
try
{
var files = string.Join('\0', paths) + "\0\0";
var ptr = Marshal.StringToHGlobalUni(files);
try
{
info.lpFile = null;
var psi = new SHFILEINFO();
var hwnd = SHMultiFileProperties(new DataObjectNative { pFiles = ptr }, 0);
_ = hwnd;
_ = psi;
// SHMultiFileProperties 需要 IDataObject 实现,较繁琐;退化为逐项打开第一个的属性
info.lpFile = paths[0];
return ShellExecuteEx(ref info);
}
finally
{
Marshal.FreeHGlobal(ptr);
}
}
catch
{
return false;
}
}
/// <summary>在系统资源管理器中定位并选中该文件(用于"在资源管理器中显示")。</summary>
public static bool RevealInExplorer(string path)
{
try
{
Process.Start(new ProcessStartInfo("explorer.exe", $"/select,\"{path}\"") { UseShellExecute = true });
return true;
}
catch
{
return false;
}
}
/// <summary>运行对话框式的"运行"入口(用于 shell: 位置)。</summary>
public static bool OpenShellLocation(string parsingName)
{
try
{
Process.Start(new ProcessStartInfo("explorer.exe", parsingName) { UseShellExecute = true });
return true;
}
catch
{
return false;
}
}
/// <summary>把"打开"当作动词执行(用于右键菜单的"打开")。</summary>
public static bool Execute(string path, string verb)
{
var info = new SHELLEXECUTEINFO
{
cbSize = Marshal.SizeOf<SHELLEXECUTEINFO>(),
fMask = SEE_MASK_INVOKEIDLIST | SEE_MASK_FLAG_NO_UI,
lpVerb = verb,
lpFile = path,
nShow = 1
};
return ShellExecuteEx(ref info);
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct SHELLEXECUTEINFO
{
public int cbSize;
public uint fMask;
public IntPtr hwnd;
[MarshalAs(UnmanagedType.LPWStr)] public string? lpVerb;
[MarshalAs(UnmanagedType.LPWStr)] public string? lpFile;
[MarshalAs(UnmanagedType.LPWStr)] public string? lpParameters;
[MarshalAs(UnmanagedType.LPWStr)] public string? lpDirectory;
public int nShow;
public IntPtr hInstApp;
public IntPtr lpIDList;
[MarshalAs(UnmanagedType.LPWStr)] public string? lpClass;
public IntPtr hkeyClass;
public uint dwHotKey;
public IntPtr hIcon;
public IntPtr hProcess;
}
[StructLayout(LayoutKind.Sequential)]
private struct SHFILEINFO
{
public IntPtr hIcon;
public int iIcon;
public uint dwAttributes;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] public string szDisplayName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 80)] public string szTypeName;
}
[StructLayout(LayoutKind.Sequential)]
private struct DataObjectNative
{
public IntPtr pFiles;
}
private const uint SEE_MASK_INVOKEIDLIST = 0x0000000C;
private const uint SEE_MASK_FLAG_NO_UI = 0x00000400;
[DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern bool ShellExecuteEx(ref SHELLEXECUTEINFO lpExecInfo);
[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
private static extern IntPtr SHMultiFileProperties(DataObjectNative pdtobj, uint dwFlags);
/// <summary>把文件放入剪贴板(CF_HDROP,与其他程序互通)。</summary>
public static bool SetClipboardFiles(IReadOnlyList<string> paths, bool cut)
{
try
{
var sb = new StringBuilder();
foreach (var p in paths) sb.Append(p).Append('\0');
sb.Append('\0');
var bytes = Encoding.Unicode.GetBytes(sb.ToString());
var hGlobal = Marshal.AllocHGlobal(bytes.Length + 20);
if (hGlobal == IntPtr.Zero) return false;
// DROPFILES 结构 + 文件名列表
var dropFiles = new byte[20 + bytes.Length];
BitConverter.GetBytes(20).CopyTo(dropFiles, 0); // pFiles 偏移
BitConverter.GetBytes(0).CopyTo(dropFiles, 4); // pt.x
BitConverter.GetBytes(0).CopyTo(dropFiles, 8); // pt.y
BitConverter.GetBytes(0).CopyTo(dropFiles, 12); // fNC
BitConverter.GetBytes(1).CopyTo(dropFiles, 16); // fWide = TRUE
bytes.CopyTo(dropFiles, 20);
Marshal.Copy(dropFiles, 0, hGlobal, dropFiles.Length);
var format = RegisterClipboardFormat(cut ? "Preferred DropEffect" : "Preferred DropEffect");
var effect = new byte[4];
BitConverter.GetBytes(cut ? 2 : 5).CopyTo(effect, 0); // DROPEFFECT_MOVE=2 / COPY=5
var hEffect = Marshal.AllocHGlobal(4);
Marshal.Copy(effect, 0, hEffect, 4);
if (!OpenClipboard(IntPtr.Zero)) { Marshal.FreeHGlobal(hGlobal); Marshal.FreeHGlobal(hEffect); return false; }
try
{
EmptyClipboard();
SetClipboardData(15 /*CF_HDROP*/, hGlobal);
SetClipboardData(format, hEffect);
}
finally
{
CloseClipboard();
}
return true;
}
catch
{
return false;
}
}
[DllImport("user32.dll", SetLastError = true)] private static extern bool OpenClipboard(IntPtr hWndNewOwner);
[DllImport("user32.dll", SetLastError = true)] private static extern bool CloseClipboard();
[DllImport("user32.dll", SetLastError = true)] private static extern bool EmptyClipboard();
[DllImport("user32.dll", SetLastError = true)] private static extern IntPtr SetClipboardData(uint uFormat, IntPtr hMem);
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] private static extern uint RegisterClipboardFormat(string lpszFormat);
}
+267
View File
@@ -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);
}
+60
View File
@@ -0,0 +1,60 @@
using System.Collections.Concurrent;
using Microsoft.Win32;
namespace FluidExplorer.Services.Shell;
/// <summary>
/// 用注册表把扩展名映射成资源管理器里显示的友好类型名(例如 "PDF 文档"、"文本文档")。
/// 结果缓存;失败时回退到 "XXX 文件"。
/// </summary>
public static class TypeNameResolver
{
private static readonly ConcurrentDictionary<string, string> Cache = new(StringComparer.OrdinalIgnoreCase);
public static string GetTypeName(string extension, bool isDirectory)
{
if (isDirectory) return "文件夹";
if (string.IsNullOrEmpty(extension)) return "文件";
return Cache.GetOrAdd(extension, static ext =>
{
try
{
var key = "." + ext;
using var extKey = Registry.ClassesRoot.OpenSubKey(key);
if (extKey is null) return $"{ext.ToUpperInvariant()} 文件";
var progId = extKey.GetValue(null) as string;
if (!string.IsNullOrEmpty(progId) && (progId.StartsWith("AppX", StringComparison.OrdinalIgnoreCase)
|| progId.Contains("_", StringComparison.Ordinal)))
{
// AppX/UWP 关联:优先用 "FriendlyTypeName"(本地化资源,直接读字符串)
var friendly = extKey.GetValue("FriendlyTypeName") as string;
if (!string.IsNullOrWhiteSpace(friendly)) return friendly;
}
if (!string.IsNullOrEmpty(progId))
{
using var progKey = Registry.ClassesRoot.OpenSubKey(progId);
if (progKey is not null)
{
var name = progKey.GetValue("FriendlyTypeName") as string ?? progKey.GetValue(null) as string;
if (!string.IsNullOrWhiteSpace(name))
{
// 间接字符串(@dll,-id)无法直接解析,交回扩展名兜底
return name.StartsWith('@') ? $"{ext.ToUpperInvariant()} 文件" : name;
}
}
}
var extFriendly = extKey.GetValue("FriendlyTypeName") as string;
if (!string.IsNullOrWhiteSpace(extFriendly) && !extFriendly.StartsWith('@')) return extFriendly;
}
catch
{
// 注册表不可读(权限/损坏)时静默回退
}
return $"{ext.ToUpperInvariant()} 文件";
});
}
}