在.NET中,我能否使用FileInfo.CopyTo()显示文件复制进度?

32
我用c# (.NET 2.0 Framework) 创建了一个复制文件、目录和子目录的工具,并有一个GUI界面,显示当前正在复制的文件、当前文件编号(顺序)、要复制的文件总数以及复制操作的完成百分比。还有一个基于当前文件/总文件的进度条。
我遇到了复制大文件的问题。使用我的当前类结构(利用FileInfo.CopyTo方法),我找不到一种指示大文件的总复制进度的方法。因此,我将文件复制操作和GUI显示分别放在它们自己的线程中,并设置了一个视觉提示,以表明正在进行工作。至少用户知道程序没有冻结,并且仍在复制文件。
更好的做法是能够根据字节数显示进度或者有一些类型的事件,可以从FileInfo.CopyTo方法中触发,指示从当前文件复制的总字节数。
我知道FileInfo.Length属性,所以我相信有一种方式可以基于此创建自己的事件,并在GUI方面设置一个处理程序来读取更新(也许是使用一些类型的计时器检查目标对象的FileInfo.Length属性)。
有没有人知道我是否忽略了某种方法来解决这个问题?如果可以避免,我不想重写我的类来通过流复制字节并跟踪它(尽管我认为我可能被迫走这条路)。
PS- 我现在只能使用.NET 2.0框架,因此任何需要 >=3.0 版本特性的解决方案对我来说都不是选项。
PPS- 我可以接受任何.NET语言版本的解决方案,而不仅仅是c#。

有完整的示例源代码吗? - Kiquenet
7个回答

38

FileInfo.CopyTo基本上是对内核32.dll中的"CopyFile" Win32 API调用的封装。该方法不支持进度回调。

然而,CopyFileEx方法支持进度回调,并且您可以在几分钟内编写自己的.NET包装器,就像这里描述的那样:http://www.pinvoke.net/default.aspx/kernel32.CopyFileEx


谢谢Gaspar。这个选项看起来像是解决问题的一种可能途径。我会再仔细研究一下。 - Jason Down
1
就我所知,我已经成功地使用了这种方法。实际上,我相当确定我直接从pinvoke.net复制了那段代码。agentidle,你可以创建一个漂亮的小类来包装它,而不是处理所有的参数。 - Brian Ensink

28
我也使用了标记答案中提供的实现。然后,我创建了一个包装器,以提供一个更加友好的API供.NET使用。 用法:
XCopy.Copy(networkFile.FullPath, temporaryFilename, true, true, (o, pce) => 
{
    worker.ReportProgress(pce.ProgressPercentage, networkFile);
});

实现

/// <summary>
/// PInvoke wrapper for CopyEx
/// http://msdn.microsoft.com/en-us/library/windows/desktop/aa363852.aspx
/// </summary>
public class XCopy
{
    public static void Copy(string source, string destination, bool overwrite, bool nobuffering)
    {
         new XCopy().CopyInternal(source, destination, overwrite, nobuffering, null);            
    }

    public static void Copy(string source, string destination, bool overwrite, bool nobuffering, EventHandler<ProgressChangedEventArgs> handler)
    {            
         new XCopy().CopyInternal(source, destination, overwrite, nobuffering, handler);            
    }

    private event EventHandler Completed;
    private event EventHandler<ProgressChangedEventArgs> ProgressChanged;

    private int IsCancelled;
    private int FilePercentCompleted;
    private string Source;
    private string Destination;        

    private XCopy()
    {
        IsCancelled = 0;
    }

