65.9K
CodeProject 正在变化。 阅读更多。
Home

自动解压缩器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (16投票s)

2011年8月29日

CPOL

4分钟阅读

viewsIcon

49901

downloadIcon

2157

自动解除封锁并解压监控文件夹中的 Zip 文件。

引言

您是否厌倦了仅仅为了打开一个刚下载的压缩文件就需要做很多事情?您需要(Vista+)

  1. 右键单击
  2. 点击属性
  3. 点击解除封锁
  4. 点击确定 
  5. 右键单击
  6. 全部提取 
  7. 提取
  8. 删除 .zip 文件

这和 Windows 中许多其他事情一样,点击次数太多,步骤也太多!然后,在 XP 机器上,如果 Zip 文件里面有大量文件而不是在子文件夹中,那么您会在 Zip 文件所在的目录中得到一百个文件被解压出来!

AutoExtractor 会不引人注意地驻留在您的系统托盘中,并监控您选择的一个目录。当有新的 Zip 文件可用时,它会自动弹出,您只需选择要提取的文件并按 Enter 键,它就会解除 Zip 文件的封锁,将其解压到与 Zip 文件同名的目录中,删除 Zip 文件,打开解压后的文件夹,然后再次隐藏自己回到系统托盘。

如果您不希望每次向文件夹添加 Zip 文件时它都弹出来,可以在设置中关闭该选项,然后使用一个按键命令 Ctrl+K(或您选择的任何键)来弹出窗口。它使用 Windows API 来确保窗口始终显示在其他窗口的顶部,因此您应该能够按下 Ctrl+K+Enter 键即可自动解压!

AutoExtractor/screenshot.jpg

注意:您必须安装 Microsoft .NET 4.0 框架才能使键盘快捷键正常工作。如果删除键盘钩子,您可能可以将其重新定位到 2.0。该解决方案是 VS 2010。

代码

该应用程序使用 FileSystemWatcher 来监控选定的目录。NotifyFilter 设置得恰到好处,因此当 Zip 文件被下载、复制或重命名时,它会收到通知,并且它也适用于 IE 和 Firefox 以及网络复制。Firefox 和 IE 处理文件下载的方式不同,因此我需要处理 Changed 事件而不是 Created 事件,并且在 Changed 事件中,检查文件是否存在。出于某种原因,对于 IE,会触发 Created 事件,然后是 Deleted 事件,然后又是 Changed 事件,所以它一定是重命名文件的方式,看起来像被删除了。无论如何,Changed 事件结合文件存在性检查似乎可以捕捉到所有情况。

void fileWatcher_Changed(object sender, FileSystemEventArgs e)
{
    Trace.WriteLine(string.Format("File '{0}' changed. Change type: {1}.", 
                    e.FullPath, e.ChangeType));
    FileInfo file = new FileInfo(e.FullPath);
    if (file.Exists)
        OnFileCreated(file);
}

一旦检测到新文件,它就会被添加到窗体上的 ListView 中,并且窗口会弹出(如果启用了该设置)。当用户选择要提取的文件并单击“提取”(或按 Enter)时,选定的文件将同时在各自的线程上提取。AutoExtractor 维护一个 List of ManualResetEvents,每个线程一个,并在另一个“监控”线程中,调用 WaitHandle.WaitAll,以便知道何时所有提取线程都已完成。

