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

目录比较器 - 递归比较两个文件夹的工具

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.71/5 (13投票s)

2012年1月9日

CPOL

14分钟阅读

viewsIcon

97762

downloadIcon

7500

目录比较器是一个可扩展的工具,可用于比较两个文件夹。

介绍 

顾名思义,目录比较器是一种比较文件夹的工具。我以一种我认为非常有帮助的方式讨论了这个应用程序。它试图介绍一些概念,并提供一些技巧。当我开始这个项目时,它的功能有限,但随着时间的推移,我加入了更多功能。我还将我对这个项目的未来想法添加到了“兴趣点”部分。目录比较器是可扩展的,因此您始终可以选择创建自己的比较器或用于表示默认比较器结果的自定义用户界面。

1.jpg

背景

是的,市面上确实有一些目录比较器。但我只是想要一些简单直接的东西。这就是我所说的“简单”——选择文件夹1,选择文件夹2,点击按钮,我应该就完成了。所以,我提出了这个递归比较工具。我还希望它是可扩展的,因此核心功能是通过接口实现的,使其真正可扩展。

3.jpg

目录比较器背后的核心逻辑

本质上,目录比较器有一个RecursiveComparer。它实现了ITwoPassComparer接口,顾名思义,该接口处理两个阶段的比较。它们是(1)相对于左文件夹的比较和(2)相对于右文件夹的比较。我认为任何复杂的问题都可以简化为多个简单的问题。如果您考虑一下,左侧的比较将完成大部分的比较工作。第一个阶段的比较又被分解为多个简单的部分。右侧比较需要做的唯一事情就是处理右侧特有的文件。我还确保通过在左侧和右侧比较之间共享代码来避免代码重复,例如文件是如何处理的。在此案例中需要注意的一个重要点是,递归比较器所做的一切都以文件名开始,并且是区分大小写的。我将尝试简化我在这里使用的逻辑(假设选中了“递归”复选框)。

  • 从左文件夹开始。获取根目录中所有文件和文件夹的列表
  • 逐个循环遍历项目。如果它是文件,则调用方法进行处理。该方法如下:
    • 对于当前文件,查看另一侧是否存在相应的文件
    • 如果文件存在,则比较这两个文件,创建一个条目并将其添加到跟踪比较的列表中(关于比较操作的讨论如下)。
    • 如果没有匹配项,则创建一个条目以指示这一点,并将其添加到跟踪比较的列表中。
  • 如果它是文件夹,则调用一个递归方法,以获取当前文件夹内更多文件/文件夹的比较信息。
    • 对于文件夹内的文件,它们会按照上述方式处理。
    • 对于文件夹,将遵循此处描述的递归过程。
    • 如果任何文件夹是空文件夹,也会为这种情况添加一个条目。这由最左侧的文件夹图标指示(在下一部分的图像中显示,稍后将对此进行更多讨论)。
  • 一旦文件夹相对于右文件夹的比较开始,我只需要处理仅存在于右侧的文件/文件夹。
文件的比较分两个阶段进行。第一阶段是通过文件名查找匹配项。第二阶段是通过计算左右文件(md5)的哈希值。如果左侧或右侧文件不存在,则根本不计算哈希值,以节省时间。

关于用户界面的一些话


用户界面布局非常简单。顶部有一个菜单栏,其中包含各种菜单和相应的下拉项。在此旁边是一个列表视图,设置为填充整个父容器,即主窗体。除此之外,我还添加了 SaveFileDialog 以使用户能够将比较结果保存为 xml 或 csv,以及一个 ContextMenuStrip 控件,用于在选中列表视图中的任何项目时为用户提供一些选项。我认为 ListView 是向用户呈现比较信息的最佳选择。

我想讨论两个菜单项的几点。分别是“视图”和“筛选”菜单项。默认情况下,用户界面显示左侧和右侧文件夹比较的合并结果。它还提供了仅显示左侧或右侧结果的选项。筛选菜单操作的是当前显示在用户界面中的元素。因此,如果选择了“仅左侧结果”,筛选菜单将仅操作这些结果,而不是整个结果数据集。

7.jpg

扩展目录比较器