    private void CopyInternal(string source, string destination, bool overwrite, bool nobuffering, EventHandler<ProgressChangedEventArgs> handler)
    {
        try
        {
            CopyFileFlags copyFileFlags = CopyFileFlags.COPY_FILE_RESTARTABLE;
            if (!overwrite)
                copyFileFlags |= CopyFileFlags.COPY_FILE_FAIL_IF_EXISTS;

            if (nobuffering)
                copyFileFlags |= CopyFileFlags.COPY_FILE_NO_BUFFERING;

            Source = source;
            Destination = destination;

            if (handler != null)
                ProgressChanged += handler;

            bool result = CopyFileEx(Source, Destination, new CopyProgressRoutine(CopyProgressHandler), IntPtr.Zero, ref IsCancelled, copyFileFlags);
            if (!result)
                throw new Win32Exception(Marshal.GetLastWin32Error());
        }
        catch (Exception)
        {
            if (handler != null)
                ProgressChanged -= handler;

            throw;
        }
    }

    private void OnProgressChanged(double percent)
    {
        // only raise an event when progress has changed
        if ((int)percent > FilePercentCompleted)
        {
            FilePercentCompleted = (int)percent;

            var handler = ProgressChanged;
            if (handler != null)
                handler(this, new ProgressChangedEventArgs((int)FilePercentCompleted, null));
        }
    }

    private void OnCompleted()
    {
        var handler = Completed;
        if (handler != null)
            handler(this, EventArgs.Empty);
    }

    #region PInvoke

    [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool CopyFileEx(string lpExistingFileName, string lpNewFileName, CopyProgressRoutine lpProgressRoutine, IntPtr lpData, ref Int32 pbCancel, CopyFileFlags dwCopyFlags);

    private delegate CopyProgressResult CopyProgressRoutine(long TotalFileSize, long TotalBytesTransferred, long StreamSize, long StreamBytesTransferred, uint dwStreamNumber, CopyProgressCallbackReason dwCallbackReason,
                                                    IntPtr hSourceFile, IntPtr hDestinationFile, IntPtr lpData);

    private enum CopyProgressResult : uint
    {
        PROGRESS_CONTINUE = 0,
        PROGRESS_CANCEL = 1,
        PROGRESS_STOP = 2,
        PROGRESS_QUIET = 3
    }

    private enum CopyProgressCallbackReason : uint
    {
        CALLBACK_CHUNK_FINISHED = 0x00000000,
        CALLBACK_STREAM_SWITCH = 0x00000001
    }

    [Flags]
    private enum CopyFileFlags : uint
    {
        COPY_FILE_FAIL_IF_EXISTS = 0x00000001,
        COPY_FILE_NO_BUFFERING = 0x00001000,
        COPY_FILE_RESTARTABLE = 0x00000002,
        COPY_FILE_OPEN_SOURCE_FOR_WRITE = 0x00000004,
        COPY_FILE_ALLOW_DECRYPTED_DESTINATION = 0x00000008
    }

    private CopyProgressResult CopyProgressHandler(long total, long transferred, long streamSize, long streamByteTrans, uint dwStreamNumber,
                                                   CopyProgressCallbackReason reason, IntPtr hSourceFile, IntPtr hDestinationFile, IntPtr lpData)
    {
        if (reason == CopyProgressCallbackReason.CALLBACK_CHUNK_FINISHED)
            OnProgressChanged((transferred / (double)total) * 100.0);

        if (transferred >= total)
            OnCompleted();

        return CopyProgressResult.PROGRESS_CONTINUE;
    }

    #endregion

}

1
很不错。实际上我最终没有使用它,因为我发现使用“WebClient”异步下载文件可以获得100倍更好的性能。 - Dennis
这个程序好像有一些部分缺失。我该如何将进度与进度条绑定? - Oh hi Mark
1
即使这是一个旧帖子:好的解决方案,但是现在没有线程安全。如果从多个线程调用此函数,则所有反馈都会发送到每个活动侦听器。也许稍后我会发布改进。 - PMF
1
嗯,看起来我错了,抱歉。事件不会静态保留,并且回调发生在一个实例上。一切都好。 - PMF
1
@KerryPerret 几乎可以确定 WebClient 的测量方式是错误的。它具有 DownloadFileAsync,该方法在操作完成之前返回。您可以调用它来下载 10TB 的数据,它将在 10 毫秒内返回。基准测试很容易出错。为了更正确地进行测量,请使用 DownloadFile() 或 DownloadFileTaskAsync.Wait()。两者都会阻塞,但对于基准测试来说这没问题。 - mafu
显示剩余6条评论

13

我知道我来晚了,但我为CopyFileEx编写了一个包装器,它返回一个Task并接受一个CancellationToken和一个IProgress<double>。不幸的是,在.NET 2.0框架中它无法工作,但对于任何使用4.5的人来说,这使您能够使用await关键字。

public static class FileEx
{
    public static Task CopyAsync(string sourceFileName, string destFileName)
    {
        return CopyAsync(sourceFileName, destFileName, CancellationToken.None);
    }

