带进度条的文件复制

Posted

技术标签:

【中文标题】带进度条的文件复制【英文标题】:File Copy with Progress Bar 【发布时间】:2011-08-28 00:40:16 【问题描述】:

我使用了这个代码:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows.Forms;
using System.IO;

namespace WindowsApplication1 
  public partial class Form1 : Form 
    // Class to report progress
    private class UIProgress 
      public UIProgress(string name_, long bytes_, long maxbytes_) 
        name = name_; bytes = bytes_; maxbytes = maxbytes_;
      
      public string name;
      public long bytes;
      public long maxbytes;
    
    // Class to report exception 
    private class UIError 
      public UIError(Exception ex, string path_) 
        msg = ex.Message; path = path_; result = DialogResult.Cancel;
      
      public string msg;
      public string path;
      public DialogResult result;
    
    private BackgroundWorker mCopier;
    private delegate void ProgressChanged(UIProgress info);
    private delegate void CopyError(UIError err);
    private ProgressChanged OnChange;
    private CopyError OnError;

    public Form1() 
      InitializeComponent();
      mCopier = new BackgroundWorker();
      mCopier.DoWork += Copier_DoWork;
      mCopier.RunWorkerCompleted += Copier_RunWorkerCompleted;
      mCopier.WorkerSupportsCancellation = true;
      OnChange += Copier_ProgressChanged;
      OnError += Copier_Error;
      button1.Click += button1_Click;
      ChangeUI(false);
    

    private void Copier_DoWork(object sender, DoWorkEventArgs e) 
      // Create list of files to copy
      string[] theExtensions =  "*.jpg", "*.jpeg", "*.bmp", "*.png", "*.gif" ;
      List<FileInfo> files = new List<FileInfo>();
      string path = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
      DirectoryInfo dir = new DirectoryInfo(path);
      long maxbytes = 0;
      foreach (string ext in theExtensions) 
        FileInfo[] folder = dir.GetFiles(ext, SearchOption.AllDirectories);
        foreach (FileInfo file in folder) 
          if ((file.Attributes & FileAttributes.Directory) != 0) continue;
          files.Add(file);
          maxbytes += file.Length;
        
      
      // Copy files
      long bytes = 0;
      foreach (FileInfo file in files) 
        try 
          this.BeginInvoke(OnChange, new object[]  new UIProgress(file.Name, bytes, maxbytes) );
          File.Copy(file.FullName, @"c:\temp\" + file.Name, true);
        
        catch (Exception ex) 
          UIError err = new UIError(ex, file.FullName); 
          this.Invoke(OnError, new object[]  err );
          if (err.result == DialogResult.Cancel) break;
        
        bytes += file.Length;
      
    
    private void Copier_ProgressChanged(UIProgress info) 
      // Update progress
      progressBar1.Value = (int)(100.0 * info.bytes / info.maxbytes);
      label1.Text = "Copying " + info.name;
    
    private void Copier_Error(UIError err) 
      // Error handler
      string msg = string.Format("Error copying file 0\n1\nClick OK to continue copying files", err.path, err.msg);
      err.result = MessageBox.Show(msg, "Copy error", MessageBoxButtons.OKCancel, MessageBoxIcon.Exclamation);
    
    private void Copier_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) 
      // Operation completed, update UI
      ChangeUI(false);
    
    private void ChangeUI(bool docopy) 
      label1.Visible = docopy;
      progressBar1.Visible = docopy;
      button1.Text = docopy ? "Cancel" : "Copy";
      label1.Text = "Starting copy...";
      progressBar1.Value = 0;
    
    private void button1_Click(object sender, EventArgs e) 
      bool docopy = button1.Text == "Copy";
      ChangeUI(docopy);
      if (docopy) mCopier.RunWorkerAsync();
      else mCopier.CancelAsync();
    
  

发布here(nobugz 发布的那个)在复制文件和显示进度条中的状态。

我想在复制时不断增加进度条的值,尤其是大文件。此示例代码中发生的情况是,进度条的值在复制的每个文件上都会停止,并且在复制一个文件后,它将增加到要复制的下一个文件的大小。我希望它像 Windows 中的 CopyFileEx 一样工作,在复制时进度条会不断增加(我不能使用 CopyFileEx,因为我想拥有自己的实现)。