private void ExtractFiles(FileInfo[] files)
{
    progressBar.Maximum = 0;
    ShowExtracting();
    _isExtracting = true;
    Trace.WriteLine(string.Format("Extracting {0} files...", files.Length));

    // update extract progress twice per second
    var progressUpdateTimer = new System.Timers.Timer(500);
    _filesExtracted = 0;
    progressUpdateTimer.Elapsed += (object sender, ElapsedEventArgs e) => 
      Dispatcher.BeginInvoke(new Action(() => progressBar.Value = _filesExtracted));
    progressUpdateTimer.Start();

    for (int i = 0; i < files.Length; i++)
    {
        FileInfo file = files[i];
        try
        {
            var resetEvent = new ManualResetEvent(false);
            threadSync.Add(resetEvent);
            var extractThread = new System.Threading.Thread(
                new ThreadStart(() => ExtractFile(file, resetEvent)));
            extractThread.SetApartmentState(System.Threading.ApartmentState.STA);
            extractThread.IsBackground = true;
            extractThread.Start();
        }
        catch (Exception ex)
        {
            if (_isExtracting)
                _cancelExtract = true;
            progressUpdateTimer.Stop();
            threadSync.Clear();
            HideExtracting();
            MessageBox.Show("Error extracting file:" + 
                       Environment.NewLine + ex.Message);
        }
    }

    Thread allFinishedThread = new Thread(new ThreadStart(() =>
    {
        Trace.WriteLine(string.Format("{0} thread(s) started. " + 
              "Waiting for all files to be extracted...", threadSync.Count));
        WaitHandle.WaitAll(threadSync.ToArray());
        Trace.WriteLine("All threads finished.");
        progressUpdateTimer.Stop();
        threadSync.Clear();
        _isExtracting = false;
        _cancelExtract = false;
        Dispatcher.Invoke(new Action(() => { HideExtracting(); MinimizeToTray(); }));
    }));
    allFinishedThread.SetApartmentState(ApartmentState.MTA);
    allFinishedThread.IsBackground = true;
    allFinishedThread.Start();
}

ExtractFile 方法是在每个线程中为每个文件调用的函数,并负责所有繁重的工作。它使用 DotNetZip 来提取文件,并且在 zip 文件中的每个条目被提取后,它会更新主窗口上的计数器。在任何提取开始之前,会启动一个计时器,该计时器每 500 毫秒触发一次,并使用计数器的当前值更新 ProgressBar。我这样做是因为我发现了一个关于 Dispatcher.BeginInvokeInvoke 的 Bug。如果我为每个 zip 条目调用 Dispatcher.BeginInvoke 来更新 ProgressBar,它就会导致 Windows 蓝屏!我猜 Dispatcher.BeginInvoke 无法处理如此快速的请求。无论如何,Zip 文件中的每个条目都被提取,然后我调用 ManualResetEventSet() 方法来告诉它线程已完成。