    public static Task CopyAsync(string sourceFileName, string destFileName, CancellationToken token)
    {
        return CopyAsync(sourceFileName, destFileName, token, null);
    }

    public static Task CopyAsync(string sourceFileName, string destFileName, IProgress<double> progress)
    {
        return CopyAsync(sourceFileName, destFileName, CancellationToken.None, progress);
    }

    public static Task CopyAsync(string sourceFileName, string destFileName, CancellationToken token, IProgress<double> progress)
    {
        int pbCancel = 0;
        CopyProgressRoutine copyProgressHandler;
        if (progress != null)
        {
            copyProgressHandler = (total, transferred, streamSize, streamByteTrans, dwStreamNumber, reason, hSourceFile, hDestinationFile, lpData) =>
            {
                progress.Report((double)transferred / total * 100);
                return CopyProgressResult.PROGRESS_CONTINUE;
            };
        }
        else
        {
            copyProgressHandler = EmptyCopyProgressHandler;
        }
        token.ThrowIfCancellationRequested();
        var ctr = token.Register(() => pbCancel = 1);
        var copyTask = Task.Run(() =>
        {
            try
            {
                CopyFileEx(sourceFileName, destFileName, copyProgressHandler, IntPtr.Zero, ref pbCancel, CopyFileFlags.COPY_FILE_RESTARTABLE);
                token.ThrowIfCancellationRequested();
            }
            finally
            {
                ctr.Dispose();
            }
        }, token);
        return copyTask;
    }

    private static CopyProgressResult EmptyCopyProgressHandler(long total, long transferred, long streamSize, long streamByteTrans, uint dwStreamNumber, CopyProgressCallbackReason reason, IntPtr hSourceFile, IntPtr hDestinationFile, IntPtr lpData)
    {
        return CopyProgressResult.PROGRESS_CONTINUE;
    }

    #region DLL Import