【问题讨论】:

你的问题对我来说不是很清楚。在您提供的代码中使用了 File.Copy 函数。它是CopyFile WinAPI 函数的托管包装器。您想拒绝文件复制过程中的任何 WinAPI 函数吗? 确实,为什么要创建自己的实现? CopyFileEx 会做你想做的事。 是的,你的权利,如果我可以使用现有的,为什么要创建一个。问题是,这就是应用规范中的内容。 informit.com/guides/content.aspx?g=dotnet&seqNum=827 可能有点用处。 【参考方案1】:

你需要这样的东西:

    public delegate void ProgressChangeDelegate(double Percentage, ref bool Cancel);
    public delegate void Completedelegate();

    class CustomFileCopier
    
        public CustomFileCopier(string Source, string Dest)
        
            this.SourceFilePath = Source;
            this.DestFilePath = Dest;

            OnProgressChanged += delegate  ;
            OnComplete += delegate  ;
        

        public void Copy()
        
            byte[] buffer = new byte[1024 * 1024]; // 1MB buffer
            bool cancelFlag = false;

            using (FileStream source = new FileStream(SourceFilePath, FileMode.Open, FileAccess.Read))
            
                long fileLength = source.Length;
                using (FileStream dest = new FileStream(DestFilePath, FileMode.CreateNew, FileAccess.Write))
                
                    long totalBytes = 0;
                    int currentBlockSize = 0;

                    while ((currentBlockSize = source.Read(buffer, 0, buffer.Length)) > 0)
                    
                        totalBytes += currentBlockSize;
                        double percentage = (double)totalBytes * 100.0 / fileLength;

                        dest.Write(buffer, 0, currentBlockSize);

                        cancelFlag = false;
                        OnProgressChanged(percentage, ref cancelFlag);

                        if (cancelFlag == true)
                        
                            // Delete dest file here
                            break;
                        
                    
                
            

            OnComplete();
        

        public string SourceFilePath  get; set; 
        public string DestFilePath  get; set; 

        public event ProgressChangeDelegate OnProgressChanged;
        public event Completedelegate OnComplete;
    

只需在单独的线程中运行它并订阅OnProgressChanged 事件。

【讨论】:

谢谢你!这正是我一直在寻找的......非常感谢兄弟! 是否有人将其也用于复制文件夹? 你能展示如何使用委托吗? “OnComplete += 委托 ;”看起来有点不安 这是一个空的委托存根,它什么也不做。我介绍它只是为了简化代码。否则代码OnProgressChanged(persentage, ref cancelFlag); 应该写成if ( OnProgressChanged != null) OnProgressChanged(persentage, ref cancelFlag); 你可以在这里阅读代表msdn.microsoft.com/en-us/library/900fyy8e(v=vs.71).aspx 好吧,你应该更加遵守事件模式,为进度事件创建 CancelEventArgs 并且不以任何方式使用 ref 。除此之外,感谢代码 sn-p。帮了大忙!【参考方案2】:

我喜欢这个解决方案,因为

复制引擎在框架中

public delegate void IntDelegate(int Int);

public static event IntDelegate FileCopyProgress;
public static void CopyFileWithProgress(string source, string destination)

    var webClient = new WebClient();
    webClient.DownloadProgressChanged += DownloadProgress;
    webClient.DownloadFileAsync(new Uri(source), destination);


private static void DownloadProgress(object sender, DownloadProgressChangedEventArgs e)

    if(FileCopyProgress != null)
        FileCopyProgress(e.ProgressPercentage);

UNC 路径

只要设置了权限,这应该适用于 UNC 路径。如果没有,你会得到这个错误,在这种情况下,我投票给经过身份验证的请求用户路由。

System.UnauthorizedAccessException: 获取路径 '\testws01\c$\foo' 被拒绝。

ASP.NET 无权访问请求的资源。 考虑 向 ASP.NET 请求授予对资源的访问权限 身份。 ASP.NET 有一个基本进程标识(通常 IIS 5 上的 MACHINE\ASPNET 或 IIS 6 和 IIS 7 上的网络服务,以及 IIS 7.5 上配置的应用程序池标识),如果 该应用程序不是模拟的。 如果应用程序是 通过&lt;identity impersonate="true"/&gt;冒充,身份将是 匿名用户(通常是 IUSR_MACHINENAME)或经过身份验证的用户 请求用户。

