using System.Diagnostics; using System.Runtime.InteropServices; using System.Text; namespace FluidExplorer.Services.Shell; /// /// 交给 Windows 外壳去做的动作(打开、属性、打开方式、在资源管理器中显示…)。 /// 全部走系统原版行为,保证和资源管理器完全一致。 /// public static class ShellActions { /// 用默认程序/默认动作打开(= 双击)。 public static bool Open(string path) { try { Process.Start(new ProcessStartInfo(path) { UseShellExecute = true }); return true; } catch { return false; } } /// 打开"打开方式"选择器。 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; } } /// 调用系统原版"属性"对话框(含只读/隐藏复选框、磁盘清理等)。 public static bool ShowProperties(IntPtr ownerHwnd, IReadOnlyList paths) { if (paths.Count == 0) return false; var info = new SHELLEXECUTEINFO { cbSize = Marshal.SizeOf(), 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; } } /// 在系统资源管理器中定位并选中该文件(用于"在资源管理器中显示")。 public static bool RevealInExplorer(string path) { try { Process.Start(new ProcessStartInfo("explorer.exe", $"/select,\"{path}\"") { UseShellExecute = true }); return true; } catch { return false; } } /// 运行对话框式的"运行"入口(用于 shell: 位置)。 public static bool OpenShellLocation(string parsingName) { try { Process.Start(new ProcessStartInfo("explorer.exe", parsingName) { UseShellExecute = true }); return true; } catch { return false; } } /// 把"打开"当作动词执行(用于右键菜单的"打开")。 public static bool Execute(string path, string verb) { var info = new SHELLEXECUTEINFO { cbSize = Marshal.SizeOf(), 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); /// 把文件放入剪贴板(CF_HDROP,与其他程序互通)。 public static bool SetClipboardFiles(IReadOnlyList 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); }