    [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool CopyFileEx(string lpExistingFileName, string lpNewFileName,
       CopyProgressRoutine lpProgressRoutine, IntPtr lpData, ref Int32 pbCancel,
       CopyFileFlags dwCopyFlags);

    delegate CopyProgressResult CopyProgressRoutine(
        long totalFileSize,
        long totalBytesTransferred,
        long streamSize,
        long streamBytesTransferred,
        uint dwStreamNumber,
        CopyProgressCallbackReason dwCallbackReason,
        IntPtr hSourceFile,
        IntPtr hDestinationFile,
        IntPtr lpData);

    enum CopyProgressResult : uint
    {
        PROGRESS_CONTINUE = 0,
        PROGRESS_CANCEL = 1,
        PROGRESS_STOP = 2,
        PROGRESS_QUIET = 3
    }

    enum CopyProgressCallbackReason : uint
    {
        CALLBACK_CHUNK_FINISHED = 0x00000000,
        CALLBACK_STREAM_SWITCH = 0x00000001
    }

    [Flags]
    enum CopyFileFlags : uint
    {
        COPY_FILE_FAIL_IF_EXISTS = 0x00000001,
        COPY_FILE_RESTARTABLE = 0x00000002,
        COPY_FILE_OPEN_SOURCE_FOR_WRITE = 0x00000004,
        COPY_FILE_ALLOW_DECRYPTED_DESTINATION = 0x00000008
    }

    #endregion
}

我在6年前提出了这个问题,所以一个符合时代背景的更新答案肯定是受欢迎的。 - Jason Down

9

拜托了,不要使用流实现文件复制!Gaspar提到的Win32 CopyFile API调用能够利用DMA等技术,而我敢打赌Will编写的代码不会“聪明”到做到这一点。

CopyFileEx会让你满意,或者你可以实现一个BackgroundWorker来监视目标文件的增长大小,并使用该信息更新进度条。后一种方法可以避免PInvoke,但前一种方法从长远来看可能更加简洁。


使用FileInfo对象监视目标文件并检查长度是我考虑的另一种选择。不过我同意,使用CopyFileEx方法可能是最好的选择。 - Jason Down
1
我很高兴这个答案存在。它应该作为一个置顶笔记出现在大多数关于文件复制的问题下面。大多数人不知道你可以直接从磁盘复制到网络,而完全不涉及CPU。 - mafu

5

对于这些事情,我已经回到了Shell32(或者是ShellUI?我不确定了)。这会给你一个本地的Windows对话框,用户习惯于在复制操作中看到。我猜它会替换你已经存在的对话框,所以它可能不是你需要的答案,但在“危急时刻”中记住它是有用的。

Microsoft.VisualBasic.FileIO.FileSystem.CopyFile(
    srcPath, 
    dstPath, 
    Microsoft.VisualBasic.FileIO.UIOption.AllDialogs,    
    Microsoft.VisualBasic.FileIO.UICancelOption.ThrowException
);

是的,你必须引用Microsoft.VisualBasic程序集。我已经逐渐喜欢这个程序集。


我没有选择这条路的唯一原因是我需要确保用户无法取消复制操作。 - Jason Down

3
感谢@Gasper和@Dennis指出CopyFileEx方法。 我已经扩展了Dennis的回答并添加了中止复制的功能。
    /// <summary>
    /// Type indicates how the copy gets completed.
    /// </summary>
    internal enum CopyCompletedType
    {
        Succeeded,
        Aborted,
        Exception
    }

/// <summary>
/// Event arguments for file copy 
/// </summary>
internal class FileCopyEventArgs : EventArgs
{
    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name="type">type of the copy completed type enum</param>
    /// <param name="exception">exception if any</param>
    public FileCopyEventArgs(CopyCompletedType type, Exception exception)
    {
        Type = type;
        Exception = exception;
    }

    /// <summary>
    /// Type of the copy completed type
    /// </summary>
    public CopyCompletedType Type
    {
        get;
        private set;

    }

    /// <summary>
    /// Exception if any happend during copy.
    /// </summary>
    public Exception Exception
    {
        get;
        private set;
    }

}

/// <summary>
/// PInvoke wrapper for CopyEx
/// http://msdn.microsoft.com/en-us/library/windows/desktop/aa363852.aspx
/// </summary>
internal class XCopy
{

    private int IsCancelled;
    private int FilePercentCompleted;

    public XCopy()
    {
        IsCancelled = 0;
    }

    /// <summary>
    /// Copies the file asynchronously
    /// </summary>
    /// <param name="source">the source path</param>
    /// <param name="destination">the destination path</param>
    /// <param name="nobuffering">Bufferig status</param>
    /// <param name="handler">Event handler to do file copy.</param>
    public void CopyAsync(string source, string destination, bool nobuffering)
    {
        try
        {
            //since we needed an async copy ..
            Action action = new Action(
                () => CopyInternal(source, destination, nobuffering)
                    );
            Task task = new Task(action);
            task.Start();
        }
        catch (AggregateException ex)
        {
            //handle the inner exception since exception thrown from task are wrapped in
            //aggreate exception.
            OnCompleted(CopyCompletedType.Exception, ex.InnerException);
        }
        catch (Exception ex)
        {
            OnCompleted(CopyCompletedType.Exception, ex);
        }
    }