假设您不想使用我上面解释的逻辑。您始终可以按您想要的方式扩展目录比较器。您可以按照下面列出的几个简单步骤来完成此操作:

  • 实现 IResults 接口(我们称之为ImprovedComparisonResults)。
  • 实现 ITwoPassCompiler 接口(我们称之为ImprovedComparer)。
  • 实现 IDirectoryComparer 接口(我们称之为ImprovedDirectoryComparer)。

完成上述类的创建后,您可以通过修改frmMain窗体中的comparerWorker_DoWork方法,使用户界面使用它们来呈现结果。

private void comparerWorker_DoWork(object sender, DoWorkEventArgs e)
{
	ITwoPassComparer comparer = new ImprovedComparer(this);
	IDirectoryComparer improvedComparer = new ImprovedDirectoryComparer(comparer);
	IResults results = improvedComparer.CompareDirectories();

	this.ReportProgress(100);

	Thread.Sleep(1000);

	e.Result = results;
}

因此,如果您希望通过使用其用户界面渲染来玩转目录比较器,您可以遵循上述过程。

反之亦然。也就是说,您可以使用以下方法编写自己的用户界面。在comparerWorker_RunWorkerCompleted方法中,不要实例化 frmCompareResults 窗体,而是可以实例化您自己的窗体来按预期显示结果。下面是一个示例。

private void comparerWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
	IResults results = (IResults)e.Result;
	
	_frmNewCompareResults = new frmNewCompareResults();
	_frmNewCompareResults.Results = results;
	_frmNewCompareResults.mainReference = this;
	this.Hide();
	_frmNewCompareResults.Show();
}

使用 BackgroundWorkers

核心上,目录比较器将 RecursiveComparer 的一个实例传递给 RecursiveDirectoryComparer。然后,它调用 RecursiveDirectoryComparer 的 CompareDirectories 方法来执行实际的比较。您可能会注意到,这是一个耗时的操作。您可以在“开始”按钮的点击事件中完成此操作。但这会阻塞主窗体,从而使窗体无响应(您经常会在某些应用程序的标题栏中注意到“无响应”消息)。因此,为了不阻塞主窗体并使其可用于其他操作,我们使用 BackgroundWorkers。要使用它,您只需在窗体上添加一个 BackgroundWorker 组件(打开侧边栏中的工具箱,滚动到“组件”部分。从那里,将 BackgroundWorker 拖放到窗体上。

为了使用这个 BackgroundWorker,您需要实现以下 3 个事件处理程序:

  • DoWork
  • ProgressChanged
  • RunWorkerCompleted

在讨论这个之前,我们需要一种方法向用户指示进度。为此,让我们在窗体上添加一个ProgressBar。要做到这一点,请打开侧边栏中的工具箱,滚动到“通用控件”部分。从那里,将 ProgressBar 控件拖放到窗体上。现在让我们看看如何设置这 3 个事件。第一步是像下面这样将这些事件连接到 BackgroundWorker 控件。

this.comparerWorker.DoWork += new DoWorkEventHandler(comparerWorker_DoWork);
this.comparerWorker.ProgressChanged += new ProgressChangedEventHandler(comparerWorker_ProgressChanged);
this.comparerWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(comparerWorker_RunWorkerCompleted);

实现 BackgroundWorker 的主要工作在DoWork事件中。在这里,我实例化一个 RecursiveComparer 并将其传递给 RecusiveDirectoryComparer。为了实际启动比较,然后我调用 CompareDirectories 方法。RecusiveComparer 是使用对主窗体的引用创建的。这是为了方便比较器更新状态。我认为我应该找到一种更好的方法来做到这一点,因为我不认为这是一种优雅的方式。我可能可以在主窗体中有一个静态方法供比较器更新,但这似乎因为某种原因不起作用。另外,完成百分比的报告方式也不尽如人意。我正在着手处理这些问题。

private void comparerWorker_DoWork(object sender, DoWorkEventArgs e)
{
	ITwoPassComparer comparer = new RecursiveComparer(this);
	IDirectoryComparer recursiveComparer = new RecursiveDirectoryComparer(comparer);
	IResults results = recursiveComparer.CompareDirectories();

	this.ReportProgress(100);

	Thread.Sleep(1000);

	e.Result = results;
}

正如我们之前所见,RecursiveComparer 接收主窗体的引用。由于我们使用 ProressBar 控件来指示状态,因此“ProgressChanged”事件将用于更新操作当前状态的百分比。这非常直接,我使用 BackgroundWorker 线程的ReportProgress方法来更新状态。

一旦 BackgroundWorker 在DoWork事件中的任务完成,就会引发RunWorkerCompleted事件。这时,我们必须获取结果并将它们传递到下一个窗体,即 frmCompareResults。传递给事件处理程序的 BackgroundWorkerEventArgs.e 包含一个 Result 属性。这可以转换为 frmCompareResults 窗体所需的 IResults,该窗体用于呈现结果。

private void comparerWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
	IResults results = (IResults)e.Result;
	
	_frmCompareResults = new frmCompareResults();
	_frmCompareResults.Results = results;
	_frmCompareResults.mainReference = this;
	this.Hide();
	_frmCompareResults.Show();
}

