带进度条的文件复制
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 上配置的应用程序池标识),如果 该应用程序不是模拟的。 如果应用程序是 通过
<identity impersonate="true"/>
冒充,身份将是 匿名用户(通常是 IUSR_MACHINENAME)或经过身份验证的用户 请求用户。
【讨论】:
我完全同意,这是最简单的解决方案。 这非常好(而且很快),但我唯一担心的是无法捕获正确的 IO 异常。DownloadFileAsync
throws ArgumentNullException
、WebException
和 InvalidOperationException
在文件处理出错时都没有多大用处。不过,如果您只需要一种快速复制文件的方法,并且您知道一切都会好起来的,那么这是一个很好的方法。
@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】:您可以从每个文件中复制部分文件流,并在您更新的每个“块”之后进行更新。 因此它将更加连续 - 您还可以轻松计算您正在复制的当前“块”相对于总流大小的相对大小,以显示完成的正确百分比。
【讨论】:
以上是关于带进度条的文件复制的主要内容,如果未能解决你的问题,请参考以下文章