    /// <summary>
    /// Event which will notify the subscribers if the copy gets completed
    /// There are three scenarios in which completed event will be thrown when
    /// 1.Copy succeeded
    /// 2.Copy aborted.
    /// 3.Any exception occured.
    /// These information can be obtained from the Event args.
    /// </summary>
    public event EventHandler<FileCopyEventArgs> Completed;
    /// <summary>
    /// Event which will notify the subscribers if there is any progress change while copying.
    /// This will indicate the progress percentage in its event args.
    /// </summary>
    public event EventHandler<ProgressChangedEventArgs> ProgressChanged;

    /// <summary>
    /// Aborts the copy asynchronously and throws Completed event when done.
    /// User may not want to wait for completed event in case of Abort since 
    /// the event will tell that copy has been aborted.
    /// </summary>
    public void AbortCopyAsync()
    {
        Trace.WriteLine("Aborting the copy");
        //setting this will cancel an operation since we pass the
        //reference to copyfileex and it will periodically check for this.
        //otherwise also We can check for iscancelled on onprogresschanged and return 
        //Progress_cancelled .
        IsCancelled = 1;

        Action completedEvent = new Action(() =>
            {
                //wait for some time because we ll not know when IsCancelled is set , at what time windows stops copying.
                //so after sometime this may become valid .
                Thread.Sleep(500);
                //do we need to wait for some time and send completed event.
                OnCompleted(CopyCompletedType.Aborted);
                //reset the value , otherwise if we try to copy again since value is 1 , 
                //it thinks that its aborted and wont allow to copy.
                IsCancelled = 0;
            });

        Task completedTask = new Task(completedEvent);
        completedTask.Start();
    }


    /// <summary>
    /// Copies the file using asynchronos task
    /// </summary>
    /// <param name="source">the source path</param>
    /// <param name="destination">the destination path</param>
    /// <param name="nobuffering">Buffering status</param>
    /// <param name="handler">Delegate to handle Progress changed</param>
    private void CopyInternal(string source, string destination, bool nobuffering)
    {
        CopyFileFlags copyFileFlags = CopyFileFlags.COPY_FILE_RESTARTABLE;

        if (nobuffering)
        {
            copyFileFlags |= CopyFileFlags.COPY_FILE_NO_BUFFERING;
        }

        try
        {
            Trace.WriteLine("File copy started with Source: " + source + " and destination: " + destination);
            //call win32 api.
            bool result = CopyFileEx(source, destination, new CopyProgressRoutine(CopyProgressHandler), IntPtr.Zero, ref IsCancelled, copyFileFlags);
            if (!result)
            {
                //when ever we get the result as false it means some error occured so get the last win 32 error.
                throw new Win32Exception(Marshal.GetLastWin32Error());
            }
        }
        catch (Exception ex)
        {
            //the mesage will contain the requested operation was aborted when the file copy
            //was cancelled. so we explicitly check for that and do a graceful exit
            if (ex.Message.Contains("aborted"))
            {
                Trace.WriteLine("Copy aborted.");
            }
            else
            {
                OnCompleted(CopyCompletedType.Exception, ex.InnerException);
            }
        }
    }

    private void OnProgressChanged(double percent)
    {
        // only raise an event when progress has changed
        if ((int)percent > FilePercentCompleted)
        {
            FilePercentCompleted = (int)percent;

            var handler = ProgressChanged;
            if (handler != null)
            {
                handler(this, new ProgressChangedEventArgs((int)FilePercentCompleted, null));
            }
        }
    }

    private void OnCompleted(CopyCompletedType type, Exception exception = null)
    {
        var handler = Completed;
        if (handler != null)
        {
            handler(this, new FileCopyEventArgs(type, exception));
        }
    }