private void ExtractFile(FileInfo file, ManualResetEvent threadFinishedEvent)
{
    // This method should be run on a separate thread.
    Trace.WriteLine(string.Format("Thread ID {0} has started to extract '{1}'.", 
                    Thread.CurrentThread.ManagedThreadId, file.FullName));

    // Create the output directory
    string extractDirectoryPath = file.FullName.Remove(
      file.FullName.LastIndexOf(".zip", 
      StringComparison.InvariantCultureIgnoreCase), 4);
    DirectoryInfo extractDirectory = null;
    try
    {
        extractDirectory = new DirectoryInfo(extractDirectoryPath);
        if (false == extractDirectory.Exists)
        {
            Trace.WriteLine(string.Format(
              "Creating directory: {0}", extractDirectory.FullName));
            extractDirectory.Create();
        }
    }
    catch (Exception ex)
    {
        if (_isExtracting)
            _cancelExtract = true; // signals to the other threads to stop!

        MessageBox.Show("Error creating directory: " + 
          extractDirectoryPath + Environment.NewLine + ex.Message);
        return;
    }

    bool retry = true;
    while (retry)
    {
        try
        {
            using (var zippedFile = Ionic.Zip.ZipFile.Read(file.FullName))
            {
                if (zippedFile.Entries.Count > 0)
                {
                    Dispatcher.Invoke(new Action(() => 
                          progressBar.Maximum += zippedFile.Entries.Count));
                    Trace.WriteLine(string.Format(
                      "Thread {0} extracting file '{1}' to directory '{2}'...", 
                      Thread.CurrentThread.ManagedThreadId, file.FullName, 
                      extractDirectory.FullName));
                    foreach (var entry in zippedFile.Entries)
                    {
                        if (_cancelExtract)
                        {
                            Trace.WriteLine(string.Format(
                              "Thread {0} detected a cancel request and will " + 
                              "stop extracting {1}.", 
                              Thread.CurrentThread.ManagedThreadId, file.FullName));
                            threadFinishedEvent.Set();
                            return;
                        }

                        Trace.WriteLine(string.Format("    Thread {0} " + 
                          "extracting from {1,-50} to {2}...", 
                          Thread.CurrentThread.ManagedThreadId, 
                          file.Name, entry.FileName));
                        entry.Extract(extractDirectory.FullName, 
                          Ionic.Zip.ExtractExistingFileAction.OverwriteSilently);

                        _filesExtracted++;
                    }

                    Trace.WriteLine(string.Format("Thread {0} finished " + 
                      "extracting '{1}'.", 
                      Thread.CurrentThread.ManagedThreadId, file.FullName));
                    System.Diagnostics.Process.Start(extractDirectory.FullName);
                    threadFinishedEvent.Set();
                }
            }

            if (Properties.Settings.Default.DeleteAfterExtraction)
                file.Delete();

            retry = false;
        }
        catch (IOException ioEx)
        {
            var userResponse = MessageBox.Show(string.Format(
                "Error extracting file '{0}'.{1}Would you like to retry?", 
                file.FullName, Environment.NewLine + ioEx.Message + 
                Environment.NewLine + Environment.NewLine),
                "File access error.", MessageBoxButton.YesNo);
            if (userResponse == MessageBoxResult.Yes)
            {
                retry = true;
            }
            else
            {
                retry = false;

                if (_isExtracting)
                    _cancelExtract = true;
                    // signals to the other threads to stop!

                threadFinishedEvent.Set();
                return;
            }
        }
        catch (Exception ex)
        {
            if (_isExtracting)
                _cancelExtract = true; // signals to the other threads to stop!

            MessageBox.Show(string.Format("Error extracting file '{0}'{1}", 
              file.FullName, Environment.NewLine + ex.ToString()));
            threadFinishedEvent.Set();
            return;
        }
    }
}

程序中最后有趣的部分是 KeyboardHook 类。它是从这里获取的:http://learnwpf.com/post/2011/08/03/Adding-a-system-wide-keyboard-hook-to-your-WPF-Application.aspx

我为这个程序稍微修改了一下,但它与作者发布的内容非常接近。显然,.NET 4.0 只能钩入键盘修饰键。作者还展示了如何定义自己的程序入口点,而不是 WPF 默认使用的抽象入口点。

关注点

正如我所提到的,追踪我出现蓝屏的原因是一场艰苦的战斗,我尝试了 DotNetZip 和 J# 的 zip 解压方法,结果都一样。最后,我发现如果关闭更新我的 ProgressBar,就不会出现蓝屏!Dispatcher.BeginInvoke(或 Invoke)是导致问题的原因。我认为这是 MS 的一个 Bug!

KeyboardHook 类非常酷,可以在各种需要程序在后台运行但需要响应键盘命令的情况下使用。

历史

  • 2011/08/26 - 初始发布。
  • 2011/09/09 - 新文件夹名称中的 Bug 修复,感谢 akemper!其他 minor 错误修复。
  • 2011/09/19 - 清理了解决方案,删除了旧文件和代码,进行了一些重构。修复了 FileSystemWatcher 会触发多个 Changed 事件导致 OnFileCreated 函数混乱的 Bug。
  • 2013/10/10 - 已经使用此程序多年了,仍然很喜欢!我上传了包含第三方 zip DLL 的新源代码。还添加了一个 Bin 可下载文件,以便您可以下载并尝试,无需先编译。
© . All rights reserved.