【讨论】:

我完全同意,这是最简单的解决方案。 这非常好(而且很快),但我唯一担心的是无法捕获正确的 IO 异常。 DownloadFileAsync throws ArgumentNullExceptionWebExceptionInvalidOperationException 在文件处理出错时都没有多大用处。不过,如果您只需要一种快速复制文件的方法,并且您知道一切都会好起来的,那么这是一个很好的方法。 @TEK,如果 IOException 不在抛出的异常的内部异常属性中,我会感到惊讶。如果你能给我指向这个类的单声道代码的 url,我会看看它实际上在做什么。 @toddmo 我可以做得更好,我可以将您直接链接到 Microsoft 的 repo:referencesource.microsoft.com/#System/net/System/Net/… 感谢您查看。 @TEK,好的,是的,它将所有其他异常包装在 WebException 中,但保留内部异常。所以我会模拟一个 IO 错误(锁定文件,无论如何)来验证它,但我敢打赌 IO 异常是存在的。我会在 .net fiddle 中运行实验,但它需要访问磁盘。【参考方案3】:

这是一个优化的解决方案,它利用 .NET 扩展和双缓冲区来获得更好的性能。一个新的 CopyTo 重载被添加到 FileInfo 中,并带有一个仅在更改时指示进度的 Action。

这个 WPF 中的示例实现带有一个名为 progressBar1 的进度条,它在后台执行复制操作。

private FileInfo _source = new FileInfo(@"C:\file.bin");
private FileInfo _destination = new FileInfo(@"C:\file2.bin");

private void CopyFile()

  if(_destination.Exists)
    _destination.Delete();

  Task.Run(()=>
    _source.CopyTo(_destination, x=>Dispatcher.Invoke(()=>progressBar1.Value = x));
  ).GetAwaiter().OnCompleted(() => MessageBox.Show("File Copied!"));

这是控制台应用程序的示例

class Program

  static void Main(string[] args)
  
    var _source = new FileInfo(@"C:\Temp\bigfile.rar");
    var _destination = new FileInfo(@"C:\Temp\bigfile2.rar");

    if (_destination.Exists) _destination.Delete();

    _source.CopyTo(_destination, x => Console.WriteLine($"x% Complete"));
    Console.WriteLine("File Copied.");
  

要使用,请创建一个新文件,例如 FileInfoExtensions.cs 并添加以下代码:

public static class FileInfoExtensions

  public static void CopyTo(this FileInfo file, FileInfo destination, Action<int> progressCallback)
  
    const int bufferSize = 1024 * 1024;  //1MB
    byte[] buffer = new byte[bufferSize], buffer2 = new byte[bufferSize];
    bool swap = false;
    int progress = 0, reportedProgress = 0, read = 0;
    long len = file.Length;
    float flen = len;
    Task writer = null;

    using (var source = file.OpenRead())
    using (var dest = destination.OpenWrite())
    
      dest.SetLength(source.Length);
      for (long size = 0; size < len; size += read)
      
        if ((progress = ((int)((size / flen) * 100))) != reportedProgress)
          progressCallback(reportedProgress = progress);
        read = source.Read(swap ? buffer : buffer2, 0, bufferSize);
        writer?.Wait();  // if < .NET4 // if (writer != null) writer.Wait(); 
        writer = dest.WriteAsync(swap ? buffer : buffer2, 0, read);
        swap = !swap;
      
      writer?.Wait();  //Fixed - Thanks @sam-hocevar
    
  

双缓冲区的工作原理是使用一个线程读取和一个线程写入,因此最大速度仅取决于两者中较慢的一个。使用了两个缓冲区(双缓冲区),确保读写线程不会同时使用同一个缓冲区。

示例:代码读入缓冲区 1,然后当读取完成时,写入操作开始写入缓冲区 1 的内容。无需等待完成写入,缓冲区被交换到缓冲区 2,数据被读入缓冲区 2,而缓冲区1还在写。一旦缓冲区 2 中的读取完成,它会等待缓冲区 1 上的写入完成,然后开始写入缓冲区 2,然后重复该过程。本质上,1 个线程始终在读取,一个线程始终在写入。

