进度条不适用于zipfile? 当程序似乎挂起时如何提供反馈

我对C#和编码很新,所以其中一些可能是错误的方式。 我编写的程序按预期工作并压缩文件,但如果源代码很大,程序会出现(对Windows)挂起。 我觉得我应该使用Thread但我不确定这会有所帮助。

我会使用进度条但来自System.IO.Compression Ionic.Zip.ZipFile的’new’(。net 4.5)库取代了Ionic.Zip.ZipFile没有报告进度的方法? 有没有解决的办法? 我应该使用Thread吗? 还是DoWork

麻烦的是用户和系统没有得到关于程序正在做什么的反馈。

我不确定我是以正确的方式提出这个问题。 下面是正在运行的代码,但同样会出现挂起系统的问题。

  private void beginBackup_Click(object sender, EventArgs e) { try { long timeTicks = DateTime.Now.Ticks; string zipName = "bak" + timeTicks + ".zip"; MessageBox.Show("This Will take a bit, there is no status bar :("); ZipFile.CreateFromDirectory(Properties.Settings.Default.source, Properties.Settings.Default.destination + "\\" + zipName); MessageBox.Show("Done!"); this.Close(); } catch (IOException err) { MessageBox.Show("Something went wrong" + System.Environment.NewLine + "IOException source: {0}", err.Source); } } 

重要的是:

  `ZipFile.CreateFromDirectory(Properties.Settings.Default.source, Properties.Settings.Default.destination + "\\" + zipName);` 

编辑

ZipFile.CreateFromDirectory()没有走在目录中,所以什么都没有增加? 它只是开始和结束而没有报告。 除非我弄错了?

