You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

咨询.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的低效根源是单次任务提交,无法利用系统批量优化
  • 优先使用IFileOperation API,实现和资源管理器完全一致的复制体验
  • 注意COM对象的释放,避免内存泄漏

内容的提问来源于stack exchange,提问作者DerAnonyme

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.04 04:06:27