欢迎光临散文网 会员登陆 & 注册

C#实际案例分析(第四弹)——By 流星

2021-11-17 14:55 作者:ForeverMeteor  | 我要投稿

实验四

题目要求

仿照windows的文件浏览器,编写一个树状视图的文件浏览器

环境设置

操作系统: Windows 10 x64

SDK: .NET Framework 4.7.2

IDE: Visual Studio 2019


题意分析

我们这一次要来写文件浏览器。在现在的Windows图形化界面中,先前的列表式或是树状的文件浏览器已经较为少见了。不过他们的原理和结构也还算相对简单,我们将在下面进行解析。

参照现有的Windows系统,该文件浏览器应该具有以下几个功能和附加功能:

基本功能:
① 打开某一指定目录
② 展示该目录下的所有文件/文件夹
③ 对于该目录下的每一个文件夹,能够递归进行①②③直至某空文件夹或是某个文件夹下全是文件
④ 能够返回上级目录,再进行①②③
⑤ 在某一具象化的数据结构中进行文件和文件夹的展示

附加功能:
⑥ 能够在窗体中打开某一应用窗口或文件夹窗口
⑦ 文件夹树的全展开

罗列出来的这些功能也正是我们要实现这个应用的思路。基本功能我们均通过调用C#提供的TreeView控件和文件读取的功能来实现。这里注意③中的递归访问,是在系统的文件操作中非常重要的一个思想,我们将在接下来的代码解析部分详细解读。附加功能⑦依然通过调用TreeView的功能来实现,而⑥就要通过调用系统进程的功能了。

笔者开发的窗体中,最初始的路径通过用户输入得到(当然也可以通过OpenDialog窗口来实现),之后在TreeView中显示路径下的子文件和子文件夹。通过右上角的两个按钮来实现树的展开/收缩。同时,在选定的某一结点做根结点的情况下,在左侧的ListBox中显示它的孩子。最终开发的窗体界面如下:

最终窗体的形态

数据结构·文件树

数据结构中的在计算机中的应用非常普遍,例如求表达式值,哈夫曼编码等。在生活中的应用也非常普遍,例如本篇文章的组织结构也是树:

文章的组织结构

操作系统中的文件目录也是通过树来组织的。例如早期的界面:

文件树(早期操作系统)

其中,文件就是叶子结点,而文件夹就是根结点内部结点。或者,若我们将文件看作原子,将文件夹看作列表,文件的组织形式也可以看作广义表(列表)。这两种理解方式本质上是一样的。

观察某一路径:

最后一个"\"后面是结点自身(可以不是叶子),例如上述的dotnet.exe。而路径上的其他文件夹便是它的祖宗

通过dir指令,我们可以在DOS下查看一个结点的子目录:

同时,DOS也提供了tree指令来具象化地展示文件树(但是只能展示文件夹):

综上所述,我们可以将要访问的路径做根,文件、文件夹和结点一一对应,将其视作一棵树来进行处理。之后我们便可以引用树的各种操作:建树,遍历,销毁等,配合C#提供的组件来实现对目录的浏览。下面详细展开。


完整代码

代码片段分析

为了防止混乱,先贴上各个控件的分布图:

控件分布图


各个控件功能代码

该事件用以完成整个文件浏览器最核心的内容,即读取,加载,展开目录。最开始先将TreeView的内容清空,便于展示。而最后加载完成时候使能(Enable)展开结点与收起结点的按钮。它的核心部分是中间的FileTraverse函数,我们接下来重点解析它。

FileTraverse函数

我们一般通过递归的方式去遍历一棵树。这里的文件访问过程即是树的先序遍历(即先访问根结点,再依次先序遍历各个子树)。文件树的递归出口即是该文件夹下全是文件(全为叶子结点)或是空文件夹(自身为叶子结点)。在这里,判断是否为递归结束的标志即是if (foldersLen == 0)。也就是说,无论文件(叶子结点)有多少,只要没有了文件夹,就应该停止递归。在停止递归之前,将叶子结点全部加载。若非如此,则按照之前的思想,先将文件加载进来(t.Nodes.Add(getNameWithoutRoute(files[i]))),再加载一个文件夹(t.Nodes.Add(getNameWithoutRoute(folders[i]));),并递归地去遍历它(FileTraverse(folders[i], t.LastNode))。

最终实现的结果就是将该路径下的全部文件都加载到了TreeView/内存中,我们可以在之后的过程中访问它了。递归思想是遍历文件树最核心的思想,一定要注意掌握。

我们接着往下看:

TreeView控件为我们提供了结点的单击和双击事件(但是ListBox却没有对Item提供相应的功能,这点笔者感到非常奇怪),我们通过调用他们来实现结点的展开和附加功能,即调用进程。

为了方便浏览文件,在单击事件中实现了将子目录加载到左边的ListBox,即一部分列表文件浏览器的功能。每一次加载,都需要判断当前单击选定的结点是否为文件夹并更新全局变量isCurrentFile,以便于例如通过ListBox打开进程等的后续操作。

在这里,为了美观,我们使用了String类的Remove函数。该函数原型如下:
string Remove(int startIndex, int length)
若省去length而只有startIndex这一参数,则默认删除至最后。调用传入的事件e的ToString方法,得到的名字默认会带上"TreeNode ",所以我们通过Remove函数将其删除,达到了较美观的效果。这个函数在下面功能的实现仍然要用到。


双击事件用于附加功能-打开进程的实现。当一个子结点存在且没有子结点时(if (e.Node.Nodes.Count == 0)),它必然是一个文件,因而我们将它的路径拼接起来就能形成它的绝对路径currentPath = rootPath + e.Node.FullPath.Remove(0, "当前目录".Length);)。其中,Fullpath属性返回的是从TreeView中根结点出发直到自己的路径,因而它开始附带着根结点的命名,而实际情况中我们不需要它,使用Remove函数把它删掉。在获取绝对路径之后,我们使用:

来调用进程。学过Python的同学就会对这个操作感觉到非常熟悉,行云流水。它等效于以下的python代码:

C#的功能中已经提供了通过WinForm调用其他WinForm的功能,也就是上述的语句。详细的底层实现笔者也不太清楚,需要查看对应的文档。不过在这个附加功能的实现中,我们能学会调用它就足够了。

可以看到,双击事件中对于文件夹的操作自然地被过滤掉了,其原因就是它自带了展开子结点的功能。

总结

实机操作过程的效果如下:

实机操作效果

通过这次实验,我们熟悉了C#中TreeView、ListBox等控件的功能和使用。加深了对数据结构中树结构的理解,并应用它来完成文件树的遍历和加载。同时,我们还进一步熟悉了字符串的操作,了解了进程调用等功能。总体来说,这次实验还是较为简单的,要注意重点掌握递归遍历的思想。

这个应用仍有部分缺陷,比如由于ListBox的特性,难以在列表中通过双击选中项目来打开文件。再例如,文件树的实现过程是将路径下的子文件夹下递归的所有子文件夹和子文件一并加载到TreeView中,也就是一次性加载到内存中。这样效率较低,浪费时间,如果能在每次点击结点的时候再进行加载,效果就会很好了。最后,在实际操作过程中发现初始路径为系统根目录(即C:\)时加载出现问题,抛出异常,初步猜测是访问权限的问题。

参考文献

李春葆,曾平,喻丹丹.C#程序设计教程(第3版):清华大学出版社,2015

Copyright @ 2021, Bilibili: ForeverMeteor, all rights reserved. 

C#实际案例分析(第四弹)——By 流星的评论 (共 条)

分享到微博请遵守国家法律