创建Windows Forms程序
Creating a Windows Forms Application
为了了解如何使用Windows Forms创建一个更加接近实际的Windows程序,本节中我们将开发一个名为FileCopier的实用工具,它可以将用户选定的一组目录中的所有文件复制到一个目标目录或者设备如软盘或者公司网络上的备份硬盘中。虽然不会实现所有可能的功能,但是可以想象,这个程序能够标记许多文件,将它们复制到多个磁盘中,尽可能
紧地打包。甚至扩展该程序能够压缩文件。这个例子的真正目的是实践前面的章节中所学到的许多C#技巧,并探查一下Windows.Forms命名空间。
就本例的目的考虑,并为使代码尽量简单,我们把注意力放在用户界面和将各控件联系起来的步骤上。最后的程序界面如图13-7所示。

图13-7:FileCopier的用户界面
FileCopier的用户界面由以下控件组成:
标签:Source Files和Target Directory
控钮:Clear, Copy, Delete和Cancel
一个复选框“Overwrite if exists”
一个文本框显示被选目标目录
两个大的树形视图控件,一个用于源目录,一个用于目标设备和目录
我们的目标是允许用户在左边的树形视图(源)中选择文件(或整个目录)。如果用户点
击Copy按钮,左边被选中的文件就被复制到右边控件中指定的目标目录。如果用户点击Delete,被选中文件将被删除。
本章接下来将实现FileCopier的许多功能,以说明Windows Forms的基本功能。
创建基本的用户界面窗体
Creating the Basic Ul Form
第一个任务是打开一个名为FileCopier的新工程。IDE进入设计器。可以将窗体扩大到所需尺寸。把标签(lblSource, lblTarget, lblStatus)、按钮(btnClear, btnCopy, btnDelete, btnCancel)、复选框(chkOverwrite)、文本框(txtTargetDir)和树形视图控件(tvwSource, tvwTargetDir)从Toolbox拖放到窗体上,设置它们的Name属性,最后生成的窗体如图13-8。