WriteAsync 使用overlapped I/O,它利用了I/O completion ports,它依靠硬件而不是线程来执行异步操作,因此非常高效。 TLDR:我谎称有 2 个线程,但概念是一样的。

【讨论】:

我确实使用了你的代码很长一段时间并且随机开始出现问题,当我因为这个实现而得到损坏的文件时 - 其中一位作家确实继续再次写入文件 - 它确实写了源文件的开头到目标文件的结尾,从而损坏它。不能推荐这个,对不起。 @halloweenlv 这段代码写得不是很好,但几乎可以工作。您可以通过将末尾的dest.Write(…) 替换为if (writer != null) writer.Wait(); 来修复它 @sam-hocevar 我更新了示例代码并对其进行了测试。感谢您发现该错误。我还添加了用于在控制台应用程序中测试它的代码。您能否详细说明“这段代码写得不是很好”?欢迎提出任何建议。 @Robear 除了 using 指令之外,代码非常“C-ish”(不是说 C 有什么问题,而是在 C# 中的做法通常非常不同),有一些冗余(swap ? buffer : buffer2 部分),当swap 为假时使用buffer2,而不是当它为真时,最后一次Read 调用会要求太多字节,变量名progress2 使它看起来像它与buffer2 有关,但它只是progress 的前一个值……当然这并没有什么问题,只是看起来不是很C#。 伟大的 sn-p。应该添加一个progressCallback(100);在作家之前?.Wait();否则进度只会在 99 处结束。【参考方案4】:

使用 Gal 提出的 2 个流来制作自己的文件复制逻辑是一个可行的选择,但不推荐这样做,因为有一个深度集成的 windows 操作,它在可靠性、安全性和性能方面进行了优化,名为 CopyFileEx。

也就是说,在下面的文章中:http://msdn.microsoft.com/en-us/magazine/cc163851.aspx 他们做的正是你想要的,但当然你必须使用 CopyFileEx

祝你好运

** EDIT **(修正了我的答案,非常愚蠢)

【讨论】:

CopyFileEx 在通过网络复制大文件时非常损坏。有关详细信息,请参阅blog.mischel.com/2008/10/14/copying-large-files-on-windows。此外,使用两个流和一点异步编码很容易提高CopyFileEx 的速度。 @JimMischel 不要发表如此广泛的声明。流不支持 DMA。异步不是一个神奇的解决方案。 @mafu 不要太快判断您不完全理解的内容。博客文章(不幸的是,目前不可用)很好地记录了 CopyFileEx 的损坏程度,或者在我写该评论时是如何损坏的。当时我正在使用一个自定义的 CopyFile 方法,它仅通过使用两个不同的线程就远远优于 CopyFileEx:一个用于读取,一个用于写入。不,异步不是魔术。但如果使用得当,它可以提高性能。 我应该选择更友好的语气,对此感到抱歉。我也不太了解这个话题,无法详细讨论。我的主要观点是,任何使用流 afaik 的东西都需要通过 CPU 移动字节,这应该是可以避免的。但是,当使用流时,您的想法可能非常好。【参考方案5】:

你可以使用 Dispatcher 来更新你的 ProgressBar。

UpdateProgressBarDelegate updatePbDelegate = new UpdateProgressBarDelegate(ProgressBar1.SetValue);

Dispatcher.Invoke(updatePbDelegate, System.Windows.Threading.DispatcherPriority.Background, new object[]  ProgressBar.ValueProperty, value );

【讨论】:

这不会与复制文件交互,这是问题的症结所在......【参考方案6】:

您可以从每个文件中复制部分文件流,并在您更新的每个“块”之后进行更新。 因此它将更加连续 - 您还可以轻松计算您正在复制的当前“块”相对于总流大小的相对大小,以显示完成的正确百分比。

【讨论】:

以上是关于带进度条的文件复制的主要内容,如果未能解决你的问题,请参考以下文章

sh 复制带进度的文件夹

Android更新带进度条的通知栏

Android更新带进度条的通知栏

带进度条的文件上传?

Ajax实现带进度条的文件上传

带进度条的文件上传