咨询.NET 6 Windows下批量复制多文件/文件夹的高效实现方法
.NET 6 Windows下实现高效批量文件复制(带原生进度对话框)
你遇到的问题核心在于循环调用FileSystem.CopyFile是逐个提交复制任务,没有利用Windows系统的批量优化能力,同时无法触发统一的进度对话框。.NET 6没有直接提供符合需求的内置批量复制API,但可以通过调用Windows原生Shell API实现和资源管理器完全一致的复制行为——包括高效批量处理和整体进度显示。
推荐方案:使用IFileOperation API(现代Windows系统首选)
IFileOperation是Windows Vista及以后版本提供的Shell COM API,和资源管理器使用同一套底层逻辑,支持批量文件操作、原生进度对话框、撤销功能等,复制效率和资源管理器完全一致。
实现代码
首先定义必要的COM接口和辅助类(无需额外NuGet包,直接在项目中添加即可):
using System; using System.Runtime.InteropServices; using System.Runtime.InteropServices.ComTypes; [ComImport] [Guid("947aab5f-0a5c-4c13-b4d6-4bf7836fc9f8")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] public interface IFileOperation { void Advise([In, MarshalAs(UnmanagedType.Interface)] IFileOperationProgressSink pfops); void Unadvise([In] uint dwCookie); void SetOperationFlags([In] uint dwOperationFlags); void SetProgressMessage([In, MarshalAs(UnmanagedType.LPWStr)] string pszMessage); void SetProgressDialog([In, MarshalAs(UnmanagedType.Interface)] IntPtr hwndParent); void SetProperties([In, MarshalAs(UnmanagedType.Interface)] IPropertyStore pStore); void SetOwnerWindow([In] IntPtr hwndOwner); void ApplyPropertiesToItems([In] uint cItems, [In, MarshalAs(UnmanagedType.LPArray)] IntPtr[] rgItems); void ApplyPropertiesToItem([In, MarshalAs(UnmanagedType.Interface)] IShellItem psiItem); void DeleteItems([In] uint cItems, [In, MarshalAs(UnmanagedType.LPArray)] IntPtr[] rgItems); void DeleteItem([In, MarshalAs(UnmanagedType.Interface)] IShellItem psiItem); void RenameItems([In] uint cItems, [In, MarshalAs(UnmanagedType.LPArray)] IntPtr[] rgItems, [In, MarshalAs(UnmanagedType.LPWStr)] string pszNewName); void RenameItem([In, MarshalAs(UnmanagedType.Interface)] IShellItem psiItem, [In, MarshalAs(UnmanagedType.LPWStr)] string pszNewName, [In, MarshalAs(UnmanagedType.Interface)] IFileOperationProgressSink pfops); void MoveItems([In] uint cItems, [In, MarshalAs(UnmanagedType.LPArray)] IntPtr[] rgItems, [In, MarshalAs(UnmanagedType.Interface)] IShellItem psiDestinationFolder, [In, MarshalAs(UnmanagedType.LPWStr)] string pszNewName); void MoveItem([In, MarshalAs(UnmanagedType.Interface)] IShellItem psiItem, [In, MarshalAs(UnmanagedType.Interface)] IShellItem psiDestinationFolder, [In, MarshalAs(UnmanagedType.LPWStr)] string pszNewName, [In, MarshalAs(UnmanagedType.Interface)] IFileOperationProgressSink pfops); void CopyItems([In] uint cItems, [In, MarshalAs(UnmanagedType.LPArray)] IntPtr[] rgItems, [In, MarshalAs(UnmanagedType.Interface)] IShellItem psiDestinationFolder); void CopyItem([In, MarshalAs(UnmanagedType.Interface)] IShellItem psiItem, [In, MarshalAs(UnmanagedType.Interface)] IShellItem psiDestinationFolder, [In, MarshalAs(UnmanagedType.LPWStr)] string pszNewName, [In, MarshalAs(UnmanagedType.Interface)] IFileOperationProgressSink pfops); void PerformOperations(); void GetAnyOperationsAborted([Out] out bool pfAnyOperationsAborted); } [ComImport] [Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] public interface IShellItem { void BindToHandler([In] IntPtr pbc, [In] ref Guid bhid, [In] ref Guid riid, [Out, MarshalAs(UnmanagedType.Interface)] out IntPtr ppv); void GetParent([Out, MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi); void GetDisplayName([In] SIGDN sigdnName, [Out, MarshalAs(UnmanagedType.LPWStr)] out string ppszName); void GetAttributes([In] uint sfgaoMask, [Out] out uint psfgaoAttribs); void Compare([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, [In] uint hint, [Out] out int piOrder); } public enum SIGDN : uint { SIGDN_DESKTOPABSOLUTEEDITING = 0x8004c000, SIGDN_DESKTOPABSOLUTEPARSING = 0x80028000, SIGDN_FILESYSPATH = 0x80058000, SIGDN_NORMALDISPLAY = 0x00000000, SIGDN_PARENTRELATIVE = 0x80080001, SIGDN_PARENTRELATIVEEDITING = 0x80031001, SIGDN_PARENTRELATIVEFORADDRESSBAR = 0x8007c001, SIGDN_PARENTRELATIVEPARSING = 0x80018001, SIGDN_URL = 0x80068000 } [ComImport] [Guid("DC1C5A9C-E88A-4dde-A5A1-60F82A20AEF7")] public class FileOperation { } public static class FileOperationHelper { public static void CopyFiles(string[] sourcePaths, string destinationPath, IntPtr ownerWindowHandle = default) { if (sourcePaths == null || sourcePaths.Length == 0) throw new ArgumentNullException(nameof(sourcePaths)); if (string.IsNullOrEmpty(destinationPath)) throw new ArgumentNullException(nameof(destinationPath)); IFileOperation fileOp = (IFileOperation)new FileOperation(); // 配置操作行为:允许撤销、自动创建目标文件夹、显示简化进度对话框 const uint FOF_ALLOWUNDO = 0x0040; const uint FOF_NOCONFIRMMKDIR = 0x0200; const uint FOF_SIMPLEPROGRESS = 0x0100; fileOp.SetOperationFlags(FOF_ALLOWUNDO | FOF_NOCONFIRMMKDIR | FOF_SIMPLEPROGRESS); if (ownerWindowHandle != IntPtr.Zero) fileOp.SetOwnerWindow(ownerWindowHandle); IShellItem destinationItem = CreateShellItemFromPath(destinationPath); IntPtr[] sourceItems = new IntPtr[sourcePaths.Length]; try { for (int i = 0; i < sourcePaths.Length; i++) { sourceItems[i] = Marshal.GetIUnknownForObject(CreateShellItemFromPath(sourcePaths[i])); } fileOp.CopyItems((uint)sourcePaths.Length, sourceItems, destinationItem); fileOp.PerformOperations(); } finally { foreach (IntPtr ptr in sourceItems) { if (ptr != IntPtr.Zero) Marshal.Release(ptr); } Marshal.ReleaseComObject(destinationItem); Marshal.ReleaseComObject(fileOp); } } private static IShellItem CreateShellItemFromPath(string path) { Guid shellItemGuid = typeof(IShellItem).GUID; int hr = SHCreateItemFromParsingName(path, IntPtr.Zero, ref shellItemGuid, out IShellItem item); if (hr < 0) Marshal.ThrowExceptionForHR(hr); return item; } [DllImport("shell32.dll", CharSet = CharSet.Unicode, PreserveSig = false)] private static extern int SHCreateItemFromParsingName( [In, MarshalAs(UnmanagedType.LPWStr)] string pszPath, [In] IntPtr pbc, [In] ref Guid riid, [Out, MarshalAs(UnmanagedType.Interface)] out IShellItem ppv); }
调用示例
// 替换为你的源文件路径数组和目标文件夹路径 string[] sourcePaths = new[] { @"C:\Photos\Img001.jpg", @"C:\Photos\Img002.jpg", @"C:\Photos\Img003.jpg" }; string destinationFolder = @"D:\Backup\Photos"; // 执行批量复制,自动显示原生进度对话框 FileOperationHelper.CopyFiles(sourcePaths, destinationFolder);
关键优势
- 速度与资源管理器一致:一次性提交所有复制任务,Windows Shell会进行批量缓存、异步调度等优化
- 原生进度对话框:显示整体复制进度、剩余时间、已复制文件数,支持用户取消操作
- 支持撤销:配置
FOF_ALLOWUNDO后,用户可通过资源管理器的撤销功能恢复操作
备选方案:SHFileOperation API(兼容旧版Windows)
如果需要兼容Windows XP及更早版本,可以使用旧版的SHFileOperation API,但该接口功能不如IFileOperation灵活,且已被微软标记为过时。这里仅提供简单示例:
using System; using System.Runtime.InteropServices; [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] public struct SHFILEOPSTRUCT { public IntPtr hwnd; public uint wFunc; [MarshalAs(UnmanagedType.LPWStr)] public string pFrom; [MarshalAs(UnmanagedType.LPWStr)] public string pTo; public ushort fFlags; public bool fAnyOperationsAborted; public IntPtr hNameMappings; [MarshalAs(UnmanagedType.LPWStr)] public string lpszProgressTitle; } public static class SHFileOperationHelper { [DllImport("shell32.dll", CharSet = CharSet.Unicode)] private static extern int SHFileOperation(ref SHFILEOPSTRUCT lpFileOp); public static void CopyFiles(string[] sourcePaths, string destinationPath) { string sourceList = string.Join("\0", sourcePaths) + "\0\0"; string destination = destinationPath + "\0\0"; SHFILEOPSTRUCT fileOp = new SHFILEOPSTRUCT { wFunc = 0x0002, // FO_COPY pFrom = sourceList, pTo = destination, fFlags = 0x0040 | 0x0200 | 0x0100 // FOF_ALLOWUNDO | FOF_NOCONFIRMMKDIR | FOF_SIMPLEPROGRESS }; int result = SHFileOperation(ref fileOp); if (result != 0) throw new System.ComponentModel.Win32Exception(result); } }
总结
- 循环调用
FileSystem.CopyFile的低效根源是单次任务提交,无法利用系统批量优化 - 优先使用
IFileOperationAPI,实现和资源管理器完全一致的复制体验 - 注意COM对象的释放,避免内存泄漏
内容的提问来源于stack exchange,提问作者DerAnonyme
相关产品推荐
相关产品推荐