在这里使用此方法:

  while (!completed) { // your code here to do something for (int i = 1; i  WriteToProgressFile(i)); t.Start(); await t; if (progress != null) { progress.Report(percentCompletedSoFar); } completed = i == 100; } } 

for循环中的代码只运行一次,因为Zipfile woudl仍然挂起程序,然后进度条会立即从0变为100?

我会使用进度条但来自System.IO.Compression Ionic.Zip.ZipFile的’new’(。net 4.5)库取代了Ionic.Zip.ZipFile没有报告进度的方法? 有没有解决的办法? 我应该使用Thread吗? 还是DoWork

你真的有两个问题:

  1. ZipFile类的.NET版本不包括进度报告。
  2. CreateFromDirectory()方法将阻塞,直到创建整个存档。

我不熟悉Ionic / DotNetZip库,但浏览文档时,我没有看到任何用于从目录创建存档的异步方法。 所以#2无论如何都是个问题。 解决它的最简单方法是在后台线程中运行工作,例如使用Task.Run()

至于#1问题,我不会将.NET ZipFile类描述为替换了Ionic库。 是的,这是新的。 但.NET在以前的版本中已经拥有.zip归档支持。 只是不像ZipFile这样的便利类。 早期对.zip档案的支持和ZipFile没有提供“开箱即用”的进度报告。 所以既不能真正取代 Ionic DLL本身。

所以恕我直言,在我看来,如果你使用Ionic DLL并且它适合你,最好的解决方案就是继续使用它。

如果你真的不想使用它,你的选择是有限的。 .NET ZipFile只是没有你想要的。 你可以做一些hacky事情,解决缺乏function的问题。 对于编写存档,您可以估计压缩大小,然后在编写文件时监视文件大小,并根据该大小计算估计进度(即,在单独的异步任务中每隔一秒左右轮询一次文件大小)。 要提取存档,您可以监视正在生成的文件,并以这种方式计算进度。

但最终,这种方法远非理想。

另一种选择是通过使用旧的基于ZipArchive的function来监视进度,明确地自己编写存档并跟踪从源文件中读取的字节。 为此,您可以编写一个包含实际输入流的Stream实现,并在读取字节时提供进度报告。

这是一个关于Stream可能是什么样子的简单示例(请注意有关此内容的注释以用于说明目的……最好委托所有虚拟方法,而不仅仅是您需要的两个):

注意:在寻找与此相关的现有问题的过程中,我发现一个基本上是重复的,除了它要求VB.NET答案而不是C# 。 除了创建存档之外,它还要求从存档中提取进度更新。 所以我在这里调整了我的答案,对于VB.NET,添加了提取方法,并稍微调整了实现。 我已经更新了下面的答案,以纳入这些更改。

StreamWithProgress.cs

 class StreamWithProgress : Stream { // NOTE: for illustration purposes. For production code, one would want to // override *all* of the virtual methods, delegating to the base _stream object, // to ensure performance optimizations in the base _stream object aren't // bypassed. private readonly Stream _stream; private readonly IProgress _readProgress; private readonly IProgress _writeProgress; public StreamWithProgress(Stream stream, IProgress readProgress, IProgress writeProgress) { _stream = stream; _readProgress = readProgress; _writeProgress = writeProgress; } public override bool CanRead { get { return _stream.CanRead; } } public override bool CanSeek { get { return _stream.CanSeek; } } public override bool CanWrite { get { return _stream.CanWrite; } } public override long Length { get { return _stream.Length; } } public override long Position { get { return _stream.Position; } set { _stream.Position = value; } } public override void Flush() { _stream.Flush(); } public override long Seek(long offset, SeekOrigin origin) { return _stream.Seek(offset, origin); } public override void SetLength(long value) { _stream.SetLength(value); } public override int Read(byte[] buffer, int offset, int count) { int bytesRead = _stream.Read(buffer, offset, count); _readProgress?.Report(bytesRead); return bytesRead; } public override void Write(byte[] buffer, int offset, int count) { _stream.Write(buffer, offset, count); _writeProgress?.Report(count); } } 

有了这些,显式处理存档创建相对简单,使用该Stream来监视进度:

ZipFileWithProgress.cs

 static class ZipFileWithProgress { public static void CreateFromDirectory(string sourceDirectoryName, string destinationArchiveFileName, IProgress progress) { sourceDirectoryName = Path.GetFullPath(sourceDirectoryName); FileInfo[] sourceFiles = new DirectoryInfo(sourceDirectoryName).GetFiles("*", SearchOption.AllDirectories); double totalBytes = sourceFiles.Sum(f => f.Length); long currentBytes = 0; using (ZipArchive archive = ZipFile.Open(destinationArchiveFileName, ZipArchiveMode.Create)) { foreach (FileInfo file in sourceFiles) { // NOTE: naive method to get sub-path from file name, relative to // input directory. Production code should be more robust than this. // Either use Path class or similar to parse directory separators and // reconstruct output file name, or change this entire method to be // recursive so that it can follow the sub-directories and include them // in the entry name as they are processed. string entryName = file.FullName.Substring(sourceDirectoryName.Length + 1); ZipArchiveEntry entry = archive.CreateEntry(entryName); entry.LastWriteTime = file.LastWriteTime; using (Stream inputStream = File.OpenRead(file.FullName)) using (Stream outputStream = entry.Open()) { Stream progressStream = new StreamWithProgress(inputStream, new BasicProgress(i => { currentBytes += i; progress.Report(currentBytes / totalBytes); }), null); progressStream.CopyTo(outputStream); } } } } public static void ExtractToDirectory(string sourceArchiveFileName, string destinationDirectoryName, IProgress progress) { using (ZipArchive archive = ZipFile.OpenRead(sourceArchiveFileName)) { double totalBytes = archive.Entries.Sum(e => e.Length); long currentBytes = 0; foreach (ZipArchiveEntry entry in archive.Entries) { string fileName = Path.Combine(destinationDirectoryName, entry.FullName); Directory.CreateDirectory(Path.GetDirectoryName(fileName)); using (Stream inputStream = entry.Open()) using(Stream outputStream = File.OpenWrite(fileName)) { Stream progressStream = new StreamWithProgress(outputStream, null, new BasicProgress(i => { currentBytes += i; progress.Report(currentBytes / totalBytes); })); inputStream.CopyTo(progressStream); } File.SetLastWriteTime(fileName, entry.LastWriteTime.LocalDateTime); } } } } 

笔记:

  • 这使用一个名为BasicProgress的类(见下文)。 我在控制台程序中测试了代码,内置的Progress类将使用线程池来执行ProgressChanged事件处理程序,这反过来又会导致无序的进度报告。 BasicProgress只是直接调用处理程序,避免了这个问题。 在使用Progress的GUI程序中,事件处理程序的执行将按顺序分派到UI线程。 恕我直言,人们仍然应该在库中使用同步BasicProgress ,但是使用Progress的UI程序的客户端代码会很好(事实上,这可能是更好的,因为它处理跨线程调度你的代表)。
  • 这可以在完成任何工作之前计算文件长度的总和。 当然,这会产生轻微的启动成本。 对于某些情况,仅报告处理的总字节数可能就足够了,让客户端代码担心是否需要执行该初始计数。

BasicProgress.cs

 class BasicProgress : IProgress { private readonly Action _handler; public BasicProgress(Action handler) { _handler = handler; } void IProgress.Report(T value) { _handler(value); } } 

当然,还有一个小程序来测试它:

Program.cs中

 class Program { static void Main(string[] args) { string sourceDirectory = args[0], archive = args[1], archiveDirectory = Path.GetDirectoryName(Path.GetFullPath(archive)), unpackDirectoryName = Guid.NewGuid().ToString(); File.Delete(archive); ZipFileWithProgress.CreateFromDirectory(sourceDirectory, archive, new BasicProgress(p => Console.WriteLine($"{p:P2} archiving complete"))); ZipFileWithProgress.ExtractToDirectory(archive, unpackDirectoryName, new BasicProgress(p => Console.WriteLine($"{p:P0} extracting complete"))); } } 

使用ZipFile.CreateFromDirectory,您可以根据输出文件的大小创建一种进度。 在另一个线程(Task.Run)中使用压缩,您应该能够更新UI并使其响应。

 void CompressFolder(string folder, string targetFilename) { bool zipping = true; long size = Directory.GetFiles(folder).Select(o => new FileInfo(o).Length).Aggregate((a, b) => a + b); Task.Run(() => { ZipFile.CreateFromDirectory(folder, targetFilename, CompressionLevel.NoCompression, false); zipping = false; }); while (zipping) { if (File.Exists(targetFilename)) { var fi = new FileInfo(targetFilename); System.Diagnostics.Debug.WriteLine($"Zip progress: {fi.Length}/{size}"); } } } 

如果你有CompressionLevel到NoCompression,这将100%工作。 使用压缩它可以部分地基于zip文件的结束大小。 但它会显示某些事情正在发生,所以当文件完成后,从任何百分比的进度跳转到100%。