1.3 使用文件系统对象
FSO(FileSystemObject)不仅可以像使用传统文件操作语句那样实现文件的创建、改变、移动和删除,而且可以检测是否存在指定的文件夹,如果存在,那么这个文件夹又位于磁盘上的什么位置。更令人高兴的是,FSO对象模型还可以获取关于文件和文件夹的信息,如名称、创建日期或修改日期等以及系统中使用的驱动器的信息,如驱动器的种类是CD-ROM还是可移动磁盘,当前磁盘的剩余空间还有多少。
FSO对象本身不属于VBA对象,要在VBA中使用FSO操作文件和路径,可以用前期绑定,也可以用后期绑定。
1.3.1 前期绑定
Office VBA不仅可以使用VBA本身的对象、成员,而且可以引入外部对象,例如在VBA中使用FSO、字典、正则表达式,以及后面讲到的操作其他Office组件,其实都是在VBA工程中引入了外部对象。
所谓的前期绑定,就是在编写程序之前,把外部对象库加入工程的引用(References)中。这样做的好处是,在写代码的时候,这些相关的对象后面输入小数点,可以自动列出成员,而且在声明变量时,也可以直接指定变量的类型。
采用前期绑定方式,可以使用New关键字或GetObject函数创建一个新的对象。
下面介绍一下采用前期绑定方式,向VBA工程引入FSO对象的步骤。
单击VBA编辑器的菜单【工具/引用】,弹出工程的引用对话框。在对话框中勾选“Microsoft Scripting Runtime”,单击“确定”按钮关闭对话框,如图1-23所示。
图1-23 添加外部引用
“Microsoft Scripting Runtime”这个外部引用位于路径“C:\Windows\System32\scrrun.dll”这个动态链接库中,每个Windows系统都有这个文件。
VBA工程一旦引入了这个外部引用,就可以使用FSO对象模型,以及后面要讲到的字典(Dictionary)对象。
下面通过一个VBA过程来测试一下。
当输入Scripting后面的小数点时,会自动弹出FSO相关的成员,这就是前期绑定的特点。运行上述过程,会执行复制文件操作。
1.3.2 后期绑定
后期绑定,就是程序中用到的外部对象,不往工程中添加引用,而是在需要该外部对象的地方,使用CreateObject函数来创建对象。针对后期绑定,由于VBA的工程没有添加对象库的引用,自然就不会自动列出成员,声明这方面的变量时,只能声明为Object或Variant类型。
下面新建一个工作簿,打开VBA编辑器在标准模块中直接书写一个过程。
书写上述代码时的感受就是不弹出成员,也没有任何语法提示。但是上述过程可以正常执行,实现文件的移动或重命名。
在实际编程过程中,前期绑定和后期绑定的代码通过改写,就可以转换,但是对于刚刚学习一个新对象库,推荐使用前期绑定方式。因为使用这种方式可以快速了解新对象的模型结构和语法特征。
1.3.3 FSO对象模型
FSO对象主要包括:Drive(分区、磁盘驱动器)、Folder(文件夹、路径)、File(文件)、TextStream(文本文件),以及FileSystemObject这五类对象。
1.3.4 遍历磁盘分区
FSO对象模型中的Drive对象可以表达一个分区。FSO.Drives是一个集合对象,用来返回所有分区。
下面的过程遍历计算机的所有分区的名称、总大小、可用空间、已用空间。其中已用空间是用总大小减去可用空间得到的。
运行上述过程,立即窗口的结果如图1-24所示。
在计算机的资源管理器中查看E:盘的属性,可以看到E:盘的总大小和可用空间与VBA运行结果是一致的,如图1-25所示。
图1-24 遍历磁盘分区
图1-25 核对磁盘分区大小
如果分区大小改写为更大容量单位的,首先要了解如下换算关系。
1GB=1024MB
1MB=1024KB
1KB=1024B,B表示字节
可以看出1GB=10243B
下面的过程单独查看E:盘的总大小和可用空间,用GB表示。
上述程序的运行结果如图1-26所示。
图1-26 查看磁盘分区属性
1.3.5 操作文件夹
FSO对象模型的Folder对象表示一个文件夹,或者称为一个路径。Folder对象本身有大量的属性、成员和方法可以使用。
要表达一个文件夹,只能使用FSO.GetFolder("文件夹路径")的方式。
下面的过程查看一个文件夹的总大小、子文件夹的个数和文件的个数。
运行结果如图1-27所示。
在资源管理器中查看dist文件的属性,对比后发现文件夹的大小和运行结果是一致的。但是,文件夹和文件的个数不一样,这是因为FSO中的SubFolders和Files是文件夹直属的文件夹和文件,不包括子文件夹以及子文件夹中的文件。
资源管理器中看到的则是该文件夹中包含的所有文件夹和文件(包括递归嵌套的子文件夹),如图1-28所示。
图1-27 查看文件夹的大小以及子文件夹和文件的数量
图1-28 查看文件夹属性
下面的过程列出了文件夹Folder对象的其他常用属性。
运行上述程序,立即窗口的结果如图1-29所示。
图1-29 文件夹的有关属性
文件夹Folder对象的常用方法主要有Copy、Move和Delete等,用于复制、移动、删除文件夹。下面的过程首先创建一个空文件夹,然后重命名文件夹,最后删除该文件夹。
需要注意的是,FSO对象模型中文件夹的Copy、Move、Delete方法对于非空文件夹同样有效,也就是说,即使被操作的文件夹包含文件和子文件夹,也被一起复制、移动和删除。
要获取和返回一个文件夹Folder对象,除了上面介绍过的GetFolder、CreateFolder方法以外,还可以使用Folder对象的SubFolders、ParentFolder得到文件夹的子文件夹和父级文件夹。
1.3.6 文件夹拒绝访问的问题
磁盘根目录下除了包含正常的文件夹外,经常还包含一些隐藏的系统文件夹,当用FSO读写这些系统文件夹时,会弹出“拒绝访问”的错误。
如果计算机的文件夹选项中设置了不显示隐藏的文件和文件夹或驱动器,那么在资源管理器中看到的全是可以正常操作的文件夹,如图1-30所示。
图1-30 不显示隐藏的文件夹
通过更改“文件夹选项”,切换到“查看”选项卡,找到“隐藏受保护的操作系统文件”,去掉勾选,并且选择“显示隐藏的文件、文件夹和驱动器”,如图1-31所示。
图1-31 显示隐藏的文件、文件夹和驱动器
设置完毕后,A:盘根目录下看到了隐藏的文件夹(图标比较虚),如图1-32所示。
图1-32 显示文件夹中隐藏的内容
这些隐藏文件夹大多数是系统文件夹,因此它们的属性是由vbHidden+vbSystem+vbDirectory组合的,结果为22。正常文件夹的结果是16。
下面的程序遍历A:盘根目录下的所有子文件夹,如果不是隐藏文件夹,就打印其名称、包含的子文件夹个数、属性值。
以上代码中,If判断语句起到过滤文件夹的作用,不处理隐藏的文件夹。
运行上述程序,立即窗口的打印结果如图1-33所示。
可以看出,只有3个文件夹是正常文件夹。
假设去掉上述代码中的If判断语句,再次运行上述程序,当遍历到“System Volume Information”这个文件夹,访问fd.SubFolders.Count属性时,弹出如图1-34所示运行时错误。
图1-33 只列举正常的文件夹
图1-34 不可访问系统文件夹
综上所述,在处理文件夹时,需要考虑到该文件夹能否被访问,必要时需要补充上述过滤条件。
1.3.7 操作文件
FSO对象模型中的File对象表示一个文件。与Folder对象类似,File对象也有很多的属性和方法。
在下面的过程中,用GetFile方法获取一个文件后,遍历该文件的常用属性。
上述程序的运行结果如图1-35所示。
图1-35 文件的属性
文件对象File的常用方法有Copy、Move、Delete。下面通过一段代码进行了解。
代码分析:先把abc.xls复制到dist文件夹下,并修改名称为xyz.xls。接着删除xyz.xls,最后把abc.xls重命名为123.xls。
1.3.8 遍历文件
文件夹对象下面有一个Files集合对象,表示该文件夹下的所有文件。可以据此来遍历文件夹下的直属文件。该方法无法遍历包含在子文件夹中的文件。
下面的程序遍历CTEX文件夹下的所有文件,并且打印每个文件的重要属性。
上述程序的运行结果如图1-36所示。
图1-36 遍历文件夹中的所有文件
在资源管理器中查看文件的属性,发现和输出结果一致,如图1-37所示。
图1-37 核对文件属性
如果要选择性地遍历文件,例如只遍历文件夹中的文本文件,只需要在For循环中嵌套If语句,判断一下扩展名即可。
代码分析:FSO的GetExtensionName可以返回指定文件的扩展名。
上述程序的运行结果如图1-38所示。
需要注意的是:在用For Each循环遍历文件夹中的所有文件时,尽量不要在遍历的同时对文件进行重命名、删除、复制、移动等操作,以免发生不可预料的结果。比较安全的做法是遍历的时候可以先把所有文件名存储到数组或字典中,后期对数组或字典进行操作。
图1-38 只遍历指定扩展名的文件
1.3.9 遍历子文件夹
一个文件夹中可能包含多个子文件夹,在FSO对象模型中,SubFolders集合对象表示磁盘分区或者文件夹下面的所有子文件夹。
下面的代码遍历CTEX文件夹下的所有子文件夹,打印每个子文件夹的路径,以及子文件夹中包含的文件总数。
上述程序的运行结果如图1-39所示。
图1-39 每个文件夹包含的内容
实际上,Windows的文件、路径管理是一个树状结构,文件夹中可以包含子文件夹和文件,子文件夹也是一种文件夹,其中还能包含子文件夹和文件,从逻辑上讲,子文件夹可以无限层嵌套。
如果要遍历到某位置下的所有子文件夹和文件,需要用递归算法反复访问SubFolders才能实现。
本书源代码文件“实例文档02.xlsm”中的UserForm1使用了Treeview控件结合递归算法来展示文件夹中的所有子文件夹和文件,如图1-40所示。
图1-40 递归遍历文件管理系统
读者可以下载源代码文件自行研究。
下面的代码使用递归算法遍历任意路径,并且把遍历的结果发送到Excel单元格中。
运行代码中的Traversal过程,从根目录“E:\ExcelObject_VSTO_VBA”反复调用Recursion过程,如图1-41所示。
图1-41 递归遍历的结果发送到单元格
上述代码的源文件为“实例文档03.xlsm”。
1.3.10 FSO的更多操作方式
前面介绍过的文件、文件夹的操作(Copy、Move、Delete)是以文件/文件夹对象为主体的。针对这种方式,一行代码只能操作一个文件或路径。
FSO允许以FSO对象作为主体,这种情形下,以通配符作为参数,从而达到一行代码就可以批处理文件或路径。常用操作有以下6个:
FSO.CopyFile
FSO.CopyFolder
FSO.MoveFile
FSO.MoveFoler
FSO.DeleteFile
FSO.DeleteFoler
文件或路径中的通配符可以使用*来匹配任意多个字符,也可以使用?匹配任意一个字符。
假设C:\temp下面有大量的文件夹,下面的过程可以把p开头的所有文件夹一次性删除。
运行上述过程,以p开头的文件夹就被删除了,如图1-42所示。
图1-42 用通配符限定被处理的文件夹
下面再举一个批量移动文件的实例。datas文件夹下有大量的记事本文件,下面的代码可以把一位数命名的文件批量转移到2018文件夹中。
代码分析:?.txt只能匹配到9.txt等,但是不能匹配12.txt,也就是说?只能代表一个字符。因此,运行上述过程后,图中框内的文件被批量转移,如图1-43所示。
图1-43 用通配符限定文件
此外,使用FSO还可以快速获取计算机中的特殊文件夹。
上述程序的运行结果如图1-44所示。
图1-44 获取特殊文件夹
1.3.11 判断是否存在
在利用FSO对计算机中的磁盘分区、文件夹、文件进行操作时,必须事先确保目标存在方可进行操作,因此,FSO提供了DriveExists、FolderExists、FileExists三个函数来快速判断目标是否存在,这三个函数都返回布尔值,如果存在则返回True。
以下三行代码分别判断计算机中是否存在K盘、文件夹C:\temp,以及是否存在Test.Spec文件。
下面的代码先判断C:盘下是否有Download文件夹,如果没有,则创建这个文件夹。
在日常办公中,经常需要在成千上万个文件中核对哪些文件存在,哪些文件没有,现在假设C:\Example文件夹下有大量的压缩包文件,理论上从1月1日到1月20日的文件名都有。现在需要核对哪些日期的文件不存在,如图1-45所示。
图1-45 文件夹中的内容
下面的过程,在日期中循环,通过变化的日期产生临时的文件名,然后用FSO的FileExists来判断该日期对应的文件是否存在,不存在的话就打印到立即窗口。
运行上述过程,文件夹中不存在的文件名打印在立即窗口中,如图1-46所示。
图1-46 检查哪些名称的文件夹不存在
1.3.12 文本文件的读写
计算机中的文件大体可以分为文本文件和二进制文件两大类,文本文件可以用Windows自带的记事本软件打开,二进制文件(图片、Word文档)则不能用记事本打开。
在编程过程中,经常需要对文本文件进行读写。下面介绍利用FSO中的TextStream对象操作文本文件。
对文件的读写操作分为以下三个环节。
打开文件(OpenTextStream)。
读/写(Read、Write、Append)。
关闭文件(Close)。
1. 打开文件
OpenTextStream函数返回一个TextStream文件对象,该函数有以下4个参数。
FileName:文件路径。
IOMode:读写模式,有ForReading、ForWriting、ForAppending三种模式,含义分别是读取、写入和追加写入。
Create:当文件不存在时,询问是否创建。该参数默认值为False。
Format:编码格式,取值必须是以下三者之一,TristateFalse(以ANSI格式打开)、TristateTrue(以Unicode格式打开)、TristateUseDefault(使用系统默认打开)。
2. 写入文件
TextStream对象写入文件的方法有如下三种。
Write:当前位置写入一个字符串。
WriteLine:当前位置写入一个字符串,并在后面自动加一个换行符。
WriteBlankLines:写入多个空行。
以下代码向记事本文件中自动写入一些内容。
代码分析:以上代码用ANSI格式打开new.txt,如果路径下不存在该记事本文件,则会自动创建一个空文件。
Write方法写入内容后,继续写入的内容会紧跟其后写入。代码中的WriteBlankLines 3表示输入三个换行符。写入操作完毕后,别忘记用Close方法关闭文件。
上述程序的运行结果如图1-47所示。
图1-47 写入文本文件
3. 读取文件
TextStream对象用于读取文本文件内容的方法有如下三种。
Read(i):在当前位置读取i个字符。
ReadLine:读取一整行。
ReadAll:读取整个文件内容。
当一个文本文件被打开时,指针处于文本内容的起始位置,读取过程中,指针(当前位置)随之向右移动。
假定new.txt文件中有一首完整的二十四节气歌,如图1-48所示。
下面用Read和ReadLine方法来读取一部分内容,赋给过程中的变量。
图1-48 文本文件的内容
代码分析:文件打开后,指针位于“春”之前,result(1)读取5个字符,指针移动到“清”之后,result(2)从当前位置再读取5个字符,也就是读取“谷天”、回车符(vbCr)、换行符(vbLf)、“夏”,总计5个,如图1-49所示。
图1-49 文件读取方法
result(3)从当前位置读取到行尾。
在实际编程过程中,经常需要把记事本中的多行文本按行读取,发送到单元格中,或者列表框控件等,此时使用ReadLine是最好的选择。
如果要一次性读取所有内容,发送给文本框控件,用ReadAll最省事。
下面两个过程分别采用ReadLine、ReadAll方法从文本文件中读取内容,发送到列表框、文本框中。
代码分析:在使用TextStream对象的Read以及ReadLine方法时,一定要判断是否已经读取到文件末尾。当读取到文件末尾时,TextStream对象的AtEndOfStream属性会返回True,因此经常利用该属性配合Do循环读取所有内容。
上述程序中,左侧是一个列表框控件,右侧是一个文本框(MultiLine为True)。
上述程序的运行结果如图1-50所示。
图1-50 从文本文件读取内容到控件
读取文件的过程中,还可以使用Skip或SkipLine方法跳过字符或跳过行,相当于主动改变指针位置。
还是以二十四节气歌为例,理解一下Skip的作用。
代码分析:打开文件后,result(1)首先读取前2个字符“春雨”,Skip 2表示跳过2个字符(“惊春”两个字被跳过),接着result(2)读取两个字符,也就是读取“清谷”两个字。
下面的代码演示了如何跳过整行。
运行上述过程,立即窗口的结果如图1-51所示。
以上内容的源代码文件为“实例文档04.xlsm”。
图1-51 跨行读取内容