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

Posted

技术标签:

【中文标题】进度条不适用于 zipfile?当程序似乎挂起时如何提供反馈【英文标题】:Progress Bar not available for zipfile? How to give feedback when program seems to hang 【发布时间】:2017-02-24 04:03:15 【问题描述】:

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

我会使用进度条,但 System.IO.Compression 的 zipfile 的“新”(.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 <= 100; i++)
        
            percentCompletedSoFar = i;
            var t = new Task(() => WriteToProgressFile(i));
            t.Start();
            await t;
            if (progress != null)
            
                progress.Report(percentCompletedSoFar);
            
            completed = i == 100;
        
    

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

【问题讨论】:

我有一个完整的例子here你可以关注。 How can i use two progressBar controls to display each file download progress and also overall progress of all the files download?的可能重复 那里的代码可以用来报告ZipFile.CreateFromDirectory 的状态,因为它不是“遍历”目录吗?所以没有什么可以迭代的? 是的,它可以用于任何类型的进步。这是我那里的一个模式。在您的情况下,您可能想要使用圆形条,因为您无法真正知道需要多长时间。或者,您可以根据平均时间显示进度条,并每 x 秒更改一次进度。或者你可以根据字节数来做 【参考方案1】:

我会使用进度条,但 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&lt;T&gt; 的类(见下文)。我在控制台程序中测试了代码,内置的Progress&lt;T&gt; 类将使用线程池来执行ProgressChanged 事件处理程序,这反过来又会导致无序的进度报告。 BasicProgress&lt;T&gt; 只是直接调用处理程序,避免了这个问题。在使用Progress&lt;T&gt; 的GUI 程序中,事件处理程序的执行将按顺序分派给UI 线程。恕我直言,仍然应该在库中使用同步 BasicProgress&lt;T&gt;,但 UI 程序的客户端代码可以使用 Progress&lt;T&gt;(实际上,这可能更可取,因为它代表您处理跨线程调度那里)。 这会在进行任何工作之前计算文件长度的总和。当然,这会产生少量的启动成本。在某些情况下,只报告已处理的总字节数可能就足够了,让客户端代码担心是否需要进行初始计数。

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")));
    

【讨论】:

这是我唯一的担心。我很好(我认为)使用 Ionic zipfile,虽然我不知道为什么,但确实需要一些哄骗才能识别该库。但这在某些时候是行不通的。我记得读过一些关于使用自定义库的注意事项。尽管您在上面指出和写的内容肯定对您有很大帮助,但谢谢。 " 识别库确实需要一些哄骗" -- 抱歉,不确定您所说的“哄骗”是什么意思。由于缺乏真正的官方规范,您可能会发现 .zip 档案“在野外”只能由实现的子集读取,有时只有编写它们的实现才能读取。但即使是 .NET 实现,这也是一个问题。有一段时间,.NET 实现无法处理大于 8GB 的​​存档数据,它仍然无法处理条目名称包含在 Windows 文件系统上无效的字符的存档。 自定义库会很有帮助。我避免使用它们的主要原因是它们通常比 .NET 的分布范围更广,因此在实际测试中也没有那么广泛,当然另一个原因是,如果我已经必须使用框架具有我需要的功能,添加另一个依赖项是不方便且不太理想的。但是在必要时使用第三方库本身并没有错。 Coaxing 在这里可能不是最适用的术语,我很抱歉,因为我仍在学习很多白话来描述问题/问题。例如,当我使用 Ionic 时,Visual Studio 会抱怨说它们不在引用中,或者我需要 .dll 的子类,这很容易,但在这种情况下这样做时,它仍然无法识别子类 ZipFile,即使据我所知它被正确引用,也就是说,我不确定是 Visual Studio 很奇怪,我缺乏知识,自定义类,还是以上所有 缺少引用始终是以下三件事之一:DLL 尚未作为引用添加,您正在尝试使用“不合格”(即只有类型名称而不是其命名空间)类型在 .cs 文件的开头没有必要的 using 指令的名称,或者你已经完成了所有这些但有错误的 DLL 版本并且类型不在那里。最后一个几乎从来不是问题。所以仔细检查前两个。 :)【参考方案2】:

我认为以下内容值得分享,通过压缩文件而不是文件夹,同时保留文件的相对路径:

    void CompressFolder(string folder, string targetFilename)
    
        string[] allFilesToZip = Directory.GetFiles(folder, "*.*", System.IO.SearchOption.AllDirectories);

        // You can use the size as the progress total size
        int size = allFilesToZip.Length;

        // You can use the progress to notify the current progress.
        int progress = 0;

        // To have relative paths in the zip.
        string pathToRemove = folder + "\\";

        using (ZipArchive zip = ZipFile.Open(targetFilename, ZipArchiveMode.Create))
        
            // Go over all files and zip them.
            foreach (var file in allFilesToZip)
            
                String fileRelativePath = file.Replace(pathToRemove, "");

                // It is not mentioned in MS documentation, but the name can be
                // a relative path with the file name, this will create a zip 
                // with folders and not only with files.
                zip.CreateEntryFromFile(file, fileRelativePath);
                progress++;

                // ---------------------------
                // TBD: Notify about progress.
                // ---------------------------
            
        
    

注意事项:

您可以使用FileInfo fileInfo = new FileInfo(file);fileInfo.Length 来推进,使用文件的权重,而不是文件的数量。有时这更现实。为此,您还需要提前累积文件夹总重量。 这个解决方案对我有用。 我没有注意到压缩整个目录和压缩目录中的每个文件之间的任何性能下降 - 不过我没有对此进行测试。

【讨论】:

【参考方案3】:

使用 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%。

【讨论】:

我将不得不尝试实现这一点。我确实尝试了另一种方法,但它正在读取单个文件的大小,这使得栏来回跳动 对于大小的计算,您可以使用 .Sum( x => x ) 代替 .Aggregate 这个建议是杂牌。它实际上不会那么糟糕,除了while (zipping) 循环中没有延迟。因此,一个线程将全速运行以监控进度。这种方法已经存在很大的限制(例如,要使其工作,您必须禁用存档中的压缩!),但是与存档过程竞争 CPU 时间真的很糟糕。至少,这里应该有一个await Task.Delay(),延迟很长(至少500-1000毫秒)。

以上是关于进度条不适用于 zipfile?当程序似乎挂起时如何提供反馈的主要内容,如果未能解决你的问题,请参考以下文章

应用程序在挂起时静默终止。 (用户在我的应用程序上按下锁定按钮)

Flutter 上传带有进度条的大文件。 App 和 Web 都支持

原生 Android 应用程序在挂起时是不是应该释放 OpenGL 资源?

javascript 文件挂起时 SignalR 连接块

水平进度条不适用于 Asynctask Android 下载文件?

为什么在写锁定挂起时可以获取读锁?