图13-8:在设计器中创建窗体
如果要使复选框靠近源选择窗口的目录和文件而不是目标窗口(其中只有一个目录被选中),将左边treeView控件tvwSource的CheckBoxes属性设为true,而将右边控件tvwTargetDir的属性设为false。这只要点击控件并在Properties窗口中修改值即可。
完成后,双击Cancel按钮,创建其事件处理方法:双击控件时,Visual Studio就会为该对象创建事件处理方法。如果双击对象,每个对象都有一个Visual Studio将会用到的“默认”
事件。对于按钮,默认事件是Click:
protected void btnCancel_Click (object sender, System.EventArgs e)
{
Application.Exit();
}
对于各种控件可以处理许多不同的事件。一个简单的实现办法是在Properties窗口中点击Event按钮,从中可以创建新的处理方法,只需填入新的事件处理方法名。Visual Studio将注册这些事件处理方法,并打开编辑器供我们输入代码,其中代码的开始已经创建,光标会停留在空的方法体中。
这些容易的步骤已经讲得够多了。Visual Studio将生成设置窗体和初始化控件的代码,但它不会填充treeView控件。这必须由我们手工进行。
针对VB6程序员的.NET Windows Forms提示
令人高兴的是,基本的.NET Windows控件与其VB6祖先有很多共同点。但是有些变化很容易使我们疏忽。设计窗体时请记住这些提示。
在VB6中,有些控件用Text属性显示文本,有些用Caption属性。而在.NET中,所有与文本相关的属性现在都称为Text。
VB6的CommandButtons使用Default和Cancel属性,用户可以通过按下回车键或者退出键进行选择。在.NET中,这些属性现在都是Form对象的一部分。使用AcceptButton和CancelButton属性表示窗体中的哪个按钮承担什么责任。
显示VB6窗体是通过调用Show()方法完成的。如果想窗体以模态对话框的形式显示,就传递vbModal枚举器给Show()方法。而在.NET中,这两个功能分给了两个不同的方法调用:Show() 和 ShowModal()。
生成treeView控件
Populating the TreeView Controls
两个treeView控件完全相似,只不过左控件tvwSource列出的是目录和文件,而右控件tvwTargetDir只列出目录。前者的CheckBoxes属性设成true,而后者是false。而且前者允许多选,这是treeView控件的默认值,而后者必须是单选。
应该把treeView控件的公用代码提取出来,放入共享办法FillDirectoryTree中,并将代码是否获取文件的标志作为参数传给控件(译注3)。然后可以从Form的构造方法中调用此方法,两个控件各一次:
FillDirectoryTree(tvwSource, true);
FillDirectoryTree(tvwTargetDir, false);
FillDirectoryTree实现将treeView参数命名为tvw。这将依次表示源treeView和目标treeView。需要用到System.IO中的类,因此在Form1.cs的开始添加了using System. IO; 语句。接下来,在Form1.cs中添加方法声明:
private void FillDirectoryTree(treeView tvw, bool isSource)
TreeNode对象
treeView控件有属性Nodes,它获取一个TreeNodeCollection对象。TreeNodeCollec- tion是TreeNode对象的集合,每个都代表树中的一个节点。我们先清空集合:
tvw.Nodes.Clear();
通过遍历所有驱动器的目录,为填充treeView的Nodes集合作准备,获取系统上所有逻辑驱动器。为此调用Environment对象的静态方法GetLogicalDrives()。Environ- ment类提供了当前平台环境的信息,并可访问环境。可以用该对象获取运行程序计算机的机器名字、操作系统版本号、系统目录等信息。
string[] strDrives = Environment.GetLogicalDrives();
GetLogicalDrives()返回一个字符串数组,每个字符串代表逻辑驱动器的根目录。可以遍历此数组,并将节点添加到treeView控件中。
foreach (string rootDirectoryName in strDrives)
{
可以在foreach循环中处理每个驱动器。
要确定的是驱动器是否准备好了。对此我的办法是通过调用DirectoryInfo对象(为根目录创建)地GetDirectories()方法,获取驱动器顶级目录的列表。
DirectoryInfo dir = new DirectoryInfo(rootDirectoryName);
dir.GetDirectories();
DirectoryInfo类向外公开了创建、移动和枚举目录及其文件和子目录的实例方法。该类将在第21章讲述。
GetDirectories()方法返回目录列表,但可以将其丢弃。这里调用它的目的只是为了在驱动器未准备好时产生一个异常。
可以将此调用放在一个try语句块中,但在catch块中没有动作。其效果就是如果异常抛出,驱动器将被跳过。
一旦知道驱动器已准备好,就可以创建一个TreeNode存放驱动器的根目录,并将其添加到treeView控件中。
TreeNode ndRoot = new TreeNode(rootDirectoryName);
tvw.Nodes.Add(ndRoot);
为了获取treeView中右边的+号,必须找到至少两级目录(这样treeView就知道哪些目录有子目录,可以在其旁边写上一个+号)。但是我们不需要遍历所有子目录,因为这样太慢了。
GetSubDirectoryNodes()方法的任务是遍历两级,传入根节点、根目录的名字、表示是否需要文件的标志和当前级别(通常从1级开始):
if ( isSource )
{
GetSubDirectoryNodes(
ndRoot, ndRoot.Text, true,1 );
}
else
{
GetSubDirectoryNodes(
ndRoot, ndRoot.Text, false,1 );
}
你可能奇怪,为什么已经传入ndRoot了,还要传入ndRoot.Text。耐住性子,等我们递归回到GetSubDirectoryNodes时就明白了。现在FillDirectoryTree()已经完成了。参见例13-3中该方法的完整程序清单。
遍历子目录
GetSubDirectoryNodes()的开始也是调用GetDirectories(),这一次要保存生成的DirectoryInfo对象数组:
private void GetSubDireoctoryNodes(
TreeNode parentNode, string fullName, bool getFileNames)
{
DirectoryInfo dir = new DirectoryInfo(fullName);
DirectoryInfo[] dirSubs = dir.GetDirectories();
注意,传入的节点名为parentNode。节点当前的级别可以认为是所传入的节点的子节点。这样我们就把目录结构映射成树状视图层次了。
遍历每个子目录,跳过那些标记为Hidden的:
foreach (DirectoryInfo dirSub in dirSubs)
{
if ( (dirSub.Attributes &
FileAttributes.Hidden) != 0 )
{
continue;
}
FileAttributes是一个枚举;其他可能的值包括Archive、Compressed、Directory、Encrypted、Hidden、Normal、ReadOnly等。
提示:属性dirSub.Attributes是目录当前属性的位模式。如果将此值与FileAttributes.Hidden的位模式做逻辑AND运算,则如果文件有hidden属性将置位,否则所有位将清零。可以通过测试生成的整数是否为零,得知hidden位的情况。
用目录名创建一个TreeNode,并将其添加到传入方法的那个节点(parentNode)的Nodes集合中:
TreeNode subNode = new TreeNode(dirSub.Name);
parentNode.Nodes.Add(subNode);
现在检查当前级别(由调用方法传入)是否等于为该类定义的一个常数:
private const int MaxLevel = 2;
因此只遍历两级:
if ( level < MaxLevel )
{
GetSubDirectoryNodes(
subNode, dirSub.FullName, getFileNames, level+1 );
}
传入刚刚创建的新的父节点,以及完全路径作为节点全名,还有标志,以及比当前级别更大的一级(这样,如果从1级开始,下面的调用将级别设置为2)。
提示:调用TreeNode构造方法要用到DirectoryInfo对象的Name属性,而调用GetSubDirectoryNodes()要用到FullName属性。如果目录是c:\WinNT\Media\Sounds,FullName属性将返回完整路径,而Name属性只返回Sounds。只传入名字就行,因为树状视图中只显示名字。将带路径的完整名字传给GetSubDir- ectoryNodes()方法是为了在硬盘中查找所有子目录。这就回答了第一次调用此方法时,为什么要传入根节点的名字。传入的不是节点的名字,而是节点所表示的目录的完全路径。
获取目录中的文件
遍历子目录后,如果getFileNames标志为true,就该获取目录中的文件了。为此要调用DirectoryInfo对象的GetFiles()方法。返回为FileInfo对象数组:
if (getFileNames)
{
// 获取节点的文件
FileInfo[] files = dir.GetFiles();
FileInfo类(第21章讲述)提供了操作文件的实例方法。
现在可以遍历集合了,访问FileInfo对象的Name属性,将名字传给TreeNode的构造方法,然后将TreeNode添加到父节点的Nodes集合中(这样就创建了一个子节点)。这里没有递归,因为file没有子目录:
foreach (FileInfo file in files)
{
TreeNode fileNode = new TreeNode(file.Name);
parentNode.Nodes.Add(fileNode);
}
这样两个树状视图就完成了。见例13-3中的完整程序清单。
提示:如果你还没有弄明白,我强烈建议将代码放入调试器中,步进执行递归过程,你可以亲眼看到treeView生成节点。
读书小趣味:and the pounds
处理treeView事件
Handling TreeView Event
本例中必须处理许多事件。首先,用户可能点击Cancel、Copy、Clear或 Delete。其次,
用户可能点击左边的treeView的复选框或右边treeView的节点。
让我们先考虑对treeView的点击,因为它更有趣,也可能更具挑战性。
点击源treeView
有两个treeView对象,每个都有自己的事件处理方法。先考虑源treeView对象。用户从中查看要复制的文件和目录。每次用户点击文件或目录时,会发生很多事件。必须处理的事件是AfterCheck。
为此,要实现一个自定义的事件处理方法,名为tvwSource_AfterCheck()。Visual Studio将会把它与事件关联起来,如果没有使用IDE,必须自己完成。
tvwSource.AfterCheck +=
new System.Windows.Forms.treeViewEventHandler
(this.tvwSource_AfterCheck);
AfterCheck()的实现将工作委托给名为SetCheck()的可递归方法(等一下来编写)。SetCheck 方法将为所有所含文件夹递归地设置选中标志。
要添加AfterCheck事件,选择tvwSource控件,在Properties窗口中点击Events,然后双击AfterCheck。IDE将添加该事件,将它连起来,并进入代码编辑器,在其中可以添加方法主体:
private void tvwSource_AfterCheck (
object sender, System.Windows.Forms.TreeViewEventArgs e)
{
SetCheck(e.Node,e.Node.Checked);
}
事件处理方法传入sender对象和一个treeViewEventArgs类型的对象。我们于是可以从treeViewEventArgs类型的对象(e)获取节点。可以调用SetCheck(),传入节点和节点的checked状态。
每个节点都有Nodes属性,它可以获取包含所有子节点的TreeNodeCollection。SetCheck()递归当前节点的Nodes集合,设置每个子节点的checked标志以反映实际的情况。换言之,当我们选中目录时,所有以文件和子目录都将递归地被选中。
对于Nodes 集合中每一个TreeNode,我们都要查看它是否是叶子节点。所谓叶子节点就是其Nodes集合中为空的节点。如果是,将check属性设置成传入的参数。如果不是,就递归。
还是乌龟呀,一只踩在另一只上面
关于递归,我最喜欢的故事是这样的:一天,有一位著名的达尔文主义者讲述关于创世神话的故事。“有些人,”他说,“相信世界位于一只巨龟的背上。”当然,问题就随之产生了:“又是什么支撑着那只乌龟呢?”
从房间后排有一位年老的妇女站了起来,说道:“这再清楚不过了,小伙子,还是乌龟呀,一只踩在另一只上面。”
private void SetCheck(TreeNode node, bool check)
{
// 寻找这个节点的所有子节点
foreach (TreeNode n in node.Nodes)
{
n.Checked = check; // 选中节点
// 如果这是树中的节点,遍历
if (n.Nodes.Count != 0)
{
SetCheck(n,check);
}
}
}
这样就把选中标志沿着整个结构传下去了(或者全部清空)。用户可以这样通过单击一个目录,表示他要选择所有子目录中的文件。
打开目录
每次点击源窗口(或者目标窗口)中目录旁的+号时,可以打开该目录。为此,需要一个事件处理方法来处理BeforeExpand事件。因为事件处理方法对于源树状视图和目标树状视图是完全一样的,所以我们可以创建一个共享的处理方法(将同一个方法赋予两者):
private void tvwExpand(object sender, TreeViewCancelEventArgs e)
{
TreeView tvw = ( TreeView ) sender;
bool getFiles = tvw == tvwSource;
TreeNode currentNode = e.Node;
string fullName = currentNode.FullPath;
currentNode.Nodes.Clear();
GetSubDirectoryNodes( currentNode, fullName, getFiles, 1 );
}
代码的第一行将委托传入的对象从object转换为treeView,这是安全的,因为我们知道只有treeView可以触发这一事件。
第二个任务是确定是否要获取已经打开的目录中的文件,只有在触发事件的treeView名为tvwSource时,才应该这样做。
通过获取事件传入的treeViewCancelEventArgs的Node属性,可以确定哪个节点的+号已经被选中:
TreeNode currentNode = e.Node;
确定当前节点后,获取完整路径名(需要作为GetSubDirectoryNodes的参数),然后必须清除其子节点集合,因为将要通过调用GetSubDirectoryNodes重新填充该集合:
currentNode.Nodes.Clear();
为什么要清除子节点然后又重新填充呢?因为这次将深入到另一层,从而确定子节点是否还有子节点,进而确定是否需要在其子目录旁绘制+号。
点击目标treeView
目标treeView的事件处理方法(除BeforeExpand外)要麻烦一些。事件本身是AfterSelect。(还记得吗,目标treeView没有复选框。)这次需要确定选中的目录,并将其完整路径放入窗体左上角的文本框中。
为此,必须遍历节点,寻找每个父目录的名字,并形成完整路径:
private void tvwTargetDir_AfterSelect (
object sender, System.Windows.Forms.TreeViewEventArgs e)
{
string theFullPath = GetParentString(e.Node);
等一会儿我们再来看GetParentString()。有了完整路径以后,必须删掉末尾的反斜线,然后就可以填入文本框了:
if (theFullPath.EndsWith("\\"))
{
theFullPath =
theFullPath.Substring(0,theFullPath.Length-1);
}
txtTargetDir.Text = theFullPath;
GetParentString()方法以一个节点为参数,返回一个含有完整路径的字符串。为此,向上遍历路径,在非叶子节点后添加反斜线:
private string GetParentString(TreeNode node)
{
if(node.Parent == null)
{
return node.Text;
}
else
{
return GetParentString(node.Parent) + node.Text +
(node.Nodes.Count == 0 ? "" : "\\");
}
}
提示:条件操作符(?)是C#中唯一的三元操作符(即有三个操作数的操作符)。其逻辑为:“测试node.Nodes.Count是否为零,如是,返回冒号前的值(这里是一个空字符串);否则返回冒号后的值(这里是一个反斜线)。”
遍历在没有父节点时结束,也就是说,我们到达根节点了。
处理Clear按钮事件
有了前面开发的SetCheck()方法,处理Clear按钮的点击事件就简单了:
protected void btnClear_Click (object sender, System.EventArgs e)
{
foreach (TreeNode node in tvwSource.Nodes)
{
SetCheck(node, false);
}
}
只需调用根节点的SetCheck()方法,并让它们递归地取消选中所包含的节点。
实现Copy按钮事件
Implementing the Copy Button Event
既然已经能够检查文件,选择目标目录,我们就可以处理Copy按钮点击事件了。要做的第一件事是获取选中的文件列表。所需要的是一个FileInfo对象数组,但是列表中有多少个对象我们不知道。这恰好可以使用ArrayList。我们把填充列表的责任委托给一个名为GetFileList()的方法:
private void btnCopy_Click (
object sender, System.EventArgs e)
{
List<FileInfo> fileList = GetFileList();
在返回事件处理方法之前,让我们再把这个方法解剖一下。
获取选中的文件
一开始是实例化一个新的List对象,保存表示所有选中文件的字符串:
private List<FileInfo> GetFileList()
{
// 创建完整文件名的未排序表
List<string> fileNames = new List<string>();
为了获取选中的文件名,可以遍历源treeView控件:
foreach (TreeNode theNode in tvwSource.Nodes)
{
GetCheckedFiles(theNode, fileNames);
}
要了解其工作原理,我们一步步地来看GetCheckedFiles()方法。此方法比较简单:查看收到的节点。如果没有子节点(node.Nodes.Count == 0),它就是一个叶子节点。如果选中的是叶子节点,获取其完整的路径[通过调用节点的GetParentString()方法],并将其作为一个参数添加到List中。
private void GetCheckedFiles( TreeNode node,
List<string> fileNames )
{
// 如果是一个叶子节点
if ( node.Nodes.Count == 0 )
{
// 如果节点选中...
if ( node.Checked )
{
// 获取完整路径,添加到List
string fullPath = GetParentString( node );
fileNames.Add( fullPath );
}
}
如果不是叶子节点,可以向下遍历树,寻找子节点:
else
{
foreach (TreeNode n in node.Nodes)
{
GetCheckedFiles(n,fileNames);
}
}
}
这将返回填充了所有文件名的List。在GetFileList()中,将使用文件名List创建另一个List,这次存放的是实际的FileInfo对象:
List<FileInfo> fileList = new List<FileInfo>();
注意这里使用类型安全的List对象可以确保编译器在有非FileInfo类型的对象添加到集合中时发出警告。
现在可以遍历fileList中的文件名,取出每个名字,并用它实例化一个FileInfo对象。可以调用Exists属性测试对象是一个文件还是目录,如果创建的File对象是一个目录,该属性返回false。如果是一个File,可以添加到新的List中:
foreach (string fileName in fileNames)
{
FileInfo file = new FileInfo(fileName);
if (file.Exists)
{
fileList.Add(file);
}
}
对选中的文件列表排序
如果需要按从大到小的顺序遍历选中的文件列表,从而可以尽可能地压缩目标磁盘,必须对List进行排序。可以调用其Sort()方法,但它怎么知道对FileInfo对象排序呢?
为解决这一问题,必须传入一个IComparer<T> 接口。我们将创建一个名为FileComparer的类,它将为FileInfo对象实现上述泛型接口:
public class FileComparer : IComparer<FileInfo>
{
此类只有一个方法Compare(),它有两个FileInfo对象参数:
public int Compare(FileInfo file1, FileInfo file2){
正常的方式是在第一个对象(file1)比第二个(file2)大时,返回1;如果小,返回-1;如果相等,返回0。但是这里我们想要从大到小排序,所以要反用返回值。
提示:因为这是compare方法的唯一用处,把实现从大到小的排序任务交给它,应该是合理的。也可以从小到大排序,然后让调用方方法反转结果,如例12-1所示。
为了测试FileInfo对象的尺寸,必须将Object参数转换为FileInfo对象(这是安全的,因为我们知道此对象不会接受其他参数):
if (file1.Length > file2.Length)
{
return -1;
}
if (file1.Length < file2.Length)
{
return 1;
}
return 0;
}
}
返回到GetFileList()方法,我们将实例化 IComparer引用,并将它传给fileList的Sort()方法:
IComparer<FileInfo> comparer = ( IComparer<FileInfo> ) new FileComparer();
fileList.Sort(comparer);
完成后,将fileList 返回给调用方法:
return fileList;
调用方方法是btnCopy_Click。请记住,我们在事件处理方法的第一行就转到GetFileList()了!
protected void btnCopy_Click (object sender, System.EventArgs e)
{
List<FileInfo> fileList = GetFileList();
这里我们返回了一个File对象的已排序列表,每个对象代表源treeView中选中的文件。
现在可以遍历列表,复制文件,更新用户界面:
foreach (FileInfo file in fileList)
{
try
{
lblStatus.Text = "Copying " +
txtTargetDir.Text + "\\" +
file.Name + "...";
Application.DoEvents();
file.CopyTo(txtTargetDir.Text + "\\" +
file.Name,chkOverwrite.Checked);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
lblStatus.Text = "Done.";
与此同时,将进度写到lblStatus文字标签中,并调用Application.DoEvents(),使用户界面有机会重绘。然后调用文件的CopyTo(),传入从文本区域获得的目标目录,以及一个表示如果文件存在是否应该覆盖的布尔标志。
请注意,传入的标志是chkOverWrite复选框的值。如果复选框已经选中,则Checked属性的值为true;如果没有,值为false。
复制是封装在一个try语句块中的,因为我们知道复制文件时可能出错。目前为止,我们都是通过弹出错误对话框来处理异常的,但在商业产品中,可能要采取一些改正措施。
好了,我们已经实现了文件复制!
处理Delete按钮事件
Handling the Delete Button Event
处理Delete按钮事件的代码更简单了。首先要询问用户,是否确定真的要删除文件:
protected void btnDelete_Click
(object sender, System.EventArgs e)
{
System.Windows.Forms.DialogResult result =
MessageBox.Show(
"Are you quite sure?", // 信息
"Delete Files", // 标题
MessageBoxButtons.OKCancel, // 按钮
MessageBoxIcon.Exclamation, // 图标
MessageBoxDefaultButton.Button2); // 默认按钮
可以使用MessageBox的静态Show()方法,传入要显示的信息,字符串"Delete Files"和标志。
MessageBox.OKCancel表示两个按钮:OK和Cancel。
MessageBox.IconExclamation 表示要显示一个感叹号图标。
MessageBox.DefaultButton.Button2 将第二个按钮 (Cancel) 设置为默认选择。
当用户选择OK或Cancel,结果作为一个System.Windows.Forms.DialogResult枚举值传回。可以通过测试这个值来看用户是否按下了OK:
if (result == System.Windows.Forms.DialogResult.OK)
{
如果是,可以获取fileNames的列表,并遍历之,同时删除:
ist<FileInfo> fileNames = GetFileList();
foreach ( FileInfo file in fileNames )
{
try
{
// 更新标签以便显示进度
lblStatus.Text = "Deleting " +
file.Name + "...";
Application.DoEvents();
// 伙计, 危险!
file.Delete();
}
catch ( Exception ex )
{
// 在这里你可以做更多事情, 不仅是显示消息
MessageBox.Show( ex.Message );
}
}
lblStatus.Text = "Done.";
Application.DoEvents();
此代码与复制代码相同,只不过对文件调用的方法是Delete()。
示例13-1是本例注释完整的源代码。
示例13-1:FileCopier源代码
#region Using directives
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Windows.Forms;
#endregion
/// <remarks>
/// File Copier - Windows Forms demonstration program
/// (c) Copyright 2005 Liberty Associates, Inc.
/// </remarks>
namespace FileCopier
{
/// <summary>
/// Form demonstrating Windows Forms implementation
/// </summary>
partial class frmFileCopier : Form
{
private const int MaxLevel = 2;
public frmFileCopier()
{
InitializeComponent();
FillDirectoryTree( tvwSource, true );
FillDirectoryTree( tvwTarget, false );
}
示例13-1:FileCopier源代码(续例)
/// <summary>
/// nested class which knows how to compare
/// two files we want to sort large to small,
/// so reverse the normal return values.
/// </summary>
public class FileComparer : IComparer<FileInfo>
{
public int Compare(FileInfo file1, FileInfo file2)
{
if ( file1.Length > file2.Length )
{
return -1;
}
if ( file1.Length < file2.Length )
{
return 1;
}
return 0;
}
public bool Equals(FileInfo x, FileInfo y) {
throw new NotImplementedException();
}
public int GetHashCode(FileInfo x) {
throw new NotImplementedException();
}
}
private void FillDirectoryTree( TreeView tvw, bool isSource )
{
// 用本地硬盘上的内容填充源TreeView对象tvwSource
tvw.Nodes.Clear();
// 取得逻辑驱动器并将其放入根节点
// 将机器中所有逻辑驱动器信息填充至数组
string[] strDrives = Environment.GetLogicalDrives();
// 遍历驱动器, 将其逐个加入树中
// 这里要使用try/catch区块, 这样一来,
// 若某个驱动器还未就绪(比如软盘驱动器或光盘驱动器中是空的),
// 该驱动器就不会被加入树中
foreach ( string rootDirectoryName in strDrives )
{
try
{
示例13-1:FileCopier源代码(续例)
// 用所有第一级的子目录填充数组
// 若驱动器还未就绪, 就抛出异常
// not ready, this will throw an exception.
DirectoryInfo dir =
new DirectoryInfo( rootDirectoryName );
dir.GetDirectories(); // 若驱动器还未就绪, 就强行制造异常
TreeNode ndRoot = new TreeNode( rootDirectoryName );
// 为每个根目录增加一个节点
tvw.Nodes.Add( ndRoot );
// 增加子目录节点
// 若信息源是Treeview, 则还要获取文件名
if ( isSource )
{
GetSubDirectoryNodes(
ndRoot, ndRoot.Text, true,1 );
}
else
{
GetSubDirectoryNodes(
ndRoot, ndRoot.Text, false,1 );
}
}
// 捕获诸如驱动器未就绪之类的异常
catch
{
}
Application.DoEvents();
}
} // FillSourceDirectoryTree结束
/// <summary>
/// Gets all the subdirectories below the
/// passed in directory node.
/// Adds to the directory tree.
/// The parameters passed in are the parent node
/// for this subdirectory,
/// the full path name of this subdirectory,
/// and a Boolean to indicate
/// whether or not to get the files in the subdirectory.
/// </summary>
private void GetSubDirectoryNodes(
TreeNode parentNode, string fullName, bool getFileNames,
int level )
{
示例13-1:FileCopier源代码(续例)
DirectoryInfo dir = new DirectoryInfo( fullName );
DirectoryInfo[] dirSubs = dir.GetDirectories();
// 为每个子目录增加一个子节点
foreach ( DirectoryInfo dirSub in dirSubs )
{
// 不显示隐藏的文件夹
if ( ( dirSub.Attributes & FileAttributes.Hidden )
!= 0 )
{
continue;
}
/// <summary>
/// Each directory contains the full path.
/// We need to split it on the backslashes,
/// and only use
/// the last node in the tree.
/// Need to double the backslash since it
/// is normally
/// an escape character
/// </summary>
TreeNode subNode = new TreeNode( dirSub.Name );
parentNode.Nodes.Add( subNode );
// 递归地调用GetSubDirectoryNodes
if ( level < MaxLevel )
{
GetSubDirectoryNodes(
subNode, dirSub.FullName, getFileNames, level+1 );
}
}
if ( getFileNames )
{
// 取得当前节点的任何文件
FileInfo[] files = dir.GetFiles();
// 放置节点之后, 将文件放入该子目录
foreach ( FileInfo file in files )
{
TreeNode fileNode = new TreeNode( file.Name );
parentNode.Nodes.Add( fileNode );
}
}
}
/// <summary>
/// Create an ordered list of all
/// the selected files, copy to the
示例13-1:FileCopier源代码(续例)
/// target directory
/// </summary>
private void btnCopy_Click( object sender,
System.EventArgs e )
{
// 取得列表
List<FileInfo> fileList = GetFileList();
// 复制文件
foreach ( FileInfo file in fileList )
{
try
{
// 更新标签以便显示进度
lblStatus.Text = "Copying " + txtTargetDir.Text +
"\\" + file.Name + "...";
Application.DoEvents();
// 将文件复制到目标位置
file.CopyTo( txtTargetDir.Text + "\\" +
file.Name, chkOverwrite.Checked );
}
catch ( Exception ex )
{
// 在这里你可以做更多事情, 不仅是显示消息
MessageBox.Show( ex.Message );
}
}
lblStatus.Text = "Done.";
Application.DoEvents();
}
/// <summary>
/// Tell the root of each tree to uncheck
/// all the nodes below
/// </summary>
private void btnClear_Click( object sender, System.EventArgs e )
{
// 获取每个驱动器所对应的最顶层的节点
// 并告知其进行递归式的清除
foreach ( TreeNode node in tvwSource.Nodes )
{
SetCheck( node, false );
}
}
/// <summary>
/// on cancel, exit
示例13-1:FileCopier源代码(续例)
/// </summary>
private void btnCancel_Click(object sender, EventArgs e)
{
Application.Exit();
}
/// <summary>
/// Given a node and an array list
/// fill the list with the names of
/// all the checked files
/// </summary>
// 用所有checked文件的完整路径填充ArrayList
private void GetCheckedFiles( TreeNode node,
List<string> fileNames )
{
// 如果是叶子节点...
if ( node.Nodes.Count == 0 )
{
// 如果节点已被checked...
if ( node.Checked )
{
// 获取完整路径并将其增加至arrayList
string fullPath = GetParentString( node );
fileNames.Add( fullPath );
}
}
else // 若该节点不是叶子节点
{
// 若该节点不是叶子节点
foreach ( TreeNode n in node.Nodes )
{
GetCheckedFiles( n, fileNames );
}
}
}
/// <summary>
/// Given a node, return the
/// full path name
/// </summary>
private string GetParentString( TreeNode node )
{
// 若这是根节点(c:\), 则返回文本信息
if ( node.Parent == null )
{
return node.Text;
}
else
{
// 向上递归获取路径,
// 然后增加该节点并追加一个斜杠
示例13-1:FileCopier源代码(续例)
// 若该节点是叶子节点, 则不要追加斜杠
return GetParentString( node.Parent ) + node.Text +
( node.Nodes.Count == 0 ? "" : "\\" );
}
}
/// <summary>
/// shared by delete and copy
/// creates an ordered list of all
/// the selected files
/// </summary>
private List<FileInfo> GetFileList()
{
// 创建一个存放完整文件名的无序数组列表
List<string> fileNames = new List<string>();
// ArrayList fileNames = new ArrayList();
// 用须要复制的文件的完整路径填充fileNames ArrayList
foreach ( TreeNode theNode in tvwSource.Nodes )
{
GetCheckedFiles( theNode, fileNames );
}
// 创建一个列表, 用来保存FileInfo对象
List<FileInfo> fileList = new List<FileInfo>();
// ArrayList fileList = new ArrayList();
// 对于无序列表中的每个文件名,
// 若该名称对应一个文件(而不是目录),
// 则将其增加至文件列表
foreach ( string fileName in fileNames )
{
// 用该文件名创建一个文件
FileInfo file = new FileInfo( fileName );
// 检查该文件是否已经存在于磁盘中
// 若是一个目录, 则该检查会失败
if ( file.Exists )
{
// key和value都是这个文件
// 若将value置为空, 是否会更简单一些?
fileList.Add( file );
}
}
// 创建一个IComparer接口的实体
IComparer<FileInfo> comparer = ( IComparer<FileInfo> )
new FileComparer();
示例13-1:FileCopier源代码(续例)
// 将comparer传递至Sort方法, 这样一来,
// 就能通过comparer提供的比较方法来对列表进行排序
fileList.Sort( comparer );
return fileList;
}
/// <summary>
/// check that the user does want to delete
/// Make a list and delete each in turn
/// </summary>
private void btnDelete_Click( object sender, System.EventArgs e )
{
// 通过提问进行确认
System.Windows.Forms.DialogResult result =
MessageBox.Show(
"Are you quite sure?", // 消息
"Delete Files", // 标题
MessageBoxButtons.OKCancel, // 按钮
MessageBoxIcon.Exclamation, // 图标
MessageBoxDefaultButton.Button2 ); // 默认按钮
// 如果确认要进行删除...
if ( result == System.Windows.Forms.DialogResult.OK )
{
// 遍历列表以便进行删除
// 获取被选中文件的列表
List<FileInfo> fileNames = GetFileList();
foreach ( FileInfo file in fileNames )
{
try
{
// 更新标签以便显示进度
lblStatus.Text = "Deleting " +
file.Name + "...";
Application.DoEvents();
// 伙计, 危险!
file.Delete();
}
catch ( Exception ex )
{
// 在这里你可以做更多事情, 不仅是显示消息
MessageBox.Show( ex.Message );
}
}
lblStatus.Text = "Done.";
Application.DoEvents();
}
}
示例13-1:FileCopier源代码(续例)
/// <summary>
/// Get the full path of the chosen directory
/// copy it to txtTargetDir
/// </summary>
private void tvwTargetDir_AfterSelect(
object sender,
System.Windows.Forms.TreeViewEventArgs e )
{
// 获取被选中的目录的完整路径
string theFullPath = GetParentString( e.Node );
// 若不是叶子节点, 则会以斜杠结尾
// 因此要移除这个斜杠
if ( theFullPath.EndsWith( "\\" ) )
{
theFullPath =
theFullPath.Substring( 0, theFullPath.Length - 1 );
}
// 将路径信息放入文本框
txtTargetDir.Text = theFullPath;
}
/// <summary>
/// Mark each node below the current
/// one with the current value of checked
/// </summary>
private void tvwSource_AfterCheck( object sender,
System.Windows.Forms.TreeViewEventArgs e )
{
// 调用一个可递归方法
// e.node是被用户check过的节点
// 当程序行进到这里时, check标记的状态已经被改变
// 因此, 我们得传递e.node.Checked的状态
if(e.Action != TreeViewAction.Unknown)
{
SetCheck(e.Node, e.Node.Checked );
}
/// <summary>
/// recursively set or clear check marks
/// </summary>
private void SetCheck( TreeNode node, bool check )
{
// 找出该节点的所有子节点
foreach ( TreeNode n in node.Nodes )
{
n.Checked = check; // check节点
//若这是树中的一个节点, 则递归地传递状态
if ( n.Nodes.Count != 0 )
示例13-1:FileCopier源代码(续例)
{
SetCheck( n, check );
}
}
}
private void tvwExpand(object sender, TreeViewCancelEventArgs e)
{
TreeView tvw = ( TreeView ) sender;
bool getFiles = tvw == tvwSource;
TreeNode currentNode = e.Node;
string fullName = currentNode.FullPath;
currentNode.Nodes.Clear();
GetSubDirectoryNodes( currentNode, fullName, getFiles, 1 );
}
}
}