    #region PInvoke

    /// <summary>
    /// Delegate which will be called by Win32 API for progress change
    /// </summary>
    /// <param name="total">the total size</param>
    /// <param name="transferred">the transferrred size</param>
    /// <param name="streamSize">size of the stream</param>
    /// <param name="streamByteTrans"></param>
    /// <param name="dwStreamNumber">stream number</param>
    /// <param name="reason">reason for callback</param>
    /// <param name="hSourceFile">the source file handle</param>
    /// <param name="hDestinationFile">the destination file handle</param>
    /// <param name="lpData">data passed by users</param>
    /// <returns>indicating whether to continue or do somthing else.</returns>
    private CopyProgressResult CopyProgressHandler(long total, long transferred, long streamSize, long streamByteTrans, uint dwStreamNumber,
                                                   CopyProgressCallbackReason reason, IntPtr hSourceFile, IntPtr hDestinationFile, IntPtr lpData)
    {
        //when a chunk is finished call the progress changed.
        if (reason == CopyProgressCallbackReason.CALLBACK_CHUNK_FINISHED)
        {
            OnProgressChanged((transferred / (double)total) * 100.0);
        }

        //transfer completed
        if (transferred >= total)
        {
            if (CloseHandle(hDestinationFile))
            {
                OnCompleted(CopyCompletedType.Succeeded, null);
            }
            else
            {
                OnCompleted(CopyCompletedType.Exception,
                    new System.IO.IOException("Unable to close the file handle"));
            }
        }

        return CopyProgressResult.PROGRESS_CONTINUE;
    }
    [System.Runtime.InteropServices.DllImport("Kernel32")]
    private extern static Boolean CloseHandle(IntPtr handle);

    [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool CopyFileEx(string lpExistingFileName, string lpNewFileName, CopyProgressRoutine lpProgressRoutine, IntPtr lpData, ref Int32 pbCancel, CopyFileFlags dwCopyFlags);

    private delegate CopyProgressResult CopyProgressRoutine(long TotalFileSize, long TotalBytesTransferred, long StreamSize, long StreamBytesTransferred, uint dwStreamNumber, CopyProgressCallbackReason dwCallbackReason,
                                                    IntPtr hSourceFile, IntPtr hDestinationFile, IntPtr lpData);

    private enum CopyProgressResult : uint
    {
        PROGRESS_CONTINUE = 0,
        PROGRESS_CANCEL = 1,
        PROGRESS_STOP = 2,
        PROGRESS_QUIET = 3
    }

    private enum CopyProgressCallbackReason : uint
    {
        CALLBACK_CHUNK_FINISHED = 0x00000000,
        CALLBACK_STREAM_SWITCH = 0x00000001
    }

    [Flags]
    private enum CopyFileFlags : uint
    {
        COPY_FILE_FAIL_IF_EXISTS = 0x00000001,
        COPY_FILE_NO_BUFFERING = 0x00001000,
        COPY_FILE_RESTARTABLE = 0x00000002,
        COPY_FILE_OPEN_SOURCE_FOR_WRITE = 0x00000004,
        COPY_FILE_ALLOW_DECRYPTED_DESTINATION = 0x00000008
    }

    #endregion

}

客户端可以创建XCopy类的对象并调用复制/中止方法。

不应该执行CloseHandle(hDestinationFile),因为操作系统也会尝试关闭句柄,并在失败时返回异常。应该通过CopyInternal()执行OnCompleted(CopyCompleteType.Succeeded...)。 - Mike

2
如果有人像我一样(在10年后!)仍然遇到这个问题,我已经创建了一个包装器,围绕CopyFileEx和MoveFileWithProgress函数(如此处的某些答案),具有一些额外的有用功能(例如异步、访问权限检查、字节格式化、复制目录...)
在这里检查它 - GitHubNuget

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接