我会使用进度条,但 System.IO.Compression 中用于 zipfile 的“新”(.net 4.5) 库替换了 Ionic.Zip.ZipFile 没有报告进度的方法?有没有解决的办法?我应该使用Thread 吗?还是DoWork?
这里确实有两个问题:
-
ZipFile 类的 .NET 版本不包括进度报告。
-
CreateFromDirectory() 方法会阻塞,直到创建整个存档。
我对 Ionic/DotNetZip 库不太熟悉,但浏览文档时,我没有看到任何用于从目录创建存档的异步方法。所以#2无论如何都是一个问题。解决它的最简单方法是在后台线程中运行工作,例如使用Task.Run()。
至于 #1 问题,我不会将 .NET ZipFile 类描述为已替换 Ionic 库。是的,它是新的。但是 .NET 在以前的版本中已经支持 .zip 存档。只是不像ZipFile 这样的便利课。早期对 .zip 档案的支持和 ZipFile 都没有提供“开箱即用”的进度报告。因此,两者都没有真正替换 Ionic DLL 本身。
恕我直言,在我看来,如果您使用的是 Ionic DLL 并且它对您有效,那么最好的解决方案就是继续使用它。
如果你真的不想使用它,你的选择是有限的。 .NET ZipFile 只是没有做你想做的事。您可以做一些骇人听闻的事情来解决缺少功能的问题。对于编写存档,您可以估计压缩大小,然后在写入文件时监控文件大小并基于此计算估计进度(即,每秒左右在单独的异步任务中轮询文件大小)。对于提取档案,您可以监控正在生成的文件,并以此方式计算进度。
但归根结底,这种方法远非理想。
另一个选项是使用基于ZipArchive 的旧功能来监控进度,自己显式编写存档并在从源文件中读取字节时跟踪它们。为此,您可以编写一个 Stream 实现来包装实际输入流,并在读取字节时提供进度报告。
这是Stream 可能看起来的一个简单示例(请注意关于此的评论是出于说明目的……最好委派 所有 虚拟方法,而不仅仅是你的两个'必须):
注意:在寻找与此相关的现有问题的过程中,我发现一个本质上是重复的,除了it's asking for a VB.NET answer instead of 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<int> _readProgress;
private readonly IProgress<int> _writeProgress;
public StreamWithProgress(Stream stream, IProgress<int> readProgress, IProgress<int> 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<double> 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<int>(i =>
{
currentBytes += i;
progress.Report(currentBytes / totalBytes);
}), null);
progressStream.CopyTo(outputStream);
}
}
}
}
public static void ExtractToDirectory(string sourceArchiveFileName, string destinationDirectoryName, IProgress<double> 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<int>(i =>
{
currentBytes += i;
progress.Report(currentBytes / totalBytes);
}));
inputStream.CopyTo(progressStream);
}
File.SetLastWriteTime(fileName, entry.LastWriteTime.LocalDateTime);
}
}
}
}
注意事项:
- 这使用了一个名为
BasicProgress<T> 的类(见下文)。我在控制台程序中测试了代码,内置的Progress<T> 类将使用线程池来执行ProgressChanged 事件处理程序,这反过来又会导致无序的进度报告。 BasicProgress<T> 只是直接调用处理程序,避免了这个问题。在使用Progress<T> 的GUI 程序中,事件处理程序的执行将按顺序分派给UI 线程。恕我直言,仍然应该在库中使用同步 BasicProgress<T>,但 UI 程序的客户端代码可以使用 Progress<T>(实际上,这可能更可取,因为它代表您处理跨线程调度那里)。
- 这会在进行任何工作之前计算文件长度的总和。当然,这会产生少量的启动成本。在某些情况下,只报告已处理的总字节数可能就足够了,让客户端代码担心是否需要进行初始计数。
BasicProgress.cs
class BasicProgress<T> : IProgress<T>
{
private readonly Action<T> _handler;
public BasicProgress(Action<T> handler)
{
_handler = handler;
}
void IProgress<T>.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<double>(p => Console.WriteLine($"{p:P2} archiving complete")));
ZipFileWithProgress.ExtractToDirectory(archive, unpackDirectoryName,
new BasicProgress<double>(p => Console.WriteLine($"{p:P0} extracting complete")));
}
}