使用具有 ListView 的上下文菜单

我觉得仅显示比较结果是不够的。提供一些选项,如打开左/右文件或文件夹,肯定会有帮助。为此,最好有一个上下文菜单,而不是在应用程序的主菜单中再添加一个菜单项。为此,请打开“工具箱”菜单,然后移至“菜单和工具栏”部分。从这里,我将一个ContextMenu控件拖放到窗体上。当单击列表项时,您可能会注意到整个项都被选中了。必须实现MouseDown事件来为选中的项显示上下文菜单。以下代码显示了如何注册事件。

private void InitializeListOperations()
{
	this.listView1.MouseDown += new MouseEventHandler(listView1_MouseDown);
}

下一步是实现注册的事件处理程序。在这种情况下,我们需要确保仅当用户单击鼠标右键时才显示窗体。因为任何鼠标按下事件都会打开上下文菜单,这会很烦人。为了实现这一点,将传递到处理程序的 MouseEventArgs.e 被使用。此属性将指示按下的鼠标按钮。代码如下所示。

void listView1_MouseDown(object sender, MouseEventArgs e)
{
	selectedItem = listView1.GetItemAt(e.X, e.Y);
	if (selectedItem != null && e.Button == System.Windows.Forms.MouseButtons.Right)
	{
		ManipulateContextMenuItems(selectedItem);
		contextMenuStrip1.Show(listView1.PointToScreen(e.Location));
	}
}

使用资源文件

为了在应用程序中包含和使用图像,有几种选择。一种方法是有一个包含应用程序将使用的所有图像的文件夹,这些图像将随应用程序一起分发,然后在窗体中使用,如下所示:

Image image = Image.FromFile("Images/folder.png");

但这并不是一个理想的方法。资源文件是更好的选择,因为我们不必分发带有分散的图像文件的应用程序。使用此选项,图像将嵌入到应用程序本身中,从而减少了分发图像文件(除了可执行文件)的需要。在这种情况下,需要两种类型的资源:

  • 项目范围资源
  • 窗体特定资源

项目范围的资源是显示在各种窗体标题栏上的图像。此时,我只为标题栏使用一个图标,并按以下步骤将其添加到项目中:

  • 右键单击项目并选择“属性”。
  • 选择“生成”选项卡。
  • 在“资源”部分,选择“图标和清单”。
  • 选择“...”按钮,导航到包含图标的文件夹并添加它。

执行上述步骤也会将其添加到项目根目录中。请注意,这只会将应用程序图标设置为选定的文件(可执行文件的图标)。为了让窗体的标题栏显示相同的图标,请右键单击窗体,找到“图标”属性,选择“...”然后,最后,选择您为应用程序选择的相同图标。对所有窗体重复这些步骤。

现在,下一步是添加将在各种窗体中使用的图标。在这种情况下,我使用了两种方法。项目中有 3 个窗体:frmMain、frmCompareResults、frmPrefernces。frmMain 不使用任何资源(除了项目范围的图标)。frmCompareResults 目前使用 1 个图标,这是一个文件夹图标,显示在用户界面中以指示某个条目是一个文件夹。将来可能会有更多图标使用,这些图标可能在各种窗体之间共享。因此,我创建了一个通用的 DirectoryComparerIcons.resx 文件,其中将存储这些图标。一旦将图标添加到此 resx 文件中,就可以这样使用:

public static ImageList GetImages()
{
	ResourceManager manager = new ResourceManager(typeof(DirectoryComparerIcons));
	ImageList imageList = new ImageList();

	Image folder = (Image)manager.GetObject("folder");            
	imageList.Images.Add(folder);

	return imageList;
}

这里通过传递我们创建的通用类型 DirectoryComparerIcons 来创建ResourceManager的实例。现在,管理器对象可用于获取我们添加到资源文件中的文件夹图标的引用。在我们的例子中,图像文件的名称是“folder.png”。因此,仅传递“folder”名称(不带扩展名)即可获得图像。这比使用 Image.FromFile 加载图像文件要好得多。

下一个方法是将图像添加到窗体本身。frmAbout 窗体使用此方法。要使用此方法,应打开 frmAbout.resx 文件。然后单击“添加资源”拆分按钮,它会为您提供一个下拉列表。在选项中,选择“添加现有文件”,然后导航到适当的文件夹,最后添加您希望使用的图像。

管理设置

设置模块当前包括保存和恢复项目中所需的以下键:

  • 左文件夹路径
  • 右文件夹路径
  • 列显示设置
5.jpg

设置存储在注册表中。这些设置有助于节省每次选择左侧和右侧文件夹所需的时间。它还包含一个设置,用于保存/恢复应用程序显示/隐藏某些可选列的方式。下面是保存偏好设置并重新启动应用程序后的屏幕外观。此时,应用程序需要重新启动,但在将来,我将更新应用程序,以便在单击“保存偏好设置”按钮后立即重绘屏幕。下面是重启后的图像。

RegManager类负责管理这些设置。相应的RegistryKeyMap类负责提供存储这些信息的注册表键。我打算提供一个接口,该接口将消除应用程序与注册表绑定的需要,如兴趣点中所述。下面是设置保存一次后注册表部分的屏幕截图。

8.jpg

一些技巧

这只是我想到的一些对他人有帮助的小建议。我只是在这里列出它们。

  • 假设您需要一个控件来填充整个父容器。为此,您只需将Control.Dock属性设置为true
  • 要在菜单项前显示复选标记,请将ToolStripMenuItem.Checked属性设置为true
  • 为了将项目添加到 ListView,请遵循此顺序。这只是为了确保添加的项目在所有项目都添加完毕之前不会显示。下面讨论的所有方法调用都是实例方法。
    • 调用ListView.Items.Clear方法。
    • 调用ListView.BeginUpdate方法。
    • 将项目添加到 ListView。
    • 调用ListView.EndUpdate方法。
  • 可以通过ToolStripMenuItem.DropDownItems检索任何一级菜单项的二级菜单项(例如,文件、视图、筛选等)。如果您注意到,一级菜单项“视图”和“筛选”有分隔符。为了在这种情况下循环遍历下拉菜单项,您需要执行以下操作:
// Iterate over the items as ToolStripItem. If ToolStripMenuItem is used, it will break
// as there are seperators in the drop-down items
foreach (ToolStripItem mnuItem in menuItem.DropDownItems)
{
	// My intention was to reset all items in the drop-down items
	// So I have to see if its a ToolStripMenuItem and then 
	// set the Checked property to false
	if (mnuItem is ToolStripMenuItem)
		((ToolStripMenuItem)mnuItem).Checked = false;
}

兴趣点 

我想花一些时间来开发这个应用程序,所以,在我完成这个应用程序的编码时,我脑海中涌现了许多新功能。我只是在下面列出它们:

  • 为应用程序添加一个扩展,该扩展可以为您提供代码行数统计。
  • 一个命令行版本,可以接受 2 个文件夹作为参数,进行比较并以 xml(或 csv)格式输出。
  • 而不是仅仅提供一个使用记事本打开的选项,提供一种使用适当应用程序打开的方式。
  • 一种提供扩展以存储目录比较器中各种任务相关偏好的方法。
  • 提供接口来执行文件的实际比较(目前计算文件的 md5 哈希值)。
  • 评估使用MEF的可能性(参见“扩展目录比较器”)。

历史

版本 1.0 发布

© . All rights reserved.