第1章 VBA编程概念和工具   本章将介绍编写VBA代码需要了解的编程基本概念和VBA语言元素,以及调试程序并处理错误的方法。本章内容不仅适用于Excel,也适用于其他支持VBA编程的Office应用程序,例如Word、PowerPoint和Access。 1.1 VBA和宏简介   本节将简要介绍VBA和宏的一些基本概念,以及宏的相关操作,包括录制宏、运行宏、设置宏的安全性和修改宏。 1.1.1 何时需要使用VBA   并非所有Excel用户都需要使用VBA,这是因为Excel界面操作几乎可以完成日常所需的大多数工作,而且学习VBA编程也需要付出大量的时间和精力。然而,使用VBA可以更好地完成以下任务或者是实现这些任务的唯一途径。 * 重复执行操作:当需要执行有规律可循的重复性操作时,非常适合使用VBA。例如,工作簿中有3个工作表,需要为每个工作表的数据区域中的偶数行设置蓝色背景,编写一段VBA代码可以快速完成这项工作。 * 增强Excel功能:使用VBA的主要原因之一是可以扩展Excel自身的功能。很多Excel无法实现或颇费周折才能完成的任务,可以通过编写VBA代码来解决。例如,在Excel中无法一次性复制不相邻的多个单元格,通过编写VBA可以解决这个问题。 * 简化专业数据的处理难度:对于一些需要具备专业知识才能处理的数据,可以通过编写VBA代码创建一个用于处理数据的特定程序,将复杂的业务逻辑包含在程序内部,只要程序的使用者为程序提供需要处理的数据,程序就会自动对数据进行处理并呈现最终结果,这样程序的使用者不需要直接面对复杂的业务逻辑,降低数据处理的难度。 * 与其他Office程序交换数据:当需要在Excel和其他Office程序之间交换数据时,虽然可以通过手动复制和粘贴的方式完成,但是使用VBA可以简化烦琐的操作过程,提高效率。 * 为大量用户统一提供Excel的增强功能:通过将编写好的VBA程序创建为加载项,可以将加载项分发给不同的用户,这些用户在自己的Excel程序中安装加载项,然后就可以像使用Excel内部功能一样,使用加载项中的Excel增强功能。 1.1.2 通过录制宏学习VBA编程   宏是一段可以反复运行的VBA代码。从技术上讲,宏是一个不包含参数的Sub过程。当用户希望重复执行某个特定操作时,可以在Excel中将该操作录制下来,Excel会自动生成相应的VBA代码。以后可以反复运行该代码来重复执行相同的操作,这就是所谓的录制宏。   在Excel中编写的VBA代码主要处理的是Excel中的各种对象,例如工作簿、工作表、单元格、字体、边框、背景色等。由于对象的数量众多,且它们之间具有错综复杂的关系,所以在编写VBA代码时经常会遇到不知道该使用哪个对象以及如何使用的情况。   用户可以从录制宏时自动生成的代码中,学习执行特定操作时所需使用的对象及其代码编写方法。虽然由Excel自动生成的很多代码不够简洁,甚至难以理解,但是它仍然是学习VBA编程的一个有用工具。   如需录制在Excel中执行的操作,可以单击Excel窗口底部状态栏中的按钮,打开“录制宏”对话框,为即将录制的宏设置名称、运行宏的快捷键、存储位置、宏的简要说明等,如图1-1所示。 图1-1 “录制宏”对话框   注意:由于录制过程中的所有误操作都会被记录下来,所以在开始录制前,应该预先演练好即将录制的一系列操作。如果录制过程中出现误操作,则在运行宏时也会执行这些误操作,严重影响宏的效率。   为宏设置快捷键时,可以在“快捷键”文本框中输入一个小写或大写的英文字母。例如,如果输入小写字母e,则可以按Ctrl+E组合键运行宏;如果输入大写字母E,则需要按Ctrl+Shift+ E组合键运行宏。   录制的宏默认存储在当前工作簿中,可以在“保存在”下拉列表中选择宏的存储位置。 * 当前工作簿:将录制的宏存储在当前工作簿中。 * 新工作簿:将录制的宏存储在新建的工作簿中。 * 个人宏工作簿:将录制的宏存储在Personal.xlsb工作簿中,如果它不存在,则Excel会自动创建。每次启动Excel时会自动打开该工作簿,但是其默认处于隐藏状态。在当前打开的任何工作簿中都可以运行Personal.xlsb工作簿中的宏。   设置好宏的相关信息后,单击“确定”按钮,关闭“录制宏”对话框,进入录制状态,用户在工作簿中的大多数操作会被录制下来。如需结束录制,可以单击Excel状态栏中的按钮。   注意:如果在停止录制前就运行宏,则将进入死循环,此时可以按Ctrl+Break组合键强制中断宏的运行。   宏的录制功能不是万能的,实际上它只能录制一些按顺序进行的简单操作。具体而言,录制的宏无法实现很多功能,包括但不限于以下这些: * 不能创建常量和变量。 * 只能创建Sub过程,不能创建Function过程,创建的Sub过程不能包含参数。 * 不能生成判断条件和执行循环的代码。 * 不能生成显示对话框的代码。 1.1.3 运行宏   如需运行已录制好的宏,可以先打开“宏”对话框。打开该对话框有以下几种方法: * 在功能区的“视图”选项卡中单击“查看宏”按钮,如图1-2所示。 * 在功能区的“开发工具”选项卡中单击“宏”按钮,如图1-3所示。 * 按Alt+F8组合键。 图1-2 单击“查看宏”按钮 图1-3 单击“宏”按钮   提示:在功能区中默认不显示“开发工具”选项卡,需要手动将其添加到功能区中。   打开“宏”对话框,选择想要运行的宏并单击“执行”按钮,即可运行该宏,如图1-4所示。如果为宏设置了快捷键,则可以使用快捷键运行宏,而无须打开“宏”对话框。 图1-4 使用“宏”对话框运行宏   提示:如果无法运行宏,则可能需要更改宏的安全性设置,具体方法请参考1.1.4小节。 1.1.4 更改宏的安全性设置   当打开一个包含宏的工作簿时,可能会在功能区下方显示如图1-5所示的提示信息,如需运行该工作簿中的宏,可以单击“启用内容”按钮。以后每次打开这个工作簿时,都会显示该提示信息,每次都需要单击“启用内容”按钮,才能运行工作簿中的宏。 图1-5 禁用宏时显示的提示信息   如果不理会该提示信息,而是直接使用1.1.3小节中的方法运行宏,则会显示如图1-6所示的提示信息,无法运行工作簿中的宏。 图1-6 运行宏时显示的阻止信息   如果不想每次依靠单击“启用内容”按钮来获得运行宏的权限,则可以在Excel的信任中心中更改宏的安全性设置,操作步骤如下:   (1)启用Excel,单击“文件”按钮,然后选择“选项”命令。   (2)打开“Excel选项”对话框,在左侧选择“信任中心”选项卡,然后单击右侧的“信任中心设置”按钮,如图1-7所示。 图1-7 单击“信任中心设置”按钮   (3)打开“信任中心”对话框,在左侧选择“宏设置”选项卡,然后在右侧选中“启用VBA宏(不推荐;可能运行危险代码)”单选钮,如图1-8所示。 图1-8 更改宏的安全性设置   提示:如果已在功能区中显示“开发工具”选项卡,则可以单击该选项卡中的“宏安全性”按钮,直接打开第(3)步中的设置界面。   (4)连续单击两次“确定”按钮,关闭之前打开的对话框。   如果既想保留Excel原来的安全性设置,又不想每次都通过单击“启用内容”按钮才能运行工作簿中的宏,那么可以将包含宏的一个或多个工作簿移动到同一个文件夹中,然后在Excel中将该文件夹标记为受信任位置,以后每次打开该文件夹中的工作簿时,Excel都会认为其中的宏是安全的,这样就不会在功能区下方显示启用宏的提示信息,并允许用户运行这些工作簿中的宏。   如需在Excel中将一个文件夹标记为受信任位置,可以使用本小节前面介绍的方法,打开“信任中心”对话框,在左侧选择“受信任位置”选项卡,右侧将显示已标记为受信任位置的文件夹,如图1-9所示。如需添加新的文件夹,可以单击“添加新位置”按钮。 图1-9 单击“添加新位置”按钮   打开如图1-10所示的对话框,单击“浏览”按钮并选择所需的文件夹,然后单击“确定”按钮,即可将选择的文件夹标记为受信任位置,如图1-11所示。 图1-10 单击“浏览”按钮选择文件夹 图1-11 在受信任位置中添加新的文件夹 1.1.5 修改宏的相关信息和VBA代码   如需修改宏的相关信息,可以使用1.1.3小节中的方法打开“宏”对话框,选择需要修改的宏,然后单击“选项”按钮(请参考图1-4),在打开的对话框中修改宏的快捷键和说明,如图1-12所示。 图1-12 修改宏的相关信息   如需查看或修改宏的VBA代码,可以在“宏”对话框中选择一个宏,然后单击“编辑”按钮,打开如图1-13所示的窗口,在左侧窗格中展开“模块”并选择其中的“模块1”,将在右侧显示录制的宏的VBA代码。 图1-13 查看宏的VBA代码   本例录制宏时执行的操作是:选择A1:C1单元格区域,将它们的字体设置为“宋体”,将字号设置为“12”。录制的宏通常包含很多无用代码,完成本例操作只需要以下几行代码: Sub 设置字体格式() Range("A1:C1").Select With Selection.Font .Name = "宋体" .Size = 12 End With End Sub   为了减少代码的总行数,还可以将上面的代码改为以下形式: Sub 设置字体格式() Range("A1:C1").Select Selection.Font.Name = "宋体" Selection.Font.Size = 12 End Sub   如果希望上面的代码适用于任意选中的单元格或区域,而不局限于A1:C1单元格区域,则可以将Range("A1:C1").Select这行代码删除。 1.2 VBA编程工具   与使用其他编程语言需要先单独安装集成开发环境(IDE)不同,开发Excel VBA程序的相关工具位于Excel功能区的“开发工具”选项卡中,无须额外安装。在Excel中编写VBA代码的工具是VBE(Visual Basic Editor),本节将介绍VBE的界面组成及其各个部分的功能和用法。 1.2.1 打开VBE窗口   打开VBE窗口有以下几种方法: * 在功能区的“开发工具”选项卡中单击Visual Basic按钮。 * 在“宏”对话框中选择一个宏,然后单击“编辑”按钮。 * 右击工作表标签,在弹出的快捷菜单中选择“查看代码”命令。 * 按Alt+F11组合键。   VBE窗口由菜单栏、工具栏、工程资源管理器、属性窗口、代码窗口等部分组成,可以使用菜单栏的“视图”菜单控制在VBE窗口中显示哪些部分,并手动调整各个部分的排列方式。在如图1-14所示的VBE窗口中显示了菜单栏、工具栏、工程资源管理器、属性窗口、代码窗口、立即窗口、监视窗口。 图1-14 VBE窗口 1.2.2 工程资源管理器   如图1-15所示,当前打开的所有工作簿都会显示在工程资源管理器中,每个工作簿都是一个VBA工程,工作簿的名称显示在VBAProject右侧的小括号中。每个工作簿包含的工作表(Sheet1、Sheet2等)、代表工作簿自身的ThisWorkbook以及在VBA工程中添加的模块,都会以缩进的形式显示在每个VBA工程的下方,单击加号或减号可以展开或折叠类别中的项目。工程资源管理器对VBA工程的组织方式类似于Windows操作系统中的文件资源管理器。   在VBA工程中除了包含ThisWorkbook和Sheet1、Sheet2等固定模块之外,还可以创建以下3种模块。 * 标准模块:录制宏时自动创建的模块就是标准模块,用户也可以手动创建标准模块,在其中创建Sub过程或Function过程并输入所需的VBA代码。 * 窗体模块:在VBA工程中创建的用户窗体就是窗体模块。 * 类模块:使用类模块创建新的对象。   如需创建上述3种模块,可以在工程资源管理器中右击任意一项,然后在弹出的快捷菜单中选择“插入”命令,在弹出的子菜单中选择想要创建的模块,如图1-16所示。 图1-15 工程资源管理器 图1-16 选择想要创建的模块   在如图1-16所示的快捷菜单中还可以对模块执行以下操作。 * 导出文件:使用“导出文件”命令,可以将指定模块以文件的形式保存到计算机中。 * 导入文件:使用“导入文件”命令,可以将以文件形式保存的模块导入到当前VBA工程中,以便重复使用其中的VBA代码,节省重新输入代码的时间。 * 删除模块:使用“移除xx”命令(xx表示模块的名称),可以删除不需要的模块。 1.2.3 属性窗口   在VBE窗口中选中的对象的所有属性的名称和值将显示在属性窗口中。如图1-17所示,由于在工程资源管理器中选择的是ThisWorkbook,所以在属性窗口中将显示与ThisWorkbook关联的工作簿的属性。   属性窗口中左列是属性的名称,右列是属性的值。如需更改某个属性的值,可以在属性窗口中单击属性名称,然后在其右侧输入或选择属性的值,如图1-18所示。 图1-17 属性窗口 图1-18 为属性设置预置值 1.2.4 代码窗口   用户编写的所有VBA代码都显示在代码窗口中。工程资源管理器中的每个模块都有一个与其对应的代码窗口,双击一个模块,将打开与其对应的代码窗口,如图1-19所示。 图1-19 代码窗口   代码窗口的顶部有两个下拉列表,对于不同类型的模块,在左、右两个下拉列表中将显示不同的项目,右侧下拉列表中显示哪些项目取决于在左侧下拉列表中做出的选择。   例如,打开ThisWorkbook模块的代码窗口,在其顶部的左侧下拉列表中只有“(通用)”和“Workbook”两项。如果选择“Workbook”,则在右侧的下拉列表中会显示Workbook对象的所有事件过程;如果选择“(通用)”,则在右侧的下拉列表中只有“(声明)”一项。   在代码窗口中输入的代码以过程的形式进行组织。VBA中最常用的3种过程分别是Sub过程(子过程)、Function过程(函数过程)和事件过程。一个模块可以包含任意数量的过程,每个过程可以完成不同的任务。在编写一个复杂的VBA程序时,将实现不同小功能的代码组织到不同的过程中是一种良好的编程习惯。   如果一个模块包含多个过程,则可以控制是将所有过程同时显示在代码窗口中,还是每次只显示一个过程。只需单击代码窗口左下角的或按钮,即可在两种显示方式之间切换。当同时显示所有过程时,两个相邻过程之间使用一条横线分隔。 1.2.5 设置VBE编程选项   在VBE中提供了一些可以提高编程效率的选项,用户可以根据自己的编程习惯更改这些选项。实际上,大多数选项的默认设置几乎是最佳选择,用户通常无须更改这些选项的默认设置。然而,了解这些选项的含义和设置方法仍然是有用的,因为用户可以根据不同的需求和习惯随时更改这些设置。   打开VBE窗口,单击菜单栏中的“工具”|“选项”命令,打开“选项”对话框,如图1-20所示。 图1-20 “选项”对话框   “选项”对话框包含4个选项卡,用于控制在代码窗口中输入代码的选项显示在“编辑器”选项卡中,此处主要介绍该选项卡中的选项。虽然目前可能还无法体会到这些选项的用处,但是随着对VBA不断深入的了解,以后会理解它们能给编程带来很多便利之处。 * 自动语法检测:启用该选项时,如果在输入VBA代码时出现语法错误,则会显示一个对话框,其中包含出错的原因。如果关闭该选项,则在出现语法错误时,将使用特定颜色标记出错代码而不显示对话框。 * 要求变量声明:启用该选项时,在VBA中使用一个变量之前必须先声明它,否则会显示出错信息。如果关闭该选项,则可以直接使用变量而无须事先声明它,但是这不是一个好的编程习惯。 * 自动列出成员:启用该选项时,在VBA代码中输入一个对象和英文句点后,会自动显示该对象的所有属性和方法,如图1-21所示。用户只需在列表中使用键盘上的方向键选择所需的属性或方法,然后按Tab键,即可自动将选中的属性或方法输入到代码窗口中,提高输入效率的同时也可避免出现拼写错误。 * 自动显示快速信息:启用该选项时,将自动在输入对象的属性、方法或VBA函数时显示参数的提示信息,如图1-22所示。 * 自动显示数据提示:启用该选项时,如果运行的VBA代码处于暂停状态,则可以将鼠标指针指向代码中的某个变量,此时会显示该变量的当前值,有利于分析和调试代码。 * 编辑时可拖放文本:启用该选项时,可以使用鼠标拖动代码来完成移动和复制操作。 * 缺省为查看所有模块:启用该选项时,在代码窗口中将同时显示当前模块中的所有过程。如果关闭该选项,则每次只显示一个过程。 * 过程分隔符:启用该选项时,将在两个过程之间显示一条分隔线。 * 自动缩进:启用该选项时,用户在输入好一行代码并按Enter键后,VBE会自动将下一行代码开头的缩进量设置为与上一行相同。如果关闭该选项,则每次按Enter键后,下一行代码的开头不进行缩进。 * Tab宽度:为了使程序具有良好的可读性,通常需要为不同的代码设置缩进格式。默认的缩进量是4个字符,用户可以根据个人习惯更改该设置。 图1-21 自动列出成员 图1-22 自动显示快速信息 1.3 输入和保存VBA代码   输入VBA代码之前,需要先在工程资源管理器中双击一个模块,打开与其关联的代码窗口。无论一个VBA程序的复杂程度如何,它都由一个或多个过程组成,每个过程完成不同的任务,组成一个VBA程序的所有代码都位于不同的过程中。在代码窗口中输入VBA代码与在Windows记事本中输入文本类似,可以使用常规的文本编辑操作,例如剪切、复制、粘贴、删除和撤销等。然而,在代码窗口中输入VBA代码有很多需要注意的问题。本节将介绍在输入VBA代码时增加代码可读性的常用方法,还将介绍如何保存VBA代码。 1.3.1 表达式和运算符   表达式由实际值、变量、常量、函数、运算符等多种元素组成。下面是表达式的一个示例,该表达式的含义是:将存储在intSum变量中的值与10相加,然后将计算结果赋值给该变量并替换原有值。 intSum = intSum + 10   在上面的表达式中有4个元素:一个变量、一个实际值,一个加号和一个等号,加号和等号都是运算符。运算符用于连接表达式中的各个元素,并决定表达式执行的运算类型和运算顺序。VBA支持以下几种运算符。 * 连接运算符:将多个部分连接成一个整体,VBA中的连接运算符只有&和+两个。 * 算术运算符:执行数学运算,例如加、减、乘、除等。 * 比较运算符:比较两部分内容并返回一个逻辑值True或False。 * 逻辑运算符:将多个由比较运算符组成的表达式组合在一起,可以构建复杂的判断条件。   不同类型的运算符在运算时具有不同的优先级,优先级决定运算的先后顺序。在所有运算符中,算术运算符的优先级最高,其次是比较运算符,然后是逻辑运算符,连接运算符的优先级最低。   VBA中的算术运算符、比较运算符和逻辑运算符如表1-1所示。表中的算术运算符和逻辑运算符包含的各个运算符按照优先级从高到低的顺序排列,比较运算符中的所有运算符具有相同的优先级。如果在一个表达式中有多个相同优先级的运算符,则这些运算符按照它们在表达式中的位置从左到右执行运算。 表1-1 算术运算符、比较运算符和逻辑运算符 算术运算符 说 明 比较运算符 说 明 逻辑运算符 说 明 ^ 求幂 = 等于 Not 逻辑非 - 负号 <> 不等于 And 逻辑与 * 乘 < 小于 Or 逻辑或 / 除 > 大于 Xor 逻辑异或 \ 整除 <= 小于或等于 Eqv 逻辑等价 Mod 求余 >= 大于或等于 Imp 逻辑蕴含 + 加 - 减      如需改变运算符的默认优先级,可以将想要优先计算的部分放在一对小括号中。下面的表达式先计算小括号中的加法,然后计算小括号外的乘法,最后的计算结果是35。如果不使用小括号改变运算优先级,则计算结果是31。 intTotal = (1 + 6) * 5 1.3.2 使用缩进格式   复杂的VBA代码通常都会包含大量用于处理条件判断和循环的代码,这些代码都具有固定的书写格式和要求,例如If Then或Do Loop,当这些具有固定结构的代码彼此嵌套在一起时,会显著增加用户理解代码的难度。   为了增加代码的可读性,同时为了在代码出错时便于排查问题根源,应该在编写代码时使用正确的缩进格式。下面的代码包含两组嵌套在一起的If Then结构,由于使用了正确的缩进格式,所以可以很容易看出每组If Then结构的起止位置和它们各自包含的代码行。 Sub 验证用户名() Dim strUserName As String strUserName = InputBox("输入用户名:") If strUserName = "Admin" Then MsgBox "你是管理员" Else If strUserName = "User" Then MsgBox "你是普通用户" Else MsgBox "你不是有效用户" End If End If End Sub   下面是代码不使用缩进格式时的情况,两组If Then结构的起止位置和各自包含哪些代码变得不太容易辨认。当代码出现错误时,这种混乱的格式将加大排查错误的难度。 Sub 验证用户名() Dim strUserName As String strUserName = InputBox("输入用户名:") If strUserName = "Admin" Then MsgBox "你是管理员" Else If strUserName = "User" Then MsgBox "你是普通用户" Else MsgBox "你不是有效用户" End If End If End Sub   除了使用缩进格式之外,当一个VBA程序包含很多行代码时,为了使代码更具可读性,可以使用空行将所有代码分隔成多个逻辑部分。 1.3.3 将长代码分成多行   代码窗口的宽度总是有限的,如果一行代码过长而超出代码窗口的宽度,那么需要拖动水平滚动条才能看到位于窗口外的代码,如图1-23所示。   如需将较长的代码完整显示在代码窗口的可视范围之内,可以将一行长代码分成多行较短的代码。首先将插入点定位到希望开始分行的位置,然后输入一个空格和一条下画线,再按Enter键,插入点右侧的代码将被移入到下一行,如图1-24所示。虽然设置分行后的两部分代码位于上下两行,但是VBA仍然将它们看作一行代码。 图1-23 代码的一部分位于窗口的可见范围之外 图1-24 将代码分成多行 1.3.4 为代码添加注释   为VBA程序中的重要代码添加注释是一个好习惯,不但可以在以后提醒自己这些代码的含义或功能,也可以在将代码交给其他人维护时为他们理清思路。注释可以单独占据一行或位于一行代码的结尾,注释显示为绿色。为代码添加注释有以下几种方法: * 以单引号(')开头的内容。 * 以Rem关键字开头的内容。如果将注释放在代码行的上方,则需要在Rem关键字和注释内容之间添加一个空格;如果将注释放在代码行的右侧,则需要在Rem关键字和代码结尾之间添加一个冒号,并在Rem和注释内容之间添加一个空格。 * 选择想要转换为注释的一行或多行内容,然后单击“编辑”工具栏中的“设置注释块”按钮。如需使注释的内容恢复为可运行的VBA代码,可以单击“编辑”工具栏中的“解除注释块”按钮。   下面的代码使用单引号和Remo关键字两种方法添加注释。 '本过程的功能是在对话框中显示用户输入的用户名 Sub 设置用户名() Dim strName As String '声明一个String数据类型的变量 Rem 下一行代码将用户输入的内容赋值给变量 strName = InputBox("输入用户名:") MsgBox "用户名是:" & strName: Rem 显示用户名 End Sub 1.3.5 使用InputBox函数获取用户输入   如果希望VBA程序可以处理用户输入的数据 ,则可以使用VBA内置的InputBox函数。使用该函数可以显示一个对话框,其中有一个接收用户输入的文本框,InputBox函数将返回用户在文本框中输入的内容。无论用户在文本框中输入什么内容,该函数的返回值都是字符串(String)数据类型。InputBox函数的语法如下: InputBox(prompt[, title] [, default] [, xpos] [, ypos] [, helpfile, context]) * prompt(必需):显示在对话框顶部标题下方的文本。 * title(可选):显示在对话框顶部的标题。 * default(可选):显示在文本框中的默认值。如果不输入任何内容,则InputBox函数将返回默认值。 * xpos、ypos(可选):对话框左上角在屏幕中的坐标值。 * helpfile、context(可选):帮助文件和帮助主题。   提示:参数名称右侧带有“必需”二字表示必需为参数提供一个值,“可选”二字表示可以省略参数的值,此时将使用参数的默认值。   运行下面的代码将显示如图1-25所示的对话框,由于没有设置title参数,所以对话框顶部的标题默认为Microsoft Excel。标题下方的文本由prompt参数指定。由于将default参数设置为admin,所以在文本框中以选中的状态显示该默认值,用户输入新数据时会自动替换默认值。为了使后面的代码可以轻松处理用户输入的内容,可以将InputBox函数的返回值赋值给一个变量,本例的变量是strName。最后使用MsgBox函数显示strName变量的值,即用户在对话框中输入的内容。 Sub InputBox函数() Dim strName As String strName = InputBox("输入用户名:", , "admin") MsgBox "用户名是:" & strName End Sub 图1-25 由InputBox函数创建的对话框   如果用户没有输入任何内容而直接单击“取消”按钮,则会显示默认值admin。如果希望单击“取消”按钮后不显示任何内容,而是直接退出程序,则可以使用If Then语句判断strName变量是否是零长度的字符串,如果是,则执行Exit Sub语句将直接退出当前的Sub过程,而不会执行后面的代码。 If strName = "" Then Exit Sub 1.3.6 使用MsgBox函数显示信息   为了在程序运行期间随时向用户发送有关程序运行状况的信息,可以使用VBA内置的MsgBox函数。使用该函数可以显示一个对话框,其中显示由用户指定的内容。MsgBox函数的语法如下: MsgBox(prompt[,buttons][,title][,helpfile,context]) * prompt(必需):显示在对话框中的内容。 * buttons(可选):显示在对话框中的按钮和图标的类型,该参数的值如表1-2所示。 * title(可选):显示在对话框顶部的标题。 * helpfile、context(可选):帮助文件和帮助主题。 表1-2 buttons参数的值 常 量 值 说 明 vbOKOnly 0 只显示“确定”按钮 vbOKCancel 1 显示“确定”和“取消”按钮 vbAbortRetryIgnore 2 显示“终止”“重试”和“忽略”按钮 vbYesNoCancel 3 显示“是”“否”和“取消”按钮 vbYesNo 4 显示“是”和“否”按钮 vbRetryCancel 5 显示“重试”和“取消”按钮 vbCritical 16 显示“关键信息”图标 vbQuestion 32 显示“询问信息”图标 vbExclamation 48 显示“警告信息”图标 vbInformation 64 显示“通知信息”图标 vbDefaultButton1 0 第1个按钮是默认按钮 vbDefaultButton2 256 第2个按钮是默认按钮 vbDefaultButton3 512 第3个按钮是默认按钮 vbDefaultButton4 768 第4个按钮是默认按钮   MsgBox函数返回一个表示用户在对话框中单击了哪一个按钮的值,该函数的返回值如表1-3所示。 表1-3 MsgBox函数的返回值 常 量 值 说 明 vbOK 1 单击了“确定”按钮 vbCancel 2 单击了“取消”按钮 vbAbort 3 单击了“终止”按钮 vbRetry 4 单击了“重试”按钮 vbIgnore 5 单击了“忽略”按钮 vbYes 6 单击了“是”按钮 vbNo 7 单击了“否”按钮      提示:在VBA代码中使用常量值或数字值均可。   运行下面的代码将显示如图1-26所示的对话框,由于只设置了prompt参数,所以在对话框中只显示“确定”按钮。显示对话框时会暂时中断代码的运行,单击“确定”按钮后将继续运行代码。 Sub MsgBox函数() MsgBox "已处理完成!" End Sub   运行下面的代码将显示如图1-27所示的对话框,由于设置了title参数,所以该参数的值将显示在对话框顶部的标题栏中。由于省略了位于title参数之前buttons的参数,所以需要为buttons参数保留一个逗号分隔符。 Sub MsgBox函数2() MsgBox "已处理完成!", , "进度提醒" End Sub 图1-26 由MsgBox函数创建的对话框 图1-27 设置对话框的标题   如果不想输入额外的逗号分隔符,则可以使用“命名参数”的方法设置参数值。使用该方法设置参数值的格式如下: 参数名:=参数值   使用命名参数设置参数值时,如果跳过了某个参数而设置下一个参数,则无须输入额外的逗号分隔符,并且可以任意顺序设置参数值,而无须按照函数语法中的参数顺序依次设置。下面是在上一个示例中使用命名参数时的代码,将原本位于后面的title参数放到了第一个位置。在代码中使用命名参数的另一个优点是可以使参数值的含义更清晰。 Sub MsgBox函数3() MsgBox title:="进度提醒", prompt:="已处理完成!" End Sub   为MsgBox函数中的buttons参数设置的值可以是表2-1中3组值的总和,3组值分别用于设置对话框中的按钮类型、图标类型和默认按钮。例如,如需在对话框中显示“是”按钮、“否”按钮和“询问信息”图标,如图1-28所示,可以使用以下代码: Sub MsgBox函数4() MsgBox "是否需要保存?", vbYesNo + vbQuestion End Sub 图1-28 自定义设置对话框中的按钮和图标   当对话框中显示不止一个按钮时,可以通过MsgBox函数的返回值,判断用户单击的是哪一个按钮。下面的代码与上一个示例类似,唯一区别是将MsgBox函数的返回值赋值给一个变量,然后使用If Then语句检查该变量中的值是否等于vbNo,如果是,则说明用户单击了“否”按钮,此时会直接退出程序,否则将执行下一行代码ActiveWorkbook.Save,以便保存当前工作簿。本例代码中的vbNo是VBA内置常量,也可以在代码中使用与该常量等价的数字7,但是使用常量会使代码更具可读性。 Sub MsgBox函数5() Dim lngAnswer As Long lngAnswer = MsgBox("是否需要保存?", vbYesNo + vbQuestion) If lngAnswer = vbNo Then Exit Sub ActiveWorkbook.Save End Sub   如果显示在对话框中的内容较长,则可以在需要换行的位置插入vbCrLf或vbNewLine常量。运行下面的代码将显示如图1-29所示的对话框,整个内容分3行显示,处理方法是不断将不同的文本连接到strMessage变量上,最终该变量将存储所有叠加在一起的文本。 Sub MsgBox函数6() Dim strMessage As String strMessage = "是否需要保存?" & vbCrLf strMessage = strMessage & "保存请单击“是”按钮" & vbCrLf strMessage = strMessage & "不保存请单击“否”按钮" MsgBox strMessage, vbYesNo + vbQuestion End Sub 图1-29 将内容显示为多行 1.3.7 保存VBA代码   从Excel 2007开始,微软为保存VBA代码的工作簿提供了专门的格式,其文件扩展名是.xlsm。无论是在工作簿中录制的宏,还是手动编写的VBA代码,如需将其保存到工作簿中,需要在“另存为”对话框中将“保存类型”设置为“Excel启用宏的工作簿”,如图1-30所示。 图1-30 将“保存类型”设置为“Excel启用宏的工作簿” 1.4 变量、常量和数据类型   变量和常量都是使用特定的名称来代表具体的值,它们之间的主要区别是,变量的值可以在程序运行期间随时修改,而常量的值始终都是固定的,不能随意修改。数据类型是VBA可以处理的数据类别,例如文本、数值、日期、逻辑值等。本节将介绍在VBA程序中创建变量和常量的方法,以及如何检测数据类型并在不同的数据类型之间转换。 1.4.1 VBA支持的数据类型   VBA支持的数据类型如表1-4所示,每一种数据类型都有特定的取值范围并占用不同的内存空间。为了提高VBA程序的运行效率,编写VBA代码时需要考虑不同数据类型之间的差异,并在完成不同任务时使用最合适的数据类型。 表1-4 VBA支持的数据类型 数 据 类 型 取 值 范 围 占用的内存空间 Boolean True或False 2字节 Byte 0~255 1字节 Currency -922337203685477.5808~922337203685477.5807 8字节 Date 100年1月1日~9999年12月31日 8字节 续表 数 据 类 型 取 值 范 围 占用的内存空间 Integer -32768~32767 2字节 Long -2147483648~2147483647 4字节 Single 负数:-3.402823E38~-1.401298E-45 正数:1.401298E-45~3.402823E38 4字节 Double 负数:-1.79769313486232E308~-4.49065645841247E-324 正数:4.49065645841247E-324~1.79769313486232E308 8字节 Decimal 不带小数点:+/-79228162514264337593543950335 小数点右边有28位:+/-7.9228162514264337593543950335 最小的非零值:+/-0.0000000000000000000000000001 14字节 String(定长) 1~65400个字符 字符串的长度 String(变长) 0~20亿个字符 10字节+字符串长度 Object 任何对象的引用 4字节 Variant(字符型) 与String(变长)的范围相同 22字节+字符串长度 Variant(数字型) 与Double的范围相同 16字节 用户定义 各个组成部分的取值范围 各个部分的空间总和 1.4.2 声明变量   变量是计算机内存中的存储位置,用于存储在VBA程序运行期间需要处理的值,变量中的值可以随时修改。每个变量都有一个名称和一种数据类型,变量的数据类型如表1-4所示。   编写VBA代码时可以直接使用变量,而无须事先声明它。然而,在使用一个变量之前先声明它,可以提高程序的运行效率,而且也是一种良好的编程习惯。为了避免忘记事先声明变量而直接在程序中使用变量,可以强制声明变量,这样在发现包含未经声明就已经使用的变量时会显示错误提示。强制声明变量有以下两种方法: * 在模块顶部的声明部分输入Option Explicit语句。 * 使用1.2.5小节中的方法,在“选项”对话框的“编辑器”选项卡中勾选“强制变量声明”复选框。该方法会自动在新建模块的顶部添加Option Explicit语句,但是对现有模块无效。   声明变量可以使用Dim语句,下面的代码声明一个名为UserName的变量: Dim UserName   如需指定变量的数据类型,可以在Dim语句中使用As子句。下面的代码声明一个字符串类型的变量,其名称是UserName。 Dim UserName As String   注意:如果不使用As子句指定变量的数据类型,则变量的数据类型默认为Variant。为了节省内存空间并提高程序的运行效率,通常应该为变量指定一种数据类型。   下面的代码声明了两个变量,只有第二个变量的数据类型是Integer,第一个变量的数据类型是Variant。 Dim Row, Col As Integer   如果希望将上面的两个变量都声明为Integer数据类型,则需要在每个变量的后面加上As子句,代码如下: Dim Row As Integer, Col As Integer   提示:除了使用Dim语句声明变量之外,还可以使用Public、Private和Static语句,它们的主要区别是声明的变量具有不同的作用域和生存期,具体内容请参考1.4.5小节。   为变量指定数据类型时,可以使用数据类型的简写形式,从而减少代码输入量。数据类型的简写形式如表1-5所示。 表1-5 数据类型的简写形式 数 据 类 型 简 写 形 式 String $ Integer % Long & Single ! Double # Currency @      下面的代码将UserName声明为String数据类型: Dim UserName$   可以在声明变量时混合使用As语句和数据类型的简写形式。 Dim Row%, Col As Integer 1.4.3 变量的命名规则   声明变量时为变量起一个有意义的名称,对变量的使用有很大帮助。然而,在VBA中对用户创建的变量的名称有一些限制,具体如下: * 不能将VBA关键字用作变量名。关键字是VBA中的保留字,用于标识VBA中的特定语言元素,声明变量的Dim就是VBA中的一个关键字。 * 变量名的首字符必须使用英文字母或汉字。 * 变量名可以包含数字和下画线,但是不能包含空格、句点、叹号等符号。 * 变量名的长度不能超过255个字符。   由于在VBA中创建的大多数变量都具有特定的数据类型,为了通过变量名就能识别其数据类型,可以在变量名的开头添加由1~3个字符组成的表示数据类型的前缀。建议的前缀及其对应的数据类型如表1-6所示。 表1-6 标识数据类型的前缀 前 缀 数 据 类 型 前 缀 数 据 类 型 str String byt Byte int Integer dat Date 续表 前 缀 数 据 类 型 前 缀 数 据 类 型 lng Long cur Currency sng Single dec Decimal dbl Double var Variant bln Boolean udf 用户定义      下面的代码将strUserName变量声明为String数据类型,使用str前缀标识该变量的数据类型是String。 Dim strUserName As String 1.4.4 为变量赋值   声明后的每个变量都有一个初始值,不同数据类型的变量具有不同的初始值。Integer、Long、Single、Double等数值数据类型的变量的初始值是0,String数据类型的变量的初始值是零长度字符串,Boolean数据类型的变量的初始值是逻辑值False。   声明变量的目的是将值存储到变量中,然后在程序中使用变量代替实际值,并在需要时修改存储在变量中的值。将值存储在变量中的操作称为“为变量赋值”。如需为一个变量赋值,需要先输入变量的名称,然后在其右侧输入一个等号,再在等号的右侧输入一个值。下面的代码将“Admin”赋值给名为UserName的变量。 UserName = "Admin"   注意:如果UserName变量的数据类型是Integer、Long、Single、Double等,则会出现类型不匹配的错误,因为赋给UserName变量的值是一个字符串而非数值。 1.4.5 变量的作用域和生存期   变量的作用域是指可以使用变量的范围。如果在一个过程中声明变量,则该变量只能在声明它的过程中使用,将这种变量称为过程级变量。下面的代码在“打开文件”过程中声明了一个名为strFileName的变量,该变量只能在该过程中使用,不能在“保存文件”过程中使用。 Sub 打开文件() Dim strFileName As String Workbooks.Open strFileName End Sub Sub 保存文件() ActiveWorkbook.Save End Sub   注意:在同一个过程中声明的多个变量不能重名。   如果希望一个变量可以被当前模块中的所有过程使用,则需要在模块的顶部声明该变量,即在模块中的所有过程之外的位置声明变量,将这种变量称为模块级变量。下面的代码在两个过程的上方声明一个变量,此时两个过程都可以使用该变量。 Dim strFileName As String Sub 打开文件() Workbooks.Open strFileName End Sub Sub 保存文件() ActiveWorkbook.Save End Sub   声明模块级变量时,还可以使用Private语句,代码如下: Private strFileName As String   提示:如果声明了一个模块级变量和一个过程级变量,它们的名称相同,则在包含该同名变量的过程中优先使用过程级变量,而忽略模块级变量。   如果希望声明的变量可以被VBA工程中的所有模块中的所有过程使用,则需要在声明模块级变量时使用Public语句,代码如下: Public strFileName As String   使用Public语句声明的模块级变量还可以被其他VBA工程使用,使用前需要在其他VBA工程中添加对该变量所在工作簿的引用。只需单击菜单栏中的“工具”|“引用”命令,然后在“引用”对话框中勾选要引用的工作簿的VBA工程名的复选框,如图1-31所示。如果要引用的工作簿未打开,则可以单击“浏览”按钮打开该工作簿。   提示:如需为VBA工程设置一个有意义的名称,可以在工程资源管理器中右击VBA工程中的任意一项,在弹出的快捷菜单中选择“VBAProject属性”命令,然后在打开的对话框中修改“工程名称”文本框中的值,如图1-32所示。 图1-31 在一个VBA工程中引用其他VBA工程 图1-32 修改VBA工程的名称   变量的生存期是指在变量中存储的值的保存期限。变量的作用域直接影响变量的生存期。过程级变量中的值在过程结束时自动消失,模块级变量中的值在工作簿打开期间始终存在。如果希望过程级变量具有模块级变量的生存期,则可以在过程中使用Static语句将变量声明为静态变量,代码如下: Static lngSales As Long   如需将一个过程中的所有变量都变成静态变量,可以在Sub过程或Function过程的开头添加Static关键字。 Static Sub 打开文件() 1.4.6 使用常量   使用常量可以存储固定不变的值。在VBA中已经内置了很多常量,例如在MsgBox函数中使用的vbYesNo。除了VBA内置常量之外,用户还可以创建新的常量,将一些不易记忆的数字或文本存储到常量中,以后可以使用常量代替这些内容。   在VBA中使用Const语句声明常量,为了从外观上与变量区分,可以将常量名称全部大写。下面的代码声明一个名为PI的常量,在其中存储圆周率的值。 Const PI = 3.14159265   与变量类似,声明常量时也可以为其指定数据类型。下面的代码声明一个String数据类型的常量。 Const USER_NAME As String = "SongXiang"   常量也具有与变量类似的命名规则和作用域,此处不再赘述。 1.4.7 检测和转换数据类型   如需检测表达式或变量的数据类型,可以使用以Is开头的一系列VBA内置函数,如表1-7所示,这类函数的返回值是一个逻辑值True或False。 表1-7 检测数据类型的Is类函数 函 数 说 明 IsNumeric 检测表达式是否是数字,如果是则返回True,否则返回False IsDate 检测表达式是否是有效的日期,如果是则返回True,否则返回False IsObject 检测变量是否是对象变量或者包含对象引用,如果是则返回True,否则返回False IsArray 检测变量是否是数组,如果是则返回True,否则返回False IsEmpty 检测变量是否已经初始化,如果未初始化则返回True,否则返回False IsNull 检测表达式是否是Null值,如果是则返回True,否则返回False IsMissing 检测参数是否已经传递给过程,如果未传递则返回True,否则返回False IsError 检测表达式是否是错误值,如果是则返回True,否则返回False      下面的代码声明一个String数据类型的变量,然后将一个数字以字符串的形式赋值给该变量,最后使用IsNumeric函数检测该变量的值是否是数字,结果返回True,如图1-33所示。 Sub 检测数据类型() Dim Num As String Num = "168" MsgBox IsNumeric(Num) End Sub 图1-33 IsNumeric函数检测结果   使用VBA内置的TypeName函数可以返回一个表示数据类型的文本,当无法确定变量中存储的值是什么数据类型时可以使用该函数。将上面示例中的代码修改为以下形式,运行结果如图1-34所示,证实Num变量中的值是字符串。 Sub 检测数据类型() Dim Num As String Num = "168" MsgBox TypeName(Num) End Sub 图1-34 使用TypeName函数返回表示数据类型的文本   如需转换表达式的数据类型,可以使用以C开头的一系列VBA内置函数,例如CBool、CByte、CStr、CInt、CLng、CSng、CDbl、CDate、CCur、CVar等,通过函数名称的拼写可以很容易了解函数将表达式转换成哪种数据类型。   下面的代码与上一个示例类似,唯一区别是使用Cint函数将Num变量的数据类型转换为Integer,然后使用TypeName函数证实数据类型是否转换成功。 Sub 转换数据类型() Dim Num As String Num = "168" MsgBox TypeName(CInt(Num)) End Sub   注意:如果转换前的值超出目标数据类型支持的范围,则会出现错误。 1.5 创建和调用Sub过程   无论一个VBA程序包含多少行代码,所有代码都会被组织到一个或多个过程中,这些过程存储在不同的模块中。使用过程可以将复杂的代码分解成多个更小的逻辑单元,以便于代码的编写和调试。在VBA中最常使用的是Sub过程和Function过程,前者也称为子过程,后者也称为函数过程。在Excel中录制的宏是Sub过程,用户可以在VBE中创建Sub过程,从而实现比录制的宏更灵活、更强大的功能。本节将介绍创建和调用Sub过程的方法。 1.5.1 创建Sub过程   一个Sub过程由Sub语句开始,End Sub语句结束,在这两个语句之间编写实现特定功能的VBA代码。Sub过程的语法如下: [Private | Public] [Static] Sub name [(arglist)] [statements] [Exit Sub] [statements] End Sub * Private(可选):声明一个模块级的Sub过程,该过程只能被其所在模块中的过程调用,不能被其他模块中的过程调用。 * Public(可选):声明一个工程级的Sub过程,任何VBA工程中的任何过程都可以调用该过程。如果在模块顶部添加Option Private Module语句,则会强制将Sub过程变成模块级过程。 * Static(可选):将Sub过程中的所有变量指定为静态变量。 * Sub(必需):标志Sub过程的开始。 * name(必需):Sub过程的名称,与变量的命名规则相同。 * arglist(可选):为Sub过程提供的一个或多个参数,参数之间使用逗号分隔。参数是Sub过程需要处理的数据,实际上参数就是变量,只不过在将变量传递给过程时,将变量称为参数,所以参数也具有与变量相同的数据类型。如果不提供参数,则必须保留一对空括号。 * statements(可选):实现特定功能的VBA代码。 * Exit Sub(可选):退出Sub过程。 * End Sub(必需):标志Sub过程的结束。   可以手动输入Sub过程,也可以使用“添加过程”对话框中的选项创建Sub过程。如需手动输入Sub过程,可以先打开一个模块的代码窗口,然后在其中输入Sub和一个名称,按Enter键后,会自动在过程名称右侧添加一对小括号以及End Sub语句。 Sub 测试() End Sub   现在可以在Sub和End Sub之间输入所需的VBA代码了。   如需使用“添加过程”对话框创建Sub过程,可以单击菜单栏中的“插入”|“过程”命令,打开“添加过程”对话框,在“名称”文本框中输入Sub过程的名称,然后在“类型”中选择“子程序”,还可以在“范围”中选择创建的Sub过程是模块级还是工程级的,最后单击“确定”按钮,如图1-35所示。 图1-35 使用“添加过程”对话框创建Sub过程   提示:用户创建的不包含参数的Sub过程会与录制的宏同时显示在“宏”对话框中。在“宏”对话框中不显示用户创建的Sub过程有两种方法,一种是在创建过程时使用Private关键字,将过程创建为模块级的;另一种是在过程名右侧的小括号中添加至少一个参数。 1.5.2 调用Sub过程   将一个VBA程序实现的所有功能分解到多个Sub过程中,每个Sub过程只实现一种特定的功能,这种离散的组织方式为代码的编写和测试提供了很大的灵活性。在所有Sub过程中,总有一些Sub过程实现的是可能被反复使用的功能,此时无须在每个需要这种功能的地方重复编写代码,而可以直接调用实现该功能的Sub过程。   调用Sub过程最简单的方法是直接输入过程名。如果过程包含参数,则需要在过程名的右侧输入所需的参数值。如果有多个参数,则各个参数之间使用逗号分隔。下面的代码调用名为“OpenFile”的Sub过程,并为其提供FileName和ReadOnly两个参数的值。 Sub Main() OpenFile "第1章.xlsm", False End Sub Sub OpenFile(FileName As String, ReadOnly As Boolean) End Sub   调用Sub过程的另一种方法是使用Call语句。首先输入Call,然后输入过程名。如果过程包含参数,则需要将所需的参数放在过程名右侧的一对小括号中,各个参数之间使用逗号分隔。下面的代码使用Call语句调用上一个示例中的OpenFile过程。 Call OpenFile("第1章.xlsm", False)   如果在VBA工程中存在多个同名的Sub过程,则在调用它们时,需要在过程名的开头添加过程所在的模块名和一个英文句点,格式如下: 模块名.过程名   如需调用其他VBA工程中的Sub过程,可以先在VBE的“引用”对话框中添加对该工程的引用,然后使用以下格式的代码调用该工程中的Sub过程。 工程名.模块名.过程名 1.5.3 按地址或按值传递参数   创建Sub过程时,可以在过程名右侧的小括号中添加所需的一个或多个参数,并为这些参数指定合适的数据类型。传递参数有“按地址”和“按值”两种方式,创建Sub过程时可以指定参数的传递方式,在参数名的左侧使用ByRef关键字表示按地址传递,使用ByVal关键字表示按值传递,省略这两个关键字时默认按地址传递。下面的Sub过程包含一个按值传递的参数。 Sub OpenFile(ByVal FileName As String) End Sub   按地址传递参数时,如果过程改变参数的值,则这种改变会直接影响变量本身。按值传递参数时,如果过程改变参数的值,则对变量本身没有任何影响。   下面的代码创建一个名为MySum的过程,该过程有一个按地址传递的参数,该过程执行的操作是对参数加1。 Sub MySum(ByRef Num As Integer) Num = Num + 1 End Sub   下面的代码在“测试”过程中声明一个名为intNum的变量,然后调用MySum过程,并将该变量传递给该过程,最后显示该变量在MySum过程中执行加1计算后返回“测试”过程时的值。由于MySum过程的Num参数是按地址传递的,所以在MySum过程内部执行加1计算的结果会影响intNum变量本身,当intNum变量返回“测试”过程后其值是1,如图1-36所示。 Sub 测试() Dim intNum As Integer MySum intNum MsgBox intNum End Sub 图1-36 按地址传递参数的结果   如果使用ByVal关键字将MySum过程中的Num参数指定为按值传递,则在“测试”过程中intNum变量的值最后会显示为0。这是因为在MySum过程中无论对参数执行什么计算,按值传递时的计算结果都不会影响传递给MySum过程的变量本身。 1.5.4 Sub过程的递归   递归是指在一个Sub过程中调用该Sub过程,即一个过程调用其自身。任何一个过程都可以递归,关键在于需要在满足条件时退出递归,否则会进入无限循环。   运行下面的代码将显示一个对话框,在其中输入一个名称,系统会检测输入的是否是“admin”。如果不是,则显示提示信息并通过调用该过程重新显示对话框,要求用户重新输入用户名。如果输入的是“admin”,则显示“登录成功”的提示信息并退出程序。 Sub 用户登录() Dim strUserName As String strUserName = InputBox("输入用户名:") If strUserName = "admin" Then MsgBox "登录成功!" Exit Sub Else MsgBox "用户名不正确,请重新输入!" Call 用户登录 End If End Sub   注意:只有输入与admin大小写完全匹配的英文字母,才会退出程序。如果希望输入任意大小写形式都能与admin匹配,则可以将If语句中的判断条件改为以下形式,使用VBA内置的LCase函数将用户输入的内容全部转换为小写字母。 If LCase(strUserName) = "admin" Then   也可以使用UCase函数将用户输入的内容全部转换为大写字母,此时需要将等号右侧的内容改为全部大写的ADMIN。 1.6 创建和调用Function过程   在本章前面介绍InputBox函数时,该函数会返回一个值,该值表示用户在对话框中输入的内容。MsgBox函数也有一个返回值,表示用户在对话框中单击了哪一个按钮。如果用户想要创建带有返回值的过程,则需要创建Function过程。是否带有返回值是Function过程和Sub过程的主要区别,该区别导致Function过程在语法和使用等方面与Sub过程存在一些差异。本节将介绍创建和调用Function过程的方法,还将介绍在VBA中使用VBA内置函数和Excel工作表函数的方法。 1.6.1 创建Function过程   Function过程的语法与Sub过程类似,主要区别在于对返回值的处理。一个Function过程由Function语句开始,End Function语句结束,在这两个语句之间编写实现特定功能的VBA代码。Function过程的语法如下: [Public | Private] [Static] Function name [(arglist)] [As type] [statements] [name = expression] [Exit Function] [statements] [name = expression] End Function * Public(可选):声明一个工程级的Function过程,任何VBA工程中的任何过程都可以调用该过程。如果在模块顶部添加Option Private Module语句,则会强制将Function过程变成模块级过程。 * Private(可选):声明一个模块级的Function过程,该过程只能被其所在模块中的过程调用,不能被其他模块中的过程调用。 * Static(可选):将Function过程中的所有变量指定为静态变量。 * Function(必需):标志Function过程的开始。 * name(必需):Function过程的名称,与变量的命名规则相同。 * arglist(可选):为Function过程提供的一个或多个参数,参数之间使用逗号分隔。参数是Function过程需要处理的数据。如果不提供参数,则必须保留一对空括号。 * type(可选):为Function过程的返回值指定数据类型,与变量的数据类型相同。 * statements(可选):实现特定功能的VBA代码。 * expression(可选):Function过程的返回值。如果希望Function过程可以返回一个值,则需要在Function过程结束前将其返回值赋值给Function过程的名称。 * Exit Function(可选):退出Function过程。 * End Function(必需):标志Function过程的结束。   创建Function过程的步骤与Sub过程类似,可以在代码窗口中手动输入Function和End Function,也可以使用“添加过程”对话框自动输入这两个语句,此时需要在该对话框的“类型”中选择“函数”选项,其他选项与Sub过程相同。   下面的代码创建一个计算两个数字之和的Function过程,该过程有两个参数,它们表示参与计算的两个数字,将该过程的返回值指定为Single数据类型,因为参与计算的数字可能是小数。 Function MySums(Num1, Num2) As Single MySums = Num1 + Num2 End Function 1.6.2 调用Function过程   调用Function过程的方法与调用Sub过程类似,包括Function过程的作用域规则和调用时的书写格式等方面。用户创建的Function过程的使用方法与VBA内置函数相同,可以使用连接运算符将Function过程与其他表达式组合在一起,也可以将Function过程的返回值赋给一个变量,然后使用该变量表示Function过程的返回值,以简化代码的输入量。   假设在1.6.1小节中创建的MySums过程位于名为“创建Function过程”的模块中,下面的代码在位于另一个模块的“测试”过程中调用MySums过程,并将其与一个字符串组合在一起,以便将它们显示在由MsgBox函数创建的对话框中,如图1-37所示。 Sub 调用Function过程() MsgBox "两个数字的总和是:" & 创建Function过程.MySums(2, 6) End Sub 图1-37 调用Function过程   如需在代码中处理Function过程的返回值并减少代码的输入量,可以将Function过程的返回值赋给一个变量,然后在代码中使用该变量表示Function过程的返回值。下面的代码将MySums过程的返回值赋给sngSum变量,然后在If语句中判断该变量的大小并显示指定的信息。 Sub 调用Function过程2() Dim sngSum As Single sngSum = 创建Function过程.MySums(2, 6) If sngSum < 10 Then MsgBox "给定的两个数字太小了!" End Sub   如果创建Function过程时,在Function的开头添加Public关键字或者省略该关键字,则创建的Function过程可以在工作表中使用,就像使用Excel内置的工作表函数一样。仍以前面创建的MySums过程为例,在工作表的A1和B1两个单元格中分别输入一个数字,然后在C1单元格中输入以下公式并按Enter键,将在C1单元格中显示两个数字之和,如图1-38所示。 =MySums(A1,B1) 图1-38 在工作表中使用用户创建的Function过程   如果不想在工作表中使用由用户创建的Function过程,则可以在创建Function过程时添加Private关键字,将其创建为模块级的Function过程。 1.6.3 使用VBA内置函数   用户创建的Function过程通常用于完成特定的计算或操作。实际上,VBA已经内置了大量的Function过程,它们都是VBA内置的函数,这些函数可以执行很多数学和日期方面的计算,还可以处理字符串、文件和文件夹等。   如果想要使用某个VBA内置函数但是不知道它的英文拼写,则可以先输入VBA和一个英文句点,然后在弹出的列表中选择所需的函数,如图1-39所示。输入函数后,可以将开头的VBA和英文句点删除。 图1-39 在列表中选择VBA内置函数   当用户创建的Function过程与VBA内置的函数同名时,如果直接输入Function过程的名称,则使用的是用户创建的Function过程。如需使用同名的VBA内置函数,需要在Function过程的开头添加VBA和一个英文句点。 1.6.4 在VBA中使用Excel工作表函数   在VBA代码中除了可以使用VBA内置函数和用户创建的Function过程之外,还可以使用Excel工作表函数。下面的代码就是使用Excel工作表函数统计A1:A10单元格区域中非空单元格的个数。 Application.WorksheetFunction.CountA(Range("A1:A10"))   Application对象的WorksheetFunction属性返回WorksheetFunction对象,该对象的方法由Excel工作表函数组成。在VBA代码中输入Application.WorksheetFunction.后,也会弹出一个列表,可以从中选择所需的工作表函数。由于WorksheetFunction是全局成员,所以在代码中可以省略对Application对象的引用,上面的代码可以改写成以下形式: WorksheetFunction.CountA(Range("A1:A10"))   需要注意的是,如果一个Excel工作表函数与某个VBA内置函数的功能相同,则在VBA中无法使用该工作表函数。例如,工作表函数UPPER的功能是将英文字母转换为大写,它与VBA内置的UCase函数的功能相同,则在VBA中不能使用UPPER函数。 1.7 有选择地执行代码   在Excel中录制的宏只能按照用户的操作顺序逐行执行代码,这意味着录制的宏无法灵活处理可能出现的各种情况。在VBA中可以使用If Then Else和Select Case两个语句为程序加入条件判断机制,根据判断结果有选择地执行所需的代码。本节将介绍这两个语句的使用方法。 1.7.1 使用If Then Else语句根据条件选择要执行的代码   使用If Then Else语句可以根据条件是否成立执行一行或多行代码,该语句有单行或多行两种形式。   1. 单行If Then Else语句   如果只有一个条件且只执行少量的操作,则可以使用单行If Then Else语句,格式如下: If 条件 Then 条件成立时执行的代码 Else 条件不成立时执行的代码   下面的代码检测用户输入的数字是否大于10,如果是,则显示“符合要求”,否则显示“数字太小”。 Sub 单行If语句() Dim intNum As Integer intNum = Val(InputBox("输入一个整数:")) If intNum > 10 Then MsgBox "符合要求" Else MsgBox "数字太小" End Sub   如果只在条件成立时才执行操作,则可以省略Else子句。下面的代码只在数字小于10时显示“不符合要求”,其他情况不显示任何信息。 Sub 单行If语句2() Dim intNum As Integer intNum = Val(InputBox("输入一个整数:")) If intNum < 10 Then MsgBox "不符合要求" End Sub   2. 多行If Then Else语句   很多复杂问题通常需要在条件成立或不成立时执行多个操作,此时可以使用多行If Then Else语句,格式如下: If 条件 Then 条件成立时执行的一行或多行代码 Else 条件不成立时执行的一行或多行代码 End If   下面的代码是本章前面的一个示例,用于检查用户输入的用户名,如果符合要求,则显示欢迎信息并退出程序,否则显示错误信息并要求用户重新输入用户名。无论条件是否成立,都需要分别执行两行代码。 Sub 多行If语句() Dim strUserName As String strUserName = InputBox("输入用户名:") If strUserName = "admin" Then MsgBox "登录成功!" Exit Sub Else MsgBox "用户名不正确,请重新输入!" Call 用户登录 End If End Sub   提示:与单行If Then Else语句类似,如果条件不成立时不需要执行任何操作,则可以省略Else子句。单行If Then Else语句在条件成立和不成立时也可以执行多行代码,只需将多行代码写在同一行,并在各句代码之间使用英文冒号进行分隔。   处理复杂条件时,可以在多行If Then Else语句的内部嵌套另一个或多个If Then Else语句,每一个If Then Else语句中的If和End If必须成对出现,格式如下: If 第1个条件 Then 第1个条件成立时执行的代码 Else If 第2个条件 Then 第2个条件成立时执行的代码 Else If 第n个条件 Then 第n个条件成立时执行的代码 Else 第n个条件不成立时执行的代码 End If 第2个条件不成立时执行的代码 End If 第1个条件不成立时执行的代码 End If   下面是一个嵌套If Then Else语句的示例,它也是本章前面的一个示例。 Sub 多行If语句2() Dim strUserName As String strUserName = InputBox("输入用户名:") If strUserName = "Admin" Then MsgBox "你是管理员" Else If strUserName = "User" Then MsgBox "你是普通用户" Else MsgBox "你不是有效用户" End If End If End Sub   如需在多个类似的条件中检查是否符合其中某个条件并执行相应的代码,则可以使用下面的格式: If 第1个条件 Then 第1个条件成立时执行的代码 ElseIf 第2个条件 Then 第1个条件不成立但第2个条件成立时执行的代码 ElseIf 第n个条件 Then 前n-1个条件都不成立但第n个条件成立时执行的代码 Else 前面所有条件都不成立时执行的代码 End If   这种结构非常适合检测多个值中是否存在与指定值相匹配的值,并执行预先为各个值编写好的代码。下面的代码检测用户输入的数字,如果大于100,则显示“数字太大”;如果小于100,则显示“数字太小”,否则显示“数字有效”。 Sub 多行If语句3() Dim intNum As Integer intNum = Val(InputBox("输入一个数字:")) If intNum > 100 Then MsgBox "数字太大" ElseIf intNum < 10 Then MsgBox "数字太小" Else MsgBox "数字有效" End If End Sub   实际上,实现上述需求更适合使用Select Case语句,将在1.7.2小节中介绍。 1.7.2 使用Select Case语句根据表达式的值执行符合条件的代码   使用Select Case语句可以根据表达式的值从多个条件中检测是否存在符合的条件,并执行与第一个符合的条件关联的代码。Select Case语句的格式如下: Select Case 表达式 Case 与表达式进行比较的第1个值 满足第1个值时执行的代码 Case 与表达式进行比较的第2个值 满足第2个值时执行的代码 Case 与表达式进行比较的第n个值 满足第n个值时执行的代码 Case Else 不满足前面所有值时执行的代码 End Select   下面是使用Select Case语句重新编写1.7.1小节最后一个示例之后的代码,代码看起来更清晰。在Case子句中使用Is关键字表示intNum变量的值,将其与特定的值进行比较来构建判断条件。 Sub SelectCase语句() Dim intNum As Integer intNum = Val(InputBox("输入一个数字:")) Select Case intNum Case Is > 100 MsgBox "数字太大" Case Is < 10 MsgBox "数字太小": Case Else MsgBox "数字有效" End Select End Sub   如需减少代码的行数,可以将每个Case子句中的两行代码写在一行上: Sub SelectCase语句() Dim intNum As Integer intNum = Val(InputBox("输入一个数字:")) Select Case intNum Case Is > 100: MsgBox "数字太大" Case Is < 10: MsgBox "数字太小": Case Else: MsgBox "数字有效" End Select End Sub   提示:上面的示例使用Is关键字指定范围的一端,还可以使用To关键字指定范围的两端,例如Case 10 To 20。   如果需要比较的不是范围而是固定的值,则可以将固定的值写在Case子句中。下面的代码根据用户输入的内容显示不同的欢迎信息。 Sub SelectCase语句2() Select Case LCase(InputBox("输入用户名:")) Case "admin" MsgBox "你好,管理员" Case "user" MsgBox "你好,普通用户" Case Else MsgBox "无效用户" End Select End Sub   可以在一个Case子句中指定需要比较的多个值,在各个值之间使用英文逗号分隔。只要表达式与其中任意一个值匹配,就执行该Case子句中的代码。下面的代码检测当前Excel程序的版本,在第一个Case子句中指定了多个值,如果表达式的值与其中任意一个值匹配,则显示“Excel 2003之后的版本”,否则显示“Excel 2003或更早版本”的提示信息。 Sub SelectCase语句3() Select Case Application.Version Case "16.0", "15.0", "14.0", "12.0" MsgBox "Excel 2003之后的版本" Case Else MsgBox "Excel 2003或更早版本" End Select End Sub   与嵌套的If Then Else语句类似,Select Case语句也可以互相嵌套,还可以将If Then Else和Select Case两个语句嵌套在一起,从而构建可以处理复杂条件的代码。 1.8 重复执行代码   编程解决的主要问题之一是可以自动处理需要重复执行的操作,最大限度地提高效率并减少人为失误。在VBA中可以使用For Next和Do Loop两个语句处理需要重复执行的操作,本节将介绍它们的使用方法。 1.8.1 使用For Next语句重复执行代码指定的次数   如果事先知道代码需要重复执行的次数,则可以使用For Next语句,该语句的语法如下: For counter = start To end [Step step] [statements] [Exit For] [statements] Next [counter] * counter(必需):计数器,其值在循环期间将不断递增或递减。 * start(必需):计数器的起始值。 * end(必需):计数器的终止值。 * Step step(可选):计数器的步长,即计数器每次递增或递减的增量。以大写字母S开头的Step是VBA中的关键字,以小写字母s开头的step在编写代码时需要换成实际值。如果步长是1,则可以写成Step 1。由于1是步长的默认值,所以可以省略Step 1。 * statements(可选):实现特定功能的VBA代码。 * Exit For(可选):退出For Next循环。   下面的代码计算自然数1~10的所有数字之和。由于两个相邻自然数的差值是1,所以本例中的For Next语句的步长是1,辨析代码时可将其省略。 Sub ForNext语句() Dim intCounter As Integer, intSum As Integer For intCounter = 1 To 10 intSum = intSum + intCounter Next intCounter MsgBox "数字的总和是:" & intSum End Sub   如需计算1~10中的所有偶数之和,可以将步长值设置为2,并将计数器的起始值设置为0,代码如下: Sub ForNext语句2() Dim intCounter As Integer, intSum As Integer For intCounter = 0 To 10 Step 2 intSum = intSum + intCounter Next intCounter MsgBox "所有偶数的总和是:" & intSum End Sub   如果希望求和的数字范围完全由用户指定,则可以使用InputBox函数接收用户输入的数字,然后将其返回值指定为计数器的起始值和终止值。为了避免在输入不能转换为数字的字符串时程序出错,需要使用VBA内置的IsNumeric函数检测用户两次输入的数字是否都是数字,如果都是数字,才会使用For Next语句对数字求和。使用And运算符连接两个IsNumeric函数可以实现同时满足是数字的条件。 Sub ForNext语句3() Dim intCounter As Integer, intSum As Integer Dim intStart As String, intEnd As String intStart = InputBox("输入数字范围的下限") intEnd = InputBox("输入数字范围的上限") If IsNumeric(intStart) And IsNumeric(intEnd) Then For intCounter = intStart To intEnd intSum = intSum + intCounter Next intCounter MsgBox "指定范围内的所有数字的总和是:" & intSum End If End Sub 1.8.2 使用Do Loop语句在满足条件时重复执行代码   如果事先无法确定代码需要重复执行的次数,但是知道在什么情况下开始或结束重复执行代码,此时应使用Do Loop语句。在Do Loop语句中使用While或Until设置开始或结束循环的条件,共有4种形式。   1. 先检测条件是否成立,如果成立则开始循环 Do While 条件 条件成立时执行的代码 Loop   下面的代码说明了这种Do Loop形式的工作方式,在开始Do Loop循环之前,先检测用户输入的数字是否小于10,如果小于10,则执行Do Loop中的代码,每循环一次intNum变量都加1,直到intNum变量的值等于10为止。如果最初输入的数字大于或等于10,则跳过Do Loop语句,直接在对话框中显示该数字。 Sub DoLoop语句() Dim intNum As Integer intNum = Val(InputBox("输入一个数字:")) Do While intNum < 10 intNum = intNum + 1 Loop MsgBox intNum End Sub   2. 循环一次后检测条件是否成立,如果成立则继续循环 Do 条件成立时执行的代码 Loop While 条件   下面的代码说明了这种Do Loop形式的工作方式,修改了上一个示例中的代码,此时无论用户输入的是什么数字,都会先进入Do Loop循环对该数字加1,然后判断数字是否小于10,如果小于10,则继续对该数字加1,直到数字等于10为止,否则不再执行Do Loop中的代码,而直接在对话框中显示intNum变量的当前值。 Sub DoLoop语句2() Dim intNum As Integer intNum = Val(InputBox("输入一个数字:")) Do intNum = intNum + 1 Loop While intNum < 10 MsgBox intNum End Sub   当在对话框中输入的数字大于或等于10时,本例最后在对话框中显示的数字会比上一个示例的数字大1,这是因为在本例中无论输入多大的数字,都会先执行加1的计算。   3. 先检测条件是否成立,如果成立则结束循环 Do Until 条件 条件不成立时执行的代码 Loop   下面的代码说明了这种Do Loop形式的工作方式,本例代码的运行结果与使用第一种Do Loop形式的示例相同。在代码的编写方式上有两个细微的区别,将While改成Until,然后将条件改成相反的条件,即将“小于10”改成“大于或等于10”。 Sub DoLoop语句3() Dim intNum As Integer intNum = Val(InputBox("输入一个数字:")) Do Until intNum >= 10 intNum = intNum + 1 Loop MsgBox intNum End Sub   4. 循环一次后检测条件是否成立,如果成立则结束循环 Do 条件不成立时执行的代码 Loop Until 条件   下面的代码说明了这种Do Loop形式的工作方式,本例与使用第二种Do Loop形式的示例在代码的编写方式上也有与上一个示例类似的两个区别。 Sub DoLoop语句4() Dim intNum As Integer intNum = Val(InputBox("输入一个数字:")) Do intNum = intNum + 1 Loop Until intNum >= 10 MsgBox intNum End Sub   无论使用以上哪种形式的Do Loop语句,都可以使用Exit Do语句立刻结束循环并执行Loop语句后面的代码,这给在Do Loop语句中加入额外的判断条件提供了机会。   下面的代码在Do Loop语句中首先执行显示接收用户输入的对话框的代码,然后使用If Then语句判断是否在未输入内容的情况下关闭了对话框,如果是,则使用Exit Do语句立刻结束Do Loop循环;如果不是,则检测输入的用户名是否是admin。如果是admin,则在对话框中显示“欢迎登录系统”,否则重新显示接收用户输入的对话框,用户需要再次输入用户名并重复之前的检测。 Sub DoLoop语句5() Dim strUserName As String Do strUserName = InputBox("输入用户名:") If strUserName = "" Then Exit Do Loop While LCase(strUserName) <> "admin" If strUserName <> "" Then MsgBox "欢迎登录系统" End Sub 1.9 对象编程   在Excel中编写的VBA代码几乎都是在处理各种对象。Excel中的对象代表Excel应用程序中的各种元素,工作簿、工作表、单元格、字体、图片、形状、图表等都是对象,Excel应用程序本身也是一个对象。编程处理Excel中的对象不但可以使界面环境中的手动操作变成自动执行,还可以完成很多在界面环境中无法实现的功能。本节将介绍在VBA中进行对象编程的基本概念和一般方法,编程处理Excel中的具体对象的方法将在后续章节中详细介绍。 1.9.1 Excel对象模型   为了通过编程完整控制Excel应用程序,微软提供了一个与Excel应用程序中的各个元素完全对应的Excel对象模型,该模型包含可以编程处理的所有Excel对象以及对象之间的关系。Excel对象模型就像一张Excel应用程序中所有对象的关系脉络图。   Application对象在Excel对象模型中是最顶层的对象,它代表Excel应用程序,该对象是其他所有对象的发源地。在Excel应用程序中创建的每一个工作簿都是一个Workbook对象,在工作簿中创建的每一个工作表都是一个Worksheet对象,工作表中的任意单元格或单元格区域都是Range对象。   上面简单介绍的几个对象的层次结构可以表示为以下形式,它们的级别从左到右逐层下降。这几个对象是Excel对象模型中最重要、使用率最高的对象,通过它们可以延伸出其他对象,直到蔓延至整个对象模型。 Application=>=>Workbook=>Worksheet=>Range   使用VBE中的对象浏览器可以查看Excel对象模型中的所有对象。打开对象浏览器有以下几种方法: * 单击菜单栏中的“视图”|“对象浏览器”命令。 * 单击“标准”工具栏中的“对象浏览器”按钮。 * 按F2键。   打开的对象浏览器如图1-40所示。如要查看Excel对象模型中的所有对象,可以在顶部的下拉列表中选择Excel,在“类”列表框中会显示所有Excel对象。选择一个Excel对象,右侧会显示该对象的属性、方法和事件,图标表示对象的属性、图标表示对象的方法,图标表示对象的事件。 图1-40 对象浏览器   提示:严格地说,在“类”列表框中显示的是每个对象的类。为了易于理解“类”的概念,可以将类看作对象的模板,每个对象都是通过类创建的,与从工作簿模板创建工作簿的方式类似。由类创建的对象称为类的实例。每一个类具有哪些属性、方法和事件是事先被定义好的,使用类创建多个对象后,可以为这些对象的同一个属性设置不同的值,使每个对象具有自己的特征,也可以使用同一种方法操作对象,但是为方法提供不同的参数,从而得到不同的操作结果。 1.9.2 引用集合中的对象   当同一种对象的数量不止一个时,这些对象就构成了集合。例如,在Excel中打开的所有工作簿构成了工作簿集合,在Excel对象模型中表示为Workbooks。一个工作簿中的所有工作表构成了工作表集合,在Excel对象模型中表示为Worksheets。   如需引用集合中的某个对象,可以使用两种方法。如果集合中的所有对象都有一个唯一的名称,则可以使用名称引用对象。如果当前工作簿包含名为2021、2022和2023的3个工作表,则下面的代码引用名为“2023”的工作表。 Worksheets("2023")   如果名为“2023”的工作表是第3个工作表,则还可以使用索引号引用该工作表。 Worksheets(3)   注意:如需一直引用某个工作表,则应该使用名称而非索引号来引用这个工作表,因为索引号会根据工作表位置的变动而改变。   如果要引用的工作表所属的工作簿不是当前工作簿,则需要在代码中添加对特定工作簿的引用。 Workbooks("总公司").Worksheets("2023") 1.9.3 使用对象变量引用对象   在VBA支持的数据类型中有一个Object类型,它表示一般对象类型。当无法确定对象的具体类型时,可以将变量声明为Object数据类型。编程处理Excel对象时,通常不会使用Object数据类型,而是将变量声明为Excel中的特定对象类型。将声明为一般或特定对象类型的变量称为对象变量。下面的代码将名为wks的变量声明为Worksheet对象类型,以后可以在代码中使用该对象变量引用一个工作表。 Dim wks As Worksheet   将一个变量声明为特定的对象类型后,还需要使用Set语句将特定的对象赋值给该变量,然后才能在代码中使用该变量引用特定的对象。下面的代码将当前工作簿中名为“2023”的工作表赋值给wks变量,以后可以使用wks变量引用该工作表。 Set wks = Worksheets("2023")   使用对象变量引用对象不仅可以减少代码的输入量,还可以提高程序的运行效率。当不再使用某个对象变量时,可以使用Set语句将Nothing赋值给该对象变量,清除在对象变量中引用的对象,使对象变量恢复到赋值前的状态。 Set wks = Nothing   在实际编程中,为了避免使用一个没有引用实际对象的对象变量而导致程序出错,可以使用Is关键字配合Nothing来检测一个对象变量是否正在引用一个对象。下面的代码检测wks对象变量是否是Nothing,如果是,说明该对象变量没有引用任何特定的对象,此时将退出Sub过程。 If wks Is Nothing Then Exit Sub   如需在对象变量不是Nothing时执行操作,可以在上面的代码中添加Not关键字。 If Not wks Is Nothing Then Exit Sub 1.9.4 对象的属性   属性是对象具有的特征。例如,Worksheet对象有一个Name属性,该属性表示工作表的名称。如需修改工作表的名称,可以为Name属性设置一个值,就像为变量赋值一样。下面的代码将名为“2023”的工作表的名称设置为“2023年”。 Worksheets("2023").Name = "2023年"   提示:如果当前工作簿中没有名为“2023”的工作表,则运行代码时会出错。   除了可以设置属性的值之外,还可以使用属性的值。下面的代码显示当前工作簿中第1个工作表的名称。 MsgBox Worksheets(1).Name   如需在代码中多次使用属性的值,可以将它赋值给一个变量,然后使用变量代表属性的值。下面的代码将当前工作簿中第1个工作表的名称存储在strSheetName变量中。 strSheetName = Worksheets(1).Name   对象的很多属性返回的是一个值,例如上面代码中的Worksheet对象的Name属性。然而,对象的某些属性返回的也可能是另一个对象。下面的代码将A1单元格的字体设置为“黑体”。 Range("A1").Font.Name = "黑体"   Range("A1")是一个Range对象,Font是该对象的属性,它表示单元格的字体格式(字体、字号、颜色等)。为Range对象使用Font属性后,返回的是一个Font对象。Font后面的Name是Font对象的属性,表示字体的名称。上面的代码中位于等号左侧的3个部分连在一起,它们的运作方式是:先使用Range对象的Font属性返回一个Font对象,然后使用Font对象的Name属性设置字体名称。编写VBA代码处理Excel应用程序时经常会遇到这种情况。 1.9.5 对象的方法   方法是对象可以执行的操作。Workbook对象有一个Close方法,该方法用于关闭工作簿。下面的代码是关闭名为“总公司”的工作簿。 Workbooks("总公司").Close   提示:如果当前没有打开名为“总公司”的工作簿,则运行代码时会出错。   如果工作簿中存在未保存的内容,则运行上面的代码会显示一个对话框,询问用户是否保存工作簿,选择后才会关闭工作簿。如果在关闭工作簿时不想被这个对话框打扰,无论工作簿中是否存在未保存的内容,都自动保存并关闭工作簿,则可以使用下面的代码,将Close方法的SaveChanges参数设置为True。 Workbooks("总公司").Close True   如需不保存修改而直接关闭工作簿,可以将True改为False。如果希望代码的含义更清晰,则可以使用命名参数,与本章前面介绍MsgBox函数时使用命名参数的方法相同。下面的代码使用命名参数的方式指定SaveChanges参数的值,在参数名及其值之间添加一个冒号和一个等号。 Workbooks("总公司").Close SaveChanges:=True   与属性类似,对象的某些方法也可以返回另一个对象。例如,Worksheets对象的Add方法将新建一个工作表,并返回一个Worksheet对象,该对象表示这个新建的工作表。为了在后面的代码中使用由Add方法新建的工作表,可以将该方法返回的Worksheet对象赋值给一个对象变量。   下面的代码是使用Worksheets对象的Add方法在当前工作簿中新建一个工作表,并将其返回的Worksheet对象赋值给wks对象变量,现在wks对象变量表示这个新建的工作表。然后为wks对象变量设置Name属性以修改新工作表的名称。 Sub 对象的方法() Dim wks As Worksheet Set wks = Worksheets.Add wks.Name = "2024" End Sub 1.9.6 父对象和子对象   Excel对象模型中的各个对象构成了复杂的层次结构,它就像一条线路图,可以在编写代码时为找到特定的对象提供方向。在Excel对象模型中,Application对象处于最顶层,比其他所有对象的级别都高。   Workbook对象位于Application对象的下一层,Worksheet对象位于Workbook对象的下一层。在相邻的上下两层对象中,将上一层对象称为父对象,将下一层对象称为子对象。所以Application对象是Workbook对象的父对象,Workbook对象是Application对象的子对象,而Workbook对象又是Worksheet对象的父对象。很多Excel对象同时充当父对象和子对象的角色,就像此处的Workbook对象。   如需从一个对象返回其子对象,只需使用可以返回特定对象的属性,正如1.9.4小节中介绍的。如需从一个对象返回其父对象,可以使用对象的Parent属性。下面的代码使用Worksheet对象的Parent属性,返回Worksheet对象的父对象Workbook对象,然后使用Workbook对象的Name属性返回特定的工作表所属工作簿的名称。 Worksheets("2023").Parent.Name   如需返回特定的工作表所属工作簿所在的Excel应用程序的版本号,可以使用两次Parent属性。第一个Parent属性返回Worksheet对象的父对象——Workbook对象,第二个Parent属性是Workbook对象的属性,返回的是Workbook对象的父对象——Application对象。 Worksheets("2023").Parent.Parent. Version   如果起始对象的层次级别很低,则可能需要使用很多个Parent属性才能返回Application对象。实际上,无论对象处于哪个层次级别,都可以使用Application属性直接返回Application对象,代码如下: Worksheets("2023").Application.Version 1.9.7 使用With语句提高处理同一个对象的效率   在VBA程序中对一个对象或对象变量的每一次引用都会消耗一定的内存。下面的代码为A1单元格设置多种字体格式,其中对A1单元格的Font属性引用了4次。 Sub 设置多个属性() Range("A1").Font.Name = "宋体" Range("A1").Font.Size = 12 Range("A1").Font.Bold = True Range("A1").Font.Color = vbRed End Sub   一种更好的方法是使用With语句在最开始只引用一次A1单元格的Font属性,之后可以为A1单元格的Font属性返回的Font对象设置一系列的属性和方法,但是必须在属性和方法的开头保留英文句点,整个With语句以With开始,End With结束。使用With语句既可以提高代码效率,还可以减少代码的输入量。 Sub 使用With语句设置多个属性() With Range("A1").Font .Name = "宋体" .Size = 12 .Bold = True .Color = vbRed End With End Sub 1.9.8 使用For Each语句处理集合中的对象   可能经常需要处理一个集合中的每一个对象,尤其是处理集合中满足条件的对象,此时可以使用For Each语句,该语句的语法如下: For Each element In group [statements] [Exit For] [statements] Next [element] * element(必需):循环访问集合中每一个对象的对象变量。 * group(必需):对象集合。 * statements(可选):处理对象的VBA代码。 * Exit For(可选):退出For Each语句。   For Each语句与For Next语句类似,区别是For Each语句不需要事先知道循环的次数。无论集合中有多少个对象,For Each语句都会依次处理每一个对象,直到处理完所有对象。如果在For Each语句中加入If Then Else语句,则可以只处理满足条件的对象,或者在满足特定条件时,使用Exit For语句提前退出循环。   下面的代码逐个查看当前工作簿中每个工作表的名称,如果发现名为“2023”的工作表,则激活该工作表并退出循环。 Sub ForEach语句() Dim wks As Worksheet For Each wks In Worksheets If wks.Name = "2023" Then wks.Activate Exit For End If Next wks End Sub 1.10 调试程序并处理错误   无论代码编写的有多么仔细,都无法避免程序出现错误。VBE提供了一些有用的调试工具,可以在程序出错时帮助用户尽快找到出错原因。本节将介绍常用调试工具的使用方法,还将介绍如何编写错误处理代码来处理运行时错误。 1.10.1 错误类型   VBA中的错误有3种类型:编译错误、运行时错误和逻辑错误。 * 编译错误:在编写代码的过程中出现不符合VBA语法规则的代码时,会立刻被检测出来,并要求用户更正错误的代码,否则禁止运行代码。这意味着在运行代码前,必须完全解决所有现存的编译错误。 * 运行时错误:运行代码期间执行无效的操作时,会立刻中断代码的运行,并使用特定颜色标记出错的代码。解决错误后,才能继续运行代码。 * 逻辑错误:逻辑错误具有很强的隐蔽性,因为它在VBA语法规则和程序运行两个方面都不会出现问题,也从不会显示任何错误信息,但是程序的运行结果与预期存在偏差或完全不同。   如需设置出错代码的标记颜色,可以在VBE窗口中单击菜单栏中的“工具”|“选项”命令,打开“选项”对话框,在“编辑器格式”选项卡中设置,如图1-41所示。 图1-41 设置出错代码的标记颜色 1.10.2 运行代码的几种方式   VBE提供了几种运行程序的方式,它们适用于不同的需求。   1. 运行整个程序   如果是第一次运行一个编写好的VBA程序,则通常会以常规方式运行该程序。将插入点定位到想要运行的过程的代码范围内,然后按F5键或单击“标准”工具栏中的按钮,开始运行程序。如果运行过程中出现运行时错误,则会显示一个包含错误信息的对话框,如图1-42所示。 图1-42 显示运行时错误信息的对话框   单击对话框中的“调试”按钮,将进入中断模式,此时程序暂停运行,并突出显示出错代码,如图1-43所示。 图1-43 在中断模式下修复错误的代码   更正错误后,按F5键从出错代码的位置继续运行程序。如需从头开始运行程序,可以单击“标准”或“调试”工具栏中的“重新设置”按钮,然后按F5键。   2. 逐行运行代码   如需了解程序中每一行代码的运行情况,可以使用F8键逐行运行代码。每按一次F8键,将从程序起始位置开始每次向下运行一行代码并进入中断模式。在中断模式下,将鼠标指针移动到变量或表达式上,将显示它们的当前值,便于检查程序是否正按照预期运行。   3. 运行到指定位置   如果希望让程序直接运行到可能有问题的代码行之前的位置,以便于逐步排查错误,并节省运行中间代码的时间,则可以为程序设置断点,有以下几种方法: * 单击要设置断点的位置,然后单击“标准”工具栏中的“切换断点”按钮。 * 单击要设置断点的位置,然后按F9键。 * 在要设置断点的位置的左边缘单击。 * 右击要设置断点的位置,在弹出的快捷菜单中选择“切换”|“断点”命令,如图1-44所示。 * 将Stop语句添加到程序中希望作为断点的位置。   设置为断点位置的代码行会突出显示,并在该行代码的左侧显示一个圆点,如图1-45所示。可以在一个程序中设置多个断点。运行包含断点的程序时,会在运行到第一个断点时暂停并进入中断模式。 图1-44 使用鼠标快捷菜单中的命令设置断点 图1-45 设置断点   清除断点与设置断点的方法相同,只需重复执行前4种方法中的任意一种,或者将第5种方法中的Stop语句从代码中删除。 1.10.3 监视程序中的特定值   如需监视程序运行过程中某个变量、属性或表达式的值的变化情况,可以在VBE窗口中单击菜单栏中的“调试”|“添加监视”命令,打开如图1-46所示的“添加监视”对话框,设置以下几项: * 在“表达式”文本框中输入想要监视的内容,此处输入的是一个变量。 * 在“上下文”类别中选择实施监视的范围。 * 在“监视类型”类别中选择监视的方式,此处选中“当监视值改变时中断”单选钮,表示当监视的对象的值发生改变时,会立刻进入中断模式。   设置完成后,单击“确定”按钮,关闭“添加监视”对话框。在VBE窗口的底部将显示监视窗口,如图1-47所示。由于本例将监视类型设置为“当监视值改变时中断”,所以在程序运行过程中,intSum变量的值每次发生改变时都会自动进入中断模式,并在监视窗口中显示该变量的值。   如需修改或删除现有的监视,可以在监视窗口中右击该项,然后在弹出的快捷菜单中选择“编辑监视”或“删除监视”命令,如图1-48所示。 图1-47 运行程序时监视变量的值 图1-48 修改或删除监视 1.10.4 在立即窗口中测试代码   使用1.10.3小节介绍的方法虽然可以监视在程序运行期间某个特定值的变化情况,但是存在一些局限: * 当表达式的值不断变化时,同一时间只能显示一个值,下一个值会覆盖上一个值,这样无法查看所有历史值。 * 每次值发生变化时,都会进入中断模式,影响程序运行的流畅性。 * 下次重新打开Excel文件时,需要重新添加监视的表达式。   使用立即窗口可以解决上述所有问题,使用该窗口可以测试表达式、变量或属性的值,还可以测试过程的执行情况。立即窗口有两种使用方法,一种是直接在立即窗口中使用Print方法测试代码,另一种方法是在程序中使用Debug.Print语句添加需要测试的内容,并在程序运行期间自动将测试结果显示在立即窗口中。   1. 在立即窗口中使用Print方法   在运行程序时手动或自动进入中断模式后,可以在立即窗口中使用Print方法测试表达式、变量或属性的值。假设要测试intSum变量的当前值,可以在立即窗口中输入以下代码,如图1-49所示。 print intsum 图1-49 在立即窗口中输入要测试的代码   按Enter键后,将在下一行显示该变量的当前值,如图1-50所示。 图1-50 显示测试结果   英文问号(?)是Print方法的简写形式,在立即窗口中测试代码时可以使用它代替Print。使用英文问号时,在它和表达式之间无须添加空格,如图1-51所示。 图1-51 使用Print方法的简写形式   2. 在代码中使用Debug.Print语句   如需查看一个变量在一段循环语句中值的变化情况,可以将Debug.Print语句添加到代码中,在运行程序后会通过该语句自动将该变量的每个值显示到立即窗口中。   在下面的代码中将Debug.Print语句添加到对intSum变量求和的代码之后,以便在每次对intSum变量求和立刻将其值显示在立即窗口中。运行该代码时将在立即窗口中显示如图1-52所示的信息。 Sub 使用DebugPrint语句() Dim intCounter As Integer, intSum As Integer For intCounter = 1 To 10 intSum = intSum + intCounter Debug.Print "intSum变量在第" & intCounter & "次循环时的值是:" & intSum Next intCounter MsgBox "数字的总和是:" & intSum End Sub 图1-52 在代码中使用Debug.Print语句 1.10.5 处理运行时错误   无论是否具备丰富的VBA编程经验,在运行编写好的VBA程序时,都可能会出现运行时错误。出现运行时错误是正常现象,VBA程序员要做的是预先考虑到程序可能会执行哪些无效操作,并在程序中事先编写处理无效操作的代码,以便在出现运行时错误,显示有指导意义的提示信息,而不是进入中断模式导致程序无法继续运行。   在VBA中可以使用On Error语句捕获运行时错误,该语句有以下几种形式: * On Error Goto Line:捕获到运行时错误,将暂停程序的正常运行,并转到由Line指定的位置开始执行事先编写好的错误处理程序。Line由一个字符串和一个冒号组成,表示错误处理程序的起点。在一个VBA程序中可以编写多个错误处理程序,每个错误处理程序都有一个对应的Line。 * On Error GoTo 0:关闭正处于活动状态的错误处理程序,并恢复正常的错误捕获状态。如果在程序中使用On Error Resume Next语句忽略任何错误,则应该在适当的位置使用On Error GoTo 0,否则会忽略所有运行时错误。 * On Error Resume Next:出现运行时错误时不显示任何提示信息,忽略所有运行时错误并从出错代码的下一行代码继续执行。   通常将处理运行时错误的错误处理程序放在VBA程序的结尾,以Line作为起点。在错误处理程序中,可以使用Resume语句决定完成错误处理后程序的走向。Resume语句有以下几种形式: * Resume:重新执行导致运行时错误的代码。 * Resume Next:从导致运行时错误的代码的下一行代码开始执行。 * Resume Line:执行由Line标记的代码,此处的Line与On Error Goto Line中的Line的功能和用法类似。   处理运行时错误还可以使用Err对象。Err对象的Number属性的值是一个错误号,错误号是一个非零数字。通过检测Number属性的值是否是0,可以判断是否出现了运行时错误。下面通过几个示例介绍使用On Error语句、Resume语句和Err对象编写错误处理程序处理运行时错误的方法。   下面的代码将用户在对话框中输入的数字作为除数并被100除,然后在对话框中显示计算结果。由于用户输入0时将导致运行时错误,所以需要在程序中编写应对该错误的错误处理程序。   首先在可能导致运行时错误的代码的上一行添加On Error GoTo Line语句,本例将Line设置为ErrTrap。然后在正常程序的结尾的下一行输入ErrTrap和一个冒号,按Enter键后,开始输入处理运行时错误的代码。本例先使用MsgBox函数显示一个“除数不能是0”的提示信息,然后递归调用当前Sub过程,重新显示接收用户输入的对话框,让用户重新输入一个数字。为了防止在正常程序结束后继续执行错误处理程序,所以应该在正常程序的最后一行代码之后添加Exit Sub语句,在未出现运行时错误时不会执行错误处理程序。 Sub 处理运行时错误() Dim strNum As String strNum = InputBox("输入一个整数:") If strNum = "" Then Exit Sub If Not IsNumeric(strNum) Then Exit Sub On Error GoTo ErrTrap MsgBox "计算结果是:" & 100 / strNum Exit Sub ErrTrap: MsgBox "除数不能是0!" Call 处理运行时错误 End Sub   下面的代码实现相同的功能,但是使用了不同的错误处理方法。先使用On Error Resume Next语句忽略所有运行时错误,然后检测Err对象的Number属性的值,从而判断是否出现了运行时错误,并显示相应的提示信息。本例没有使用On Error GoTo 0语句,因为处理完错误后没有其他需要执行的代码了,否则应该使用该语句恢复正常的错误捕获状态。 Sub 处理运行时错误2() Dim strNum As String strNum = InputBox("输入一个整数:") If strNum = "" Then Exit Sub If Not IsNumeric(strNum) Then Exit Sub On Error Resume Next MsgBox "计算结果是:" & 100 / strNum If Err.Number <> 0 Then MsgBox "除数不能是0!" Call 处理运行时错误2 End If End Sub   下面的代码在错误处理程序的结尾使用Resume语句,完成错误处理后会重新执行导致运行时错误的代码。首先使用Set语句将名为“2023”的工作表赋值给一个Worksheet类型的对象变量,如果该工作表不存在,则会导致运行时错误,此时将跳转到ErrTrap标签处开始执行错误处理程序。在错误处理程序中,先显示一个对话框,询问用户是否需要新建一个工作表,如图1-53所示。 图1-53 是否创建工作表的提示信息   如果单击对话框中的“是”按钮,则将新建一个工作表,并将其命名为“2023”,然后使用Resume语句重新执行Set语句,此时不会再出现运行时错误,所以可以正常执行后面的代码。如果单击对话框中的“否”按钮,则直接退出程序,不再执行任何操作。 Sub 处理运行时错误3() Dim wks As Worksheet, lngAnswer As Long On Error GoTo ErrTrap Set wks = Worksheets("2023") With wks.Range("A1") .Value = "编号" .Font.Name = "黑体" .Font.Bold = True End With Exit Sub ErrTrap: lngAnswer = MsgBox("指定的工作表不存在,是否创建?", vbYesNo + vbQuestion) If lngAnswer = vbYes Then Worksheets.Add.Name = "2023" Resume Else Exit Sub End If End Sub          第2章 控制Excel应用程序   Application对象位于Excel对象模型的顶层,它代表Excel应用程序。通过Application对象可以编程控制Excel应用程序,该对象的很多属性和方法用于设置Excel应用程序的界面环境和相关选项,与在“Excel选项”对话框中设置的效果相同。Application对象的某些方法还可以实现一些特殊操作,例如定时运行程序。本章将介绍使用Application对象控制Excel应用程序的方法。 2.1 Application对象和全局成员   Application对象的很多属性和方法都是“全局”成员。“全局”意味着不需要为属性或方法添加对象引用,即可直接使用该对象的属性和方法。例如,ActiveWorkbook引用活动工作簿,由于它是全局成员,所以下面的两行代码等效。 Application.ActiveWorkbook ActiveWorkbook   在Excel中有很多与ActiveWorkbook类似的全局成员,它们引用不同的活动对象。例如,ActiveCell引用活动单元格,ActiveSheet引用活动工作表,ActiveChart引用活动图表,ActiveWindow引用活动窗口。   如需查看哪些属性和方法是全局成员,可以在VBE窗口中打开对象浏览器,然后在“工程/库”下拉列表中选择“Excel”选项,再在“类”列表框中选择“全局”选项,将在右侧显示Excel中的所有全局成员,如图2-1所示。 图2-1 查看Excel中的所有全局成员   用于引用活动对象的全局成员为编写通用的VBA代码提供了极大的方便。例如,选择一个工作表时,会使其成为活动工作表。如需显示活动工作表的名称,由于事先无法预知哪个工作表会成为活动工作表,所以可以使用ActiveSheet动态引用已经成为活动工作表的那个工作表,代码如下。 ActiveSheet.Name   提示:ActiveSheet返回一个Worksheet对象,使用该对象的Name属性可以获取工作表的名称。   Application对象的Selection属性也是全局成员,它表示在活动工作簿的活动工作表中选中的对象,可以是单元格、单元格区域、图表、图形、图片等任何可以选中的对象。下面的代码将选中的单元格区域的字体设置为“宋体”。 Selection.Font.Name = "宋体"   由于上面的代码事先假设选中的是单元格区域,所以可以正常运行。如果选中的对象不支持Font属性,例如图片,则运行上面的代码将导致运行时错误。为了避免这种问题,可以先使用VBA内置的TypeName函数检测选中对象的类型,然后再执行相应的操作。   TypeName函数返回表示对象类型的字符串,如果被检测的对象类型是单元格或单元格区域,则TypeName函数将返回“Range”。下面的代码使用If Then Else语句检测/Selection的类型,如果选中的对象不是单元格或单元格区域,则会显示提示信息。 Sub Selection属性() If TypeName(Selection) = "Range" Then Selection.Font.Name = "宋体" Else MsgBox "需要先选择一个单元格或单元格区域" End If End Sub   注意:在使用TypeName函数的表达式的等号右侧输入的字符串的大小写形式,必须与TypeName函数的返回值的大小写形式完全相同。   在代码中输入Selection和一个英文句点之后,不会自动显示包含属性和方法的成员列表,这是因为Selection默认是Object类型,它代表一般对象类型,这意味着VBA无法确定Selection到底是哪种对象。如需解决这个问题,可以先声明一个特定对象类型的变量,然后将Selection赋值给该变量,之后就会显示包含属性和方法的成员列表了,如图2-2所示。 图2-2 为Selection显示包含属性和方法的成员列表 2.2 获取Excel应用程序的基本信息   编程创建Excel VBA程序时,事先检查程序的一些关键信息是很有必要的,包括Excel版本号、用户名、安装路径、启动文件夹路径和工作簿模板路径等。本节将介绍使用Application对象获取这些信息的方法。 2.2.1 使用Version属性获取Excel版本号   使用Application对象的Version属性将返回Excel应用程序的版本号,代码如下。 Application.Version   Version属性的返回值与Excel版本的对应情况如表2-1所示。 表2-1 Version属性返回的版本号与Excel版本之间的对应情况 Version属性的返回值 对应的Excel版本 16.0 Excel 2021、Excel 2019和Excel 2016 15.0 Excel 2013 14.0 Excel 2010 12.0 Excel 2007 11.0 Excel 2003      如果VBA程序中的某些功能受到不同Excel版本的影响,为了避免程序出现无法预料的问题,应该检测Excel应用程序的版本,然后根据不同的Excel版本选择相应的处理方式。下面的代码检测当前正在使用的Excel应用程序的版本,如果Excel版本是Excel 2007或更低版本,则显示“当前版本过低,无法使用本程序”,并退出程序,否则显示另一条信息,如图2-3所示。 图2-3 检测Excel版本并显示相应的信息 Sub 检测Excel版本() Select Case Val(Application.Version) Case Is >= 14 MsgBox "当前版本符合要求,可以正常使用本程序" Case Else MsgBox "当前版本过低,无法使用本程序," & vbCrLf & "请安装Excel 2010或更高版本" Exit Sub End Select End Sub   提示:Val函数是一个VBA内置函数,该函数返回字符串中的第一个数字。如果字符串的第一个字符不是数字,则返回0。例如,Val("666Like888")返回666。vbCrLf是一个VBA内置常数,表示将插入点移动到下一行的起始位置,可以使用Chr(13)& Chr(10)代替vbCrLf。 2.2.2 使用UserName属性获取Excel用户名   使用Application对象的UserName属性将返回Excel用户,即在“Excel选项”对话框中设置的用户名,如图2-4所示。 图2-4 在“Excel选项”对话框中设置的用户名   下面的代码将用户在对话框中输入的名称设置为Excel用户名。如果用户什么都没输入就单击“确定”按钮,或者单击“取消”按钮,则会再次显示输入对话框,直到用户输入内容为止。 Sub 设置Excel用户名() Dim strUserName As String Do strUserName = InputBox("输入用户名:") Loop While strUserName = "" Application.UserName = strUserName End Sub 2.2.3 使用Path属性获取Excel的安装路径   使用Application对象的Path属性将返回Excel应用程序的安装路径。 Application.Path   可以在立即窗口中测试上述代码的运行结果,如图2-5所示。 图2-5 在立即窗口中测试Path属性的返回值 2.2.4 使用StartupPath属性获取启动文件夹路径   位于启动文件夹中的工作簿会在启动Excel时自动打开,第1章介绍过的Personal.xlsb工作簿就位于启动文件夹中。使用Application对象的StartupPath属性将返回Excel启动文件夹的路径,如图2-6所示。 Application.StartupPath 图2-6 在立即窗口中测试StartupPath属性的返回值 2.2.5 使用TemplatesPath属性获取工作簿模板路径   如需在VBA程序中处理工作簿模板,需要先知道工作簿模板的存储位置。使用Application对象的TemplatesPath属性将返回Excel工作簿模板的路径,如图2-7所示。 Application.TemplatesPath 图2-7 在立即窗口中测试TemplatesPath属性的返回值 2.3 设置Excel应用程序的界面环境   本节将介绍编程控制Excel界面环境的方法,其中的一些设置可以在“Excel选项”对话框中手动完成,而另一些设置只能通过编程才能实现。 2.3.1 使用Visible属性设置Excel应用程序的可见性   启动Excel应用程序时,默认会正常显示程序窗口。当编写一个需要验证用户的VBA程序时,可能需要在显示程序窗口之前先验证用户的有效性,此时就需要在启动Excel时隐藏暂时隐藏程序窗口。   使用Application对象的Visible属性可以控制Excel程序窗口的可见性,该属性为True表示正常显示程序窗口,该属性为False表示隐藏程序窗口。下面的代码将隐藏Excel程序窗口。 Application.Visible = False 2.3.2 使用WindowState属性设置Excel窗口的显示状态   窗口的显示状态是指以最大化、最小化等方式显示窗口。使用Application对象的WindowState属性可以返回或设置Excel窗口的状态,该属性返回或设置的值由XlWindowState常量提供,如表2-2所示。 表2-2 XlWindowState常量 名 称 值 说 明 xlMaximized -4137 最大化 xlMinimized -4140 最小化 xlNormal -4143 正常      下面的代码将活动的Excel窗口最大化显示。 Application.WindowState = xlMaximized   下面的代码使用For Each语句将当前打开的所有Excel窗口都设置为最大化。 Sub 最大化显示所有Excel窗口() Dim win As Window For Each win In Windows win.WindowState = xlMaximized Next win End Sub   下面的代码实现相同的功能,但是使用的是For Next语句,此处使用Windows对象的Count属性获取所有打开窗口的总数,然后使用索引号引用每一个打开的窗口。Windows是Application对象的属性,该属性返回Windows集合,使用该集合的Count属性返回窗口总数。 Sub 最大化显示所有Excel窗口2() Dim intIndex As Integer For intIndex = 1 To Windows.Count Windows(intIndex).WindowwState = xlMaximized Next intIndex End Sub 2.3.3 使用DisplayFullScreen属性设置是否全屏显示Excel   与最大化显示Excel窗口不同,全屏显示会将Excel窗口中的标题栏、功能区、编辑栏和状态栏都隐藏起来。使用Application对象的DisplayFullScreen属性可以设置Excel窗口是否全屏显示,该属性为True表示全屏显示,该属性为False表示不全屏显示。下面的代码将Excel窗口全屏显示。 Application.DisplayFullScreen = True 2.3.4 使用Caption属性设置Excel标题栏   在Excel窗口顶部的标题栏中默认显示工作簿的名称和Excel应用程序的名称,例如“工作簿1 - Excel”,如图2-8所示。 图2-8 Excel窗口顶部的标题栏   使用Application对象的Caption属性可以设置标题栏中的Excel应用程序的名称。下面的代码将标题栏中默认显示的“Excel”替换为“测试系统”,如图2-9所示。 Application.Caption = "测试系统" 图2-9 修改标题栏中的Excel应用程序的名称   如果不想在标题栏中显示任何内容,可以将Caption属性设置为包含空格的字符串。 Application.Caption = " "   如需恢复标题栏的默认名称,可以将Caption属性设置为零长度字符串。 Application.Caption = ""   使用Application对象的Caption属性可以返回标题栏中的所有内容,而不只是Excel应用程序的名称。下面的代码将在对话框中显示标题栏中的所有内容,如图2-10所示。   如果只想返回标题栏中位于“-”后面的内容,则可以使用下面的代码,使用Mid函数在整个标题栏内容中查找每一个字符以确定“-”的位置,然后使用Right函数提取位于“-”后面的所有字符。代码的运行效果如图2-11所示。 图2-10 使用Caption属性返回标题栏内容 图2-11 提取标题栏中的程序名称 Sub 提取标题栏中的程序名称() Dim strTitle As String Dim strAppName As String Dim intStart As Integer strTitle = Application.Caption For intStart = 1 To Len(strTitle) If Mid(strTitle, intStart, 1) = "-" Then strAppName = Right(strTitle, Len(strTitle) - intStart) Exit For End If Next intStart MsgBox strAppName End Sub 2.3.5 使用DisplayFormulaBar属性设置是否显示编辑栏   Excel中的编辑栏显示在单元格区域的上方,是一个类似文本框的长条矩形区域,可以在编辑栏中输入公式来计算工作表中的数据,如图2-12所示。 图2-12 编辑栏   使用Application对象的DisplayFormulaBar属性可以设置是否显示编辑栏,该属性为True表示显示编辑栏,该属性为False表示隐藏编辑栏。下面的代码将隐藏编辑栏。 Application.DisplayFormulaBar = False 2.3.6 使用ShowMenuFloaties属性设置右击单元格是否显示浮动工具栏   浮动工具栏是在单元格进入编辑模式后选择内容时,或右击单元格时,在单元格附近显示的微型工具栏,其中包含常用的格式选项,可以提高设置格式的效率,如图2-13所示。 图2-13 浮动工具栏   使用Application对象的ShowSelectionFloaties属性可以设置在单元格中选择内容时是否显示浮动工具栏,该属性为False表示显示浮动工具栏,该属性为True表示不显示浮动工具栏。下面的代码在单元格中选择内容时不显示浮动工具栏: Application.ShowSelectionFloaties = True   即使将ShowSelectionFloaties属性设置为False不显示浮动工具栏,但是在右击单元格时仍会显示浮动工具栏。如果希望在右击单元格时也不显示浮动工具栏,则可以将Application对象的ShowMenuFloaties属性设置为True。 Application.ShowMenuFloaties = True   如需彻底禁止显示浮动工具栏,可以将上面两个属性都设置为True。 2.3.7 使用ShowDevTools属性设置是否显示“开发工具”选项卡   使用Application对象的ShowDevTools属性可以设置在功能区中是否显示“开发工具”选项卡,该属性为True表示在功能区中显示“开发工具”选项卡,该属性设置为False表示在功能区中不显示“开发工具”选项卡。下面的代码在功能区中显示“开发工具”选项卡。 Application.ShowDevTools = True 2.3.8 使用StatusBar属性设置在状态栏中显示的信息   状态栏位于Excel窗口的底部,其中显示在Excel中执行操作时的状态信息,例如单元格当前所处的编辑模式。当执行耗时较长的操作时,可以在状态栏中显示有关程序处理进度的信息。使用Application对象的StatusBar属性可以返回或设置在状态栏中显示的信息。   下面的代码在1万个单元格中依次输入从1开始的连续自然数,程序运行期间在状态栏中实时显示当前正在处理第几个单元格,如图2-14所示。在程序结束前,应该将StatusBar属性设置为False,使状态栏恢复到Excel默认状态,否则在状态栏中会一直显示由程序设置的信息。 图2-14 在状态栏中显示操作进度 Sub 在状态栏中显示操作进度() Dim intCell As Integer For intCell = 1 To 10000 Cells(intCell).Value = intCell Application.StatusBar = "正在处理第" & intCell & "个单元格" Next intCell Application.StatusBar = False End Sub 2.3.9 使用DisplayAlerts属性设置警告信息的显示方式   运行VBA程序时,可能会显示来自Excel的警告信息,只有对这类信息进行处理后,程序才能继续运行。例如,当VBA程序执行删除工作表的操作时,将显示如图2-15所示的对话框,只有单击“删除”按钮或“取消”按钮后,程序才能继续运行。 图2-15 Excel中的警告信息   如果不想显示该对话框而直接执行删除操作,则可以将Application对象的DisplayAlerts属性设置为False,屏蔽任何Excel警告信息,此时会自动执行与对话框中默认按钮关联的操作。默认按钮是指对话框中处于焦点的按钮,即在不改变选择的情况下直接按Enter键生效的按钮。从外观上看,默认按钮的边缘显示为虚线或蓝色。   下面的代码执行活动工作表,为了避免出现警告信息,在执行该操作前将DisplayAlerts属性设置为False。如果在删除工作表之后还有很多代码,则应该将DisplayAlerts属性设置为True,使Excel可以正常显示警告信息,否则会屏蔽后面可能出现的所有警告信息。 Sub 设置警告信息的显示方式() Application.DisplayAlerts = False ActiveSheet.Delete Application.DisplayAlerts = True End Sub 2.3.10 使用DefaultFilePath属性设置打开文件的默认路径   如需频繁从同一个文件夹中打开Excel工作簿,可以将该文件夹设置为打开文件的默认路径。以后每次执行打开文件的操作时,默认都会显示该文件夹。在“Excel选项”对话框的“保存”选项卡中,通过设置“默认本地文件位置”选项可以实现该功能,如图2-16所示。 图2-16 设置打开文件的默认路径   在VBA中可以使用Application对象的DefaultFilePath属性设置打开文件的默认路径。如果设置的路径不存在,则不会使无效路径的设置生效,也不会出现任何错误提示。下面的代码将指定的路径存储在strPath变量中,然后使用VBA内置的Dir函数检测该路径是否有效,如果路径有效,则Dir函数将返回路径结尾的文件夹名称,此时将该路径设置为DefaultFilePath属性的值;如果路径不存在,则Dir函数将返回零长度字符串,此时会显示提示信息。 Sub 设置打开文件的默认路径() Dim strPath As String strPath = "E:\测试数据\Excel" If Dir(strPath, vbDirectory) <> "" Then Application.DefaultFilePath = strPath Else MsgBox "指定的路径无效" End If End Sub 2.3.11 使用SheetsInNewWorkbook属性设置新工作簿中的工作表数   在不同版本的Excel中,新建的工作簿中默认包含不同数量的工作表。实际上,可以在“Excel选项”对话框的“常规”选项卡中通过修改“包含的工作表数”选项来更改默认的工作表数,如图2-17所示。 图2-17 更改新建的工作簿中默认包含的工作表数   在VBA中可以使用Application对象的SheetsInNewWorkbook属性设置新建的工作簿中默认包含的工作表数。下面的代码将在每次新建的工作簿中自动包含6个工作表。 Application.SheetsInNewWorkbook = 6 2.3.12 使用StandardFont和StandardFontSize属性设置工作簿的默认字体和字号   为新建的工作簿设置默认的字体和字号有两种方法,一种是创建一个工作簿模板,在其中设置好字体和字号,保存后将其放置到2.2.4小节介绍的Excel启动文件夹中,以后每次在快速访问工具栏中单击“新建”按钮时,新建的工作簿中的字体和字号都与工作簿模板保持一致。   另一种方法是在“Excel选项”对话框的“常规”选项卡中设置“使用此字体作为默认字体”和“字号”两个选项,如图2-17所示。以后每次单击“文件”按钮,然后选择“新建”|“空白工作簿”命令时,创建的空白工作簿中的字体和字号将由这两个选项决定。   使用Application对象的StandardFont和StandardFontSize属性可以设置第二种方法中的默认字体和字号。下面的代码将新建的工作簿中的默认字体设置为“黑体”,默认字号设置为“12”。 Application.StandardFont = "黑体" Application.StandardFontSize = 12   注意:运行代码会立刻在“Excel选项”对话框中显示设置结果,但是只有在退出Excel应用程序并再次启动它之后,设置才会真正对以后新建的工作簿有效。 2.4 Excel应用程序的特殊操作   Application对象的一些属性和方法可以实现一些比较特殊的功能,包括控制屏幕刷新、计算字符串表达式、定时运行程序和为程序设置快捷键,这些功能通常很难在Excel界面环境中实现。本节将介绍在VBA中使用Application对象实现这些功能的方法。 2.4.1 使用ScreenUpdating属性控制屏幕刷新   屏幕刷新是指在程序运行期间,程序执行的各种操作将导致屏幕中显示的内容不断变化和闪烁,这种现象通常会给用户带来不好的体验。使用Application对象的ScreenUpdating属性可以开启或关闭屏幕刷新,该属性为True表示开启屏幕刷新,该属性为False表示关闭屏幕刷新。   在即将开始执行一段包含大量操作的代码时,可以将ScreenUpdating属性设置为False,暂时关闭屏幕刷新,这样在代码运行期间将不会让屏幕显示有任何变化,还能加快代码的运行速度。 Application.ScreenUpdating = False   在包含大量操作的代码运行结束后,可以将ScreenUpdating属性设置为True,重新开启屏幕刷新。 Application.ScreenUpdating = True   注意:如需在程序运行期间显示Excel内置对话框或用户窗体,应该开启屏幕刷新,否则拖动对话框时会在屏幕上产生橡皮擦的效果。 2.4.2 使用Evaluate方法将字符串转换为对象或值   使用Application对象的Evaluate方法可以将一个字符串转换为Excel对象或值。Evaluate方法有以下两种语法: Evaluate("字符串") [字符串]   使用第一种语法时,必须将字符串放在一对英文双引号中。使用第二种语法时,直接将字符串放在中括号内,不能在字符串的两侧添加英文双引号。下面两行代码都引用A1单元格: Evaluate("A1") [A1]   下面两行代码都返回3个数字的乘积: Evaluate("2*3*5") [2*3*5]   在上面两个示例中,使用第一种语法的优点是可以在字符串中使用&连接符连接变量,从而灵活构建表达式。使用第二种语法的优点是代码更简洁。下面的代码使用第一种语法,将变量和字符串组合在一起,用作Evaluate方法的参数,计算10和strNumber变量的乘积,该变量的值由用户指定。 Sub Evaluate方法() Dim strNumber As String strNumber = Val(InputBox("输入一个整数")) MsgBox Application.Evaluate("10*" & strNumber) End Sub   第1章曾经介绍过,如果在Excel工作表函数和VBA内置函数中同时存在完成同一个功能的函数,则在VBA程序中不能使用完成该功能的Excel工作表函数。然而,Application对象的Evaluate方法打破了这种限制,可以在该方法中以字符串的形式编写使用Excel工作表函数的公式,Evaluate方法会计算其结果。   下面两行代码使用工作表函数ISBLANK判断A1单元格是否为空。由于该函数与VBA内置的IsEmpty函数等效,所以不能在VBA中使用ISBLANK函数,但是可以在Evaluate方法中使用该函数。 Evaluate("ISBLANK(A1)") [ISBLANK(A1)] 2.4.3 使用OnTime方法定时运行VBA程序   使用Application对象的OnTime方法可以在指定的时间自动运行VBA程序,只要Excel应用程序一直处于运行状态,但是不必打开包含VBA程序的工作簿,Excel会在需要时自动打开它。在到达指定的时间之前,用户在Excel中的各种操作不受影响。   OnTime方法的语法如下: Application.OnTime(EarliestTime, Procedure, LatestTime, Schedule) * EarliestTime(必需):运行VBA程序的时间。 * Procedure(必需):VBA程序的名称。 * LatestTime(可选):运行VBA程序的最后时间。如果在到达由EarliestTime参数指定的时间时,Excel正在执行其他程序,则会等待该程序结束后再运行由Procedure参数指定的VBA程序。如果将LatestTime参数设置为一个时间,则会等到达该时间时再运行指定的VBA程序。 * Schedule(可选):该参数为True表示安排一个新的运行计划,该参数为False表示清除当前正处于活动状态的运行计划。省略该参数时,其默认值是True。   设置EarliestTime参数时可以使用VBA内置的TimeValue或TimeSerial函数。TimeValue函数只有一个参数,它是一个表示时间的字符串。TimeSerial函数有3个参数,分别表示时间中的时、分、秒。下面两种形式都表示当天下午3点30分: TimeValue("15:30:00") TimeSerial(15, 30, 0)   如需表示指定时间间隔之后的时间,可以使用+连接符连接当前时间和由TimeValue或TimeSerial函数设置的时间间隔。下面两种形式都表示30分钟后的时间,其中的Now函数返回当前系统时间。 Now + TimeValue("00:30:00") Now + TimeSerial(0, 30, 0)   了解如何设置时间后,就可以很容易使用On Time 方法定时运行VBA程序了。下面的代码在当天上午9点半自动运行名为“例会提醒”的Sub过程,向用户发送开会提醒的信息。为了使该功能生效,需要先运行包含On Time方法的Sub过程。 Sub OnTime方法() Application.OnTime TimeValue("9:30:00"), "例会提醒" End Sub Sub 例会提醒() MsgBox "今天上午10点开例会" End Sub   运行下面的代码,将在15分钟后自动运行“例会提醒”Sub过程,此处使用的是TimeSerial函数。 Sub OnTime方法2() Application.OnTime Now + TimeSerial(0, 15, 0), "例会提醒" End Sub   如需定时重复运行一个VBA程序,可以在该程序内使用On Time方法,并将该方法的Procedure参数设置为该程序。运行下面的代码,每隔10分钟会向用户发送一次例会提醒的信息。 Sub 例会提醒2() Application.OnTime Now + TimeSerial(0, 10, 0), "例会提醒2" MsgBox "今天上午10点开例会" End Sub   如需停止定时重复运行某个VBA程序,需要在一个单独的Sub过程中编写代码,获取为运行该程序设置的时间,并将OnTime方法的Schedule参数设置为False,即取消定时运行计划。为了在同一个模块中的不同Sub过程之间共享同一个时间,需要在该模块中声明一个模块级变量,然后在定时重复运行程序的Sub过程和停止定时重复运行的Sub过程中使用该变量传递时间。   在下面的示例中,先运行名为“例会提醒3”的Sub过程,将每隔10分钟发送一次例会提醒。在该过程中,将定制运行程序的时间赋值给名为datTime的模块级变量,然后在“停止例会提醒”Sub过程中将该变量设置为On Time方法的EarliestTime参数的值,从而确保两个过程中的时间完全相同。 Dim datTime As Date Sub 例会提醒3() datTime = Now + TimeSerial(0, 10, 0) Application.OnTime datTime, "例会提醒3" MsgBox "今天上午10点开例会" End Sub Sub 停止例会提醒() Application.OnTime datTime, "例会提醒3", , False End Sub 2.4.4 使用OnKey方法为VBA程序设置快捷键   使用Application对象的OnKey方法可以为VBA程序设置快捷键,之后可以通过快捷键运行指定的VBA程序。OnKey方法的语法如下: Application.OnKey(Key, Procedure) * Key(必需):为VBA程序设置的快捷键。表2-3列出了在OnKey方法中表示按键的代码,在该表中没有列出字母键、数字键和符号键,因为这些按键在OnKey方法中直接使用按键本身的字符表示,例如A键在OnKey中表示为"a",如果写为"A",则表示同时按Shift键和A键。如需在快捷键中包含+、^或%,则需要将它们放在一对大括号中,此时表示它们是按键本身,而不代表Shift键、Ctrl键或Alt键,例如"^{+}"表示按Ctrl键和加号键。 * Procedure(可选):设置快捷键的VBA程序的名称。 表2-3 OnKey方法中与按键对应的代码 按 键 代 码 按 键 代 码 Shift + F1~F15 {F1}~{F15} Ctrl ^ Tab {TAB} Alt % Ins {INSERT} Enter {ENTER}或~(波形符) Break {BREAK} Esc {ESCAPE}或{ESC} 向上 {UP} Backspace {BACKSPACE}或{BS} 向下 {DOWN} Delete或Del {DELETE}或{DEL} 向左 {LEFT} Home {HOME} 向右 {RIGHT} End {END} Caps Lock {CAPSLOCK} Pageup {PGUP} Num Lock {NUMLOCK} Pagedown {PGDN} Scroll Lock {SCROLLLOCK}      下面的代码将“显示欢迎信息”过程的快捷键设置为Ctrl+1,然后运行名为“设置快捷键”的过程,之后可以按Ctrl+1组合键运行“显示欢迎信息”过程。在Excel中按Ctrl+1组合键默认将打开“设置单元格格式”对话框,现在失去该功能,而是运行“显示欢迎信息”过程。 Sub 设置快捷键() Application.OnKey "^1", "显示欢迎信息" End Sub Sub 显示欢迎信息() MsgBox "你好" End Sub   提示:退出Excel应用程序之前,使用OnKey方法设置的快捷键一直有效。   如需恢复上面示例中的Ctrl+1组合键在Excel中的默认功能,可以省略OnKey方法的第2个参数。 Application.OnKey "^1"   如需彻底禁用Ctrl+1组合键在Excel中的功能,可以将OnKey方法的第2个参数设置为零长度字符串。 Application.OnKey "^1", ""       第3章 处理工作簿和工作表   Excel对象模型中的Workbooks集合和Workbook对象,以及Worksheets集合和Worksheet对象分别代表Excel中的工作簿和工作表,本章将介绍使用这几个对象编程处理工作簿和工作表的方法。 3.1 使用Workbooks集合和Workbook对象处理工作簿   使用Application对象的Workbooks属性可以返回Workbooks集合,该集合由在Excel中打开的所有工作簿组成,其中的每一个工作簿都是一个Workbook对象。本节将介绍使用Workbooks集合和Workbook对象编程处理工作簿的方法。 3.1.1 从Workbooks集合中引用工作簿   在编程处理一个工作簿之前,需要先从Workbooks集合中引用一个特定的工作簿。假设在打开的所有工作簿中有一个名为“总公司”的工作簿,该工作簿是第1个被打开的,下面两种方法都可以引用该工作簿。 Workbooks("总公司") Workbooks(1)   还可以使用ActiveWorkbook和ThisWorkbook两个全局成员引用工作簿,它们都是Application对象的属性。使用ActiveWorkbook可以引用活动工作簿,使用ThisWorkbook可以引用运行当前VBA代码的工作簿。当运行VBA代码的工作簿是活动工作簿时,ActiveWorkbook和ThisWorkbook引用的是同一个工作簿。 3.1.2 使用Add方法创建新的工作簿   使用Workbooks集合的Add方法可以创建新的工作簿。下面的代码是创建一个空白工作簿。 Workbooks.Add   如需使用某个工作簿作为模板创建新的工作簿,可以为Add方法指定Template参数的值,该参数表示一个Excel文件的完整路径。下面的代码使用E盘“测试数据”文件夹中的“总公司.xlsx”工作簿作为模板创建新的工作簿。 Workbooks.Add "E:\测试数据\总公司.xlsx"   使用Add方法创建一个新的工作簿后,该工作簿会自动成为活动工作簿,可以使用ActiveWorkbook引用该工作簿。下面的代码在创建一个工作簿后显示其名称,由于还未保存刚创建的工作簿,所以显示的是默认名称,例如“工作簿1”。 Workbooks.Add MsgBox ActiveWorkbook.Name   为了便于在后面的代码引用和处理新建的工作簿,可以将使用Add方法创建的工作簿赋值给一个Workbook对象变量。下面的代码将新建的工作簿赋值给名为wks的对象变量。 Set wkb = Workbooks.Add   使用这种方法时如需为Add方法提供Template参数,则需要将作为模板的工作簿文件的完整路径放在一对小括号中。格式类似于为函数设置参数并将其返回值赋值给一个变量。 Set wkb = Workbooks.Add("E:\测试数据\总公司.xlsx")   如需一次性创建多个工作簿,可以在For Next语句中使用Add方法。下面的代码将创建由用户指定数量的工作簿。在进入For Next循环之前,需要检测用户在对话框中输入的是否是数字,以及是否不为空,即输入了至少一个字符。 Sub 创建多个工作簿() Dim strCount As String, intIndex As Integer strCount = InputBox("输入创建工作簿的数量:") If IsNumeric(strCount) And strCount <> "" Then For intIndex = 1 To strCount Workbooks.Add Next intIndex MsgBox "已创建" & strCount & "个工作簿" End If End Sub 3.1.3 使用Open方法打开工作簿   使用Workbooks集合的Open方法可以打开一个工作簿,该方法有多个参数,第一个参数用于指定要打开的工作簿的完整路径。下面的代码打开E盘“测试数据”文件夹中名为“总公司.xlsx”的工作簿: Workbooks.Open "E:\测试数据\总公司.xlsx"   提示:虽然省略文件的扩展名也能使代码正常运行,但是如果存在同名不同文件类型的工作簿(例如总公司.xlsx和总公司.xls),则在省略扩展名时打开的工作簿可能不是希望的文件类型,所以输入文件名时最好加上扩展名。   如果要打开的工作簿与运行VBA代码的工作簿存储在E盘“测试数据”文件夹中,则可以使用ThisWorkbook.Path自动获取文件路径,后面添加一个反斜线和工作簿的名称,从而组成工作簿的完整路径。 Workbooks.Open ThisWorkbook.Path & "\总公司xlsx"   与Add方法类似,使用Open方法也返回一个Workbook对象,表示刚打开的工作簿,并且该工作簿也会成为活动工作簿,使用ActiveWorkbook可以引用该工作簿。   如果为Open方法指定的工作簿不存在或路径有误,则将出现运行时错误。为了避免出错,可以在执行Open方法之前使用On Error Resume Next语句,它会忽略由Open方法产生的运行时错误。执行Open方法后检查Err对象的Number属性,如果是一个不为0的数字,则说明出现了运行时错误,此时会显示一条有意义的信息,而不是进入中断模式。 Sub 打开工作簿() Dim strFile As String strFile = "E:\测试数据\总公司.xlsx" On Error Resume Next Workbooks.Open strFile If Err.Number <> 0 Then MsgBox "指定的文件不存在或路径有误" End Sub   如需一次性打开多个工作簿,可以使用Array函数存储这些工作簿的名称。首先需要将所需打开的每一个工作簿的名称指定为Array函数的参数,Array函数会返回包含这些名称的Variant数据类型的数组,然后使用For Each语句逐一访问数组中的每一个元素,即可以获取每个工作簿的名称,然后使用Open方法依次打开它们。使用For Each语句访问数组中的每个元素的方法与访问集合中的对象类似。 Sub 打开多个工作簿() Dim varFile As Variant, varFiles As Variant varFiles = Array("北京分公司", "天津分公司", "上海分公司") On Error Resume Next For Each varFile In varFiles Workbooks.Open "E:\测试数据\" & varFile & ".xlsx" If Err.Number <> 0 Then MsgBox "无法打开:" & varFile & ".xlsx" End If Next varFile End Sub   提示:有关数组的更多内容将在第5章进行介绍。 3.1.4 获取工作簿的路径和名称   编程处理工作簿时,经常需要使用工作簿的名称和路径等相关信息,可以使用Workbook对象的Name、Path和FullName三个属性获取这些信息。   1. 获取工作簿的名称   使用Name属性可以获取工作簿的名称。如果已将工作簿保存到计算机中,则返回带有扩展名的工作簿名称,否则返回不带扩展名的工作簿名称。假设当前打开了一个工作簿,下面的代码会先显示该工作簿的名称,如图3-1所示,然后新建一个工作簿并显示其名称,如图3-2所示。由于新建的工作簿还未保存到计算机中,所以显示的名称不带扩展名。 MsgBox ActiveWorkbook.Name Workbooks.Add MsgBox ActiveWorkbook.Name 图3-1 已保存的工作簿带有扩展名 图3-2 未保存的工作簿不带扩展名      2. 获取工作簿的路径   使用Workbook对象的Path属性可以获取工作簿的路径,该路径是工作簿所在文件夹的路径,路径中不包含工作簿的名称以及名称左侧的分隔符。下面的代码显示活动工作簿的路径,如图3-3所示,如果是一个新建还未保存到计算机中的工作簿,则显示为空。 ActiveWorkbook.Path   同时使用Name和Path属性可以获取工作簿的完整路径,如图3-4所示。 ActiveWorkbook.Path & "\" & ActiveWorkbook.Name 图3-3 显示工作簿的路径 图3-4 同时使用Name和Path属性   提示:工作簿名称左侧的那个分隔符还可以使用Application对象的PathSeparator属性自动创建,使用该属性的优点是可以根据不同的操作平台返回正确的分隔符。   3. 获取工作簿的完整路径   使用Workbook对象的FullName属性可以实现同时使用Name属性和Path属性的功能。如果是一个新建还未保存到计算机中的工作簿,使用FullName属性将只返回工作簿的默认名称,没有路径和扩展名。 ActiveWorkbook.FullName 3.1.5 使用Save和SaveAs方法保存工作簿   如果已将工作簿以指定的文件名保存到计算机中,则可以使用Workbook对象的Save方法保存工作簿的最新修改。下面的代码保存所有打开的工作簿的最新修改。 Sub 保存所有打开的工作簿() Dim wkb As Workbook For Each wkb In Workbooks wkb.Save Next wkb End Sub   如果一个新建的工作簿还未保存到计算机中,或者想将现有工作簿保存为另一个名称或保存到不同的位置,则可以使用Workbook对象的SaveAs方法。该方法有多个参数,第一个参数用于指定工作簿的保存位置和文件名。下面的代码将新建的工作簿以“总公司”名称保存到E盘“测试数据”文件夹中。 Workbooks.Add ActiveWorkbook.SaveAs "E:\测试数据\总公司.xlsx"   如果路径无效,则会出现运行时错误。可以在保存工作簿之前,使用VBA内置的Dir函数检测路径是否有效,如果有效,则Dir函数返回表示路径的字符串,否则返回零长度字符串。使用Len函数计算Dir函数返回值的字符数,如果大于0,则说明Dir函数返回了一个路径,否则Dir函数返回的是一个零长度字符串,表示路径无效。 Sub 检查保存路径是否有效() Dim strPath As String strPath = "E:\测试数据" If Len(Dir(strPath, vbDirectory)) > 0 Then ActiveWorkbook.SaveAs strPath & "\总公司.xlsx" Else MsgBox "请检查路径是否有效,然后重试" End If End Sub   提示:如果省略SaveAs方法的Filename参数,则自动将工作簿以默认名称保存到默认位置,该位置是使用Application对象的DefaultFilePath属性设置的路径。   如果为SaveAs方法指定的保存路径中已经存在同名的工作簿,则使用该方法保存工作簿时会显示如图3-5所示的提示信息,做出选择后才能完成保存操作。如果不想显示该提示信息,则可以在保存前将Application对象的DisplayAlerts属性设置为False,Excel会自动替换同名文件,完成保存后再将该属性设置为True。 图3-5 是否替换同名文件的提示信息 Application.DisplayAlerts = False ActiveWorkbook.SaveAs "E:\测试数据\总公司.xlsx" Application.DisplayAlerts = True 3.1.6 使用Close方法关闭工作簿   使用Workbook对象的Close方法可以关闭指定的工作簿。Close方法有3个参数,第一个参数用于指定关闭工作簿时,是否保存该工作簿中的最新修改。将该参数设置为True,表示保存最新修改;将该参数设置为False,表示不保存最新修改。下面的代码是保存并关闭名为“总公司”的工作簿。 Workbooks("总公司").Close True   如果将Close方法的第一个参数设置为True,并且该工作簿是一个新建的还未保存到计算机中的工作簿,则可以使用Close方法的第二个参数指定保存路径和文件名。   下面的代码判断当前正在关闭的工作簿是否从未保存到计算机中,如果从未保存过,则在关闭该工作簿时将其以“总公司”文件名保存到E盘“测试数据”文件夹中,否则保存最新修改并关闭工作簿。 Sub 保存并关闭工作簿() If Len(ActiveWorkbook.Path) = 0 Then ActiveWorkbook.Close True, "E:\测试数据\总公司.xlsx" Else ActiveWorkbook.Close True End If End Sub   注意:如果在新建的工作簿中没有任何编辑操作,则关闭前不会保存该工作簿。   Workbook对象有一个Saved属性,如果工作簿包含未保存的内容,则该属性返回False,否则返回True。如果将该属性设置为True,则无论工作簿是否包含未保存的内容,Excel都认为不存在未保存的内容,在关闭工作簿时将不会显示确认保存的提示信息。 ActiveWorkbook.Saved = True   如需一次性关闭所有打开的工作簿,可以使用Workbooks集合的Close方法。遇到存在未保存修改的工作簿时,会显示确认保存的提示信息。 Workbooks.Close   如需关闭除了运行VBA代码的工作簿之外的其他所有工作簿,可以使用下面的代码,检测打开的每一个工作簿的名称,只要不与ThisWorkbook.Name相同,就将其保存并关闭。 Sub 关闭多个工作簿() Dim wkb As Workbook For Each wkb In Workbooks If wkb.Name <> ThisWorkbook.Name Then wkb.Close True End If Next wkb End Sub   在End Sub语句之前添加下面的代码,最后关闭运行VBA程序的工作簿。 ThisWorkbook.Close True 3.1.7 关闭多余的工作簿窗口   在Excel中打开的每一个工作簿都显示在各自独立的窗口中,Application对象的Windows属性返回的Windows集合由所有工作簿窗口组成,每个窗口都是一个Window对象。每个工作簿也有自己的Windows集合,该集合由特定工作簿的所有窗口组成。为一个工作簿打开多个窗口时,在每个窗口中可以显示该工作簿的不同部分,在窗口顶部的标题栏中显示工作簿名和窗口编号并使用冒号连接,例如“总公司:1”和“总公司:2”。   在为一个工作簿打开多个窗口的情况下,如果只想保留一个窗口而关闭该工作簿的其他多余窗口,则可以使用下面的代码。首先检测与活动工作簿关联的窗口数量,如果不止一个,则使用For Each语句逐个检查每一个窗口,使用VBA内置的InStr函数在窗口标题中查找冒号,如果找到冒号,则返回一个大于0的值,此时执行Window对象的Close方法将该窗口关闭。当只剩下一个窗口时,由于无法在窗口标题中找到冒号,所以InStr函数返回0,此时不会关闭该窗口。 Sub 关闭多余的工作簿窗口() Dim win As Window, wins As Windows Set wins = ActiveWorkbook.Windows If wins.Count > 1 Then For Each win In wins If InStr(win.Caption, ":") <> 0 Then win.Close End If Next win End If End Sub 3.1.8 设置打开工作簿的密码   如需防止用户随意打开工作簿,可以为工作簿设置一个密码,每次打开工作簿时,只有输入正确的密码,才能打开该工作簿。使用Workbook对象的Password属性可以设置打开工作簿的密码。   运行下面的代码将显示一个对话框,用户需要在其中输入作为密码的字符,如果没有输入任何内容就单击“确定”按钮,或者单击“取消”按钮,则会重新显示该对话框并要求用户输入密码。当用户输入一个不为空的密码后,会将其设置为打开工作簿的密码,并将该密码保存到工作簿中。以后打开这个工作簿时,将显示如图3-6所示的对话框,只有输入正确的密码,才能打开该工作簿。 Sub 设置打开工作簿的密码() Dim strPassword As String strPassword = InputBox("输入打开工作簿的密码:") If strPassword = "" Then MsgBox "密码不能为空" 设置打开工作簿的密码 End If Workbooks("总公司").Password = strPassword Workbooks("总公司").Close True End Sub 图3-6 只有输入正确的密码才能打开工作簿   如果想要限制密码的位数,例如输入的密码不能少于6个字符,则可以使用VBA内置的Len函数检测InputBox函数的返回值的字符数。 Sub 设置打开工作簿的密码2() Dim strPassword As String strPassword = InputBox("输入打开工作簿的密码:") If strPassword = "" Or Len(strPassword) < 6 Then MsgBox "密码不能为空或不足6位" 设置打开工作簿的密码2 End If Workbooks("总公司").Password = strPassword Workbooks("总公司").Close True End Sub 3.1.9 删除所有已打开的工作簿中的密码   如需删除打开工作簿的密码,可以将一个零长度字符串赋值给Workbook对象的Password属性。删除密码前可以先使用Workbook对象的HasPassword属性检测工作簿是否包含密码,如果该属性返回True,则表示工作簿包含密码;如果该属性返回False,则表示工作簿不包含密码。下面的代码是检测打开的每一个工作簿中是否包含密码,如果有密码,则将其删除并保存工作簿。 Sub 删除所有已打开的工作簿中的密码() Dim wkb As Workbook For Each wkb In Workbooks If wkb.HasPassword Then wkb.Password = "" wkb.Save End If Next wkb End Sub 3.2 使用Worksheets集合和Worksheet对象处理工作表   使用Workbook对象的Worksheets属性可以返回Worksheets集合。打开的每一个工作簿都有一个Worksheets集合,该集合由特定工作簿中的所有工作表组成,其中的每一个工作表都是一个Worksheet对象。本节将介绍使用Worksheets集合和Worksheet对象编程处理工作表的方法。 3.2.1 从Worksheets集合和Sheets集合中引用工作表   Workbook对象有一个Worksheets集合,它由特定工作簿中的所有工作表组成,每一个工作表都是一个Worksheet对象。Workbook对象还有一个Sheets集合,该集合除了包含Worksheet对象之外,还包含Chart对象,即图表工作表。   可以使用名称或索引号从Worksheets集合和Sheets集合中引用工作表。如图3-7所示,在工作簿中有3个工作表和1个图表工作表,图表工作表位于工作表Sheet1和Sheet2之间。 图3-7 工作簿包含3个工作表和1个图表工作表   下面的两行代码分别使用Worksheets集合和Sheets集合引用名为Sheet2的工作表。 Worksheets("Sheet2") Sheets("Sheet2")   如需引用名为Chart1的图表工作表,则只能使用Sheets集合。 Sheets("Chart1")   使用索引号引用工作表时需要格外注意,如果在工作簿中同时包含工作表和图表工作表,则在Worksheets集合和Sheets集合中使用同一个索引号时,引用的可能不是同一个工作表。   仍以如图3-7所示的工作簿为例,下面两行代码都使用2作为索引号,但是它们引用的不是同一个对象,第一行代码引用的是名为Sheet2的工作表,第二行代码引用的是名为Chart1的图表工作表。对于Worksheets集合来说,索引号2表示所有工作表中第2个位置上的工作表,本例是Sheet2。对于Sheets集合来说,索引号2表示所有工作表和图表工作表中第2个位置上的对象,本例是Chart1。 Worksheets(2) Sheets(2)   使用Worksheet对象的Index属性将返回工作表在Sheets集合中的索引号。下面的代码返回Sheet2工作表在Sheets集合的索引号,本例是3,因为Sheet1工作表位于第一个位置,Chart1图表工作表位于第二个位置,Sheet2工作表位于第三个位置。 Worksheets("Sheet2").Index   使用全局成员ActiveSheet可以引用活动工作表。无论活动工作簿中有几个工作表,下面的代码始终返回活动工作表的名称。 ActiveSheet.Name   到目前为止,在本小节中使用Worksheets集合时,都省略了对其父对象Workbook的引用,所以表示的都是活动工作簿中的工作表。如需引用某个非活动工作簿中的工作表,需要在Worksheets集合的开头添加对该工作簿的引用。下面的代码引用名为“总公司”的工作簿中名为“2023”的工作表。 Workbooks("总公司").Worksheets("2023") 3.2.2 判断工作表的类型   使用ActiveSheet引用活动工作表时,由于引用前无法确定引用的是工作表还是图表工作表,为了避免执行无效操作导致运行时错误,可以先使用VBA内置的TypeName函数检测活动工作表的类型。TypeName函数返回一个字符串,它表示被检测对象的数据类型或对象类型。下面的代码使用TypeName函数检测活动工作表的类型,根据检测结果显示不同的信息。 Sub 判断工作表的类型() Select Case TypeName(ActiveSheet) Case "Worksheet" MsgBox "工作表" Case "Chart" MsgBox "图表工作表" Case Else MsgBox "其他类型" End Select End Sub 3.2.3 判断工作表是否处于保护状态   当工作表处于保护状态时,对工作表中的单元格执行操作将导致运行时错误。为了避免出错,可以先使用Worksheet对象的ProtectContents属性判断工作表是否处于保护状态,如果该属性返回True,则表示工作表处于保护状态;如果该属性返回False,则表示工作表未处于保护状态。   下面的代码判断活动工作表是否处于保护状态,如果未处于保护状态,则将该工作表中的A1:C1单元格区域的字体设置为黑体,否则提醒用户解除工作表的保护状态。 Sub 判断工作表是否处于保护状态() If Not ActiveSheet.ProtectContents Then Range("A1:C1").Font.Name = "黑体" Else MsgBox "需要先解除工作表的保护状态" End If End Sub   如果活动工作表是图表工作表,则运行上述代码将导致运行时错误。可以先判断活动工作表的类型,如果是Worksheet,再对单元格执行操作。 Sub 判断工作表是否处于保护状态2() If TypeName(ActiveSheet) = "Worksheet" Then If Not ActiveSheet.ProtectContents Then Range("A1:C1").Font.Name = "黑体" Else MsgBox "需要先解除工作表的保护状态" End If Else MsgBox "需要选择一个工作表" End If End Sub 3.2.4 使用Add方法添加新的工作表   使用Worksheets集合的Add方法可以在工作簿中添加新的工作表,Add方法的语法如下: Add(Before, After, Count, Type) * Before(可选):将添加的工作表放在指定工作表之前。 * After(可选):将添加的工作表放在指定工作表之后。如果同时省略Before和After,则默认将工作表添加到活动工作表之前。 * Count(可选):添加的工作表的数量,省略该参数时默认值是1。 * Type(可选):添加的工作表的类型,该参数为xlWorksheet表示添加工作表,省略该参数时默认添加工作表。   下面的代码在活动工作簿中添加一个新的工作表,由于没有为Add方法提供参数,所以默认将新的工作表放在活动工作表之前。 Worksheets.Add   如需将新工作表添加到工作簿中的最后一个工作表之后,可以使用Worksheets集合的Count属性获取工作表的总数,将其作为Worksheets集合的索引号以引用最后一个工作表,然后将该工作表设置为After参数的值。下面的代码在活动工作簿中的最后一个工作表之后添加3个工作表。 Worksheets.Add count:=3, after:=Worksheets(Worksheets.Count)   使用Add方法添加一个工作表时,该工作表会自动成为活动工作表,可以使用ActiveSheet引用该工作表。下面的代码判断新添加的工作表的类型。 Worksheets.Add MsgBox TypeName(ActiveSheet)   为了减少代码行数,可以将上面两行代码合并为一行。 MsgBox TypeName(Worksheets.Add)   下面的代码可以根据用户在对话框中输入的代表工作表类型的字母,自动在活动工作簿中添加相应类型的工作表。为了在对话框中显示多行信息,可以使用VBA内置常量vbCrlf在所需位置换行。由于Worksheets集合的Add方法只能添加工作表,所以添加图表工作表时需要使用Sheets集合的Add方法,并将其Type参数设置为xlChart。使用Sheets集合的Add方法也可以添加工作表,此时需要将该方法的Type参数设置为xlWorksheet。 Sub 灵活添加工作表或图表工作表() Dim strType As String, strMsg As String strMsg = "输入一个字母以添加相应类型的工作表:" strMsg = strMsg & vbCrLf & "W:工作表" strMsg = strMsg & vbCrLf & "C :图表工作表" strType = InputBox(strMsg) If strType <> "" Then Select Case LCase(strType) Case "w" Worksheets.Add MsgBox "添加了一个工作表" Case "c" Sheets.Add Type:=xlChart MsgBox "添加了一个图表工作表" Case Else MsgBox "输入的内容无效" Exit Sub End Select End If End Sub   运行上面的代码将显示如图3-8所示的对话框,输入w或c,然后单击“确定”按钮,将添加工作表或图表工作表。如果输入其他字母,则显示一条信息并退出程序。 图3-8 输入代表工作表类型的字母   如果使用功能区中的“审阅”|“保护工作簿”命令使工作簿的结构处于保护状态,则使用VBA代码添加工作表时将导致运行时错误。为了避免出错,可以先使用Workbook对象的ProtectStructure 属性判断工作簿结构是否处于保护状态,如果该属性返回True,则表示处于保护状态;如果该属性返回False,则表示未处于保护状态。下面的代码判断活动工作簿的结构的保护状态,如果处于保护状态,则显示一条信息,否则添加一个新的工作表。 Sub 判断工作簿结构的保护状态() If ActiveWorkbook.ProtectStructure Then MsgBox "需要先解除工作簿结构的保护状态" Else Worksheets.Add End If End Sub   如果只想在工作簿结构未处于保护状态时添加新的该工作表,否则不执行操作,则可以使用下面的代码。 If Not ActiveWorkbook.ProtectStructure Then Worksheets.Add 3.2.5 使用Activate和Select方法激活和选择工作表   使用Worksheet对象的Activate方法可以激活一个工作表,使其成为活动工作表,然后可以使用ActiveSheet引用活动工作表。使用Worksheet对象的Select方法可以选择一个或多个工作表,在VBA中处理特定工作表之前,不需要先使用Select方法选择工作表。   Activate方法没有参数,Select方法有一个Replace参数。当使用Select方法选择多个工作表时,需要将该方法的Replace参数设置为False。下面的代码选择活动工作簿中的前5个工作表,先选择第一个工作表,然后选择第2~5个工作表,并在每次选择时将Replace参数设置为False,选择下一个工作表时保留上一个工作表的选中状态。 Sub 选择多个工作表() Dim intIndex As Integer If Worksheets.Count >= 5 Then Worksheets(1).Select For intIndex = 2 To 5 Worksheets(intIndex).Select False Next intIndex End If End Sub   选择多个工作表的另一种方法是使用Array函数。下面的代码实现相同的功能,此处将Array函数的返回值用作Worksheets集合的索引号,以便引用想要选择的每一个工作表。 Worksheets(Array(1, 2, 3, 4, 5)).Select   Window对象的SelectedSheets属性返回的Sheets集合由选中的所有工作表和图表工作表组成。下面的代码在每一个选中的工作表的A1单元格中输入“姓名”,Windows(1)表示Excel中的活动窗口。 Sub 选择多个工作表并输入数据() Dim sht As Object Worksheets(Array(1, 2, 3, 4, 5)).Select For Each sht In Windows(1).SelectedSheets sht.Range("A1").Value = "姓名" Next sht End Sub 3.2.6 使用Name属性设置工作表的名称   每次添加新的工作表时,Excel会为其设置一个默认名称,例如“Sheet2”。为了便于识别工作表中的内容,应该为其设置一个有意义的名称。使用Worksheet对象的Name属性可以返回或设置工作表的名称。下面的代码添加一个新的工作表,并将其名称设置为“2023”。 Worksheets.Add ActiveSheet.Name = "2023"   下面的代码是在活动工作簿中现有工作表的末尾添加了3个工作表,并将它们的名称依次设置为2021、2022和2023。 Sub 添加工作表并设置名称() Dim intName As Integer For intName = 2021 To 2023 Worksheets.Add after:=Worksheets(Worksheets.Count) ActiveSheet.Name = intName Next intName End Sub   如需修改特定工作表的名称,可以将Name属性的返回值与特定字符串进行比较,在找到匹配的工作表时修改其名称。下面的代码将活动工作簿中名为“第一分公司”“第二分公司”和“第三分公司”的3个工作表的名称分别修改为“北京分公司”“天津分公司”和“上海分公司”。 Sub 修改工作表的名称() Dim wks As Worksheet For Each wks In Worksheets Select Case wks.Name Case "第一分公司" wks.Name = "北京分公司" Case "第二分公司" wks.Name = "天津分公司" Case "第三分公司" wks.Name = "上海分公司" End Select Next wks End Sub 3.2.7 使用Move方法移动工作表   使用Worksheet对象的Move方法可以在工作簿中移动工作表的位置,或者将工作表移动到其他工作簿中。Move方法有两个参数,Before参数表示将工作表移动到指定工作表之前,After参数表示将工作表移动到指定工作表之后。下面的代码将活动工作簿中的第一个工作表移动到该工作簿中的最后一个工作表之前。 Worksheets(1).Move Before:=Worksheets(Worksheets.Count)   提示:使用Move方法移动一个工作表后,该工作表将称为活动工作表。   使用Move方法还可以将一个工作表移动到其他已打开的工作簿或新建的工作簿中。如果使用不带任何参数的Move方法,则将工作表移动到一个新建的工作簿中。下面的代码将活动工作表移动到一个新建的工作簿中。 ActiveSheet.Move   如需将工作表移动到一个已打开的工作簿中,需要在Move方法的Before或After参数中添加对目标工作簿的引用。下面的代码将活动工作表移动到名为“总公司”的工作簿中的第一个工作表之前。为了避免在未打开该工作簿时出现运行时错误,加入了错误处理程序,以便在未打开该工作簿时提醒用户。 Sub 将工作表移动到其他工作簿() Dim wkb As Workbook, strName As String strName = "总公司" On Error GoTo ErrTrap Set wkb = Workbooks(strName) ActiveSheet.Move before:=wkb.Worksheets(1) Exit Sub ErrTrap: MsgBox "请先打开名为【" & strName & "】的工作簿" End Sub 3.2.8 使用Copy方法复制工作表   使用Worksheet对象的Copy方法可以复制工作表,该方法包含与Move方法完全相同的两个参数,复制后的工作表将成为活动工作表。下面的代码将活动工作表复制到一个新建的工作簿中。 ActiveSheet.Copy 3.2.9 使用Visible属性设置工作表的可见性   使用Worksheet对象的Visible属性可以返回或设置工作表的可见性,该属性的值由XlSheetVisibility常量提供,如表3-1所示。除了使用表3-1中的值之外,还可以使用True代替xlSheetVisible,使用False代替xlSheetHidden。 表3-1 XlSheetVisibility常量 名 称 值 说 明 xlSheetVisible -1 显示工作表 xlSheetHidden 0 隐藏工作表,可以使用功能区或鼠标快捷菜单中的命令重新显示工作表 xlSheetVeryHidden 2 隐藏工作表,重新显示工作表的唯一方法是将Visible属性设置为xlSheetVisible或True      隐藏一个工作表后,如需在后面的代码中引用该工作表,可以在隐藏前将该工作表赋值给一个对象变量,以后可以使用该对象变量引用这个工作表。下面的代码先隐藏活动工作表,然后询问用户是否重新显示该工作表,如果单击“是”按钮,则重新显示刚隐藏的这个工作表。 Sub 隐藏后重新显示工作表() Dim wks As Worksheet Set wks = ActiveSheet wks.Visible = xlSheetVeryHidden If MsgBox("重新显示刚隐藏的工作表吗?", vbQuestion + vbYesNo) = vbYes Then wks.Visible = True End If End Sub   下面的代码判断活动工作簿中是否存在隐藏的工作表,无论工作表是使用哪种方式隐藏的,都会使隐藏的工作表重新显示出来。 Sub 重新显示处于隐藏状态的工作表() Dim wks As Worksheet For Each wks In Worksheets If wks.Visible <> xlSheetVisible Then wks.Visible = True End If Next wks End Sub 3.2.10 使用Delete方法删除工作表   使用Worksheet对象的Delete方法可以删除工作表,删除工作表时将显示一个对话框中包含的“删除”和“取消”两个按钮,用户需要单击其中一个按钮后,VBA程序才会继续运行。如果单击“删除”按钮,则Delete方法返回True;如果单击“取消”按钮,则Delete方法返回False。   下面的代码判断用户是否真的删除了活动工作表,无论单击的是“删除”还是“取消”按钮,都会显示一条信息以告知操作状态。 Sub 判断工作表是否已被删除() If ActiveSheet.Delete Then MsgBox "活动工作表已被删除" Else MsgBox "未删除活动工作表" End If End Sub   下面的代码是删除活动工作簿中的前3个工作表。 Worksheets(Array(1, 2, 3)).Delete   下面的代码是删除活动工作簿中的最后两个工作表。 Worksheets(Array(Worksheets.Count - 1, Worksheets.Count)).Delete 3.2.11 将工作簿中的每个工作表保存为独立的工作簿   有时可能需要将一个工作簿中的每一个工作表保存为独立的工作簿,每个工作簿以工作表标签命名。下面的代码可以实现该功能,将活动工作簿中的每一个工作表保存为独立的工作簿。   本例代码由两个If Then语句组成,第一个If Then语句的功能是设置保存工作簿的路径。用户需要在如图3-9所示的对话框中选择是否使用活动工作簿的路径作为保存位置,如果单击“否”按钮,则会显示如图3-10所示的对话框,用户需要在其中输入一个路径作为保存位置。第二个If Then语句的功能是先检查路径是否有效,如果有效,则将活动工作簿中的每个工作表依次保存到指定路径中。为了避免在存在同名文件时显示替换文件的提示信息,在保存工作表之前将Application对象的DisplayAlerts属性设置为False。 图3-9 选择保存位置 图3-10 输入新的路径 Sub 将工作簿中的每个工作表保存为独立的工作簿() Dim strPath As String, wks As Worksheet If MsgBox("将活动工作簿的路径设置为保存位置吗?", vbQuestion + vbYesNo) = vbYes Then strPath = ActiveWorkbook.Path Else strPath = InputBox("输入结尾不带反斜线的路径:") End If If strPath <> "" And Dir(strPath, vbDirectory) <> "" Then Application.DisplayAlerts = False For Each wks In Worksheets wks.Copy ActiveWorkbook.SaveAs strPath & "\" & wks.Name & ".xlsx" ActiveWorkbook.Close Next wks End If End Sub       第4章 引用单元格和单元格区域   使用VBA编程处理Excel的大多数工作都是针对单元格的。Excel对象模型中的Range对象代表工作表中的单个单元格或单元格区域,单元格区域由相邻或不相邻的多个单元格组成。本章将介绍引用单元格和单元格区域的多种方法,每一种方法引用的单元格或单元格区域都是一个Range对象,可以使用Range对象的属性和方法对引用的单元格或单元格区域执行所需的操作。 4.1 使用Activate方法和ActiveCell属性引用活动单元格   正如在第2章中介绍的,可以使用全局成员引用Excel中的活动对象,例如使用ActiveSheet引用活动工作表。对于单元格来说,可以使用ActiveCell引用活动工作表中的活动单元格。当选择不止一个单元格时,选区中呈现白色背景的单元格是活动单元格,如图4-1所示的B2单元格是活动单元格。 图4-1 选区中呈现白色背景的单元格是活动单元格   在不改变选区的情况下,可以使用Range对象的Activate方法激活选区内的任意一个单元格,使其成为活动单元格。下面的代码激活如图4-1所示的选区中的D3单元格,使其成为活动单元格,如图4-2所示。 Range("D3").Activate 图4-2 激活选区中的某个单元格   如果使用Activate方法激活位于选区外的单元格,则会取消该选区的选中状态,并选中新激活的单元格,此时选区和活动单元格是同一个单元格。 4.2 使用Select方法和Selection属性引用选中的单元格   Application对象的Selection属性也是全局成员,使用Selection可以引用当前选中的对象,可能是单元格,也可能是图片、图表等对象。为了确保Selection引用的是单元格,需要在使用它之前,先使用Range对象的Select方法选择一个或多个单元格。下面的代码先选择B2:D6单元格区域,然后使用Selection引用该单元格区域,并使用VBS内置的TypeName函数显示选区的数据类型,如图4-3所示。 Range("B2:D6").Select MsgBox TypeName(Selection) 图4-3 使用Selection属性引用选中的单元格或单元格区域 4.3 使用Range属性引用单元格   Application对象、Worksheet对象和Range对象都有Range属性,Application对象的Range属性用于引用活动工作表中的单元格,Worksheet对象的Range属性用于引用特定工作表中的单元格,Range对象的Range属性用于引用单元格区域中的单元格,但是很少使用这种用法。 4.3.1 引用活动工作表中的单个单元格   下面的代码是引用活动工作表中的A1单元格。 Application.Range("A1")   使用ActiveSheet可以引用活动工作表,所以下面的代码与上面的代码等效。 ActiveSheet.Range("A1")   由于Application对象的Range属性是全局成员,所以可以省略对Application对象的引用,可将上述代码简化为以下形式: Range("A1") 4.3.2 引用活动工作表中的单元格区域   如需引用活动工作表中的一个单元格区域,可以在双引号中输入区域左上角和右下角的单元格地址,并使用冒号分隔它们。下面的代码引用活动工作表中的A1:B6单元格区域。 Range("A1:B6")   下面的代码仍然引用A1:B6单元格区域,此处是将该区域的左上角单元格和右下角单元格分别设置为Range属性的两个参数。 Range("A1", "B6")   下面的代码引用活动工作表中两个不相邻的单元格区域A1:B6和D3:E5。引用多个单元格区域时,需要使用逗号分隔各个区域。 Range("A1:B6,D3:E5") 4.3.3 引用非活动工作表中的单元格   如需引用非活动工作表中的单元格,需要为Range属性添加对特定工作表的引用。下面的代码引用名为“2023”的工作表中的A1:B6单元格区域。 Worksheets("2023").Range("A1:B6") 4.3.4 在Range属性中使用变量   可以在Range属性中使用变量,从而动态引用同一列中的不同单元格。下面的代码依次显示活动工作表中的A列前10个单元格中的值,此处使用intRow变量动态引用不同的行号,并将其与字母A组合为单元格地址。 Sub 显示A列前10个单元格中的值() Dim intRow As Integer For intRow = 1 To 10 MsgBox Range("A" & intRow).Value Next intRow End Sub 4.4 使用Cells属性引用单元格   与Range属性类似,Cells也是Application对象、Worksheet对象和Range对象的属性,该属性用于引用工作表或单元格区域中的所有单元格或特定单元格。Application对象的Cells属性也是全局成员,所以使用Cells属性引用活动工作表中的单元格时,可以省略对Application对象的引用。 4.4.1 引用工作表或单元格区域中的所有单元格   下面的3行代码都引用活动工作表中的所有单元格。 Application.Cells ActiveSheet.Cells Cells   如需引用特定范围内的单元格,可以使用Range对象的Cells属性。下面的两行代码都引用活动工作表中的A1:B6单元格区域,此处使用Cells属性显然是多余的。 Range("A1:B6") Range("A1:B6").Cells   使用Cells属性引用非活动工作表中的单元格的方法与Range属性类似,也需要添加对特定工作表的引用。下面的代码引用名为“2023”的工作表中的所有单元格。 Worksheets("2023").Cells 4.4.2 引用工作表或单元格区域中的特定单元格   如需使用Cells属性引用特定单元格,需要为Cells属性提供两个参数,第一个参数表示单元格的行号,第二个参数表示单元格的列号。下面的代码是引用活动工作表中的B6单元格。 Cells(6, 2)   可以使用列的英文字母作为Cells属性的第二个参数,下面的代码仍然引用B6单元格。 Cells(6, "B")   如果使用Range对象的Cells属性引用特定单元格,则该单元格的行列号表示的是Range对象引用的单元格区域中的相对位置。下面的代码引用位于B2:E6单元格区域中第2行第3列的单元格,即D3单元格。由于B2:E6单元格区域的第1行位于工作表的第2行,所以该区域的第2行就位于工作表的第3行。该区域的第一列位于工作表的B列,所以该区域的第3列就位于工作表的D列,最后得到的是D3单元格。 Range("B2:E6").Cells(2, 3) 4.4.3 在Cells属性中使用变量   由于Cells属性使用行列号来引用单元格,所以可以更方便地使用变量动态表示一系列单元格的行号或列号。下面的代码使用Cells属性改写4.3.4小节中的示例,此处引用单元格的代码更易于理解。 Sub 显示A列前10个单元格中的值2() Dim intRow As Integer For intRow = 1 To 10 MsgBox Cells(intRow, 1).Value Next intRow End Sub   下面的代码创建一个九九乘法表,其中声明了两个变量,分别表示Cells属性中的行号和列号。使用两个For Next语句分别在第1~9行和每一行的第1~9列中处理每个单元格,将每个单元格的值设置为行号和列号的乘积,最后将得到九九乘法表。 Sub 九九乘法表() Dim intRow As Integer, intCol As Integer For intRow = 1 To 9 For intCol = 1 To 9 Cells(intRow, intCol).Value = intRow * intCol Next intCol Next intRow End Sub 4.4.4 使用Cells属性以索引号的方式引用单元格   Cells属性的第二个参数是可选参数,当省略第二个参数时,第一个参数表示单元格的索引号,按照先行后列的顺序标识单元格。下面的代码引用活动工作表中的A2单元格,一个工作表共有16384列,16385中的前16384个数字分别引用第一行中的第1~16384个单元格,16385中的最后一个数字引用的就是第二行中的第一个单元格,即A2单元格。 Cells(16385) 4.4.5 在Range属性中使用Cells属性   可以将Range属性中的两个参数都设置为Cells形式,两个Cells分别指定单元格区域中的左上角和右下角的单元格。下面的代码引用活动工作表中的A2:B6单元格区域。 Range(Cells(2, 1), Cells(6, 2))   当使用这种形式引用非活动工作表中的单元格时需要格外注意工作表的引用问题。可能认为下面的代码引用名为“2023”的工作表中的A2:B6单元格区域,但是却会导致运行时错误。 Worksheets("2023").Range(Cells(2, 1), Cells(6, 2))   出错的原因是没有为两个Cells属性添加相同的工作表引用,将代码改写成以下形式才能使程序正确运行。 Worksheets("2023").Range(Worksheets("2023").Cells(2, 1), Worksheets("2023").Cells(6, 2))   为了减少代码的输入量并使代码更直观,可以使用With语句简化对工作表的引用。 With Worksheets("2023") .Range(.Cells(2, 1), .Cells(6, 2)) End With 4.5 使用Rows和EntireRow属性引用行   Application对象、Worksheet对象和Range对象都有Rows属性,Application对象的Rows属性用于引用活动工作表中的行,Worksheet对象的Rows属性用于引用特定工作表中的行,Range对象的Rows属性用于引用单元格区域中的行。Range对象还有一个EntireRow属性,该属性用于引用单元格区域中的整行,这些行横向贯穿工作表中的所有列。 4.5.1 使用Rows属性引用工作表或单元格区域中的行   下面的3行代码都引用活动工作表中的所有行。 Application.Rows ActiveSheet.Rows Rows   下面的代码引用活动工作表中的A2:B6单元格区域中的所有行。 Range("A2:B6").Rows   如需引用非活动工作表中的行,需要为Rows属性添加对特定工作表的引用。下面的代码引用名为“2023”的工作表中的A2:B6单元格区域中的所有行。 Worksheets("2023").Range("A2:B6").Rows   如果只想引用工作表或单元格区域中的某一行,则可以在Rows右侧的括号中输入表示行号的数字。下面的代码引用活动工作表中的第6行。 Rows(6)   引用单元格区域中的某一行时,Rows右侧的数字表示的是该单元格区域中的相对行号,而非工作表中的绝对行号。下面的代码引用活动工作表中的A2:B6单元格区域中的第2行,该行是工作表中的第3行,因为单元格区域是从工作表的第2行开始的。 Range("A2:B6").Rows(2)   提示:使用Range对象的Row属性可以返回单元格区域中的第一行的行号。使用该属性可以验证上面的代码引用A2:B6单元格区域中的第2行在整个工作表中的行号是3。 Range("A2:B6").Rows(2).Row   如需引用相邻的多行,可以在Rows右侧的括号中使用冒号连接表示起止行号的两个数字。下面的代码是引用活动工作表中的第3~5行。 Rows("3:5")   如需引用不相邻的多行,需要使用Application对象的Union方法。下面的代码引用活动工作表中的第1行、第3行和第5~7行。 Union(Rows(1), Rows(3), Rows("5:7")) 4.5.2 使用EntireRow属性引用单元格区域中的整行   只有Range对象才有EntireRow属性,Application对象和Worksheet对象没有该属性,这是因为使用Application对象和Worksheet对象的Rows属性引用的就是工作表中的整行,而Range对象的Rows属性引用的是单元格区域内的行。如需引用单元格区域在整个工作表中的整行,可以使用Range对象的EntireRow属性。   下面的代码引用活动工作表中的A2:B6单元格区域在该工作表中的所有整行。 Range("A2:B6").EntireRow   比较下面两行代码的显示结果,会更容易理解Range对象的Rows属性和EntireRow属性之间的区别。第1行代码的运行结果如图4-4所示,第2行代码的运行结果如图4-5所示。 MsgBox Range("A2:B6").Rows.Address(0, 0) MsgBox Range("A2:B6").EntireRow.Address(0, 0) 图4-4 Range对象的Rows属性 图4-5 Range对象的EntireRow属性 4.6 使用Columns和EntireColumn属性引用列   Application对象、Worksheet对象和Range对象都有Columns属性,Application对象的Columns属性用于引用活动工作表中的列,Worksheet对象的Columns属性用于引用特定工作表中的列,Range对象的Columns属性用于引用单元格区域中的列。Range对象还有一个EntireColumn属性,该属性用于引用单元格区域中的整列,这些列纵向贯穿工作表中的所有行。 4.6.1 使用Columns属性引用工作表或单元格区域中的列   下面的3行代码都引用活动工作表中的所有列。 Application.Columns ActiveSheet.Columns Columns   下面的代码引用活动工作表中的A2:B6单元格区域中的所有列。 Range("A2:B6").Columns   如需引用非活动工作表中的列,需要为Columns属性添加对特定工作表的引用。下面的代码是引用名为“2023”的工作表中的A2:B6单元格区域中的所有列。 Worksheets("2023").Range("A2:B6").Columns   如果只想引用工作表或单元格区域中的某一列,则可以在Columns右侧的括号中输入表示列号的数字或字母。下面的两行代码都是引用活动工作表中的第6列。 Columns(6) Columns("F")   引用单元格区域中的某一列时,Columns右侧括号中的数字表示的是该单元格区域中的相对列号,而非工作表中的绝对列号。下面的代码引用活动工作表中的B2:F6单元格区域中的第2列,该列是工作表中的第3列,因为单元格区域是从工作表的第2列开始的。 Range("B2:F6").Columns(2)   提示:使用Range对象的Column属性可以返回单元格区域中的第一列的列号。使用该属性可以验证上面的代码引用B2:F6单元格区域中的第2列在整个工作表中的列号是3。 Range("B2:F6").Columns(2).Column   如需引用相邻的多列,可以在Columns右侧的括号中使用冒号连接表示起止列的两个字母。下面的代码是引用活动工作表中的C~E列。 Columns("C:E")   注意:引用相邻的多列时,不能为Columns属性指定表示列号的数字。   如需引用不相邻的多列,需要使用Application对象的Union方法。下面的两行代码都引用活动工作表中的A列、C列和E~G列。 Union(Columns("A"), Columns("C"), Columns("E:G")) Union(Columns("1"), Columns("3"), Columns("E:G")) 4.6.2 使用EntireColumn属性引用单元格区域中的整列   只有Range对象才有EntireColumn属性,Application对象和Worksheet对象没有该属性,这是因为使用Application对象和Worksheet对象的Columns属性引用的就是工作表中的整列,而Range对象的Columns属性引用的是单元格区域内的列。如需引用单元格区域在整个工作表中的整列,可以使用Range对象的EntireColumn属性。   下面的代码引用活动工作表中的A2:B6单元格区域在该工作表中的所有整列。 Range("A2:B6").EntireColumn 4.7 使用Offset属性引用偏移后的单元格   Excel中的OFFSET工作表函数可以对一个单元格或单元格区域执行偏移操作,并可控制偏移后的单元格区域的大小。在VBA中,使用Range对象的Offset属性和Resize属性可以实现OFFSET函数的完整功能,Offset属性用于执行偏移操作,Resize属性用于执行调整单元格区域大小的操作。使用Offset属性可以将一个单元格或单元格区域偏移指定的行数或列数,然后返回对偏移后的单元格或单元格区域的引用。 4.7.1 偏移单元格   Offset属性有两个可选参数,第一个参数用于指定偏移的行数,正数表示向下偏移,负数表示向上偏移。第二个参数用于指定偏移的列数,正数表示向右偏移,负数表示向左偏移。将参数设置为0或省略参数的值,表示不进行相应方向上的偏移。   下面的代码是将C3单元格向下偏移3行,向右偏移两列,偏移后引用的是E6单元格。 Range("C3").Offset(3, 2)   下面的代码是将C3单元格向上偏移2行,向左偏移1列,偏移后引用的是B1单元格。 Range("C3").Offset(-2, -1)   下面的代码是将C3单元格向上偏移1行,不偏移列,偏移后引用的是C2单元格。 Range("C3").Offset(-1)   下面的代码是将C3单元格向左偏移1列,不偏移行,偏移后引用的是B3单元格。 Range("C3").Offset(0, -1)   上面的代码也可以写成下面的形式,无须输入0,但是必须保留逗号分隔符。 Range("C3").Offset(, -1)   下面的代码将导致运行时错误,这是因为将C3单元格向上偏移6行后,已经超出了工作表的范围,引用的是一个不存在的单元格。 Range("C3").Offset(-6)   上面引用单元格时都使用的是Range属性,也可以改用Cells属性。下面的代码使用Cells属性引用C3单元格,并对其执行偏移操作。 Cells(3, 3).Offset(3, 2) 4.7.2 偏移单元格区域   如果偏移的是一个单元格区域,则偏移后得到的单元格区域与偏移前的单元格区域具有相同的大小。下面的代码将C3:D5单元格区域向下偏移两行,向右偏移3列,偏移后引用的是F5:G7单元格区域。 Range("C3:D5").Offset(2, 3)   下面使用Cells属性重写上面的代码。 Range(Cells(3, 3), Cells(5, 4)).Offset(2, 3) 4.8 使用Resize属性调整引用的单元格区域的大小   使用Range对象的Resize属性可以调整单元格区域的大小,并返回对调整大小后的单元格区域的引用。Resize属性有两个可选参数,第一个参数用于指定调整大小后的区域包含的行数,第二个参数用于指定调整大小后的区域包含的列数,两个参数都必须是正数。 4.8.1 扩大单元格区域   无论起始单元格是单个单元格还是单元格区域,使用Resize属性扩大单元格区域都是以Range对象左上角单元格作为起点。下面的代码以B2单元格为起点,引用一个包含1行3列的单元格区域,即B2:D2。 Range("B2").Resize(1, 3)   如需将上面示例中的单元格区域扩大到两行,可以将Resize属性的第1个参数设置为2。下面的代码引用B2:D3单元格区域。 Range("B2").Resize(2, 3)   下面的代码将B2:D2单元格区域扩大到5行6列,即B2:G6。 Range("B2:D2").Resize(5, 6)   假设想要从B3单元格通过Resize属性引用A1:C6单元格区域,为了完成该操作,需要先使用Offset属性将B2单元格向上偏移两行,向左偏移一列,得到A1单元格。然后使用Resize属性以A1单元格为起点,将单元格区域扩大到6行3列。代码如下: Range("B3").Offset(-2, -1).Resize(6, 3) 4.8.2 缩小单元格区域   如果为Resize属性设置的参数值小于原始单元格区域中的行数或列数,则使用Resize属性调整后的单元格区域将变小。下面的代码将A1:E6单元格区域缩小到3行2列,即A1:B3。 Range("A1:E6").Resize(3, 2) 4.9 使用Union方法引用不相邻的单元格区域   虽然可以使用Range属性引用不相邻的单元格区域,但是这些单元格区域需要使用逗号分隔并组合为一个字符串,编程处理其中的每一个单元格区域不太方便。   使用Application对象的Union方法可以将多个单元格区域组合为一个整体,其中的每一个单元格区域都是一个独立的Range对象,编程处理多个Range对象要比处理拆分后的多个字符串方便且灵活得多。   使用Union方法时必须至少为其提供两个参数。下面的代码引用A1:A6和C1:C6两个单元格区域。由于Union方法是全局成员,所以可以省略对Application对象的引用。 Union(Range("A1:A6"), Range("C1:C6"))   Union方法返回一个Range对象,表示引用的多个单元格区域。为了便于在代码中使用由Union方法引用的多个单元格区域,可以声明一个Range类型的对象变量,然后将Union方法返回的多个单元格区域赋值给该对象变量。下面的代码将A1:A6和C1:C6两个单元格区域存储在rng变量中。 Dim rng As Range Set rng = Union(Range("A1:A6"), Range("C1:C6"))   当需要处理Union方法返回的多个单元格区域时,可以使用Range对象的Areas属性。该属性返回一个Areas集合,该集合由组成多个单元格区域中的每一个单元格区域组成,其中的每一个单元格区域都是一个Range对象。使用For Each语句可以逐一处理Areas集合中的每一个单元格区域。   下面的代码使用Union方法引用3个单元格区域,并在立即窗口中显示每个单元格区域包含的单元格数量,如图4-6所示。 Sub 统计每个单元格区域包含的单元格数量() Dim rngUnion As Range, rng As Range Set rngUnion = Union(Range("A1:A3"), Range("C1:C5"), Range("E1:E7")) For Each rng In rngUnion.Areas Debug.Print rng.Count Next rng End Sub 图4-6 显示每个单元格区域包含的单元格数量 4.10 使用Intersect方法引用多个单元格区域的重叠部分   当需要处理多个单元格区域的重叠部分时,可以使用Application对象的Intersect方法。如果将Union方法看作是获取多个单元格区域的并集,那么Intersect方法获取的就是多个单元格区域的交集。   在实际应用中,经常使用Intersect方法判断选择或右击的单元格是否位于特定的单元格区域之内,然后根据判断结果执行不同的操作。下面的代码检查活动单元格是否在A列中,如果不在,则显示如图4-7所示的提示信息。 Sub 检查活动单元格是否位于A列() If Intersect(ActiveCell, Columns("A")) Is Nothing Then MsgBox "活动单元格不在A列中" End If End Sub 图4-7 检查活动单元格是否位于A列 4.11 使用CurrentRegion属性引用连续数据区域   使用Range对象的CurrentRegion属性可以引用连续的数据区域,连续数据区域是指一个不包含空行或空列的数据区域。如图4-8所示是两个连续数据区域,它们被两个空行分隔。使用CurrentRegion属性可以很容易引用其中任意一个数据区域。 图4-8 使用CurrentRegion属性引用连续数据区域   下面的代码引用位于上方的数据区域。 Range("A1").CurrentRegion   下面的代码引用位于下方的数据区域。 Range("A7").CurrentRegion   在上面的两行代码中,Range属性引用的单元格可以是其所在的数据区域中的任意一个单元格。下面的任意一行代码都引用位于上方的数据区域。 Range("A2").CurrentRegion Range("B2").CurrentRegion Range("B3").CurrentRegion   同理,下面的任意一行代码都引用位于下方的数据区域。 Range("A8").CurrentRegion Range("B9").CurrentRegion Range("A10").CurrentRegion   如果希望让用户选择要引用哪个数据区域,则可以使用InputBox函数提供一个对话框,如果在其中输入1,则引用位于上方的数据区域;如果在其中输入2,则引用位于下方的数据区域。用户做出选择后,将自动选中相应的数据区域。下面的代码可以实现该功能。 Sub 自动选择用户指定的数据区域() Dim strRegionNumber As String strRegionNumber = InputBox("输入1选择上方的区域,输入2选择下方的区域") Select Case strRegionNumber Case 1 Range("A1").CurrentRegion.Select Case 2 Range("A7").CurrentRegion.Select Case Else MsgBox "输入的内容无效" End Select End Sub 4.12 使用UsedRange属性引用已使用的单元格区域   UsedRange是Worksheet对象的属性,该属性引用工作表中已使用的单元格区域。“已使用的单元格区域”既包括有数据的单元格,也包括设置了格式的空单元格。   在如图4-9所示的活动工作表中,A1:A6单元格区域包含数据,C5单元格也包含数据,下面的代码引用该工作表中已使用的单元格区域,将返回A1:C6,而不是A1:A6和C5。 ActiveSheet.UsedRange 图4-9 使用UsedRange属性引用已使用的单元格区域   如图4-10所示的已使用的单元格区域是A1:D8,这是因为在D8单元格中设置了填充色,即使该单元格不包含数据,Excel也会将其识别为已使用的单元格。 图4-10 已使用的单元格区域是A1:D8 4.13 使用End属性引用数据区域的边界   End是Range对象的属性,使用该属性可以实现按键盘上的Ctrl键+方向键的功能。End属性只有一个参数,该参数用于设置单元格的跳转方向,其值由XlDirection常量提供,如表4-1所示。 表4-1 XlDirection常量 名 称 值 说 明 xlUp -4162 向上 xlDown -4121 向下 xlToLeft -4159 向左 xlToRight -4161 向右      如图4-11所示,下面的代码从A1单元格开始,向下定位到包含连续数据的最后一个单元格。由于A7单元格是空单元格,所以包含连续数据的最后一个单元格是A6单元格。 Range("A1").End(xlDown) 图4-11 包含数据的单元格区域   将上面代码中的A1改成A6后继续向下定位,引用的将是A9单元格。这是因为A6下方的A7和A8都是空单元格,所以从A6向下定位到的是下一个包含数据的单元格,即A9单元格。 Range("A6").End(xlToRight)   与向下定位的方式类似,下面的代码从A1单元格开始,向右定位到包含连续数据的最后一个单元格,即E1单元格。从E1单元格继续向右定位,定位到的将是G1单元格。 Range("A1").End(xlDown)   当希望引用一行或一列中最后一个包含数据的单元格时,如果该行或该列中的数据不是连续的,则使用上面的方法将无法得到准确的结果。由于在Excel中很少会将数据存储到工作表中的最后一行或最后一列,所以可以从一行或一列中的最后一个单元格向前定位到下一个包含数据的单元格,该单元格就是该行或该列中最后一个包含数据的单元格。   Excel工作表的最大行数是1048576,下面的代码从A列中的最后一个单元格A1048576开始,向上查找包含数据的单元格,最先找到的单元格就是A列中最后一个包含数据的单元格。 Range("A1048576").End(xlUp)   为了可以自动获取最后一行的行号,而不是在代码中输入固定的数字,可以使用Worksheet对象的Rows属性,返回一个表示工作表中的所有行的Range对象,然后使用Range对象的Count属性计算所有行的总数,该数字相当于最后一行的行号。 Cells(Rows.Count, 1).End(xlUp) 4.14 使用SpecialCells方法引用特定类型的单元格   使用Range对象的SpecialCells方法可以引用特定数据类型的单元格,该方法实现的功能与Excel中的“定位条件”对话框相同,如图4-12所示。 图4-12 “定位条件”对话框   SpecialCells方法有两个参数,语法如下: SpecialCells(Type, Value) * Type(必需):单元格的类型,其值由XlCellType常量提供,如表4-2所示。 * Value(可选):该参数只有在将Type参数设置为xlCellTypeConstants或xlCellTypeFormulas时才有效。Value参数的值由XlSpecialCellsValue常量提供,如表4-3所示,在代码中可以同时使用该表中的多个值,它们的总和表示希望返回的多个数据类型,例如1+2+4表示返回包含数字、文本或逻辑值的单元格。 表4-2 XlCellType常量 名 称 值 说 明 xlCellTypeBlanks 4 空单元格 xlCellTypeConstants 2 包含常量的单元格 xlCellTypeFormulas -4123 包含公式的单元格 xlCellTypeComments -4144 包含批注的单元格 xlCellTypeVisible 12 所有可见单元格 xlCellTypeLastCell 11 已用区域中的最后一个单元格 xlCellTypeAllFormatConditions -4172 包含条件格式的单元格 xlCellTypeSameFormatConditions -4173 包含相同条件格式的单元格 xlCellTypeAllValidation -4174 包含数据验证的单元格 xlCellTypeSameValidation -4175 包含相同数据验证的单元格 表4-3 XlSpecialCellsValue常量 名 称 值 说 明 xlNumbers 1 数字 xlTextValues 2 文本 xlLogical 4 逻辑值 xlErrors 16 错误值      下面代码在活动工作表的独立数据区域中查找包含数字的单元格,并在立即窗口中显示这些单元格的地址,如图4-13所示。 Sub 查找包含数字但不包含日期的单元格() Dim rng As Range, rngResult As Range Set rngResult = Range("A1").CurrentRegion.SpecialCells(xlCellTypeConstants, xlNumbers) For Each rng In rngResult If Not IsDate(rng) Then Debug.Print rng.Address(0, 0) End If Next rng End Sub 图4-13 在立即窗口中显示匹配的单元格地址   代码解析:由于日期也被认为是数字,为了只匹配纯数字,需要使用VBA内置的IsDate函数逐一判断找到的每个单元格中的值是否是日期,如果不是日期,则在立即窗口中显示该单元格的地址。   下面的代码在如图4-13所示的数据区域中查找有公式返回的错误值,由于该区域中没有错误值而无法找到匹配的单元格,所以将导致运行时错误。为了避免出现运行时错误进入中断模式,在代码中加入了错误处理程序,在没有找到匹配单元格时显示提示信息。 Sub 查找错误值() Dim rng As Range, rngResult As Range Set rng = Range("A1").CurrentRegion On Error Resume Next Set rngResult = rng.SpecialCells(xlCellTypeFormulas, xlErrors) If Err.Number <> 0 Then MsgBox "没有找到错误值" End If End Sub 4.15 使用Find方法查找数据区域中的最后一个单元格   使用Range对象的Find方法可以在单元格区域中查找符合条件的单元格,其功能与“查找和替换”对话框中的“查找”选项卡相同,如图4-14所示。 图4-14 “查找和替换”对话框中的“查找”选项卡   Find方法的语法如下: Find(What, After, LookIn, LookAt, SearchOrder, SearchDirection, MatchCase, MatchByte, SearchFormat) * What(必需):要查找的内容,可以使用通配符*或?。 * After(可选):在查找的区域指定一个单元格,查找数据时,将从该单元格之后开始查找,到达区域结尾后才会查找该单元格。省略该参数时,默认将其指定为区域左上角的单元格。 * LookIn(可选):查找的内容类型,可以是值、公式或批注,该参数的值由XlFindLookIn常量提供,如表4-4所示。将该参数设置为xlFormulas时,将在组成公式的各个字符中进行查找。 * LookAt(可选):完全匹配或部分匹配,该参数的值由XlLookAt常量提供,如表4-5所示。“完全匹配”要求单元格中的内容必须与查找的内容完全一致。“部分匹配”只要求单元格中的部分内容与查找的内容相同即可。 * SearchOrder(可选):查找顺序,可以按行或按列,该参数的值由XlSearchOrder常量提供,如表4-6所示。 * SearchDirection(可选):查找方向,向区域开头或向区域末尾,该参数的值由XlSearchDirection常量提供,如表4-7所示。 * MatchCase(可选):是否区分英文字母大小写。如果该参数为True,则区分英文字母大小写;如果该参数为False,则不区分英文字母大小写。 * MatchByte(可选):是否区分全角和半角字符。如果该参数为True,则区分全角和半角字符;如果该参数为False,则不区分全角和半角字符。 * SearchFormat(可选):要查找的格式。 表4-4 XlFindLookIn常量 名 称 值 说 明 xlFormulas -4123 公式 xlComments -4144 批注 xlValues -4163 值 表4-5 XlLookAt常量 名 称 值 说 明 xlWhole 1 匹配全部搜索文本 xlPart 2 匹配任一部分搜索文本 表4-6 XlSearchOrder常量 名 称 值 说 明 xlByRows 1 从第一行开始,一行一行地查找 xlByColumns 2 从第一列开始,一列一列地查找 表4-7 XlSearchDirection常量 名 称 值 说 明 xlNext 1 在区域中查找下一个匹配值 xlPrevious 2 在区域中查找上一个匹配值      如果找到了匹配的单元格,则Find 方法将返回一个表示该单元格的Range对象。如果未找到匹配的单元格,则该方法将返回Nothing。找到一个匹配单元格后,可以使用Range对象的FindNext或FindPrevious方法,继续以相同条件查找下一个或上一个匹配的单元格。   使用Range对象的SpecialCells方法可以查找已用区域中的最后一个单元格,该单元格可能只包含格式而没有数据。如图4-15所示,E1单元格中只包含填充色而没有数据,但是Excel会认为A1:E5单元格区域是已用区域,使用SpecialCells方法找到的最后一个单元格是E5。然而,真正包含数据的最后一个单元格是D5。 图4-15 查找数据区域中的最后一个单元格   如需找到数据区域中的最后一个单元格,可以使用Range对象的Find方法。下面的代码查找活动工作表中的数据区域的最后一个单元格并显示其地址。 Sub 查找数据区域中的最后一个单元格() Dim rng As Range Dim lngLastRow As Long, lngLastCol As Long Set rng = Cells.Find("*", Range("A1"), xlValues, , xlByRows, xlPrevious) lngLastRow = rng.Row Set rng = Cells.Find("*", Range("A1"), xlValues, , xlByColumns, xlPrevious) lngLastCol = rng.Column MsgBox Cells(lngLastRow, lngLastCol).Address(0, 0) End Sub   代码解析:将SearchDirection参数设置为xlPrevious,从A1单元格向上绕到工作表的底部开始查找。按照行和按照列查找两次,并获取每次找到的单元格的行号和列号,然后将它们作为Cells属性的参数,从而返回数据区域中的最后一个单元格。   使用通配符“*”进行查找时,将Find方法的LookIn参数设置为xlFormulas或xlValues具有同等效果。xlFormulas表示查找公式,它包括用户输入的数据和组成公式的字符;xlValues表示查找值,它包括用户输入的数据和公式的计算结果。由于通配符“*”表示零个或任意个字符,所以无论设置为xlFormulas或xlValues,只要单元格中包含内容就会与“*”匹配。 4.16 使用InputBox方法引用由用户选择的单元格   第1章曾经介绍过VBA内置的InputBox函数,该函数用于创建一个对话框,并以字符串形式返回用户在对话框中输入的内容。Application对象有一个InputBox方法,其功能要比InputBox函数更强大。   InputBox方法的返回值不仅可以是字符串,还可以是数字、逻辑值或Range对象。InputBox方法还会验证用户输入的数据是否符合指定的类型,如果不符合,则将禁止后续操作。使用VBA内置的InputBox函数时,不输入任何内容而单击“确定”按钮与直接单击“取消”按钮都将返回零长度字符串,导致无法准确判断用户单击的是哪个按钮。而Application对象的InputBox方法可以很容易判断用户单击的是“确定”按钮还是“取消”按钮。   注意:Application对象的InputBox方法不是全局成员,使用该方法时必须为其添加对Application对象的引用。在代码中直接输入InputBox而不带Application对象引用时,表示使用的是VBA内置的InputBox函数。   Application对象的InputBox方法有8个参数,前7个参数与VBA内置的InputBox函数相同,第8个参数Type用于设置返回的数据类型,其值如表4-8所示。省略该参数时,InputBox方法将返回String数据类型。可以将表4-8中的多个值相加后用作Type参数的值,以使InputBox方法接受并返回多种数据类型。 表4-8 Type参数的值 值 数 据 类 型 0 公式 1 数字 2 文本 4 逻辑值 8 单元格引用 16 错误值 64 数值数组 4.16.1 让用户选择要引用的单元格   如果将InputBox方法的Type参数设置为8,则用户可以在对话框中输入有效的单元格地址,或者直接在工作表中选择所需的单元格,单击“确定”按钮后,将引用指定的单元格或单元格区域,然后使用Set语句将InputBox方法返回的Range对象赋值给一个对象变量。   下面的代码将用户选择的单元格区域赋值给一个对象变量,然后显示该单元格区域中包含数据的单元格总数,如图4-16所示。在代码中加入On Error Resume Next语句是为了避免单击“取消”按钮时,使用Set语句赋值失败而导致的运行时错误。 Sub 让用户选择要引用的单元格() Dim rng As Range On Error Resume Next Set rng = Application.InputBox("选择要引用的单元格", Type:=8) MsgBox WorksheetFunction.CountA(rng) & "个单元格包含数据" End Sub 图4-16 让用户选择所需的单元格区域并显示包含数据的单元格总数   注意:如果将InputBox方法的返回值赋值给一个非对象变量,则该变量存储的将是Range对象的值而非Range对象本身。 4.16.2 判断是否单击了“取消”按钮   如果在InputBox方法创建的对话框中单击“取消”按钮,则该方法将返回False,这样就可以判断用户是否单击了“取消”按钮。下面的代码是在用户单击“取消”按钮时显示一条信息。 Sub 判断是否单击了取消按钮() Dim lngNUmber As Long lngNUmber = Application.InputBox("输入一个整数", Type:=1) If lngNUmber = False Then MsgBox "单击了“取消”按钮" End If End Sub   如果将InputBox方法的返回值赋值给一个Range类型的对象变量,则在单击“取消”按钮时将导致运行时错误。如需避免该错误并处理单击“取消”按钮时要执行的操作,可以先忽略所有错误,然后检查对象变量是否是Nothing,如果是,则说明单击了“取消”按钮。下面的代码在用户单击“取消”按钮时,自动将A1:B6单元格区域指定为要引用的默认区域。 Sub 判断是否单击了取消按钮2() Dim rng As Range On Error Resume Next Set rng = Application.InputBox("选择一个单元格区域", Type:=8) If rng Is Nothing Then Set rng = Range("A1:B6") MsgBox "单击了“取消”按钮,自动将【" & rng.Address(0, 0) & "】指定为默认区域" End If End Sub       第5章 处理单元格中的数据   掌握第4章介绍的引用单元格的各种方法后,接下来就可以编程处理单元格中的数据了。如果能够熟练引用单元格和单元格区域,则在编程处理单元格中的数据时会觉得游刃有余。本章将介绍通过编写VBA代码在单元格中输入数据和公式、设置数据格式,以及编辑数据的方法,其中介绍了Range对象的很多属性和方法。本章还将介绍在VBA中使用数组和字典提高数据处理效率的方法,最后将介绍通过创建自定义函数来实现Excel自身不具备的数据处理功能。 5.1 在单元格中输入数据和公式   Range对象的Value属性用于在单元格中输入数据。Value是Range对象的默认属性,这意味着当在代码中省略Range对象的Value属性时,默认操作就是在与Range对象关联的单元格中输入数据。然而,为Range对象显式指定Value属性是使代码更易读的好习惯。本节将介绍不同情况下在单元格或单元格区域中输入数据的方法,还将介绍使用VBA在单元格中输入公式的方法。 5.1.1 在单个单元格中输入数据   最简单的情况是在单个单元格中输入数据,只需将要输入的数据赋值给Range对象的Value属性即可。下面的代码是在活动工作表的A2单元格中输入数字168。 Range("A2").Value = 168   下面的代码是在活动工作表的A1单元格中输入“数量”。输入文本时,需要将文本放在一对英文双引号中。 Range("A1").Value = "数量"   在单元格中输入日期的方法与输入文本相同,可以将日期放在一对英文双引号中,并将其赋值给Range对象的Value属性。下面的代码是在活动工作表的B2单元格中输入“2023/12/6”。 Range("B2").Value = "2023/12/6"   还可以使用VBA内置的DateSerial函数输入日期,该函数的3个参数分别用于指定日期中的年、月、日。下面的代码是在活动工作表的B2单元格中输入的日期是2023年12月6日。 Range("B2").Value = DateSerial(2023, 12, 6)   如需输入系统日期,可以使用VBA内置的Date函数。 Range("B2").Value = Date    5.1.2 在单元格区域中输入数据   如需在一行多列的单元格区域中输入数据,可以使用Array函数。下面的代码是在活动工作表的A1:C1单元格区域的各个单元格中依次输入“姓名”“性别”和“籍贯”。 Range("A1:C1").Value = Array("姓名", "性别", "籍贯")   下面的代码是使用Range对象的Resize属性实现相同的功能。 Range("A1").Resize(1, 3).Value = Array("姓名", "性别", "籍贯")   如需在一列多行的单元格区域中输入数据,仍然可以使用Array函数,不过需要使用WorksheetFunction对象的Transpose方法将Array函数返回的水平数组转换为垂直数组,以便与一列多行的单元格区域保持相同的方向。下面的代码是在活动工作表的A1:A3单元格区域中输入“姓名”“性别”和“籍贯”。 Range("A1:A3").Value = WorksheetFunction.Transpose(Array("姓名", "性别", "籍贯"))   如需在单元格区域中输入有规律可循的数据,可以使用For Next语句或For Each语句。下面的代码是使用For Next语句在活动工作表的A1:A100单元格区域中输入数字1~100。 Sub 输入从1开始的100个连续自然数() Dim intIndex As Integer For intIndex = 1 To 100 Range("A" & intIndex).Value = intIndex Next intIndex End Sub   下面的代码实现相同的功能,但是使用Cells属性代替Range属性来引用单元格。 Sub 输入从1开始的100个连续自然数2() Dim intIndex As Integer For intIndex = 1 To 100 Cells(intIndex, 1).Value = intIndex Next intIndex End Sub   下面的代码是使用For Each语句实现相同的功能,此处使用每个单元格的行号表示1~100个数字。 Sub 输入从1开始的100个连续自然数3() Dim rng As Range For Each rng In Range("A1:A100") rng.Value = rng.Row Next rng End Sub   第4章介绍的九九乘法表是一个在多行多列的单元格区域中输入数据的典型示例。使用两组嵌套的For Next语句分别控制行和列的循环。 Sub 九九乘法表() Dim intRow As Integer, intCol As Integer For intRow = 1 To 9 For intCol = 1 To 9 Cells(intRow, intCol).Value = intRow * intCol Next intCol Next intRow End Sub   如果希望从A列的第一行开始,每隔一行输入一个连续的编号,从1开始一直输入到100,则可以使用下面的代码。 Sub 隔行输入从1开始的100个连续编号() Dim intNumber As Integer, intRowOffset As Integer For intNumber = 1 To 100 Range("A1").Offset(intRowOffset).Value = intNumber intRowOffset = intRowOffset + 2 Next intNumber End Sub   代码解析:intNumber变量表示要输入的1~100个编号,intRowOffset变量表示输入编号的两个单元格之间的行偏移量。由于要输入100个编号,所以需要执行100次循环,使用intNumber变量作为循环计数器,从1开始,每次递增到下一个编号。由于为intRowOffset变量赋值前其初始值是0,将其设置为Offset属性的第一个参数表示不执行偏移操作,所以For Next语句中的第一次循环将在A1单元格中输入1。然后将intRowOffset变量的值加2,在进行第2次循环时,从A1单元格向下偏移2行,到达A3单元格,此时intNumber变量的值从1变成2,即在A3单元格中输入2。后续操作以此类推,每次循环都会将intRowOffset的当前值加2,并从上一个输入编号的单元格向下偏移2行。   下面的代码也可以实现相同的功能,但是不如第一种方法更容易理解。该方法是判断单元格的行号是否是奇数,如果是,则在该单元格中输入从1开始的编号。每次输入编号前,需要检查存储编号的变量的值是否大于100,如果是,则退出For Each循环,表示已经输入好了100个编号;如果不是,则继续输入下一个编号。 Sub 隔行输入从1开始的100个连续编号2() Dim intNumber As Integer, rng As Range For Each rng In Columns(1).Cells If rng.Row Mod 2 = 1 Then intNumber = intNumber + 1 If intNumber > 100 Then Exit For rng.Value = intNumber End If Next rng End Sub 5.1.3 在多个不相邻的单元格区域中输入数据   如需在多个不相邻的单元格区域中输入数据,可以先使用Application对象的Union方法引用这些单元格区域,然后使用Range对象的Areas属性返回包含这些区域的Areas集合,再使用For Each语句逐一处理该集合中的每一个单元格区域。   下面的代码在A1:A10、C1:C10和E1:E10三个单元格区域中都输入从1开始的连续编号,如图5-1所示。 Sub 在3个单元格区域中都输入从1开始的10个编号() Dim rngUnion As Range, rngs As Range, rng As Range Set rngUnion = Union(Range("A1:A10"), Range("C1:C10"), Range("E1:E10")) For Each rngs In rngUnion.Areas For Each rng In rngs rng.Value = rng.Row Next rng Next rngs End Sub 图5-1 在3个单元格区域中都输入从1开始的连续编号   如果希望3个单元格区域中的编号是连续的,则可以使用下面的代码,效果如图5-2所示。 Sub 在3个单元格区域中输入连续的编号() Dim rngUnion As Range, rngs As Range Dim rng As Range, intNumber As Integer Set rngUnion = Union(Range("A1:A10"), Range("C1:C10"), Range("E1:E10")) For Each rngs In rngUnion.Areas For Each rng In rngs intNumber = intNumber + 1 rng.Value = intNumber Next rng Next rngs End Sub 图5-2 在3个单元格区域中输入连续的编号   为了缩短代码的长度,可以使用Range属性代替Union方法来引用多个单元格区域。即使用下面的语句: Range("A1:A10, C1:C10,E1:E10")   代替下面的语句: Union(Range("A1:A10"), Range("C1:C10"), Range("E1:E10"))   其他语句不变。 5.1.4 根据一列中的值在同行的另一列中输入数据   如图5-3所示,如果希望根据B列的销售额在C列填入员工的业绩评定,则可以使用下面的代码。评定标准:销售额大于或等于5000,评为“优秀”;销售额大于或等于2000,评为“一般”;销售额低于2000,评为“不达标”。 图5-3 评定员工业绩 Sub 评定员工业绩() Dim rngs As Range, rng As Range Set rngs = Range("B2").Resize(Range("A1").CurrentRegion.Rows.Count - 1, 1) For Each rng In rngs Select Case rng.Value Case Is >= 5000: rng.Offset(0, 1).Value = "优秀" Case Is >= 2000: rng.Offset(0, 1).Value = "一般" Case Else: rng.Offset(0, 1).Value = "不达标" End Select Next rng End Sub   代码解析:首先需要获取对B列所有包含销售额的单元格的引用。方法有很多种,本例的方法是,以第一个销售额所在的B2单元格为起点,向下扩大单元格区域,直到最后一个包含销售额的单元格为止。该区域的行数由当前数据区域的总行数减1得到。当前数据区域的总行数由Range("A1").CurrentRegion.Rows.Count得到,然后在For Each语句中逐一检查每个销售额,使用Select Case语句根据销售额的值在其同行向右偏移一列的单元格中输入业绩评定结果。 5.1.5 在一列中输入某月每一天的日期   由于VBA内置的DateSerial函数分别处理日期中的年、月、日,所以可以使用变量表示灵活处理该函数中的年、月、日。下面的代码是在A列中输入2023年8月每一天的日期。 Sub 输入8月每一天的日期() Dim intRowOffset As Integer, intDay As Integer For intDay = 1 To 31 Range("A1").Offset(intRowOffset).Value = DateSerial(2023, 8, intDay) intRowOffset = intRowOffset + 1 Next intDay End Sub 5.1.6 使用Value属性或Formula属性输入公式   如需在单元格中输入公式,可以使用Range对象的Value属性或Formula属性。下面的两行代码都可以在活动工作表的E2单元格中输入一个计算B2单元格和C2单元格乘积的公式。输入后将在E2单元格中显示计算结果,公式显示在该单元格的编辑栏中,如图5-4所示。 Range("E2").Value = "=B2*C2" Range("E2").Formula = "=B2*C2" 图5-4 在单元格中输入公式   如需将E2单元格中的公式填充到其下方的3个单元格中,可以使用Range对象的FillDown方法,该方法会将单元格区域中的第一个单元格中的公式向下填充到该区域中的其他单元格。下面的代码将E2单元格中的公式填充到E3、E4和E5三个单元格中,如图5-5所示。 Range("E2:E5").FillDown 图5-5 向下填充公式   如果是新输入的公式,则可以直接将公式一次性输入到单元格区域中,Excel会自动调整每个公式中的单元格地址,从而得到正确的计算结果。下面的代码在E2:E5单元格区域中输入同一个公式。 Range("E2:E5").Formula = "=B2*C2"   Value属性和Formula属性的主要区别在于它们的返回值。对于一个包含公式的单元格,使用Value属性将返回该单元格中的公式的计算结果,使用Formula属性将以文本的形式返回该单元格中的公式。   下面的代码显示使用Value属性返回的E2单元格中的公式的计算结果,如图5-6所示。 MsgBox Range("E2").Value   下面的代码显示使用Formula属性返回的E2单元格中的公式的文本,如图5-6所示。 MsgBox Range("E2").Formula 图5-6 Value属性(左)和Formula属性(右)的返回值 5.1.7 使用FormulaArray属性输入数组公式   如需在单元格中输入数组公式,可以使用Range对象的FormulaArray属性。下面的代码是在B7单元格中输入一个用于计算所有商品总金额的数组公式,Excel会自动在公式的两侧添加大括号,如图5-7所示。 Range("B7").FormulaArray = "=SUM(B2:B5*C2:C5)" 图5-7 输入数组公式   提示:使用FormulaArray属性返回的数组公式文本不包含大括号。 5.2 设置数据格式   Range对象提供的很多属性都可以为单元格中的数据设置格式,本节将介绍几种最常见的格式,包括字体格式、对齐方式、填充格式,最后还将介绍清除数据格式的方式。 5.2.1 设置字体格式   使用Font对象可以设置单元格的字体格式,Range对象的Font属性将返回Font对象。下面的代码将活动单元格的字体设置为“宋体”。 ActiveCell.Font.Name = "宋体"   当需要设置一系列字体格式时,可以使用With语句简化代码的输入量。下面的代码将活动单元格的字体设置为“宋体”,将字号设置为“16”,将字体颜色设置为“红色”,并将字体加粗。 Sub 设置一系列字体格式() With ActiveCell.Font .Name = "宋体" .Size = 16 .Color = vbRed .Bold = True End With End Sub 5.2.2 设置对齐方式   单元格中的内容分为水平对齐和垂直对齐两种方式,只有将单元格的高度调整到足够大时,才会看到垂直对齐的效果。Range对象的HorizontalAlignment属性用于设置水平对齐方式,该属性的值由XlHAlign常量提供,如表5-1所示。Range对象的VerticalAlignment属性用于设置垂直对齐方式。该属性的值由XlVAlign常量提供,如表5-2所示。 表5-1 XlHAlign常量 名 称 值 说 明 xlHAlignGeneral 1 按照数据类型对齐 xlHAlignFill 5 填充对齐 xlHAlignCenterAcrossSelection 7 跨列居中 xlHAlignCenter -4108 居中对齐 xlHAlignDistributed -4117 分散对齐 xlHAlignJustify -4130 两端对齐 xlHAlignLeft -4131 左对齐 xlHAlignRight -4152 右对齐 表5-2 XlVAlign常量 名 称 值 说 明 xlVAlignBottom -4107 底部对齐 xlVAlignCenter -4108 居中对齐 xlVAlignDistributed -4117 分散对齐 xlVAlignJustify -4130 两端对齐 xlVAlignTop -4160 顶部对齐      下面的代码是将活动工作表中的A1:C1单元格区域中的数据在水平方向和垂直方向上都居中对齐。 Range("A1:C1").HorizontalAlignment = xlHAlignCenter Range("A1:C1").VerticalAlignment = xlVAlignCenter 5.2.3 设置填充格式   使用Interior对象可以设置单元格的填充色,Range对象的Interior属性将返回Interior对象。下面的代码将活动单元格的填充色设置为黄色。 ActiveCell.Interior.Color = vbYellow   可以使用VBA内置的RBG函数为Color属性设置颜色,下面的代码使用RGB函数为活动单元格设置黄色填充色。 ActiveCell.Interior.Color = RGB(255, 255, 0)   如需为单元格的填充色设置一种随机颜色,可以使用VBA内置的Rnd函数产生3个随机数,然后将其设置为RGB函数的参数。 Sub 设置随机填充色() Dim intR As Integer, intG As Integer, intB As Integer intR = Int(256 * Rnd) intG = Int(256 * Rnd) intB = Int(256 * Rnd) ActiveCell.Interior.Color = RGB(intR, intG, intB) End Sub   下面的代码是将活动工作表中的A1:C6单元格区域的单元格的填充色从黄色改为红色。 Sub 更改填充色() Dim rng As Range For Each rng In Range("A1:C6") If rng.Interior.Color = vbYellow Then rng.Interior.Color = vbRed End If Next rng End Sub   下面的代码是清除活动工作表中的A1:C6单元格区域中的填充色。 ActiveCell.Interior.ColorIndex = xlColorIndexNone 5.3 编辑单元格中的数据   使用Range对象的很多方法可以编辑单元格中的数据,包括复制、选择性粘贴、替换和删除等,本节将介绍这些常用的编辑操作。 5.3.1 使用PasteSpecial方法执行选择性粘贴   默认情况下,当复制并粘贴数据时,会将数据及其具有的格式一起粘贴到目标单元格中。有时可能只想粘贴数据的格式,或将公式粘贴为固定不变的值,使用Excel中的“选择性粘贴”对话框可以实现不同的粘贴需求,如图5-8所示。 图5-8 “选择性粘贴”对话框   在VBA中可以使用Range对象的PasteSpecial方法实现“选择性粘贴”对话框中的功能。PasteSpecial方法有4个参数,语法如下: PasteSpecial(Paste, Operation, SkipBlanks, Transpose) * Paste(可选):粘贴方式,该参数的值由XlPasteType常量提供,如表5-3所示。 * Operation(可选):粘贴时与目标单元格中的数据执行的运算类型,该参数的值由XlPasteSpecialOperation常量提供,如表5-4所示。 * SkipBlanks(可选):是否将源数据中的空白单元格粘贴到目标单元格。如果该参数为True,则不粘贴空白单元格;如果该参数为False,则粘贴空白单元格。默认值是False。 * Transpose(可选):粘贴时是否转置行列位置。如果该参数为True,则转置行列位置;如果该参数为False,则不转置行列位置。默认值是False。 表5-3 XlPasteType常量 名 称 值 说 明 xlPasteValidation 6 粘贴有效性 xlPasteAllExceptBorders 7 粘贴除了边框之外的所有内容 xlPasteColumnWidths 8 粘贴复制的列宽 xlPasteFormulasAndNumberFormats 11 粘贴公式和数字格式 xlPasteValuesAndNumberFormats 12 粘贴值和数字格式 续表 名 称 值 说 明 xlPasteAllUsingSourceTheme 13 使用源主题粘贴全部内容 xlPasteAllMergingConditionalFormats 14 将粘贴所有内容,并且将合并条件格式 xlPasteAll -4104 粘贴全部内容 xlPasteFormats -4122 粘贴源数据的格式 xlPasteFormulas -4123 粘贴公式 xlPasteComments -4144 粘贴批注 xlPasteValues -4163 粘贴值 表5-4 XlPasteSpecialOperation常量 名 称 值 说 明 xlPasteSpecialOperationAdd 2 复制的数据将添加到目标单元格中的值 xlPasteSpecialOperationSubtract 3 复制的数据将从目标单元格中的值中减去 xlPasteSpecialOperationMultiply 4 复制的数据会将目标单元格中的值相乘 xlPasteSpecialOperationDivide 5 复制的数据将除以目标单元格中的值 xlPasteSpecialOperationNone -4142 粘贴操作中不执行任何计算      下面的代码是将数据区域中的所有公式转换为值。 Sub 将所有公式转换为值() Dim rngFormula As Range, rng As Range Application.ScreenUpdating = False On Error Resume Next Set rngFormula = ActiveSheet.UsedRange.SpecialCells(xlCellTypeFormulas) For Each rng In rngFormula rng.Copy rng.PasteSpecial xlPasteValues Next rng Application.CutCopyMode = False End Sub   代码解析:由于无法同时复制多个不相邻的单元格,所以需要使用For Each语句逐一处理每一个包含公式的单元格。为了加快程序的运行速度,可以将Application对象的ScreenUpdating属性设置为False,关闭屏幕刷新。为了避免操作完成后,复制单元格的虚线框停留在单元格中,需要将Application对象的CutCopyMode属性设置为False。 5.3.2 使用Replace方法替换多个单元格中的数据   使用Range对象的Replace方法可以替换单元格区域中符合条件的单元格的内容,其功能与“查找和替换”对话框中的“替换”选项卡相同,如图5-9所示。   Replace方法的语法如下: Replace(What, Replacement, LookAt, SearchOrder, MatchCase, MatchByte, SearchFormat, ReplaceFormat) 图5-9 “查找和替换”对话框中的“替换”选项卡   Replace方法的大多数参数的功能与Find方法相同,下面是不同的几个参数: * Replacement(必需):替换后的内容。 * ReplaceFormat(可选):替换后的格式。   Replace方法返回一个Boolean类型的值,表示是否替换成功。   如图5-10所示,下面的代码是将活动工作表中的C列数量中的50改成60。将LookAt参数设置为xlWhole,以便严格匹配单元格中的内容,即只会修改C4和C5单元格中的内容。 Columns("C").Replace 50, 60, xlWhole   如果将LookAt参数设置为xlPart,则会将C3单元格中的150改成160,因为50与150的后两位相匹配,所以就会对其执行替换操作。 图5-10 替换数据   如需将活动工作表中的D列日期中的“10月5日”改为“10月3日”,下面的代码不会成功完成该操作,因为D列中的数据是日期类型而非普通文本。 Columns("D").Replace "10月5日", "10月3日", xlWhole   如需成功修改D列中的日期,需要使用VBA内置的DateValue函数构建替换前、后的两个日期,然后将其设置为Replace方法的What和Replacement参数。 Columns("D").Replace DateValue("10月5日"), DateValue("10月3日"), xlWhole   也可以将包含要替换的日期的单元格设置为Replace方法的What参数,代码如下: Columns("D").Replace Range("D2"), DateValue("10月3日"), xlWhole 5.3.3 删除数据   如需删除单元格中的数据和公式,可以使用Range对象的ClearContents方法。下面的代码是删除活动工作表中的A1:C6单元格区域的所有数据和公式,但是会保留为该单元格区域设置的格式。 Range("A1:C6").ClearContents   如需同时删除数据、公式和格式,可以使用Range对象的Clear方法。 Range("A1:C6").Clear   如果只想删除单元格的格式,而保留其中的数据和公式,则可以使用Range对象的ClearFormats方法。 Range("A1:C6").ClearFormats 5.3.4 删除数据区域中的所有日期   由于日期的本质是数字,所以在数据区域中同时包含数字和日期的情况下,如果只想删除其中的日期,则不能直接使用Clear或ClearContents方法,这样会删除包括日期在内的所有数据,而是需要使用VBA内置的IsDate函数判断每一个数据是否是日期,如果是日期,则将其删除。   下面的代码是使用Range对象的SpecialCells方法获取活动工作表中的所有包含数字的单元格,然后检查其中的每个单元格,如果包含日期,则将其删除。 Sub 删除数据区域中的所有日期() Dim rng As Range, rngs As Range On Error Resume Next Set rngs = Cells.SpecialCells(xlCellTypeConstants, xlNumbers) If Not rngs Is Nothing Then For Each rng In rngs If IsDate(rng) Then rng.ClearContents End If Next rng End If End Sub 5.3.5 删除数据区域中的所有空行   当需要删除数据区域中的所有空行时,应该从数据区域的底部自下而上进行删除。如果从数据区域的顶部自上而下进行删除,由于每次删除行时,位于其下方的行的行号会发生变化,所以很可能会导致错误的结果。下面的代码是删除活动工作表中已用区域的所有空行。 Sub 删除数据区域中的所有空行() Dim lngLastRow As Long, lngRow As Long lngLastRow = ActiveSheet.UsedRange.Rows.Count For lngRow = lngLastRow To 1 Step -1 If WorksheetFunction.CountA(Rows(lngRow).Cells) = 0 Then Rows(lngRow).Delete End If Next lngRow End Sub 5.4 使用数组提高数据处理效率   数组是一种特殊的变量,普通变量只能存储一个值,而在一个数组中可以存储多个值,并可以使用For Next语句或For Each语句快速处理数组中的每一个值。数组中的每一个值称为数组元素,它们在数组中都有一个索引号,通过索引号可以引用特定的数组元素,这种方式类似于使用索引号从Workbooks集合或Worksheets集合中引用特定的工作簿或工作表。本节将介绍创建和使用数组的相关技术,并介绍使用数组在单元格区域中读取和写入数据的方法。 5.4.1 创建索引号从0开始的数组   由于数组是一种特殊的变量,所以创建数组的第一步就是先声明它,方法与声明普通变量类似,可以使用Dim、Static、Private或Public关键字。与声明普通变量的区别是,在声明数组时,需要在数组名称的右侧添加一对小括号,并在其中输入一个数字,该数字指明数组的元素个数。   下面的代码声明一个名为Names的数组,虽然右侧括号中的数字是2,但是该数组包含3个元素,这是因为数组的索引号默认从0开始。 Dim Names(2)   数组也具有和普通变量一样的数据类型,下面的代码将上面的Names数组声明为String类型。 Dim Names(2) As String   为数组赋值时,需要单独为数组中的每一个元素赋值。使用索引号从数组中引用不同的元素,然后使用等号为每个数组元素赋值。下面的代码是将3个名称赋值给Names数组中的3个元素。 Sub 为数组赋值() Dim Names(2) As String Names(0) = "北京" Names(1) = "天津" Names(2) = "上海" End Sub   与为数组赋值的方法类似,当使用数组中的值时,也需要以数组名+索引号的形式引用特定的数组元素。下面的代码将在对话框中显示Names数组中第二个元素的值。 MsgBox Names(2) 5.4.2 创建索引号从1开始的数组   在VBA中,数组中的第一个元素的索引号默认为0,导致数组包含的元素个数比声明数组时指定的数字大1。如果希望数组中第一个元素的索引号从1开始,则可以使用以下两种方法: * 在模块顶部的声明部分输入Option Base 1语句,该模块中的任意过程中的数组的起始索引号都会从1开始。 * 声明数组时,使用To关键字设置数组的起始索引号和终止索引号。   下面的代码使用To关键字声明一个包含3个元素的数组,该数组的起始索引号是1,终止索引号是3。 Dim Names(1 To 3) As String 5.4.3 确定数组的下限和上限   数组的下限是指数组中第一个元素的索引号,数组的上限是指数组中最后一个元素的索引号。使用VBA内置的Lbound函数和Ubound函数可以获取数组的下限和上限。然后将其指定为For Next语句中的计数器的起始值和终止值,以便使用该语句处理数组中的每一个元素。   下面的代码是计算数组包含的元素个数。 Sub 计算数据包含的元素个数() Dim Names(2) As String, intCount As Integer intCount = UBound(Names) - LBound(Names) + 1 MsgBox "Names数组包含" & intCount & "个元素" End Sub   下面的代码使用For Next语句为Names数组中的每一个元素赋值,每个元素的值等于计数器的当前值与100的乘积,最后在对话框中显示所有数组元素的值,如图5-11所示。 Sub 自动为数组中的所有元素赋值() Dim Names(2) As String, intIndex As Integer Dim strMsg As String For intIndex = LBound(Names) To UBound(Names) Names(intIndex) = intIndex * 100 strMsg = strMsg & Names(intIndex) & vbCrLf Next intIndex MsgBox strMsg End Sub 图5-11 显示所有数组元素的值 5.4.4 使用Array函数创建数组   使用VBA内置的Array函数可以创建一个Variant类型的数组,并自动完成数组元素的赋值。下面的代码与5.4.1小节中的示例类似,但是此处只需两行代码即可完成数组的创建和赋值。 Dim Names As Variant Names = Array("北京", "天津", "上海")   使用Array函数创建的数组的下限默认为0,使用Option Base 1语句可使其下限变为1。如果使用VBA.Array的形式创建数组,则该数组的下限始终是0,不会受Option Base 1语句的影响。 5.4.5 创建二维数组   前面介绍的都是一维数组,它只有一个维度,可以将其看作存储在一行中的数据。如需处理行、列两个方向上的数据,可以创建二维数组。声明二维数组时,需要在数组名称右侧的括号中添加两个数字,第一个数字表示数组第一维的上限,第二个数字表示数组第二维的上限,使用逗号分隔两个数字。声明一维数组的方法同样适用于二维数组。      下面的两行代码声明的都是一个3行6列的二维数组,该数组包含3×6=18个元素,两个数组的区别是具有不同的下限。 Dim Names(2, 5) As String Dim Names(1 To 3, 1 To 6) As String   使用Lbound函数和Ubound函数可以检测二维数组中每一维的下限和上限。下面的代码检测并显示Names(2, 5)数组中第一维和第二维的下限和上限,如图5-12所示。 Sub 检测二维数组的下限和上限() Dim Names(2, 5) As String Dim strFD As String, strSD As String strFD = "第一维的下限和上限是:" & LBound(Names, 1) & "和" & UBound(Names, 1) strSD = "第二维的下限和上限是:" & LBound(Names, 2) & "和" & UBound(Names, 2) MsgBox strFD & vbCrLf & strSD End Sub 图5-12 检测二维数组的下限和上限   为二维数组中的元素赋值或引用二维数组中的元素时,需要为每个数组元素提供两个索引号。下面的代码将从A开始的18个连续的英文字母赋值给包含18个元素的Names二维数组。 Sub 为二维数组赋值() Dim Names(1 To 3, 1 To 6) As String, intIndex As Integer Dim intRow As Integer, intCol As Integer intIndex = 65 For intRow = LBound(Names, 1) To UBound(Names, 1) For intCol = LBound(Names, 2) To UBound(Names, 2) Names(intRow, intCol) = Chr(intIndex) intIndex = intIndex + 1 Next intCol Next intRow End Sub   代码解析:Chr是一个VBA的内置函数,用于将字符编码转换为相应的字符,本例使用该函数将字符编码转换为从A开始的大写英文字母。大写字母A的编码是65,所以声明一个变量,将其初始值设置为65,然后在For Next循环中持续为该变量加1,从而得到连续的大写字母。 5.4.6 创建动态数组   动态数组是在创建数组时不指定数组包含的元素个数,在程序运行过程中指定数组包含的元素个数。声明动态数组时,不需要在数组名称右侧的括号中输入数字,保留一对空括号即可。下面的代码声明一个名为Names的动态数组。 Dim Names() As String   在后面的代码中使用ReDim语句设置动态数组的维度数,以及每个维度的下限和上限。下面的代码将Names动态数组指定为包含3个元素的一维数组,其下限是1,上限是3。 ReDim Names(1 To 3)   由于在程序运行前,无法获悉在Excel中打开的工作簿总数,所以需要创建一个动态数组,通过在程序运行时获取打开的工作簿总数,然后使用该值重新定义数组的大小。下面的代码将当前打开的所有工作簿的名称存储在Names动态数组中。 Sub 使用动态数组存储所有打开工作簿的名称() Dim Names() As String, intIndex As Integer, wkb As Workbook ReDim Names(1 To Workbooks.Count) For Each wkb In Workbooks intIndex = intIndex + 1 Names(intIndex) = wkb.Name Next wkb End Sub   注意:在程序中可以多次使用ReDim语句调整数组的大小,但是会删除数组中现有的值。如需保留数组中的值,可以在ReDim语句的右侧添加Preserve关键字。在二维数组中使用Preserve关键字时,只能调整第二维的大小,且不能改变数组的维数。 5.4.7 使用数组在单元格区域中读取和写入数据   前面曾经多次在For Each语句使用一个Range类型的对象变量来处理单元格区域中的每一个单元格,使用这种方式处理数据的效率并不是最高的,尤其在处理大范围单元格区域时,程序的运行速度会显著变慢。   为了提高程序的运行效率,可以将单元格区域中的所有数据赋值给一个数组,此时会创建一个二维数组,数组的第一维代表单元格区域中的行,数组的第二维代表单元格区域中的列。使用只有一行或一列的单元格区域创建的数组也是二维数组。对数组中的数据处理完成后,再将数组中的所有数据一次性写入单元格区域,这种方式比直接处理单元格区域快得多。   将单元格区域中的所有数据赋值给一个数组时,该数组必须是Variant类型。下面的代码是将B2:C5单元格区域中的数据赋值给varData变量。 Dim varData As Variant varData = Range("B2:C5").Value   无论在模块顶部的声明部分中是否包含Option Base 1语句,将单元格区域中的数据赋值给Variant类型的变量时,创建的二维数组中的第一维和第二维的下限始终都是1。   如图5-13所示的数据在前面的示例中出现过,但是此处将使用数组来处理这些数据,计算每个商品的金额,并填入E2:E5单元格区域。 图5-13 使用数组处理单元格区域中的数据      代码如下: Sub 使用数组处理单元格区域中的数据() Dim varData As Variant, intRow As Integer Dim varDataResult() As Variant varData = Range("B2:C5").Value ReDim varDataResult(1 To UBound(varData, 1)) For intRow = 1 To UBound(varData, 1) varDataResult(intRow) = varData(intRow, 1) * varData(intRow, 2) Next intRow Range("E2").Resize(UBound(varDataResult)).Value = WorksheetFunction.Transpose (varDataResult) End Sub   代码解析:声明了3个变量,varData变量用于存储B2:C5单元格区域中的数据,intRow变量用作For Next语句中的计数器,用于表示B2:C5单元格区域中的每一行,varDataResult是一个动态数组,用于存储每一行中的两列数据的乘积。将B2:C5单元格区域中的数据赋值给varData变量后,使用ReDim语句将动态数组的下限设置为1,将上限设置为varData数组第一维的上限,即B2:C5单元格区域的总行数。接下来在For Next语句中处理varData数组中的每一行数据,将每一行中的第一列和第二列数据相乘,并将乘积赋值给varDataResult数组中的每一个元素,元素的索引号由intRow变量确定。最后以E2单元格为起点,使用Resize属性根据varDataResult数组的上限来确定写入数据的单元格区域需要多少行,然后将varDataResult数组中的数据赋值给该单元格区域。由于varDataResult是一个一维数组,而写入数据的单元格区域是在一列的方向上,所以需要将水平方向的一维数组转换为垂直方向。 5.5 使用字典提高数据处理效率   字典与数组类似,也可以存储多个值,但是它比数组具有更多的优点。数组只是一个变量,而字典是一个对象。与前面介绍过的其他对象类似,字典对象也有自己的属性和方法,为处理数据提供方便。本节将介绍创建和使用字典对象处理数据的方法。 5.5.1 创建字典对象   在VBA中不能直接使用字典,需要先加载字典对象所属的类型库,在VBE窗口中单击菜单栏中的“工具”|“引用”命令,打开“引用”对话框,勾选“Microsoft Scripting Runtime”复选框,然后单击“确定”按钮,如图5-14所示。   完成上述操作后,可以在对象浏览器中选择Scripting库,然后在下方选择Dictionary,右侧将显示字典对象的属性和方法,如图5-15所示。   现在可以在VBA中使用字典了。与使用其他对象类似,首先需要创建一个类型为Dictionary的对象变量,然后使用Set语句和New关键字将一个Dictionary对象赋值给该对象变量,代码如下: Dim dic As Dictionary Set dic = New Dictionary 图5-14 加载字典对象所属的类型库 图5-15 在对象浏览器中查看字典对象的属性和方法   也可以使用下面的一行代码代替上面的两行代码,在声明变量的同时为其赋值。 Dim dic As New Dictionary   提示:上面介绍的方法称为前期绑定,还有一种无须预先加载类型库而在VBA中直接创建Dictionary对象的方法,这种技术被称为后期绑定。使用后期绑定技术创建Dictionary对象需要使用VBA内置的CreateObject函数,此时需要声明一个Object类型的对象变量,然后使用Set语句将Dictionary对象赋值给该对象变量,双引号中的Scripting表示Dictionary对象所属的类型库的名称。前期绑定和后期绑定的更多内容将在第11章进行详细介绍。 Dim dic As Object Set dic = CreateObject("Scripting.Dictionary")   接下来就可以使用Dictionary对象的属性和方法处理数据了。 5.5.2 在字典中添加数据   在字典中可以包含一项或多项数据,每项数据由关键字和值两部分组成,关键字和值是关联在一起的,这样可以通过关键字找到与其对应的值。在字典中添加数据有两种方法。   1. 使用Add方法   使用Dictionary对象的Add方法可以添加数据,Add方法的语法如下: Add(Key Item) * Key(必需):一项数据中的关键字。 * Item(必需):一项数据中的值。   下面的代码在字典中添加两项数据,第一项数据的关键字是“牛奶”,与该关键字关联的值是2。第二项数据的关键字是“酸奶”,与该关键字关联的值是3.5。两项数据表示的都是商品的名称和单价。 Dim dic As New Dictionary dic.Add "牛奶", 2 dic.Add "酸奶", 3.5   注意:由于字典中的所有数据的关键字都是唯一的,不能出现重复的关键字,所以在使用Add方法添加数据时,如果正在添加的关键字与字典中现有的关键字相同,则将导致运行时错误。   2. 使用Item属性   使用Dictionary对象的Item属性可以设置或返回特定关键字的值。如果关键字不存在,则会使用该关键字和为其设置的值在字典中添加一项新数据。 Dictionary.Item(Key)=Item   下面的代码与前面使用Add方法添加数据的效果相同,但是此处使用的是Item属性。 Dim dic As New Dictionary dic.Item("牛奶") = 2 dic.Item("酸奶") = 3.5   由于Item是Dictionary对象的默认属性,所以可以省略Item属性,代码如下: dic("牛奶") = 2 dic("酸奶") = 3.5 5.5.3 删除字典中的数据   如需删除字典中的一项数据,可以使用Dictionary对象的Remove方法。只需为该方法提供要删除的数据中的关键字,即可将该项数据删除。下面的代码是删除关键字为“酸奶”的数据。 dic.Remove "酸奶"   如果关键字不存在,则会出现运行时错误。为了避免错误,应该在删除前使用Dictionary对象的Exists方法检查特定的关键字是否存在。下面的代码是在删除以“酸奶”为关键字的数据之前,先检查该关键字是否存在,如果存在则将其删除,否则显示一条信息。 Sub 删除数据前检查关键字是否存在() Dim dic As New Dictionary, strKey As String strKey = "酸奶" If dic.Exists(strKey) Then dic.Remove strKey Else MsgBox "不存在【" & strKey & "】关键字" End If End Sub   如需删除字典中的所有数据,可以使用Dictionary对象的RemoveAll方法。即使字典不包含任何数据,执行该方法也不会出现运行时错误。 dic.RemoveAll 5.5.4 获取字典中的所有关键字和值   使用Dictionary对象的Keys方法可以获取字典中所有数据的关键字,使用Dictionary对象的Items方法可以获取字典中所有数据的值。这两个方法返回的都是一个下限始终为0的Variant类型的一维数组。   下面的代码先向字典中添加3项数据,然后将字典中的所有关键字写入以A1单元格为起点的一列中,将字典中的所有值写入以B1单元格为起点的一列中,如图5-16所示。 Sub 获取所有关键字和值() Dim dic As New Dictionary dic.Add "牛奶", 2 dic.Add "酸奶", 3.5 dic.Add "早餐奶", 2.5 Range("A1").Resize(dic.Count, 1).Value = WorksheetFunction.Transpose(dic.Keys) Range("B1").Resize(dic.Count, 1).Value = WorksheetFunction.Transpose(dic.Items) End Sub 图5-16 获取字典中的所有关键字和值   代码解析:dic是一个声明为Dictionary类型的对象变量,dic.Count获取字典中的值的数量,其设置为Resize属性的第一个参数,以便确定输入关键字和值时所需的行数。由于Dictionary对象的Keys方法和Items方法返回的都是行方向上的一维数组,为了将它们的值输入到列中,需要使用Transpose方法将行转换为列。 5.5.5 使用字典提取数据区域中的不重复数据   字典和数组有很多类似之处,但是在访问数据方面比数组更有优势,最显著的优点是可以在字典中存储不重复的关键字,这样就可以在Excel中使用字典从数据区域中提取不重复的数据。   如图5-17所示,A列中的最后3行数据与前几行数据有重复,下面的代码使用字典从A1:B8单元格区域中提取不重复数据,并将提取后的数据写入以D1单元格为起点的单元格区域中。 Sub 提取数据区域中的不重复数据() Dim dic As New Dictionary, varData As Variant, intRow As Integer varData = Range("A1:B8").Value For intRow = 1 To UBound(varData) If Not dic.Exists(varData(intRow, 1)) Then dic.Add varData(intRow, 1), varData(intRow, 2) End If Next intRow Range("D1").Resize(dic.Count, 1).Value = WorksheetFunction.Transpose(dic.Keys) Range("E1").Resize(dic.Count, 1).Value = WorksheetFunction.Transpose(dic.Items) End Sub 图5-17 提取数据区域中的不重复数据 5.6 创建自定义函数以增强数据处理能力   本节将介绍通过创建自定义函数来增强处理单元格中数据的能力,它们实现的是Excel内置的工作表函数不具备的功能,或者需要编写复杂的公式才能实现的功能。本节创建的自定义函数的工作方式类似于Excel内置的工作表函数,都以公式的形式输入到单元格中。 5.6.1 创建自定义函数的注意事项   为了与Excel内置的工作表函数的英文名称统一,用户创建的自定义函数也应该使用英文名称。每个自定义函数都是一个Function过程,Function过程可以没有参数,类似于Excel内置的NOW函数,也可以有一个或多个参数,类似于Excel内置的SUM函数。参数分为必需和可选两类。   如需创建带有参数的Function过程,需要在Function过程名称右侧的小括号中指定参数的名称和数据类型,参数的语法如下: [Optional] [ByVal | ByRef] [ParamArray] varname[( )] [As type] [= defaultvalue] * Optional(可选):将参数指定为可选参数。如果将一个参数指定为可选参数,则位于该参数后面的其他参数都需要使用Optional关键字指定为可选参数。如果使用函数时省略可选参数的值,则将使用由defaultvalue定义的默认值。使用VBA内置的IsMissing函数可以判断是否省略了可选参数。 * ByVal和ByRef(可选):使用ByVal关键字将按值传递参数,使用ByRef关键字将按地址传递参数,默认按地址传递参数。 * ParamArray(可选):将参数指定为不限数量的可选参数。使用ParamArray关键字指定的参数必须是Function过程的最后一个参数,且该参数的数据类型必须是Variant。在一个Function过程中,Optional和ParamArray两个关键字只能使用其中之一。 * varname(可选):参数的名称。 * type(可选):参数的数据类型,与变量的数据类型相同。 * defaultvalue(可选):为使用Optional关键字指定的可选参数设置默认值。   Excel内置的某些工作表函数具有易失性,这种特性是指对工作表中的任意单元格执行计算或编辑时,将使公式中的函数自动重新计算。在用户创建的Function过程中可以使用Application对象的Volatile方法实现易失性功能。Volatile方法有一个参数,该参数为True表示启用易失性功能,该参数为False表示禁用易失性功能,省略该参数时默认为True。 Application.Volatile True 5.6.2 为自定义函数添加帮助信息   在“插入函数”对话框中选择一个Excel内置的工作表函数时,在下方会显示该函数的语法格式和功能的帮助信息,如图5-18所示。用户也可以为创建的自定义函数添加帮助信息,以及将自定义函数添加到特定的函数类别中。   Application对象的MacroOptions方法用于为自定义函数创建帮助信息并设置其所属的函数类别,MacroOptions方法的语法如下: MacroOptions(Macro, Description, HasMenu, MenuText, HasShortcutKey, ShortcutKey, Category, StatusBar, HelpContextID, HelpFile, ArgumentDescriptions)   MacroOptions方法有多个参数,最常用的是以下几个参数。 * Macro:设置帮助信息的自定义函数的名称。 图5-18 Excel内置的工作表函数的帮助信息 * Description:函数功能的简要说明。 * Category:将自定义函数添加到的函数类别的名称或编号,可以是Excel内置的函数类别,也可以是新增的类别。Excel内置的函数类别的名称及其编号如表5-5所示,某些类别不会显示在“插入函数”对话框中。 * ArgumentDescriptions:参数的简要说明,该说明信息存储在一个Variant类型的数组中,可以使用Array函数设置该参数的值。 表5-5 Excel内置的函数类别 类 别 名 称 类 别 编 号 类 别 名 称 类 别 编 号 全部 0 命令 10 财务 1 自定义 11 日期与时间 2 宏控件 12 数字与三角函数 3 DDE/外部 13 统计 4 用户定义 14 查找与引用 5 工程 15 数据库 6 多维数据集 16 文本 7 兼容性 17 逻辑 8 Web 18 信息 9      下面的代码为名为MyFunction的自定义函数设置帮助信息,由于没有设置Category参数,所以该函数默认位于“用户定义”类别中。 Sub 为自定义函数添加帮助信息() Dim strDes As String, varArg As Variant strDes = "这是一个自定义函数" varArg = Array("要计算的一个或多个单元格", "计算类型") Application.MacroOptions macro:="MyFunction", Description:=strDes, ArgumentDescriptions: =varArg End Sub   上面的Sub过程只需运行一次,即使关闭Excel再重新打开,为MyFunction自定义函数设置的帮助信息始终都会显示在 “插入函数”对话框的“用户定义”类别中,如图5-19所示。单击“确定”按钮,在打开的“函数参数”对话框中会显示参数的说明信息,如图5-20所示。 图5-19 为自定义函数添加的帮助信息 图5-20 为函数的参数添加的帮助信息 5.6.3 将数据按照字符倒序排列   下面的代码是创建名为URevChar的自定义函数,用于将用户输入的数据或单元格中的数据按照字符倒序排列,如图5-21所示。URevChar的函数只有一个参数,表示要倒序排列的数据。 Function URevChar(varData) As String Dim intIndex As Integer, strChar As String If TypeName(varData) = "Range" Then strChar = varData.Value Else strChar = varData End If For intIndex = 1 To Len(strChar) URevChar = Mid(strChar, intIndex, 1) & URevChar Next intIndex End Function 图5-21 倒序排列单元格中的内容   代码解析:由于本例中的函数支持用户输入的数据或者单元格中的数据,所以需要判断用户为函数指定的varData参数是否是单元格,如果是单元格,则通过Range对象的Value属性获取单元格中的数据,并将其赋值给在函数内部声明的strChar变量;如果不是单元格,则说明是用户手动输入的数据,此时将varData参数直接赋值给strChar变量。然后在For Next语句中逐个提取strChar变量中保存的数据,并以反方向写入到UrevChar函数名中,这样该函数返回的就是按照倒序排列的数据。   提示:为了便于区分自定义函数和Excel内置函数,可以在自定义函数名称的开头添加大写字母U。 5.6.4 提取文本中的多段数字   下面的代码是创建名为USplitNumbers的自定义函数,用于将混合在文本中的多段数字分别提取到多个单元格中,使用该函数时,需要选择一行中的多个单元格,输入公式后按Ctrl+Shift+ Enter组合键,以数组公式的方式完成输入,如图5-22所示。USplitNumbers函数只有一个参数,表示包含分段数字的单元格。 Function USplitNumbers(rng As Range) Dim strText As String, intPos As Integer Dim Numbers() As Integer, intIndex As Integer Dim x As Variant strText = rng.Value For intPos = 1 To Len(strText) If Not IsNumeric(Mid(strText, intPos, 1)) Then strText = Replace(strText, Mid(strText, intPos, 1), "*") End If Next intPos For Each x In Split(strText, "*") If IsNumeric(x) Then intIndex = intIndex + 1 ReDim Preserve Numbers(1 To intIndex) Numbers(intIndex) = x End If Next x USplitNumbers = Numbers End Function 图5-22 提取文本中的多段数字   代码解析:首先将USplitNumbers函数的rng参数表示的单元格中的值赋值给strText变量,然后在For Next语句中逐个检查strText变量中的每一个字符。如果当前字符不是数字,则使用星号“*”替换该字符,并将替换后的整个字符串赋值给strText变量,以对其进行更新,这样后续就会在新的字符串中查找下一个字符,并继续将非数字字符替换为“*”。全部完成后,得到的是由数字和“*”组成的文本,本例为1*23**456***。然后使用Split函数将该文本以“*”符号拆分为多个部分,得到的将是一个数组。使用For Each语句在该数组中检查每一个元素是否是数字,如果是,则重新定义Numbers动态数组的下限和上限,将下限始终都设置为1,而上限是当前已确定是数字的数组元素的数量,该数量由intIndex变量控制。然后将当前是数字的数组元素赋值给动态数组中的当前元素,元素的索引号就是intIndex变量的值。将所有是数字的数组元素赋值到Numbers动态数组中之后,退出For Each语句,并将该动态数组赋值给函数的名称。 5.6.5 根据单元格填充色对数据求和   下面的代码是创建名为USumByColor的自定义函数,用于根据指定单元格中的填充色,对单元格区域中具有相同填充色的单元格中的值进行求和,如图5-23所示。USumByColor函数有两个参数,rngColor参数表示用作填充色参照基准的单元格,rngSum参数表示要求和的单元格区域。 Function USumByColor(rngColor As Range, rngSum As Range) Dim rngCell As Range For Each rngCell In rngSum If rngCell.Interior.Color = rngColor.Interior.Color Then USumByColor = USumByColor + rngCell.Value End If Next rngCell End Function 图5-23 根据单元格填充色对数据求和   代码解析:在Function过程中声明一个Range类型的对象变量,使用它在由rngSum参数指定的单元格区域中逐个检查每一个单元格的填充色,如果与由rngColor参数指定的单元格的填充色相同,则对单元格的值进行累加。最后得到的就是与rngColor参数指定的单元格的填充色相同的所有单元格中的值的总和。本例假设单元格区域中的值都是数字,如果存在文本,则可以在代码中使用IsNumeric函数判断值是否是数字,然后再执行累加操作。 5.6.6 统计数据区域中不重复值的数量   下面的代码是创建名为UUniqueCount的自定义函数,用于统计单元格区域中不重复值的数量,如图5-24所示。UUniqueCount函数只有一个参数,表示要统计不重复值数量的单元格区域。 Function UUniqueCount(rngs As Range) Dim rng As Range, dic As Object Set dic = CreateObject("Scripting.Dictionary") For Each rng In rngs If Not dic.Exists(rng.Value) And Not IsEmpty(rng.Value) Then dic.Add rng.Value, rng.Value End If Next rng UUniqueCount = dic.Count End Function 图5-24 统计数据区域中不重复值的数量   代码解析:本例使用后期绑定技术创建Dictionary对象,然后检查指定单元格区域中的每一个单元格,如果字典中不存在与当前单元格中的值相同的关键字,并且单元格不为空,则将该值作为关键字添加到字典中。检查完所有单元格后,将字典中包含数据的数量赋值给函数名称,得到的就是不重复值的数量。如果不使用IsEmpty函数判断单元格是否为空,则最后计算出的不重复值会多1。 5.6.7 提取数据区域中的不重复值   只需对5.6.6小节中的代码稍加改动,即可实现提取不重复值的功能,将函数名称改为UgetUniqueValues,并修改最后为函数名赋值的代码,将原来的UUniqueCount = dic.Count改为下面的代码: UGetUniqueValues = WorksheetFunction.Transpose(dic.Keys)   本例的完整代码如下,使用UgetUniqueValues函数提取不重复值的效果如图5-25所示,需要按Ctrl+Enter+Shift组合键,以数组公式的形式完成输入。 Function UGetUniqueValues(rngs As Range) Dim rng As Range, dic As Object Set dic = CreateObject("Scripting.Dictionary") For Each rng In rngs If Not dic.Exists(rng.Value) And Not IsEmpty(rng.Value) Then dic.Add rng.Value, rng.Value End If Next rng UGetUniqueValues = WorksheetFunction.Transpose(dic.Keys) End Function 图5-25 提取数据区域中的不重复值 5.6.8 在一行或一列中输入指定起始值和终止值的连续编号   下面的代码是创建名为UInputNumbers的自定义函数,用于在选中的单元格区域中自动填充连续编号,其中的起始编号和终止编号由用户指定,并可以指定将连续编号填充在一行或一列中,如图5-26所示。 图5-26 在一行或一列中输入指定起始值和终止值的连续编号   UInputNumbers函数有3个参数,第一个参数用于指定起始编号,第二个参数用于指定终止编号,第三个参数是一个可选参数,用于指定编号的填充方向,输入H表示填充在一行,输入V表示填充在一列,省略该参数将默认填充在一列中。如果输入H和V之外的字符,则UinputNumbers将返回“第三个参数只能输入H或V”的文本信息。 Function UInputNumbers(intStart As Integer, intEnd As Integer, Optional HV As String = "V") Dim Numbers() As Integer, intIndex As Integer, intNumbersCount As Integer intNumbersCount = intEnd - intStart + 1 ReDim Numbers(1 To intNumbersCount) For intIndex = 1 To intNumbersCount Numbers(intIndex) = intStart intStart = intStart + 1 Next intIndex Select Case LCase(HV) Case "h" UInputNumbers = Numbers Case "v" UInputNumbers = WorksheetFunction.Transpose(Numbers) Case Else UInputNumbers = "第三个参数只能输入H或V" End Select End Function   代码解析:首先使用由用户指定的intStart和intEnd两个参数计算出连续编号包含的数字个数,然后定义动态数组,将该值设置为动态数组的上限,将1设置为动态数组的下限。在For Next循环中将从起始编号开始的各个编号依次赋值给动态数组中的每个元素。接着检查用户为第三个参数输入的是哪个值,如果输入的是H,则将动态数组中的所有值赋值给函数名;如果输入的是V,则需要使用Transpose函数将动态数组转换成水平方向后再赋值给函数名;如果输入的是其他字符,则将“第三个参数只能输入H或V”赋值给函数名。       第6章 处理图形对象   Excel对象模型中的Shapes集合和Shape对象专门用于处理工作簿中的图片、形状、文本框、艺术字等图形对象。由于处理这些对象的方法基本相同或相似,所以本章使用术语“图形对象”统一描述这些对象。本章将介绍使用Shapes集合和Shape对象编程处理图形对象的方法。 6.1 从Shapes或ShapeRange集合中引用图形对象   使用Worksheet对象的Shapes属性可以返回Shapes集合,该集合由特定工作表中的所有图形对象组成,其中的每一个图形对象都是一个Shape对象。工作簿中的每个工作表都有自己的Shapes集合。   与引用工作簿或工作表的方法类似,可以使用图形对象的名称或索引号从Shapes集合中引用特定的图形对象。如需获悉一个图形对象的名称,可以选择该图形对象,其名称将显示在名称框中,例如“图片 1”,名称中的汉字和数字之间有一个空格,如图6-1所示。 图6-1 在名称框中显示图形对象的名称   如需在VBA中引用图6-1中的图片,可以使用下面任意一行代码。 ActiveSheet.Shapes("图片 1") ActiveSheet.Shapes("Picture 1")   如果该图片是第一个添加到活动工作表中的图形对象,则其索引号是1,此时可以使用索引号引用该图片。 ActiveSheet.Shapes(1)   如需引用多个图形对象,可以使用Shapes集合的Range属性,该属性返回ShapeRange集合,该集合由指定范围内的所有图形对象组成。下面的代码是引用活动工作表中索引号为1和3的两个图形对象。 ActiveSheet.Shapes.Range(Array(1, 3))   下面的代码是引用活动工作表中名为“图片1”和“矩形2”的两个图形对象。 ActiveSheet.Shapes.Range(Array("图片 1", "矩形 2"))   下面的代码是引用当前选中的所有图形对象。 Selection.ShapeRange   下面的代码是引用当前选中的所有图形对象中的第1个图形对象。 Selection.ShapeRange(1)   如需选择一个工作表中的所有图形对象,可以使用Shapes集合的SelectAll方法。下面的代码是选择活动工作表中的所有图形对象。 ActiveSheet.Shapes.SelectAll 6.2 获取和设置图形对象的基本信息   本节将介绍获取和设置图形对象的基本信息的方法,包括图形对象的名称、索引号、类型和位置,掌握这些信息可以更精准地处理图形对象,避免程序出错。 6.2.1 使用Name属性获取和设置图形对象的名称   在工作表中添加的每一个图形对象都有一个默认名称,除了可以通过名称框查看图形对象的名称之外,还可以在VBA中使用Shape对象的Name属性获取图形对象的名称。下面的代码是返回活动工作表中的第一个图形对象的名称。 ActiveSheet.Shapes(1).Name   下面的代码是返回所有选中的图形对象中的第一个图形对象的名称。 Selection.ShapeRange(1).Name   如果为Name属性赋值,则可以修改图形对象的名称。下面的代码将活动工作表中的第一个图形对象的名称修改为“橙子”。 ActiveSheet.Shapes(1).Name = "橙子" 6.2.2 使用ZOrderPosition属性获取图形对象的索引号   虽然可以在名称框中查看图形对象的名称,但是无法看到其索引号。在VBA中可以使用Shape对象的ZOrderPosition属性获取图形对象的索引号。下面的代码将活动工作表中的所有图形对象的名称和索引号记录到一个新添加的工作表的A、B两列中,如图6-2所示。 Sub 获取所有图形对象的名称和索引号() Dim intRow As Integer, shp As Shape Dim wks As Worksheet, wksNew As Worksheet Set wks = ActiveSheet If wks.Shapes.Count > 0 Then Set wksNew = Worksheets.Add For Each shp In wks.Shapes wksNew.Range("A1").Offset(intRow).Value = shp.Name wksNew.Range("B1").Offset(intRow).Value = shp.ZOrderPosition intRow = intRow + 1 Next shp Else MsgBox "活动工作表中没有图形对象" End If End Sub 图6-2 记录活动工作表中的所有图形对象的名称和索引号   代码解析:首先使用Shapes集合的Count属性判断活动工作表中的图形对象的总数是否大于0,如果是,则说明活动工作表中有图形对象,开始处理进程;如果不是,则说明活动工作表中没有图形对象,直接退出程序。由于要处理的图形对象位于活动工作表,而记录名称和索引号的工作表是一个新添加的工作表,添加后该工作表会变成活动工作表,之前包含图形对象的工作表将不再是活动工作表。为了明确区分这两个工作表,所以使用两个对象变量分别引用前后两个不同的活动工作表。在For Each语句,逐一处理活动工作表中的每个图形对象,将第一个图形对象的名称和索引号分别输入到新添加的工作表中的A1和B1单元格。后续的图形对象的名称和索引号依次添加到A1和B1单元格下面的连续单元格中,使用Range对象的Offset属性可以每次向下偏移一行,以获得连续的单元格,每次向下的偏移量由intRow变量控制。 6.2.3 使用Type属性获取图形对象的类型   如需处理特定类型的图形对象,可以在开始处理前先使用Shape对象的Type属性判断图形对象的类型,该属性的值由MsoShapeType常量提供,如表6-1所示,其中列出了常见的图形对象的类型。 表6-1 MsoShapeType常量 名 称 值 说 明 msoAutoShape 1 自选图形 msoChart 3 图表 msoComment 4 批注 msoGroup 6 组合图形 msoEmbeddedOLEObject 7 嵌入的OLE对象 续表 名 称 值 说 明 msoFormControl 8 窗体控件 msoLine 9 线条 msoLinkedOLEObject 10 链接的OLE对象 msoPicture 13 图片 msoTextBox 17 文本框 msoIgxGraphic 24 SmartArt      下面的代码返回活动工作表中第1个图形对象的类型。 ActiveSheet.Shapes(1).Type   由于Type属性返回的是数字值而非常量值,如果不熟悉它们之间的对应关系,则无法快速从Type属性的返回值判断图形对象的类型。为了解决这个问题,可以创建一个名为sGetShapeType的自定义函数,该函数有一个shp参数,表示需要判断类型的图形对象。 Function sGetShapeType(shp As Shape) As String Select Case shp.Type Case 1: sGetShapeType = "自选图形" Case 3: sGetShapeType = "图表" Case 6: sGetShapeType = "组合图形" Case 8: sGetShapeType = "窗体控件" Case 9: sGetShapeType = "线条" Case 13: sGetShapeType = "图片" Case 17: sGetShapeType = "文本框" End Select End Function   创建好自定义函数后,可以使用该函数检查特定的图形对象,并返回表示该图形对象类型的文本。下面的代码是返回选中的图形对象的类型,如图6-3所示。 sGetShapeType(Selection.ShapeRange(1)) 图6-3 以文本形式显示图形对象的类型 6.2.4 使用Left和Top属性获取和设置图形对象的位置   使用Shape对象的Left属性可以返回或设置图形对象的左边缘与工作表A列左边缘之间的距离,使用Shape对象的Top属性可以返回或设置图形对象的上边缘与工作表第一行上边缘之间的距离。   如需将图形对象的左上角与某个单元格的左上角对齐,可以将该单元格的Left属性和Top属性的值分别赋值给图形对象的Left属性和Top属性。下面的代码将活动工作表中的第一个图形对象的左上角对齐到B3单元格的左上角,如图6-4所示。 ActiveSheet.Shapes(1).Left = Range("B3").Left ActiveSheet.Shapes(1).Top = Range("B3").Top 图6-4 将图形对象的左上角对齐到B3单元格的左上角   如需精确指定图形对象左上角的位置,可以为Shape对象的Left属性和Top属性各设置一个值,这两个属性的值以磅为单位。如果希望设置时使用厘米作为单位,则需要使用Application对象的CentimetersToPoints方法,将输入的厘米值转换为磅值。   下面的代码是将活动工作表中的第一个图形对象的左边缘与A列左边缘的间距设置为2厘米,将其上边缘与工作表第一行上边缘的间距设置为3厘米。 ActiveSheet.Shapes(1).Left = Application.CentimetersToPoints(2) ActiveSheet.Shapes(1).Top = Application.CentimetersToPoints(3) 6.2.5 使用TopLeftCell和BottomRightCell属性获取图形对象的位置   使用Shape对象的TopLeftCell属性可以判断图形对象的左上角位于哪个单元格中,使用Shape对象的BottomRightCell属性可以判断图形对象的右下角位于哪个单元格中。这两个属性都是只读的,这意味着只能通过它们返回信息,而不能改变它们的值。下面的代码是返回活动工作表中的第一个图形对象的左上角和右下角所在的单元格地址。 ActiveSheet.Shapes(1).TopLeftCell.Address ActiveSheet.Shapes(1).BottomRightCell.Address   运行上面的代码返回的是绝对引用的单元格地址。如需返回相对引用的单元格地址,可以将Address属性的前两个参数都设置为0。 ActiveSheet.Shapes(1).TopLeftCell.Address(0, 0) ActiveSheet.Shapes(1).BottomRightCell.Address(0, 0) 6.3 插入和删除图形对象   无论在工作表中插入哪种类型的图形对象,在VBA中都需要使用Shapes集合的特定方法来完成操作,Shapes集合为插入每一种类型的图形对象都提供了一个特定的方法。本节以自选图形和图片为例,介绍使用VBA代码插入、选择和删除图形对象的方法。 6.3.1 使用AddShape方法插入自选图形   自选图形是在功能区的“插入”选项卡中单击“形状”按钮后打开的列表中的形状。如需使用VBA在工作表中插入自选图形,可以使用Shapes集合的AddShape方法,AddShape方法的语法如下: AddShape(Type, Left, Top, Width, Height) * Type(必需):自选图形的类型,该参数的值由MsoAutoShapeType常量提供,一些常用的自选图形的常量值如表6-2所示。 * Left(必需):自选图形的左边缘与工作表A列左边缘的间距,以磅为单位。 * Top(必需):自选图形的上边缘与工作表第一行上边缘的间距,以磅为单位。 * Width(必需):自选图形的宽度,以磅为单位。 * Height(必需):自选图形的高度,以磅为单位。 表6-2 MsoAutoShapeType常量 名 称 值 说 明 msoShapeRectangle 1 矩形 msoShapeParallelogram 2 平行四边形 msoShapeTrapezoid 3 梯形 msoShapeDiamond 4 菱形 msoShapeRoundedRectangle 5 圆角矩形 msoShapeIsoscelesTriangle 7 等腰三角形 msoShapeRightTriangle 8 直角三角形 msoShapeOval 9 椭圆形 msoShapeCan 13 圆柱形 msoShapeCross 11 十字形 msoShapeCube 14 立方体 msoShapeRightArrow 33 右箭头 msoShapeLeftArrow 34 左箭头 msoShapeUpArrow 35 上箭头 msoShapeDownArrow 36 下箭头      下面的代码是在活动工作表中插入一个矩形,该形状的左上角与B2单元格的左上角对齐,该形状的宽度是6厘米,高度是3厘米,如图6-5所示。选择插入后的矩形,可以在功能区的“形状格式”选项卡中看到其中显示的尺寸与在代码中设置的尺寸是相同的,如图6-6所示。 图6-5 使用AddShape方法插入一个矩形 图6-6 在功能区中查看矩形的尺寸 Sub 插入矩形() Dim sngLeft As Single, sngTop As Single Dim sngWidth As Single, sngHeight As Single sngLeft = Range("B2").Left sngTop = Range("B2").Top sngWidth = Application.CentimetersToPoints(6) sngHeight = Application.CentimetersToPoints(3) ActiveSheet.Shapes.AddShape msoShapeRectangle, sngLeft, sngTop, sngWidth, sngHeight End Sub   如果希望插入的矩形的尺寸与一个单元格区域等大且两者的边界完全对齐,则可以将插入的矩形的宽度和高度分别设置为该单元格区域的宽度和高度。下面的代码是在活动工作表中插入一个矩形,该矩形与B2:D8单元格区域等大且边界对齐,如图6-7所示。 图6-7 插入一个与指定单元格区域等大且边界对齐的矩形 Sub 插入与单元格区域等大且边界对齐的矩形() Dim sngLeft As Single, sngTop As Single Dim sngWidth As Single, sngHeight As Single sngLeft = Range("B2").Left sngTop = Range("B2").Top sngWidth = Range("B2:D8").Width sngHeight = Range("B2:D8").Height ActiveSheet.Shapes.AddShape msoShapeRectangle, sngLeft, sngTop, sngWidth, sngHeight End Sub 6.3.2 使用AddPicture方法插入图片   在VBA中可以使用Shapes集合的AddPicture方法插入计算机中的图片文件,AddPicture方法的语法如下: AddPicture(Filename, LinkToFile, SaveWithDocument, Left, Top, Width, Height) * Filename(必需):图片文件的完整路径。 * LinkToFile(必需):该参数为True将以链接的形式插入图片,该参数为False将以嵌入的形式插入图片。 * SaveWithDocument(必需):是否将插入的图片与工作簿一起保存。如果以嵌入的形式插入图片,则必须将该参数设置为True。 * Left(必需):图片的左边缘与工作表A列左边缘的间距,以磅为单位。 * Top(必需):图片的上边缘与工作表第一行上边缘的间距,以磅为单位。 * Width(必需):图片的宽度,以磅为单位。 * Height(必需):图片的高度,以磅为单位。   提示:Shapes方法还有一个AddPicture2方法,该方法的前7个参数与AddPicture方法相同,该方法新增的第8个参数表示是否对图片进行压缩。   下面的代码将E盘“测试数据”文件夹中名为“天空.jpg”的图片插入到活动工作表中,将该图片放置在B2:D8单元格区域中,图片的尺寸与该区域的大小相同且边界对齐。如果该文件夹中没有该图片或者该路径无效,则会显示一条提示信息并退出程序。 Sub 插入图片() Dim strFileName As String Dim sngLeft As Single, sngTop As Single Dim sngWidth As Single, sngHeight As Single strFileName = "E:\测试数据\天空.jpg" If Dir(strFileName) = "" Then MsgBox "文件名或路径有误" Exit Sub End If sngLeft = Range("B2").Left sngTop = Range("B2").Top sngWidth = Range("B2:D8").Width sngHeight = Range("B2:D8").Height ActiveSheet.Shapes.AddPicture strFileName, False, True, sngLeft, sngTop, sngWidth, sngHeight End Sub   下面的代码是在活动工作表中一次性插入3张图片,每张图片都与B、C、D三列宽度的总和等宽且占据6行,图片的边界与单元格区域的边界对齐,相邻的两张图片之间相隔两行,第一张图片的左上角与B2单元格的左上角对齐,如图6-8所示。 图6-8 一次性隔行插入多张图片 Sub 一次性插入多张图片() Dim strFullName As String, avarName As Variant Dim intIndex As Integer, rng As Range avarName = Array("天空1.jpg", "天空2.jpg", "天空3.jpg") Set rng = Range("B2:D7") For intIndex = LBound(avarName) To UBound(avarName) strFullName = "E:\测试数据\" & avarName(intIndex) If Dir(strFullName) <> "" Then ActiveSheet.Shapes.AddPicture strFullName, False, True, rng.Left, rng.Top, rng.Width, rng.Height Set rng = rng.Offset(rng.Rows.Count + 2) End If Next intIndex End Sub   代码解析:首先使用Array函数将3张图片的名称创建为一个数组,然后在For Next语句中逐个从该数组中获取一个名称,并与指定的路径组成图片文件的完整路径。使用Dir判断该完整路径是否有效,如果有效,则使用AddPicture方法以B2单元格为左上角位置插入第一张图片,将该图片的宽度设置为B2:D7单元格区域中所有列的宽度总和,将该案图片的高度设置为B2:D7单元格区域中所有行的高度总和。然后将表示第一张图片所在区域的B2:D7向下偏移,为了使两张图片之间相隔两行,向下偏移的量应该是区域的总行数+2。完成上述操作后,继续获取数组中的第二个图片名称,并将其插入到偏移后的下一个单元格区域中,直到插入所有图片为止。 6.3.3 选择特定类型的图形对象   使用Shapes集合的SelectAll方法可以选择特定工作表中的所有图形对象。然而,有时可能只想选择特定类型的图形对象并对其执行操作,此时可以使用Shapes集合的Range属性创建一个ShapeRange集合,其中包含所需类型的图形对象。下面的代码只选择活动工作表中的所有自选图形,而不会选择其他类型的图形对象,如图6-9所示。 图6-9 选择所有自选图形 Sub 选择所有自选图形() Dim shp As Shape, intShapeCount As Integer Dim astrShapeNames() As String For Each shp In ActiveSheet.Shapes If shp.Type = msoAutoShape Then intShapeCount = intShapeCount + 1 ReDim Preserve astrShapeNames(1 To intShapeCount) astrShapeNames(intShapeCount) = shp.Name End If Next shp If intShapeCount > 0 Then ActiveSheet.Shapes.Range(astrShapeNames).Select Else MsgBox "活动工作表中没有自选图形" End If End Sub   代码解析:由于最开始无法确定活动工作表中包含的自选图形的数量,所以需要声明一个动态数组,每找到一个自选图形,会重新定义该动态数组的大小,并将找到的自选图形的名称存储到该数组中。使用intShapeCount变量记录自选图形的数量,最后需要判断该变量的值是否大于0,如果是,说明至少存在一个自选图形,此时将存储自选图形名称的数组作为Shapes集合的Range属性的参数,返回包含所有自选图形的ShapeRange集合,然后使用Select方法选择所有自选图形。   实际上,使用Shape对象的Select方法也可以同时选择多个形状,而且代码更简洁,与选择多个工作表创建工作表组类似,需要将Select方法的Replace参数设置为False。下面的代码是选择活动工作表中的所有自选图形。 Sub 选择所有自选图形2() Dim shp As Shape For Each shp In ActiveSheet.Shapes If shp.Type = msoAutoShape Then shp.Select False End If Next shp End Sub 6.3.4 使用Delete方法删除工作表中的图形对象   如需删除工作表中的所有图形对象,可以先使用Shapes集合的SelectAll方法选择工作表中的所有图形对象,然后使用ShapeRange集合的Delete方法删除选中的图形对象。 ActiveSheet.Shapes.SelectAll Selection.ShapeRange.Delete   如需删除工作簿中的所有图形对象,可以使用For Each语句逐个处理每个工作表中的图形对象。下面的代码是删除活动工作簿中的每一个工作表中的所有图形对象。 Sub 删除活动工作簿中的所有图形对象() Dim wks As Worksheet, wksOld As Worksheet Set wksOld = ActiveSheet For Each wks In Worksheets wks.Activate wks.Shapes.SelectAll Selection.ShapeRange.Delete Next wks wksOld.Activate End Sub   代码解析:由于Selection是针对活动工作表的,所以每次需要激活当前正在处理的工作表,否则使用Selection会出现运行时错误。为了恢复最初处于活动状态的工作表,开始操作前,将最初的活动工作表赋值给一个对象变量,完成操作后,再使用Activate方法激活该工作表。   如需删除特定类型的图形对象,可以使用Shape对象的Type属性判断图形对象的类型,如果符合指定的类型,则将其删除。下面的代码是删除活动工作表中的所有图片。 Sub 删除活动工作表中的所有图片() Dim shp As Shape For Each shp In ActiveSheet.Shapes If shp.Type = msoPicture Then shp.Delete End If Next shp End Sub 6.4 设置图形对象的填充格式   在VBA中可以使用FillFormat对象为图形对象设置填充格式,使用Shape对象的Fill属性可以返回FillFormat对象。对于自选图形和图片来说,填充格式只适用于前者。本节将介绍为自选图形设置不同填充格式的方法。 6.4.1 为自选图形设置纯色填充   如需为自选图形设置纯色填充,可以使用FillFormat对象的ForeColor属性返回ColorFormat对象,然后为该对象的RGB属性设置颜色值。使用VBA内置的RGB函数可以获取所需的颜色值,该函数有3个参数,表示颜色中的红、绿、蓝3个颜色分量,每个颜色分量的取值范围是0~255,如表6-3所示。 表6-3 常用颜色的RGB值 名 称 红 色 分 量 绿 色 分 量 蓝 色 分 量 黑色 0 0 0 白色 255 255 255 红色 255 0 0 绿色 0 255 0 蓝色 0 0 255 黄色 255 255 0 粉色 255 0 255 青色 0 255 255      下面的代码将活动工作表中的所有自选图形的填充色设置为红色。如果其中的某些自选图形已经设置为渐变填充,则需要使用FillFormat对象的Solid方法将填充方式改为纯色填充,然后才能使用ColorFormat对象的RGB属性设置填充色。 Sub 为自选图形设置纯色填充() Dim shp As Shape For Each shp In ActiveSheet.Shapes If shp.Type = msoAutoShape Then shp.Fill.Solid shp.Fill.ForeColor.RGB = RGB(255, 0, 0) End If Next shp End Sub   提示:可以使用VBA内置常量vbRed代替RGB函数生成的红色值。   可以对具有特定填充色的自选图形执行所需的操作。下面的代码是将红色填充的自选图形的填充色改为蓝色,其他自选图形的填充色保持不变。 Sub 修改自选图形的填充色() Dim shp As Shape For Each shp In ActiveSheet.Shapes If shp.Type = msoAutoShape Then If shp.Fill.ForeColor.RGB = vbRed Then shp.Fill.ForeColor.RGB = vbBlue End If End If Next shp End Sub 6.4.2 为自选图形设置渐变填充   在VBA中可以使用FillFormat对象的OneColorGradient方法和TwoColorGradient方法为自选图形设置渐变填充,前者设置单色渐变,后者设置双色渐变。OneColorGradient方法和TwoColorGradient方法的第一个参数用于设置渐变填充的样式,该参数的值由MsoGradientStyle常量提供,如表6-4所示。 表6-4 MsoGradientStyle常量 名 称 值 说 明 msoGradientHorizontal 1 水平经过图形的渐变 msoGradientVertical 2 垂直向下填充图形的渐变 msoGradientDiagonalUp 3 从一个底角到另一侧顶角的对角渐变 msoGradientDiagonalDown 4 从一个顶角到另一侧底角的对角渐变 msoGradientFromCorner 5 从一个角到其他三个角的渐变 msoGradientFromTitle 6 从标题向外的渐变 msoGradientFromCenter 7 从中心到各个角的渐变 msoGradientMixed -2 渐变是混和的      使用OneColorGradient方法设置单色渐变时,需要先指定渐变样式,然后为ColorFormat对象的RGB属性设置颜色值。使用TwoColorGradient方法设置双色渐变与OneColorGradient方法类似,唯一区别是需要同时为ForeColor属性和BackColor属性设置颜色值。下面的代码是为活动工作表中所有选中的自选图形中的第一个自选图形设置由红色和蓝色组成的双色渐变填充。 Sub 设置双色渐变填充() Dim shp As Shape For Each shp In ActiveSheet.Shapes If shp.Type = msoAutoShape Then shp.Select False End If Next shp With Selection.ShapeRange(1) .Fill.TwoColorGradient msoGradientHorizontal, 1 .Fill.BackColor.RGB = vbRed .Fill.ForeColor.RGB = vbBlue End With End Sub 6.4.3 为自选图形设置图片填充   如需使用图片填充自选图形,可以使用FillFormat对象的UserPicture方法。该方法只有一个参数,表示用于填充的图片文件的完整路径。下面的代码是为活动工作表中的所有自选图形设置图片填充,如图6-10所示。 图6-10 为自选图形设置图片填充 Sub 设置图片填充() Dim shp As Shape For Each shp In ActiveSheet.Shapes If shp.Type = msoAutoShape Then shp.Select False End If Next shp Dim strFullName As String strFullName = "E:\测试数据\天空.jpg" Selection.ShapeRange.Fill.UserPicture strFullName End Sub 6.5 设置图形对象的边框格式   在VBA中可以使用LineFormat对象为图形对象设置边框格式,使用Shape对象的Line属性可以返回LineFormat对象。对于自选图形和图片来说,两者都可以设置边框格式。边框格式主要包括边框的线型、颜色和粗细,设置方法如下。 * 线型:使用LineFormat对象的DashStyle属性设置边框的线型,该属性的值由MsoLineDashStyle常量提供,如表6-5所示。 * 颜色:使用LineFormat对象的ForeColor属性返回ColorFormat对象,然后使用该对象的RGB属性设置边框的颜色。 * 粗细:使用LineFormat对象的Weight属性设置边框的粗细,以磅为单位。 表6-5 MsoLineDashStyle常量 名 称 值 说 明 msoLineSolid 1 边框是实线 续表 名 称 值 说 明 msoLineSquareDot 2 边框由方点构成 msoLineRoundDot 3 边框由圆点构成 msoLineDash 4 边框是短画线 msoLineDashDot 5 边框是点画线 msoLineDashDotDot 6 边框是点点画线 msoLineLongDash 7 边框是长画线 msoLineLongDashDot 8 边框是长点画线      下面的代码是将活动工作表中的所有图形对象的边框的线型设置为短画线,将边框的颜色设置为红色,将边框的粗细设置为6磅。 Sub 设置图形对象的边框格式() With Selection.ShapeRange .Line.DashStyle = msoLineDash .Line.ForeColor.RGB = RGB(255, 0, 0) .Line.Weight = 6 End With End Sub       第7章 事件编程   到目前为止,编写的VBA代码都组织在不同的Sub过程或Function过程中,用户需要手动运行或调用这些过程,才能执行相应的操作。在VBA中还有一类称为“事件”的过程,它们是对象特有的过程。当用户对对象执行操作时,会自动触发相应的事件过程。只需事先在事件过程中编写VBA代码,即可在触发事件过程时自动运行其中的代码,响应用户操作并自动运行代码是事件过程与其他过程最大的区别。本章将介绍在VBA中编写事件过程所需了解的知识,以及编程处理Excel中的工作簿事件和工作表事件的方法。 7.1 事件编程基础   在开始编程处理Excel对象的事件过程之前,需要先了解事件编程的基本知识和操作方法,包括Excel中的事件类型和触发顺序、事件代码的存储位置和编写方法,以及启用和禁用事件。 7.1.1 Excel支持的事件类型和触发顺序   Excel对象模型中的Application对象、Workbook对象、Worksheet对象和Chart对象都有各自的事件过程,位于工作表中的嵌入式图表也有其事件过程。此外,在VBA工程中添加的用户窗体及其中的控件都有相应的事件过程。 * 应用程序事件:Application对象的应用程序事件可以监视在Excel应用程序运行期间发生的操作,该类事件对打开的任意一个工作簿都有效。无法直接使用应用程序事件,需要先在类模块和标准模块中编写少量代码,然后才能使用该类事件。 * 工作簿事件:每个Workbook对象都有与其关联的工作簿事件,每个工作簿的工作簿事件只对该工作簿有效。对工作簿执行操作时将触发工作簿事件,例如新建或打开工作簿时。对工作簿中的任意一个工作表执行操作时也会触发工作簿事件,例如激活工作簿中的任意一个工作表时。 * 工作表事件:每个Worksheet对象都有与其关联的工作表事件,每个工作表的工作表事件只对该工作表有效,在工作表中执行操作时将触发工作表事件,例如选择或编辑单元格时。 * 图表工作表事件:Chart对象的图表工作表事件只对特定图表工作表有效。 * 嵌入式图表事件:嵌入式图表事件只作用于特定嵌入式图表的操作。与应用程序事件类似,默认无法使用嵌入式图表事件,需要在类模块和标准模块中编写少量代码后才能使用嵌入式图表事件。 * 用户窗体和控件事件:用户窗体事件和控件事件只对特定的用户窗体和控件有效。   在Excel中执行某个操作时,可能会自动触发多个事件。例如,在工作簿中添加新的工作表时,将依次触发以下几个工作簿事件: * 先触发NewSheet事件:添加工作表时将触发NewSheet事件。 * 然后触发SheetDeactivate事件:由于添加一个新的工作表会自动使其成为活动工作表,原来的活动工作表将失去焦点,所以添加工作表后会触发SheetDeactivate事件。 * 最后触发SheetActivate事件:由于添加的新工作表会自动成为活动工作表,所以在前两个事件之后会接着触发SheetActivate事件。   当不同对象拥有同一种事件时,将遵循从低到高的顺序触发事件。例如,Application对象、Workbook对象和Worksheet对象都有SelectionChange事件,如果为这3个事件过程编写了代码,则在特定的工作表中选择不同的单元格时,会先触发该工作表的SelectionChange事件,然后触发该工作表所属工作簿的SelectionChange事件,最后触发Excel应用程序的SelectionChange事件。 7.1.2 为事件编写代码   为事件编写代码之前,首先需要了解事件的存储位置,不同对象的事件存储在不同的位置。 * 应用程序事件和嵌入式图表事件存储在用户创建的类模块中。 * 工作簿事件存储在ThisWorkbook模块中。 * 工作表事件存储在Sheet1、Sheet2、Sheet3等模块中。 * 图表工作表事件存储在Chart1、Chart2、Chart3等模块中。 * 用户窗体事件和控件事件存储在用户窗体模块中。   为事件编写代码时,需要将代码输入到相应的事件过程中。无论哪个对象,事件过程都具有以下固定格式。 Private Sub 对象名_事件名(参数) End Sub   下面是工作簿的NewSheet事件过程和工作表的Change事件过程的基本结构。 Private Sub Workbook_NewSheet(ByVal Sh As Object) End Sub Private Sub Worksheet_Change(ByVal Target As Range) End Sub   为事件过程编写代码时,需要打开该事件过程所属的模块的代码窗口,然后在代码窗口顶部左侧的下拉列表中选择对象名,在右侧下拉列表中选择事件名,事件过程的基本结构会被自动添加到代码窗口中,用户只需在其中编写所需的代码,如图7-1所示。 图7-1 选择事件过程的对象名和事件名 7.1.3 启用和禁用事件   每个事件默认是处于启用状态的,只要在事件过程中编写了代码,当触发事件时,就会自动执行该事件过程中的代码。虽然这种自动响应机制为代码的执行提供了便利条件,但是有时可能需要临时禁用事件。   例如,如果在工作表的Change事件过程中包含修改单元格的代码,则在触发Change事件时会执行其中的代码,由于代码会对单元格执行修改操作,则会再次触发Change事件,使这一过程变成无限循环,最终可能会导致Excel应用程序无影响。   解决这种问题的方法是在执行修改单元格的代码之前先禁用Change事件,在执行完这部分代码之后再重新启用Change事件。使用Application对象的EnableEvents属性可以启用或禁用事件,禁用事件时将该属性设置为False,启用事件时将该属性设置为True。 Application.EnableEvents = False Application.EnableEvents = True 7.2 工作簿的Open事件   打开一个工作簿时将触发该工作簿的Workbook_Open事件过程,该事件过程主要有以下用途: * 显示欢迎信息。 * 验证用户名并分配操作权限。 * 配置工作簿的界面环境。 * 激活特定的工作表和单元格。 7.2.1 打开工作簿时显示欢迎信息   下面的代码位于工作簿的ThisWorkbook模块中,每次打开该工作簿时,都会显示“您好,今天是”+与当前系统日期对应的星期几,如图7-2所示。 Private Sub Workbook_Open() MsgBox "您好,今天是" & WeekdayName(Weekday(Date, 2)) End Sub 图7-2 显示欢迎信息 7.2.2 打开工作簿时验证用户名   下面的代码位于工作簿的ThisWorkbook模块中,每次打开该工作簿时,会检查Excel中的用户名是否是“admin”,如果不是,则立即关闭该工作簿。 Private Sub Workbook_Open() If LCase(Application.UserName) <> "admin" Then MsgBox "用户名不正确,没有操作权限" ThisWorkbook.Close False End If End Sub   如果为Excel设置的用户名不是本例中的admin(大小写均可),则在每次打开该工作簿时,在显示一条信息后会立即将其关闭。如需修改本例工作簿中的代码,可以在显示提示信息时按Ctrl+Break组合键进入中断模式。 7.2.3 打开工作簿时创建鼠标快捷菜单   下面的代码是位于工作簿的ThisWorkbook模块中,每次打开该工作簿时,会调用名为“创建自定义快捷菜单”的Sub过程创建鼠标快捷菜单,该Sub过程位于该工作簿的标准模块中。 Private Sub Workbook_Open() Call 创建鼠标快捷菜单 End Sub Sub创建鼠标快捷菜单() 创建鼠标快捷菜单的VBA代码 End Sub   提示:在VBA中创建鼠标快捷菜单的方法将在第12章进行介绍。 7.3 工作簿和工作表的Activate事件   工作簿和工作表都有Activate事件,激活一个工作簿时将触发该工作簿的Workbook_ Activate事件过程,打开工作簿时也会触发该事件过程。激活特定的工作表时将触发该工作表的Worksheet_Activate事件过程。 7.3.1 激活工作簿时设置其显示方式   下面的代码位于工作簿的ThisWorkbook模块中,每次激活该工作簿时,都将该工作簿的窗口最大化显示,并隐藏水平滚动条和垂直滚动条。 Private Sub Workbook_Activate() With ThisWorkbook.Windows(1) .WindowState = xlMaximized .DisplayHorizontalScrollBar = False .DisplayVerticalScrollBar = False End With End Sub 7.3.2 激活工作表时显示其中已使用的单元格范围   下面的代码位于工作簿的Sheet1模块中,每次激活Sheet1工作表时,都会显示该工作表中已经使用的单元格范围的地址。 Private Sub Worksheet_Activate() Dim rng As Range Set rng = Worksheets("Sheet1").Cells.SpecialCells(xlCellTypeLastCell) MsgBox "已使用的单元格范围是:" & Range("A1", rng).Address(0, 0) End Sub   提示:由于Worksheet_Activate事件过程中的代码处理的是该过程所属的Sheet1模块所关联的Sheet1工作表,为了简化代码并在修改工作表名称时始终使引用的工作表有效,可以使用Me关键字代替Worksheets("Sheet1"),即将代码改为以下形式。 Private Sub Worksheet_Activate() Dim rng As Range Set rng = Me.Cells.SpecialCells(xlCellTypeLastCell) MsgBox "已使用的单元格范围是:" & Range("A1", rng).Address(0, 0) End Sub   提示:在Excel VBA中,Me关键字除了可以引用与Sheet1、Sheet2等模块关联的工作表之外,还可以引用与ThisWorkbook模块关联的工作簿,以及用户窗体和用户创建的类。 7.4 工作簿和工作表的Deactivate事件   执行以下几种操作时将触发工作簿的Workbook_Deactivate事件过程: * 打开或激活另一个工作簿。 * 最小化工作簿窗口。 * 关闭工作簿。   工作表也有Deactivate事件,其过程名是Worksheet_Deactive,该事件只在特定工作表失去焦点时才会触发。 7.4.1 在工作簿失去焦点时恢复原始的显示方式   下面的代码位于工作簿的ThisWorkbook模块中,当该工作簿失去焦点时,会将该工作簿的窗口最小化显示,并恢复水平滚动条和垂直滚动条的正常显示。 Private Sub Workbook_Deactivate() With ThisWorkbook.Windows(1) .WindowState = xlMinimized .DisplayHorizontalScrollBar = True .DisplayVerticalScrollBar = True End With End Sub 7.4.2 在工作表失去焦点时显示提示信息   下面的代码位于工作簿的Sheet1模块中,当Sheet1工作表失去焦点时,将显示“Sheet工作表已失去焦点”的提示信息。 Private Sub Worksheet_Deactivate() MsgBox Worksheets("Sheet1").Name & "工作表已失去焦点" End Sub 7.5 工作簿的BeforeClose事件   关闭工作簿时,将触发该工作簿的Workbook_BeforeClose事件过程。使用该事件过程可以在真正关闭工作簿之前执行所需的操作,常见的有以下几种: * 可以预先指定保存或不保存工作簿的修改,以便直接关闭工作簿,而不会显示保存提示对话框。 * 使用自定义对话框代替默认的保存提示对话框。 * 删除在打开工作簿时加载的菜单和快捷菜单。   Workbook_BeforeClose事件过程有一个Cancel参数,如果将该参数设置为True,则不会关闭工作簿。 7.5.1 不显示保存提示而直接保存并关闭工作簿   下面的代码是位于工作簿的ThisWorkbook模块中,关闭工作簿时,直接保存该工作簿的最新修改,然后将其关闭,不会显示保存提示对话框。 Private Sub Workbook_BeforeClose(Cancel As Boolean) If Not ThisWorkbook.Saved Then ThisWorkbook.Save End If End Sub   同理,如不显示保存提示而直接关闭工作簿且放弃所有未保存的修改,可以使用下面的代码。 Private Sub Workbook_BeforeClose(Cancel As Boolean) ThisWorkbook.Saved = True End Sub 7.5.2 使用自定义对话框代替默认的保存提示对话框   用户可以在Workbook_BeforeClose事件过程中创建一个自定义对话框,用于指定关闭工作簿时执行哪些操作。下面的代码是位于工作簿的ThisWorkbook模块中,使用VBA内置的MsgBox函数创建一个对话框,其中显示“是”“否”和“取消”3个按钮,如图7-3所示。单击“是”按钮将保存并关闭工作簿;单击“否”按钮将不保存并关闭工作簿;单击“取消”按钮将返回工作簿窗口而不会关闭工作簿,并退出VBA程序,这种处理方式可以避免在默认的保存提示对话框中单击“取消”按钮后执行意外的操作,例如未关闭工作簿却删除了已加载的菜单和快捷菜单。 Private Sub Workbook_BeforeClose(Cancel As Boolean) Dim strMsg As String, intAns As Integer If Not ThisWorkbook.Saved Then strMsg = "是否保存对【" & ThisWorkbook.Name & "】的更改?" intAns = MsgBox(strMsg, vbQuestion + vbYesNoCancel) Select Case intAns Case vbYes: ThisWorkbook.Save Case vbNo: ThisWorkbook.Saved = True Case vbCancel Cancel = True Exit Sub End Select End If End Sub 图7-3 自定义关闭工作簿时执行的操作 7.6 工作簿的BeforeSave事件   保存或另存工作簿时将触发该工作簿的Workbook_BeforeSave事件过程,该事件过程有SaveAsUI和Cancel两个参数。SaveAsUI参数表示触发Workbook_BeforeSave事件过程时是否显示了“另存为”对话框,如果显示该对话框,则SaveAsUI参数返回True;如果未显示该对话框,则SaveAsUI参数返回False。Cancel参数决定是否保存工作簿,该参数默认为False,表示保存工作簿。如果将该参数设置为True,则不保存工作簿。 7.6.1 禁止保存工作簿   下面的代码是位于工作簿的ThisWorkbook模块中,如果对现有而非新建的工作簿执行“保存”命令,则SaveAsUI参数将返回False,表示用户正在保存而非另存工作簿,此时将Cancel参数设置为True,将禁止真正保存工作簿。 Private Sub Workbook_BeforeSave(ByVal SaveAsUI As Boolean, Cancel As Boolean) If Not SaveAsUI Then Cancel = True End Sub 7.6.2 禁止另存工作簿   下面的代码是位于工作簿的ThisWorkbook模块中,如果对工作簿执行“另存为”命令,无论该工作簿是现有的还是新建的,SaveAsUI参数都将返回True,此时将Cancel参数设置为True,则不会显示“另存为”对话框,禁止以其他名称或路径保存工作簿。 Private Sub Workbook_BeforeSave(ByVal SaveAsUI As Boolean, Cancel As Boolean) If SaveAsUI Then Cancel = True End Sub 7.6.3 禁止保存和另存工作簿   下面的代码是位于工作簿的ThisWorkbook模块中,通过将Cancel参数的值设置为True,从而完全禁止保存和另存工作簿的操作。为了避免在关闭未保存的工作簿时弹出是否保存的确认对话框,可以在工作簿的BeforeClose事件过程中将Workbook对象的Saved属性设置为True。 Private Sub Workbook_BeforeSave(ByVal SaveAsUI As Boolean, Cancel As Boolean) Cancel = True End Sub Private Sub Workbook_BeforeClose(Cancel As Boolean) ThisWorkbook.Saved = True End Sub 7.7 工作簿的BeforePrint事件   打印工作簿时将触发Workbook_BeforePrint事件过程,该事件过程只有一个Cancel参数,默认值False表示正常打印,将其设置为True表示取消打印。 7.7.1 确定是否真正开始打印   下面的代码是位于工作簿的ThisWorkbook模块中,当用户执行“打印”命令时,将显示如图7-4所示的对话框,如果单击“取消”按钮,则取消打印操作。 Private Sub Workbook_BeforePrint(Cancel As Boolean) If MsgBox("确定开始打印吗?", vbOKCancel) = vbCancel Then Cancel = True End If End Sub 图7-4 将Cancel设置为True将取消打印操作 7.7.2 打印前检查数据是否填写完整   下面的代码是位于工作簿的ThisWorkbook模块中,打印前先检查A1:B10单元格区域中的数据是否填写完整,如果存在空白项,则禁止打印,如图7-5所示。 Private Sub Workbook_BeforePrint(Cancel As Boolean) Dim rng As Range Set rng = Range("A1").CurrentRegion If WorksheetFunction.CountA(rng) <> rng.Count Then MsgBox "数据填写不完整,无法打印" Cancel = True End If End Sub 图7-5 打印前检查数据是否填写完整   可能希望在发现空白项时,将空白项的位置告知用户,此时可以使用下面的代码,运行效果如图7-6所示。 图7-6 检查数据是否填写完整并将空白项的位置告知用户 Private Sub Workbook_BeforePrint(Cancel As Boolean) Dim rng As Range, rngBlank As Range, strMsg As String For Each rng In Range("A1").CurrentRegion If IsEmpty(rng) Then If rngBlank Is Nothing Then Set rngBlank = rng Else Set rngBlank = Union(rngBlank, rng) End If End If Next rng If Not rngBlank Is Nothing Then MsgBox "需要为" & rngBlank.Address(0, 0) & "单元格填写数据后才能打印" Cancel = True End If End Sub   代码解析:在For Each语句中使用VBA内置的IsEmpty函数检查包含A1单元格的连续数据区域中的每一个单元格是否是空白,每次找到一个空白单元格时,将该单元格添加到rngBlank变量中,该变量用于合并找到的所有空白单元格。由于在找到第一个空白单元格之前,rngBlank变量是空的,所以将第一个空白单元格直接赋值给rngBlank变量。从找到的第二个空白单元格开始,使用Application对象的Union方法将每次找到的空白单元格与rngBlank变量中现有的空白单元格合并在一起。检查完数据区域中的所有单元格后,判断rngBlank变量是否包含空白单元格,如果包含空白单元格,则在对话框中显示这些空白单元格的地址,并将Workbook_BeforePrint 事件过程中的Cancel参数设置为True,禁止打印。 7.8 工作簿的SheetActivate事件   在工作簿中激活任意一个工作表时将会触发该工作簿的Workbook_SheetActivate事件过程。该事件过程有一个Sh参数,表示激活的工作表。 7.8.1 显示激活的工作表的名称和类型   下面的代码位于工作簿的ThisWorkbook模块中,每次激活该工作簿中的任意一个工作表时,都会显示该工作表的名称和类型,如图7-7所示。 Private Sub Workbook_SheetActivate(ByVal Sh As Object) Dim strMsg As String strMsg = "激活的工作表的名称是:" & Sh.Name strMsg = strMsg & vbCrLf & "激活的工作表的类型是:" & TypeName(Sh) MsgBox strMsg End Sub 图7-7 显示激活的工作表的名称和类型 7.8.2 检查激活的工作表是否为空并询问用户是否将其删除   下面的代码是位于工作簿的ThisWorkbook模块中,在该工作簿中激活一个工作表时,如果其中不包含任何数据,则询问用户是否将其删除,单击“是”按钮将删除该工作表,如图7-8所示。 Private Sub Workbook_SheetActivate(ByVal Sh As Object) If TypeName(Sh) = "Worksheet" Then If WorksheetFunction.CountA(Sh.Cells) = 0 Then If MsgBox("是否删除空的活动工作表?", vbQuestion + vbYesNo) = vbYes Then Application.DisplayAlerts = False Sh.Delete Application.DisplayAlerts = True End If End If End If End Sub 图7-8 显示激活的工作表中的数据区域的地址   代码解析:每次激活一个工作表时,先判断该工作表的类型。如果是Worksheet,则使用CountA函数判断该工作表是否包含数据,如果不包含任何数据,则显示一个对话框,询问用户是否删除该工作表并提供“是”和“否”两个按钮。如果单击“是”按钮,则将删除该工作表。为了不显示删除工作表时的提示信息,将Application对象的DisplayAlerts属性设置为False,删除后再将其设置为True。 7.9 工作簿的SheetDeactivate事件   工作簿的SheetDeactivate事件与6.4节介绍的工作表的Deactivate事件类似,唯一区别是本节中的SheetDeactivate事件可以作用于工作簿中的任意一个工作表, 而6.4节中的Deactivate事件只能作用于特定工作表。   在工作簿中激活一个工作表时,之前的活动工作表将失去焦点并触发Workbook_ SheetDeactivate事件过程,该事件过程中的Sh参数表示失去焦点的工作表。下面的代码是位于工作簿的ThisWorkbook模块中,每次在该工作簿中激活一个工作表时,将显示刚失去焦点的工作表的名称和类型。 Private Sub Workbook_SheetDeactivate(ByVal Sh As Object) Dim strMsg As String strMsg = "失去焦点的工作表的名称是:" & Sh.Name strMsg = strMsg & vbCrLf & "失去焦点的工作表的类型是:" & TypeName(Sh) MsgBox strMsg End Sub 7.10 工作簿的NewSheet事件   在工作簿中添加新的工作表时,将触发该工作簿的Workbook_NewSheet事件过程,该事件过程中的Sh参数表示刚添加的工作表。 7.10.1 添加工作表时显示该工作表的类型和工作表总数   下面的代码是位于工作簿的ThisWorkbook模块中,每次在该工作簿中添加新的工作表时,都会显示该工作表的类型和工作簿中的工作表总数,如图7-9所示。 Private Sub Workbook_NewSheet(ByVal Sh As Object) Dim strMsg As String strMsg = "添加的工作表的类型是:" & TypeName(Sh) strMsg = strMsg & vbCrLf & "工作簿中共有" & Sheets.Count & "个工作表" MsgBox strMsg End Sub 图7-9 添加工作表时显示该工作表的类型和工作表总数 7.10.2 添加工作表时自动以月份命名   下面的代码是位于工作簿的ThisWorkbook模块中,每次在该工作簿中添加新的工作表时,都会自动以月份为新工作表命名,例如“1月”“2月”“3月”。如果工作簿中已经存在名为“1月”的工作表,则添加的新工作表的名称需要设置为“2月”,即检查现有工作表名称中的最大月份,然后将新工作表的月份设置为现有最大月份+1。 Private Sub Workbook_NewSheet(ByVal Sh As Object) Dim intMonth As Integer If TypeName(Sh) = "Worksheet" Then intMonth = 1 On Error GoTo ErrTrap Sh.Name = intMonth & "月" Exit Sub End If ErrTrap: intMonth = intMonth + 1 Resume End Sub   代码解析:由于一个工作簿中的工作表不能同名,所以可以先将新添加的工作表的名称设置为“1月”,月份中的数字使用一个变量表示,以便可以动态增加编号值。如果已经存在名为“1月”的工作表,则会出现运行时错误。此时将进入由ErrTrap标记的错误处理程序。由于名为“1月”的工作表已经存在,所以将表示月份数字的变量加1,然后将工作表命名为下个月份。如果仍然出现运行时错误,则继续增加月份值,直到不出现运行时错误或者已经添加了1~12个月的工作表为止。 7.11 工作簿的SheetChange事件   在工作簿中编辑任意一个工作表中的单元格时,将触发该工作簿的Workbook_SheetChange事件过程。该事件过程有两个参数,Sh参数表示编辑的单元格所属的工作表,Target参数表示编辑的单元格。下面几种操作都会触发Worksheet_Change事件过程: * 输入或修改单元格中的数据。 * 复制并粘贴数据。 * 按Delete键。 * 清除单元格的格式。   下面的代码位于工作簿的ThisWorkbook模块中,无论编辑该工作簿中的哪个工作表的单元格,都会在状态栏中显示该单元格地址及其所属的工作表名称,如图7-10所示。 Private Sub Workbook_SheetChange(ByVal Sh As Object, ByVal Target As Range) Dim strMsg As String strMsg = "刚编辑过的单元格是" & Target.Address(0, 0) strMsg = strMsg & ",该单元格所属的工作表是" & Sh.Name Application.StatusBar = strMsg End Sub 图7-10 显示编辑的单元格地址及其所属的工作表名称 7.12 工作簿的SheetSelectionChange事件   在工作簿中的任意一个工作表中选择不同的单元格时,将触发该工作簿的Workbook_ SheetSelectionChange事件过程。该事件过程有两个参数,Sh参数表示选择的单元格所属的工作表,Target参数表示选择的单元格。 7.12.1 动态显示选区地址及其中的空白单元格的数量   下面的代码是位于工作簿的ThisWorkbook模块中,每次在该工作簿中选择一个不同的单元格或单元格区域时,将显示选区的地址及其中包含的空白单元格的数量,如图7-11所示。 Private Sub Workbook_SheetSelectionChange(ByVal Sh As Object, ByVal Target As Range) Dim strMsg As String strMsg = "选取地址:" & Target.Address(0, 0) strMsg = strMsg & vbCrLf & "选区中的空白单元格的数量:" strMsg = strMsg & Target.Count - WorksheetFunction.CountA(Target) MsgBox strMsg End Sub 图7-11 动态显示选区地址及其中的空白单元格的数量 7.12.2 自动高亮显示选区所在的整行和整列   下面的代码是位于工作簿的ThisWorkbook模块中,在该工作簿的任意一个工作表中每次选择不同的单元格或单元格区域时,会自动以黄色高亮显示选区所在的整行和整列,如图7-12所示。 Private Sub Workbook_SheetSelectionChange(ByVal Sh As Object, ByVal Target As Range) Cells.Interior.ColorIndex = xlColorIndexNone Target.EntireColumn.Interior.Color = vbYellow Target.EntireRow.Interior.Color = vbYellow End Sub 图7-12 自动高亮显示选区所在的整行和整列   提示:Workbook_SheetSelectionChange事件过程中的第一行代码用于清除工作表中现有的单元格填充色,从而避免选择不同单元格时的填充色堆叠在一起。 7.13 工作簿的SheetBeforeRightClick事件   在工作簿中的任意一个工作表中右击单元格时,将触发该工作簿的Workbook_ SheetBeforeRightClick事件过程。该事件过程有3个参数,Sh参数表示右击的单元格所属的工作表,Target参数表示右击的单元格,Cancel参数表示是否禁止右击单元格时弹出快捷菜单。   下面的代码是位于工作簿的ThisWorkbook模块中,在该工作簿的任意一个工作表中右击单元格时,将弹出用户自定义的快捷菜单,假设创建该快捷菜单的Sub过程的名称是AddShortCutMenu。 Private Sub Workbook_SheetBeforeRightClick(ByVal Sh As Object, ByVal Target As Range, Cancel As Boolean) On Error Resume Next CommandBars("创建自定义快捷菜单").ShowPopup Cancel = True If Err.Number <> 0 Then MsgBox "无法显示自定义快捷菜单" Exit Sub End If End Sub 7.14 工作簿的SheetBeforeDoubleClick事件   在工作簿中的任意一个工作表中双击单元格时,将触发该工作簿的Workbook_Sheet- BeforeDoubleClick事件过程。该事件过程有3个参数,Sh参数表示双击的单元格所属的工作表,Target参数表示双击的单元格,Cancel参数表示是否取消双击单元格时默认执行的操作。   下面的代码位于工作簿的ThisWorkbook模块中,在该工作簿中的任意一个工作表中双击单元格时,将自动删除该单元格中的内容和格式。 Private Sub Workbook_SheetBeforeDoubleClick(ByVal Sh As Object, ByVal Target As Range, Cancel As Boolean) Target.Clear Cancel = True End Sub 7.15 工作表的Change事件   在工作表中编辑任意单元格时,将触发该工作表的Worksheet_Change事件过程。该事件过程只有一个参数,表示编辑的单元格。可以触发工作表的Change事件的操作与触发工作簿的SheetChange事件的操作相同。   如果Worksheet_Change事件过程中的代码包含编辑单元格的操作,为了避免陷入无限循环,应该在执行这部分代码之前,先禁用Worksheet_Change事件过程,完成编辑单元格的操作后再启用该事件过程。 Application.EnableEvents = False 编辑单元格的代码 Application.EnableEvents = True 7.16 工作表的SelectionChange事件   在工作表中选择不同的单元格时,将触发该工作表的Worksheet_SelectionChange事件过程。该事件过程只有一个参数,表示选择的单元格。   工作表的Worksheet_SelectionChange事件过程的功能与工作簿的Workbook_SheetSelection- Change事件过程类似,只不过前者只作用于特定工作表,而后者作用于工作簿中的任意工作表。 7.17 工作表的BeforeRightClick事件   在工作表中右击单元格时,将触发该工作表的Worksheet_BeforeRightClick事件过程。该事件过程有两个参数,Target参数表示右击的单元格,Cancel参数表示是否禁止右击单元格时弹出快捷菜单。   工作表的Worksheet_BeforeRightClick事件过程的功能与工作簿的Workbook_ SheetBeforeRightClick事件过程类似,只不过前者只作用于特定工作表,而后者作用于工作簿中的任意工作表。 7.18 工作表的BeforeDoubleClick事件   在工作表中双击单元格时,将触发该工作表的Worksheet_BeforeDoubleClick事件过程。该事件过程有两个参数,Target参数表示双击的单元格,Cancel参数表示是否取消双击单元格时默认执行的操作。   工作表的Worksheet_BeforeDoubleClick事件过程的功能与工作簿的Workbook_ SheetBeforeDoubleClick事件过程类似,只不过前者只作用于特定工作表,而后者作用于工作簿中的任意工作表。       第8章 使用对话框和用户窗体   前几章介绍了一些可在VBA中使用的对话框,包括使用InputBox函数和MsgBox函数创建的对话框,以及使用Application对象的InputBox方法创建的对话框。实际上,Application对象的其他一些方法还可以创建用于打开文件或保存文件的对话框,而使用Office对象模型中的FileDialog对象可以创建适用性更强的对话框,并可进行更多的控制。本章将介绍使用Application对象和FileDialog对象创建的对话框,以及由用户手动创建的对话框,后者在VBA中称为用户窗体。 8.1 使用Application对象创建对话框   除了使用Application对象的InputBox方法创建用于输入信息的对话框之外,还可以使用该对象的GetOpenFilename和GetSaveAsFilename两个方法创建对话框。GetOpenFilename方法用于创建“打开”对话框,GetSaveAsFilename方法用于创建“另存为”对话框,这两个对话框与在Excel中执行“打开”和“另存为”两个命令时打开的对话框相同。 8.1.1 使用GetOpenFilename方法创建“打开”对话框   与执行“打开”命令时显示的“打开”对话框的功能有所不同,在VBA中使用Application对象的GetOpenFilename方法创建的“打开”对话框只记录用户选择的文件的完整路径,而不会真正打开任何文件。如果用户在“打开”对话框中单击“取消”按钮,GetOpenFilename方法将返回False。GetOpenFilename方法的语法如下: GetOpenFilename(FileFilter, FilterIndex, Title, ButtonText, MultiSelect) * FileFilter(可选):设置在对话框中显示的文件类型,每一种文件类型由文本筛选字符串和MS-DOS通配符组成,它们之间以逗号分隔。如需显示多个文件类型,各个文件类型之间也使用逗号分隔。如需为一个文件类型设置多个MS-DOS通配符,则需要使用分号分隔它们。省略FileFilter参数时默认为“All Files(*.*),*.*”,即显示所有文件类型。例如“Excel文件(*.xlsx; *.xlsm),*.xlsx; *.xlsm”。 * FilterIndex(可选):默认文件筛选条件的索引号,取值范围为1到由FileFilter参数指定的筛选条件的总数。如果省略该参数,或该参数的值大于筛选条件总数,则该参数的值为1,即使用FileFilter参数中指定的第一个文件筛选条件。 * Title(可选):设置对话框的标题,默认为“打开”。 * ButtonText(可选):仅用于Macintosh计算机。 * MultiSelect(可选):选择一个或多个文件,该参数为True表示可以选择多个文件,该参数为False表示只能选择一个文件,默认为False。当该参数为True时,无论选择一个或多个文件,GetOpenFilename方法都会返回一个数组。   使用GetOpenFilename方法的优点是,用户可以自由选择要打开的工作簿,而不是将某个工作簿的完整路径预先写入VBA程序中。下面的代码为“打开”对话框设置了两种文件类型,一种是扩展名为.xlsx或.xlsm的Excel文件,另一种是扩展名为.txt的文本文件。打开“打开”对话框时,可以在右下角的下拉列表中选择要显示的文件类型,如图8-1所示。选择一个文件并单击“打开”按钮,将显示该文件的完整路径,如图8-2所示。 Sub 在打开对话框中选择一个文件() Dim strFilter As String, strTitle As String, varFileName strFilter = "Excel文件(*.xlsx;*.xlsm),*.xlsx;*.xlsm,文本文件(*.txt),*.txt" strTitle = "选择一个文件" varFileName = Application.GetOpenFilename(strFilter, , strTitle) If varFileName <> False Then MsgBox "选择的文件是:" & varFileName End If End Sub 图8-1 使用GetOpenFilename方法创建的对话框 图8-2 显示所选文件的完整路径   如需在Excel中真正打开在“打开”对话框中选择的文件,可以将GetOpenFilename方法返回的文件完整路径指定为Workbooks集合的Open方法的FileName参数。下面的代码将在Excel中打开上面选择的文件。 Workbooks.Open varFileName   如需在“打开”对话框中选择多个文件,可以将GetOpenFilename方法的MultiSelect参数设置为True。下面的代码以数组的方式处理用户在“打开”对话框中选择的多个文件,并在立即窗口中显示这些文件的完整路径,如图8-3所示。为了在单击“取消”按钮时退出程序,需要使用VBA内置的IsArray函数检查GetOpenFilename方法的返回值是否是数组,如果不是,则说明单击了“取消”按钮,此时使用Exit Sub语句退出程序。 Sub 在打开对话框中选择多个文件() Dim strFilter As String, strTitle As String, varFileName Dim intIndex As Integer strFilter = "Excel文件(*.xlsx;*.xlsm),*.xlsx;*.xlsm,文本文件(*.txt),*.txt" strTitle = "选择多个文件" varFileName = Application.GetOpenFilename(strFilter, , strTitle, , True) If Not IsArray(varFileName) Then Exit Sub For intIndex = LBound(varFileName) To UBound(varFileName) Debug.Print varFileName(intIndex) Next intIndex End Sub 图8-3 同时选择多个文件   提示:如需在Excel中真正打开选择的多个文件,需要将Workbooks.Open语句放置在For Next语句内部。 8.1.2 使用GetSaveAsFilename方法创建“另存为”对话框   与GetOpenFilename方法的处理方式类似,使用GetSaveAsFilename方法将创建“另存为”对话框并记录用户设置的文件名和存储路径,但是不会真正保存任何文件。GetSaveAsFilename方法的语法如下: Application.GetSaveAsFilename(InitialFilename, FileFilter, FilterIndex, Title, ButtonText)   GetSaveAsFilename方法的参数与GetOpenFilename方法基本相同,两种方法都有5个参数,但是GetSaveAsFilename方法没有MultiSelect参数,取而代之的是InitialFilename参数。该参数表示保存文件时的文件名,省略该参数时默认为活动工作簿的名称。   下面的代码将打开“另存为”对话框,默认的保存名称是当前活动工作簿的名称,用户可以修改保存的文件名,并设置保存位置,如图8-4所示。单击“保存”按钮,将显示保存文件的完整路径,如图8-5所示。 Sub 在另存为对话框中设置保存选项() Dim strFilter As String, strTitle As String, varFileName strFilter = "Excel文件(*.xlsx;*.xlsm),*.xlsx;*.xlsm,文本文件(*.txt),*.txt" strTitle = "设置文件名和保存位置" varFileName = Application.GetSaveAsFilename(, strFilter, , strTitle) If varFileName <> False Then MsgBox "保存文件的完整路径是:" & varFileName End If End Sub 图8-4 使用GetSaveAsFilename方法创建的对话框 图8-5 显示保存文件的完整路径 8.2 使用FileDialog对象创建对话框   FileDialog是Office对象模型中的对象,使用该对象可以实现比Application对象的GetOpenFilename和GetSaveAsFilename两个方法更强大的功能,使用FileDialog对象创建的对话框不仅用于选择文件和文件夹,还可以真正打开和保存文件。通过对FileDialog对象的属性和方法进行编程,可以灵活控制对话框中的选项和处理文件的方式。 8.2.1 显示不同类型的对话框   在Excel中可以使用Application对象的FileDialog属性返回FileDialog对象,FileDialog属性有一个参数,用于指定对话框的类型,其值由msoFileDialogType常量提供,如表8-1所示。 表8-1 msoFileDialogType常量 名 称 值 说 明 msoFileDialogOpen 1 “打开文件”对话框 msoFileDialogSaveAs 2 “保存文件”对话框 msoFileDialogFilePicker 3 “文件选取器”对话框 msoFileDialogFolderPicker 4 “文件夹选取器”对话框      FileDialog对象的Show方法用于显示指定类型的对话框。下面的代码将Application对象的FileDialog属性的参数设置为msoFileDialogOpen,所以打开的是“打开文件”对话框,并显示默认的标题和文件类型,如图8-6所示。 Application.FileDialog(msoFileDialogOpen).Show 图8-6 使用FileDialog对象创建“打开文件”对话框 8.2.2 在对话框中显示默认文件夹   使用FileDialog对象的InitialFileName属性可以指定在对话框中默认显示的文件夹。如果每次都希望从相同的位置打开或保存文件,则该属性会非常有用。下面的代码将E盘根目录中的“测试数据”文件夹设置为默认文件夹,然后打开“打开文件”对话框,其中默认显示“测试数据”文件夹中的文件。为了便于编程控制FileDialog对象的属性和方法,本例先声明一个FileDialog类型的对象变量,然后将要打开的对话框赋值给该变量。 Sub 在对话框中显示默认文件夹() Dim fdlOpen As FileDialog Set fdlOpen = Application.FileDialog(msoFileDialogOpen) With fdlOpen .InitialFileName = "E:\测试数据\" .Show End With End Sub 8.2.3 设置在对话框中显示的文件类型   使用FileDialog对象的Filters属性可以返回FileDialogFilters集合,使用该集合的Add方法可以在对话框中添加要显示的文件类型,每个文件类型由说明性文本和MS-DOS文件通配符两个部分组成。添加新的文件类型时,不会自动删除原有的文件类型,所以通常需要在添加文件类型之前,使用FileDialogFilters集合的Clear方法清除现有的文件类型。   FileDialogFilters集合的Add方法有3个参数,语法如下: FileDialogFilters.Add(Description, Extensions, Position) * Description:设置文件类型的说明性文本。 * Extensions:设置文件类型的扩展名,即MS-DOS通配符。可以设置一个或多个文件扩展名,每个文件扩展名必须以分号分隔。 * Position:设置文件类型在文件类型列表中的位置。省略该参数时,将文件类型添加到列表的底部。   下面的代码在“打开文件”对话框中只显示Excel文件中扩展名为.xlsx和.xlsm的两种文件类型,然后显示该对话框。 Sub 设置在对话框中显示的文件类型() Dim fdlOpen As FileDialog Set fdlOpen = Application.FileDialog(msoFileDialogOpen) With fdlOpen .Filters.Clear .Filters.Add "Excel文件", "*.xlsx;*.xlsm" .Show End With End Sub 8.2.4 在对话框中选择一个或多个文件   FileDialog对象的AllowMultiSelect属性控制用户可以在对话框中选择文件的数量,该属性为True表示可以选择多个文件,该属性为False表示只能选择一个文件。无论在对话框中选择一个文件还是多个文件,FileDialog对象的SelectedItems属性都会返回一个包含所选文件的完整路径的集合,可以使用For Each语句处理该集合中的每一个成员。   下面的代码将在对话框中显示用户在“打开文件”对话框中选择的所有文件的完整路径,如图8-7所示。 Sub 在对话框中选择多个文件() Dim fdlOpen As FileDialog, varFileName As Variant Dim strMsg As String Set fdlOpen = Application.FileDialog(msoFileDialogOpen) With fdlOpen .Filters.Clear .Filters.Add "Excel文件", "*.xlsx;*.xlsm" .AllowMultiSelect = True .Show End With For Each varFileName In fdlOpen.SelectedItems strMsg = strMsg & varFileName & vbCrLf Next varFileName MsgBox strMsg End Sub 图8-7 在对话框中显示所选文件的完整路径 8.2.5 打开或保存文件   FileDialog对象的Show方法只提供一个可以选择文件的对话框,并不能对文件执行实际的打开或保存操作。如需真正打开或保存文件,需要在使用FileDialog对象的Show方法之后,再使用该对象的Execute方法。   为了避免在对话框中单击“取消”按钮后执行打开或保存文件的操作,需要检查Show方法的返回值。如果在对话框中单击“打开”或“保存”按钮,则Show方法返回True;如果在对话框中单击“取消”按钮,则Show方法返回False。   下面的代码在Excel中打开由用户在“打开文件”对话框中选择的所有文件。本例代码与上例基本类似,只是加入了If Then语句判断Show方法的返回值是否是True,如果是,则在For Each语句中使用Workbooks.Open打开用户在对话框中选择的每一个文件。 Sub 在Excel中打开由用户选择的所有文件() Dim fdlOpen As FileDialog, varFileName As Variant Set fdlOpen = Application.FileDialog(msoFileDialogOpen) With fdlOpen .Filters.Clear .Filters.Add "Excel文件", "*.xlsx;*.xlsm" .AllowMultiSelect = True End With If fdlOpen.Show Then For Each varFileName In fdlOpen.SelectedItems Workbooks.Open varFileName Next varFileName End If End Sub 8.3 创建和操作用户窗体   本章前几节介绍的都是由特定对象的方法创建的具有固定外观和功能的对话框。如果希望创建适合不同应用需求的对话框,则需要使用用户窗体。用户可以在用户窗体中添加不同类型的控件,以便设计对话框的布局结构和可供操作的部件,然后为控件和用户窗体编写能够响应用户操作的事件过程。本节主要介绍在VBA中创建和操作用户窗体的方法,有关控件的内容将在第9章进行介绍。 8.3.1 创建用户窗体的基本流程   无论创建简单或复杂的用户窗体,通常都遵循以下基本流程:   (1)在VBA工程中添加一个用户窗体模块。   (2)在用户窗体中添加控件,并调整控件的位置。   (3)设置用户窗体和控件的属性,使它们的外观和默认行为符合最终目标。   (4)在用户窗体模块的代码窗口中,为用户窗体和控件的事件过程编写代码。   (5)在标准模块中编写用于加载、显示、隐藏和关闭用户窗体的代码。   (6)测试用户窗体和控件能否按照预期目标正常工作。 8.3.2 创建用户窗体   创建用户窗体实际上就是创建用户窗体模块,有以下两种方法: * 在VBA工程中选择任意一项,然后单击菜单栏中的“插入”|“用户窗体”命令。 * 在VBA工程中右击任意一项,然后在弹出的菜单中选择“插入”|“用户窗体”命令。   无论使用哪种方法,都会在当前VBA工程中添加一个用户窗体模块,其默认名称是UserForm1,如图8-8所示。如果继续创建更多的用户窗体模块,则默认名称结尾的数字会持续递增。   应该为用户窗体模块设置一个有意义的名称,以使其在多个用户窗体模块中更易于识别,或者在VBA代码中引用用户窗体时使代码更易读。如需修改用户窗体模块的名称,可以在工程资源管理器中选择用户窗体模块,然后按F4键,在属性窗口中修改“(名称)”属性的值,如图8-9所示。 图8-8 创建一个用户窗体模块 图8-9 修改用户窗体模块的名称 8.3.3 设置用户窗体的属性   在8.3.2小节修改用户窗体模块的名称时,实际上是在修改其Name属性的值。与前几章介绍过的对象类似,每一个与用户窗体模块关联的用户窗体是一个UserForm对象,它包含大量的属性,通过设置这些属性,可以改变用户窗体的外观和行为方式。   由于用户窗体是具有可视化界面的对象,所以可以在属性窗口中设置其属性,类似于在VBE窗口中设置ThisWorkbook对象或工作簿中任意一个Sheet对象的属性。如需设置用户窗体的属性,可以在工程资源管理器中双击用户窗体模块,打开用户窗体的设计窗口,如果其中包含控件,则确保未选中任何控件。按F4键,然后在打开的属性窗口设置用户窗体的属性。用户窗体的常用属性如表8-2所示。 表8-2 用户窗体的常用属性 属 性 说 明 (名称)(Name) 设置用户窗体的名称,在代码中将使用该名称引用用户窗体 BackColor 设置用户窗体的背景色 BorderStyle 设置用户窗体的边框样式 Caption 设置用户窗体的标题,即用户窗体标题栏中显示的文本 Enabled 设置用户窗体是否可用,包括是否可以接受焦点以及响应用户的操作 ForeColor 设置用户窗体的前景色 Height 设置用户窗体的高度 Left 设置用户窗体的左边缘与屏幕左边缘之间的距离 Picture 设置用户窗体的背景图 续表 属 性 说 明 ScrollBars 设置在用户窗体中是否显示水平滚动条和垂直滚动条 ShowModal 设置用户窗体的显示模式,分为模式和无模式两种 StartUpPosition 设置用户窗体显示时的位置 Top 设置用户窗体的上边缘与屏幕上边缘之间的距离 Width 设置用户窗体的宽度      不同的属性具有不同的设置方法,主要有以下几种: * 有的属性可以直接输入一个值,例如Caption属性。 * 有的属性提供了几个值,需要选择其中之一,例如StartUpPosition属性 * 有的属性有一个按钮,单击该按钮将打开一个对话框,然后在其中进行设置,例如Picture属性。 * 有的属性只能在设计用户窗体时设计,不能在运行用户窗体时设置,例如“(名称)”属性。   也可以使用VBA代码设置用户窗体的属性,方法与设置其他对象的属性相同,可以使用以下格式: 用户窗体的名称.属性名=属性值   例如,下面的代码将名为UserForm1的用户窗体顶部的标题设置为“欢迎界面”,运行该代码后的用户窗体如图8-10所示。 UserForm1.Caption = "欢迎界面" 图8-10 修改用户窗体的Caption属性 8.3.4 显示和关闭用户窗体   设计好用户窗体后,需要在程序中显示它,用户才能真正与用户窗体进行交互,发挥用户窗体的功能。在设计阶段可以随时显示用户窗体,以便测试用户窗体的外观和功能。   在“工程资源管理器”中双击用户窗体模块,打开用户窗体的设计窗口,然后按F5键,或者单击“标准”工具栏中的“运行宏”按钮,将运行用户窗体,如图8-11所示。此时可以检查用户窗体的外观是否符合要求,还可以执行各种操作,以便测试为用户窗体和其内部的控件编写的事件过程是否能够正确响应用户的操作。 图8-11 运行用户窗体   如需关闭用户窗体并返回其设计窗口,可以单击用户窗体右上角的“关闭”按钮。   在实际应用中,通常需要在程序运行期间使用VBA代码控制何时显示和关闭用户窗体。UserForm对象的Show方法用于显示指定的用户窗体,下面的代码位于工作簿的Workbook_Open事件过程中,每次打开该工作簿时会自动显示名为frmLogin的用户窗体。 Private Sub Workbook_Open() frmLogin.Show End Sub   为了加快用户窗体的显示速度,可以使用Load语句先将用户窗体加载到内存中,需要时再使用Show方法显示该用户窗体。下面的代码将名为frmLogin的用户窗体加载到内存中,但是不会显示出来。 Load frmLogin   如需将正在显示的某个用户窗体暂时隐藏起来,可以使用UserForm对象的Hide方法。下面的代码将名为frmLogin的用户窗体隐藏起来,但是它仍然存在于内存中,可以随时使用Show方法显示该用户窗体。 frmLogin.Hide   如需彻底关闭用户窗体,而不是让其隐藏后驻留在内存中,可以使用UnLoad语句。下面的代码将名为frmLogin的用户窗体关闭并从内存中清除。 UnLoad frmLogin   在代码中引用用户窗体时,可以使用Me关键字代替用户窗体的名称,从而简化代码的输入量,并且即使用户窗体的名称发生改变,Me关键字可以确保引用的是同一个用户窗体而不会出错。下面的代码位于名为frmLogin的用户窗体模块中,使用Me关键字代替frmLogin。 UnLoad Me 8.3.5 模式和无模式的用户窗体   Show方法有一个modal参数,用于指定用户窗体显示为模式还是无模式。该参数为vbModal表示模式,该参数为vbModeless表示无模式,省略该参数时默认为模式。下面的代码将名为frmLogin的用户窗体显示为无模式。 frmLogin.Show vbModeless   用户窗体显示为模式时,用户只能处理该用户窗体,不能操作Excel应用程序中的其他部分,Excel中的“字体”对话框是模式的一个示例。用户窗体显示为无模式时,用户既可以处理该用户窗体,也可以操作Excel应用程序中的其他部分,Excel中的“查找和替换”对话框是无模式的一个示例。 8.3.6 使用变量引用用户窗体   如需动态引用名称不同的多个用户窗体,可以将这些用户窗体的名称存储到变量中,然后将变量设置为UserForms集合的Add方法的参数,从而将与指定名称关联的用户窗体添加到UserForms集合中。UserForms集合由已加载到内存中的所有用户窗体组成。   下面的代码将用户窗体的名称存储到一个变量中,然后使用Add方法将与该名称对应的用户窗体添加到UserForms集合中,最后使用Show方法将该用户窗体显示为无模式。 Sub 使用变量引用用户窗体() Dim strFormName As String strFormName = "frmLogin" UserForms.Add(strFormName).Show vbModeless End Sub   下面的代码使用Array函数将一个包含3个名称的数组赋值给一个Variant类型的变量,然后使用For Each语句将该数组中的每一个名称所代表的用户窗体添加到UserForms集合中,最后显示UserForms集合包含的用户窗体总数。 Sub 使用变量引用多个用户窗体() Dim varFormNames As Variant, varFormName As Variant varFormNames = Array("frmLogin", "frmGreet", "frmMain") For Each varFormName In varFormNames UserForms.Add varFormName Next varFormName MsgBox UserForms.Count End Sub   当需要从UserForms集合中引用某个用户窗体时,只能使用该用户窗体的索引号来引用它。索引号对应于在UserForms集合中添加用户窗体的顺序,添加的第一个用户窗体的索引号是0,第二个用户窗体的索引号是1,其他用户窗体的索引号以此类推。最后一个用户窗体的索引号是UserForms集合包含的所有用户窗体的总数减1。下面的代码可以获取最后一个用户窗体的索引号。与其他集合的Count属性相同,UserForms集合的Count属性用于返回该集合包含的成员总数。 UserForms.Count - 1 8.3.7 使用一个用户窗体模块创建多个用户窗体   在VBA工程中添加的用户窗体本身是一个类,这个类的名称就是用户窗体模块的名称,所以在声明变量时,可以将用户窗体模块的名称用作变量的数据类型。这与声明一个Workbook或Worksheet类型的对象变量类似,但是在声明语句的格式上稍有不同。   假如在VBA工程中有一个名为frmLogin的用户窗体模块,该类的名称就是frmLogin。如需声明一个frmLogin类型的对象变量,可以使用下面两行代码,书写方式与使用前期绑定时创建字典对象类似。 Dim frm As frmLogin Set frm = New frmLogin   还可以将上面两行代码合并为一行,在Dim语句中使用New关键字声明变量并为其赋值。 Dim frm As New frmLogin   下面的代码声明了3个frmLogin类型的变量,相当于创建了3个名为frmLogin的用户窗体,然后为它们设置不同的标题,最后显示这3个用户窗体。除了标题不同之外,3个用户窗体的其他部分完全相同,如图8-12所示。 Sub 使用一个用户窗体模块创建多个用户窗体() Dim frm1 As New frmLogin Dim frm2 As New frmLogin Dim frm3 As New frmLogin frm1.Caption = "第一个窗体" frm2.Caption = "第二个窗体" frm3.Caption = "第三个窗体" frm1.Show frm2.Show frm3.Show End Sub 图8-12 使用一个用户窗体模块创建多个用户窗体   提示:如需同时显示3个用户窗体,需要在使用Show方法时为其添加vbModeless参数,以无模式显示。否则只有关闭上一个用户窗体后,才能看到下一个用户窗体。 8.3.8 编写用户窗体的事件过程   与工作簿事件和工作表事件类似,用户窗体也有很多事件,触发这些事件时,会自动执行预先为其编写的代码。用户窗体事件及其触发条件如表8-3所示。 表8-3 用户窗体事件及其触发条件 事 件 名 称 触 发 条 件 Activate 激活用户窗体时 AddControl 代码运行期间向用户窗体中添加一个控件时 BeforeDragOver 鼠标指针位于用户窗体上并准备进行拖放操作之前 BeforeDropOrPaste 在一个对象上放置或粘贴数据之前 Click 单击用户窗体时 DblClick 双击用户窗体时 Deactivate 用户窗体失去焦点时,即激活另一个用户窗体时 Error 控件检测出错误但不能将错误信息返回调用过程时 续表 事 件 名 称 触 发 条 件 Initialize 加载用户窗体时 KeyDown 在用户窗体上按下按键时 KeyPress 在用户窗体上按下任意按键时 KeyUp 在用户窗体上释放按键时 Layout 改变用户窗体的大小时 MouseDown 在用户窗体上按下鼠标按键时 MouseMove 在用户窗体上移动鼠标时 MouseUp 在用户窗体上释放鼠标按键时 QueryClose 关闭用户窗体时 RemoveControl 代码运行期间从用户窗体中删除一个控件时 Resize 改变用户窗体的大小时 Scroll 滚动用户窗体时 Terminate 终止用户窗体时 Zoom 缩放用户窗体时      在程序中显示并关闭一个用户窗体时,会自动触发以下事件: Initialize=>Activate=>QueryClose=>Terminate * 使用Show方法显示用户窗体时,先触发Initialize事件,然后触发Activate事件。 * 使用UnLoad语句关闭用户窗体时,先触发QueryClose事件,然后触发Terminate事件。使用Hide方法隐藏用户窗体不会触发这两个事件。   为用户窗体事件编写的代码存储在该用户窗体模块中,需要在与用户窗体模块关联的代码窗口中输入代码。如需打开用户窗体的代码窗口,可以在“工程资源管理器”中双击用户窗体模块,打开用户窗体的设计窗口,然后在设计窗口中双击用户窗体,如图8-13所示。 图8-13 在设计窗口中双击用户窗体   打开代码窗口后,从左侧顶部的下拉列表中选择用户窗体模块的名称,在右侧顶部的下拉列表中选择所需的事件名,然后在自动显示的事件过程中编写代码。   下面的代码位于一个标准模块中,运行该Sub过程,将在屏幕的正中央显示一个标题为“测试”的用户窗体,如图8-14所示。 Sub 显示用户窗体() frmTest.Show End Sub 图8-14 以指定的位置和标题显示用户窗体   下面的代码用于设置用户窗体的显示位置和顶部标题的代码位于该用户窗体的Initialize事件过程中。 Private Sub UserForm_Initialize() Me.Caption = "测试" Me.StartUpPosition = 2 End Sub   下面的代码位于用户窗体的DblClick事件过程中,当双击用户窗体时,将触发DblClick事件,并执行其中的Unload语句关闭用户窗体。 Private Sub UserForm_DblClick(ByVal Cancel As MSForms.ReturnBoolean) Unload Me End Sub   下面的代码位于用户窗体的QueryClose事件过程中,当执行Unload语句时,将触发QueryClose事件,此时会执行该事件过程中的代码,询问用户是否关闭用户窗体,如图8-15所示。如果单击“是”按钮,则关闭用户窗体;如果单击“否”按钮,则不关闭用户窗体。 Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer) Dim lngIsClose As Long lngIsClose = MsgBox("是否关闭该用户窗体?", vbYesNo, "确认信息") If lngIsClose = vbNo Then Cancel = True End Sub 图8-15 确认是否关闭用户窗体   下面的代码位于用户窗体的Terminate事件过程中,如果用户在上一步单击“是”按钮,将触发Terminate事件,此时会显示一个对话框,其中只有一个“确定”按钮,单击该按钮将彻底关闭用户窗体,如图8-16所示。 Private Sub UserForm_Terminate() MsgBox "单击【确定】按钮,将真正关闭用户窗体", vbOKOnly, "关闭窗体" End Sub 图8-16 真正关闭用户窗体前的提示信息 8.3.9 禁用用户窗体中的“关闭”按钮   用户窗体右上角有一个“关闭”按钮,单击该按钮可以关闭用户窗体。在实际应用中,通常会在用户窗体中添加命令按钮控件,用户通过单击该控件来关闭用户窗体。在这种情况下,可能希望禁用用户窗体右上角的“关闭”按钮。   由于单击用户窗体右上角的“关闭”按钮时会触发用户窗体的QueryClose事件,所以可以将QueryClose事件过程中的Cancel参数设置为True,从而实现禁止用户窗体右上角的“关闭”按钮的功能。QueryClose事件过程的另一个参数CloseMode用于判断触发该事件时执行的是哪种操作,该参数的值如表8-4所示。 表8-4 CloseMode参数值 常 量 值 说 明 vbFormControlMenu 0 单击用户窗体右上角的“关闭”按钮 vbFormCode 1 使用UnLoad语句关闭用户窗体 vbAppWindows 2 正在关闭Windows操作系统 vbAppTaskManager 3 使用Windows任务管理器关闭Excel应用程序      下面的代码将在用户单击用户窗体右上角的“关闭”按钮时,显示一条信息并禁止关闭该用户窗体。 Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer) If CloseMode = vbFormControlMenu Then MsgBox "关闭按钮的功能已被禁用" Cancel = True End If End Sub   提示:测试上面的代码时,将无法使用用户窗体右上角的“关闭”按钮来“关闭”用户窗体,此时可以在VBE窗口中单击“标准”工具栏的“重新设置”按钮来关闭用户窗体。       第9章 在用户窗体中使用控件   用户窗体只是一个容器,要使其真正发挥作用,还需要在用户窗体中添加可供用户操作的不同类型的控件,并为控件的事件过程编写代码。当用户操作控件时,将触发相应的事件过程并执行其中的代码,从而实现用户与用户窗体及其中的控件进行交互的功能。本章先介绍控件的基本概念和通用操作,这些内容是本章后续内容的基础。然后介绍编程处理常用类型控件的方法,并列举了大量示例。 9.1 控件的基本概念和通用操作   本节将介绍控件的基本概念和通用操作,这些操作适用于不同类型的控件。在本章后面介绍特定类型的控件时,将不再重复介绍这些操作。 9.1.1 控件类型   在VBA工程中添加一个用户窗体模块时,将自动显示用户窗体的设计窗口和工具箱。如果未显示工具箱,则可以单击菜单栏中的“视图”|“工具箱”命令。在工具箱中默认显示两行图标,每一个图标对应一种控件类型,如图9-1所示。工具箱中默认的各类控件的图标、中文名、英文名和功能如表9-1所示。 图9-1 工具箱 表9-1 工具箱中的控件类型 图 标 中 文 名 英 文 名 功 能 标签 Label 显示特定内容,或作为其他对象的说明性文字 文本框 TextBox 接收用户输入的内容 复合框 组合框 ComboBox 既可以在复合框中选择一项,也可以在复合框顶部的文本框中进行输入 列表框 ListBox 显示多个项目,用户可从中选择一项或多项 复选框 CheckBox 在两种状态之间切换或同时选择多项 选项按钮 OptionButton 在一组选项中只能选择一个 切换按钮 ToggleButton 在两种状态之间切换 续表 图 标 中 文 名 英 文 名 功 能 框架 Frame 对选项分组 命令按钮 CommandButton 执行指定的操作 选项卡 TabStrip 显示一个或多个选项卡,每个选项卡包含完全相同的选项,选项的位置在每个选项中也都相同 多页 MultiPage 与TabStrip类似,但是可以在每个选项卡中包含不同的选项,选项的位置也可以各不相同 滚动条 ScrollBar 对大量项目或信息的快速定位和浏览 旋转按钮 微调按钮 数值调节钮 SpinButton 调整数值大小,并将其显示在文本框中 图像 Image 在用户窗体中显示图片和图标 单元格选择器 RefEdit 从工作表中选择单元格区域,并将其地址添加到对话框中      提示:在Excel功能区的“开发工具”选项卡中有两类控件:表单控件和ActiveX控件,它们只能在工作表中使用,而不能用于用户窗体。不过ActiveX控件与用户窗体的工具箱中的控件具有相同的功能,只是它们使用在不同的环境中。 9.1.2 管理工具箱中的控件   用户可以随时在工具箱中添加或删除控件。如需添加控件,可以右击工具箱中的任意一个图标,在弹出的快捷菜单中选择“附加控件”命令,如图9-2所示。然后在打开的对话框中勾选一个或多个控件开头的复选框,最后单击“确定”按钮,如图9-3所示。 图9-2 选择“附加控件”命令 图9-3 勾选需要添加的控件   如需删除工具箱中的控件,可以右击该控件的图标,在弹出的快捷菜单中选择“删除xxx”命令,xxx表示控件的名称。   如需在工具箱中添加大量的控件,可以将这些控件组织到不同的“页”中,然后可以在各页之间切换来选择所需的控件。右击工具箱中标题栏下方的任意位置,在弹出的快捷菜单中选择“新建页”“删除页”或“重命名”等命令,可以创建新的页、删除现有的页或修改页的名称,如图9-4所示。“导入页”和“导出页”两个命令还可以将工具箱中的现有页以文件的形式备份到计算机中,并可在工具箱中随时恢复已备份的页。 图9-4 使用“页”管理工具箱中的控件 9.1.3 在用户窗体中添加控件   在用户窗体中添加控件有以下几种方法: * 在工具箱中单击并按住一个控件,然后将其拖动到用户窗体中,将该控件以默认大小添加到用户窗体中。 * 在工具箱中单击一个控件,然后在用户窗体中的任意位置单击,将该控件以默认大小添加到用户窗体中。 * 在工具箱中单击一个控件,然后在用户窗体中沿对角线方向拖动鼠标指针,将该控件以用户指定的大小添加到用户窗体中。 * 在工具箱中双击一个控件,进入该控件的锁定模式,然后使用上述任意一种方法在用户窗体中反复添加多个该控件。如需退出锁定模式,可以在工具箱中单击该控件。   如图9-5所示,在用户窗体中添加了两个文本框和一个命令按钮,在命令按钮上显示的文本由Caption属性决定。 图9-5 在用户窗体中添加控件 9.1.4 选择一个或多个控件   为控件设置属性或执行其他操作时,需要先选择控件。如需选择一个控件,只需在用户窗体中单击该控件即可。如需选择多个控件,可以使用以下几种方法。 * 选择所有控件:在用户窗体中的任意位置单击,然后按Ctrl+A组合键。 * 选择相邻的多个控件:拖动鼠标指针划过一个矩形范围,位于该范围内的控件都会被选中。即使只有控件的一部分位于范围内,该控件也会被选中。还可以先选择一个控件,然后按住Shift键并单击另一个控件,将选中这两个控件以及位于它们之间的控件。 * 选择不相邻的多个控件:按住Ctrl键,然后单击要选择的每一个控件。   选择多个控件后,其中会有一个控件的四周显示白色控制点,而其他选中的控件四周显示黑色控制点。具有白色控制点的控件是基准控件,在设置诸如控件对齐方式等格式时,其他控件将以基准控件的位置为参考基准。   在如图9-6所示的用户窗体中,最上方的文本框的四周显示白色控制点,所以它是基准控件。如需将命令按钮设置为基准控件,可以按住Ctrl键并在命令按钮上分别单击两次,如图9-7所示。 图9-6 具有白色控制点的控件是基准控件 图9-7 更改基准控件 9.1.5 设置控件的属性   与设置用户窗体的属性类似,也可以为用户窗体中的控件设置属性。设置前需要先选择一个或多个控件,然后在属性窗口中设置控件的属性。在属性窗口顶部的下拉列表中列出了当前用户窗体及其包含的所有控件的名称,如图9-8所示,可在此处切换要设置属性的控件,而无须在用户窗体中选择控件。   控件的很多属性与用户窗体的同名属性的功能和设置方法都相同,具体请参考第8章,此处不再赘述。   当选择不同类型的多个控件时,在属性窗口中只会显示这些控件共同拥有的属性,例如BackColor、Left、Top、Width、Height、Enabled、Visible、Tag等,而不会显示特定类型控件特有的属性,如图9-9所示。 图9-8 在下拉列表中选择要设置属性的控件 图9-9 显示选中的所有控件的共同属性   提示:虽然每个控件都有Name属性,但是选择多个控件时,Name属性不会出现在属性窗口中,因为同一个用户窗体上的控件不能具有相同的名称。控件的Name属性在属性窗口中显示为“(名称)”,但是在代码中使用该属性时必须写成“Name”。 9.1.6 调整控件的大小   在用户窗体中添加控件时,其中的一种方法是拖动鼠标指针绘制出指定大小的控件。如果已将控件添加到用户窗体中,则可以选择该控件,然后使用鼠标拖动控件四周的控制点,即可调整控件的大小。如需为控件设置精确的大小,可以选择控件,然后在属性窗口中设置Height属性和Width属性。   如需将多个控件设置为相同的大小,可以选中这些控件,然后右击其中的任意一个控件,在弹出的菜单中选择“统一尺寸”命令,再在子菜单中选择调整方式,如图9-10所示。多个控件的尺寸调整以四周显示为白色控制点的控件的尺寸为参照基准。 图9-10 将多个控件调整为相同的大小 9.1.7 设置控件的位置和对齐方式   如需改变控件在用户窗体中的位置,可以直接拖动该控件到目标位置。如需为控件设置精确的位置,可以在属性窗口中设置控件的Left属性和Top属性。   如需对齐多个控件,可以使用以下两种方法: * 无论将控件拖动到哪个位置,默认都会自动与用户窗体中的某条网格线对齐,这样就可以利用网格线对齐多个控件。 * 选择多个控件,然后右击选中的任意一个控件,在弹出的快捷菜单中选择“对齐”命令,再在子菜单中选择对齐方式,如图9-11所示。多个控件的对齐以四周显示为白色控制点的控件的位置为参照基准。 图9-11 设置多个控件的对齐方式   提示:可以设置网格的尺寸,以及是否使用和显示网格。单击菜单栏中的“工具”|“选项”命令,打开“选项”对话框。在“通用”选项卡的“窗体网格设置”类别中进行设置,如图9-12所示。 图9-12 设置网格选项 9.1.8 设置控件的Tab键顺序   在设置Tab键顺序之前,需要先了解“焦点”的概念。当一个控件获得焦点时,用户执行的操作会自动作用于该控件。例如,当一个文本框获得焦点时,其内部会显示一条闪烁的竖线,默认会将内容输入到该文本框中。当一个命令按钮获得焦点时,其四周会显示虚线,按Enter键时相当于单击该命令按钮。   如图9-13所示,运行本例用户窗体,当前获得焦点的控件是命令按钮。可以按Tab键或Shift+Tab组合键,在各个控件之间转移焦点。转移焦点的先后次序就是Tab键顺序,在Tab键顺序中位于第一位的控件将在运行用户窗体后最先获得焦点。 图9-13 获得焦点的控件是命令按钮   注意:无论在用户窗体中有多少个控件,每次只能有一个控件获得焦点,并且只有控件的Enabled属性和Visible属性都设置为True,该控件才能获得焦点。   设置控件的Tab键顺序有以下两种方法: * 在用户窗体的设计窗口中右击用户窗体,然后在弹出的快捷菜单中选择“Tab键顺序”命令,如图9-14所示。打开“Tab键顺序”对话框,选择一个或多个控件,然后单击“上移”或“下移”按钮,即可调整所选控件的Tab键顺序,如图9-15所示。 * 在用户窗体中选择一个控件,然后在属性窗口中为该控件的TabIndex属性设置一个值。第一个获得焦点的控件的TabIndex属性的值是0,第二个是1,第三个是2,以此类推。不同控件的TabIndex属性的值不能相同。 图9-14 选择“Tab键顺序”命令 图9-15 设置控件的Tab键顺序   提示:如果不想让某个控件获得焦点,则可以将该控件的TabStop属性设置为False。   如果在框架、多页等容器类的控件内部包含其他控件,则内部的这些控件也有自己的Tab键顺序。设置这些控件的Tab键顺序时,需要先选择这些控件所属的框架或多页控件。 9.1.9 在代码中引用控件   在设计用户窗体时,可以通过设置控件的“(名称)”属性为控件起一个有意义的名称,编程处理控件时,将使用该名称引用控件。为了通过名称识别控件的类型,可以使用表示控件类型的字符作为控件名称的前缀。例如,cmd表示命令按钮,txt表示文本框,chk表示复选框。   当编程处理用户窗体中的控件时,需要使用名称引用特定的控件,分为以下两种情况。   1. 在用户窗体模块中引用该用户窗体中的控件   如果在用户窗体模块的代码窗口中引用该用户窗体中的控件,则可以直接使用该控件的名称。下面的代码位于名为frmLogin的用户窗体模块的代码窗口中,当单击其中名为cmdOk的命令按钮时,将显示该按钮的标题。 Private Sub cmdOk_Click() MsgBox cmdOk.Caption End Sub   2. 在其他模块中引用用户窗体中的控件   如果引用控件的代码来自于其他模块,而非控件所属的用户窗体模块,则在引用控件时必须添加对控件所属的用户窗体模块名称的引用。下面的代码位于一个标准模块中,引用名为frmLogin的用户窗体中名为cmdOk的命令按钮。 Sub 测试() MsgBox frmLogin.cmdOk.Caption End Sub 9.1.10 编写控件的事件过程   为了使控件能够响应用户的操作,需要为用户窗体中的控件的事件过程编写代码,这些代码位于控件所属的用户窗体模块中,在用户窗体的代码窗口中编写控件的事件过程。一旦触发控件的某个事件,就会立刻运行其中的代码。   在用户窗体中双击一个控件,将打开用户窗体的代码窗口,并显示刚才双击的控件的默认事件过程。可以在代码窗口顶部的左、右两个下拉列表中分别选择控件的名称和事件过程,然后为指定的事件过程编写代码。   9.1.9小节中的第一个示例使用的是命令按钮的Click事件过程。当单击名为cmdOk的命令按钮时,将执行该命令按钮的Click事件过程中的代码。 9.1.11 控件的Controls集合   每个用户窗体中的所有控件组成了Controls集合,该集合的父对象是这些控件所属的用户窗体。由于框架和多页两种控件可以作为其他控件的容器,所以这两种控件也有自己的Controls集合。   由于不存在特定控件类型的集合,当需要处理用户窗体中的某一类控件时,可以使用For Each语句检查Controls集合中的每一个控件的类型,如果符合指定的类型,则对该类控件执行所需的操作。可以使用VBA内置的TypeName函数判断控件的类型。   下面的代码在双击用户窗体时,自动显示该用户窗体中命令按钮的数量,如图9-16所示。代码中的Me关键字在前面曾经介绍过,它代表代码所属的用户窗体。 Private Sub UserForm_DblClick(ByVal Cancel As MSForms.ReturnBoolean) Dim ctl As Control, intCount As Integer For Each ctl In Me.Controls If TypeName(ctl) = "CommandButton" Then intCount = intCount + 1 End If Next ctl MsgBox "命令按钮的数量是:" & intCount End Sub 图9-16 显示命令按钮的数量 9.2 命令按钮   无论用户窗体实现什么功能,通常在用户窗体中都会至少包含一个按钮。最常见的示例是使用VBA内置的MsgBox函数创建的对话框,其中至少包含一个“确定”按钮。Click事件是命令按钮的默认事件,单击命令按钮时将触发该事件。命令按钮的常用属性如表9-2所示。 表9-2 命令按钮的常用属性 属 性 说 明 Cancel 将该属性设置为True时,按Esc键与单击该命令按钮等效 Default 将该属性设置为True时,按Enter键与单击该命令按钮等效 TakeFocusOnClick 单击命令按钮时是否使其获得焦点,为True表示获得,为False表示不获得   无论命令按钮是否获得焦点,将Cancel属性设置为True和将Default属性设置为True对命令按钮始终有效。   提示:为了避免浪费篇幅,像Name、Left、Top、Width、Height、Enabled、Visible等每类控件都拥有的属性,就不在本节及后续各节中重复列出了,它们的含义与用户窗体的同名属性相同,请参考第8章。   下面通过几个示例介绍命令按钮的用法。 9.2.1 指定默认的“确定”按钮和“取消”按钮   运行本例用户窗体,在文本框中输入任意内容。按Enter键时,相当于单击“确定”按钮,此时会在一个对话框中显示文本框中的内容。如果文本框中还没有内容,则会显示“还未输入任何内容”。如图9-17所示。关闭显示信息的对话框,焦点仍然位于文本框中。不再使用该用户窗体时,按Esc键可将其关闭,相当于单击“取消”按钮。 图9-17 测试用户窗体   用户窗体中有3个控件,它们的属性如表9-3所示。 表9-3 各个控件的属性 控 件 类 型 Name Caption Default Cancel 文本框 txtTitle / / / 命令按钮 cmdOk 确定 True False 命令按钮 cmdCancel 取消 False True      下面的代码位于用户窗体模块中,代码分为3个部分: * 用户窗体的Initialize事件过程:显示用户窗体时,将命令按钮设置为单击它是不接受焦点,这意味着单击该按钮时,焦点不会转移到该按钮上。 * “确定”按钮的Click事件过程:单击“确定”按钮时,判断文本框中是否有内容,并根据判断结果显示不同的信息。 * “取消”按钮的Click事件过程:关闭用户窗体。 Private Sub UserForm_Initialize() cmdOk.TakeFocusOnClick = False End Sub Private Sub cmdOk_Click() If Len(txtTitle.Text) = 0 Then MsgBox "还未输入任何内容" Else MsgBox "文本框中的内容是:" & txtTitle.Text End If End Sub Private Sub cmdCancel_Click() Unload Me End Sub 9.2.2 单击按钮时自动切换显示标题   下面的代码位于用户窗体模块中,用户窗体中有一个名为cmdSwitch的命令按钮。运行用户窗体,反复单击该命令按钮,命令按钮上的标题会自动在“确定”和“取消”之间切换,如图9-18所示。 Private Sub cmdSwitch_Click() Select Case cmdSwitch.Caption Case "确定" cmdSwitch.Caption = "取消" Case "取消" cmdSwitch.Caption = "确定" End Select End Sub 图9-18 切换显示命令按钮上的标题 9.3 文本框   文本框用于接收用户输入的数据或显示信息。Change事件是文本框的默认事件,修改文本框中的文本时将触发该事件。文本框的常用属性如表9-4所示。 表9-4 文本框的常用属性 属 性 说 明 EnterKeyBehavior 在文本框中按Enter键后的行为,为True时将在文本框中创建一个新行,为False时将焦点移动到Tab键顺序中的下一个控件 LineCount 返回文本框中文本的总行数 Locked 锁定文本框,无法在其中输入内容 MaxLength 设置可在文本框中输入的字符总数 MultiLine 设置在文本框中是否显示多行文本,为True将显示为多行文本,为False将显示为单行文本 续表 属 性 说 明 PasswordChar 使用指定的字符代替显示在文本框中的实际字符,仅用于显示,不会改变实际输入的字符 ScrollBars 设置是否为文本框添加水平滚动条和垂直滚动条 SelLength 设置在文本框中选中的字符数 SelStart 设置选中文本的起始位置,未选中文本时表示插入点的位置 SelText 返回或设置在文本框中选中的文本 Text 返回或设置文本框中的所有文本 TextAlign 设置文本在文本框中的对齐方式,有左对齐、居中对齐和右对齐3种 TextLength 返回文本框中所有文本的字符数 WordWrap 设置文本框中的文本是否可以自动换行。只有将MultiLine属性设置为True,并且将ScrollBars属性设置为不显示水平滚动条时,WordWrap属性才有效      下面通过几个示例介绍文本框的用法。 9.3.1 创建限制字符长度的密码文本框   运行本例用户窗体,在文本框中可以输入任意字符,如果字符的个数小于6,则在单击“确定”按钮时,将显示如图9-19所示的提示信息。 图9-19 创建密码文本框   下面的代码位于用户窗体模块中,用户窗体中有两个控件,一个是名为txtPassword的文本框,另一个是名为cmdOk的命令按钮。用户窗体的Initialize事件过程用于在显示用户窗体时,将命令按钮设置为单击时不接受焦点,并为文本框设置两个属性,一个是将文本框中可输入的最大字符设置为6,另一个是将*号设置为在文本框中输入内容时显示的字符。在命令按钮的Click事件过程中,使用VBA内置的Len函数计算文本框中的字符个数,如果它小于文本框中可输入的最大字符数,则显示提示信息,并删除文本框中的所有内容。 Private Sub UserForm_Initialize() cmdOk.TakeFocusOnClick = False txtPassword.MaxLength = 6 txtPassword.PasswordChar = "*" End Sub Private Sub cmdOk_Click() If Len(txtPassword.Text) < txtPassword.MaxLength Then MsgBox "密码位数不够,请输入6位密码" txtPassword.Text = "" End If End Sub 9.3.2 创建显示多行文本的文本框   运行本例用户窗体,在文本框中默认包含多行文本,单击“确定”按钮,将显示文本框中文本的总行数,如图9-20所示。 图9-20 创建显示多行文本的文本框   下面的代码位于用户窗体模块中,用户窗体中有两个控件,一个是名为txtMulti的文本框,另一个是名为cmdOk的命令按钮。用户窗体的Initialize事件过程用于在显示用户窗体时,将命令按钮设置为单击时不接受焦点,并为文本框设置以下4个属性:自动在文本框中填入预置文本,允许在文本框中显示多行文本,在文本框中显示垂直滚动条,允许文本在文本框中换行显示。命令按钮的Click事件过程用于在单击“确定”按钮时,显示文本框中文本的总行数。 Private Sub UserForm_Initialize() cmdOk.TakeFocusOnClick = False With txtMulti .Text = "文本框用于接收用户输入的数据或显示信息。Change事件是文本框的默认事件,修改文本框中的文本时将触发该事件。" .MultiLine = True .ScrollBars = fmScrollBarsVertical .WordWrap = True End With End Sub Private Sub cmdOk_Click() MsgBox "文本的总行数是:" & txtMulti.LineCount End Sub 9.3.3 将文本框中的内容添加到工作表的A列   运行本例用户窗体,每次单击“添加”按钮时,将文本框中的内容添加到A列中的下一个空单元格中,如图9-21所示。 图9-21 将文本框中的内容添加到工作表的A列   下面的代码位于用户窗体模块中,用户窗体中有两个控件,一个是名为txtAdd的文本框,另一个是名为cmdAdd的命令按钮。用户窗体的Initialize事件过程用于在显示用户窗体时,将命令按钮设置为单击时不接受焦点。命令按钮的Click事件过程用于在单击“添加”按钮时,将文本框中的内容添加到工作表的A列中。 Private Sub UserForm_Initialize() cmdAdd.TakeFocusOnClick = False End Sub Private Sub cmdAdd_Click() Dim lngLastRow As Long lngLastRow = Cells(Rows.Count, 1).End(xlUp).Row If IsEmpty(Cells(lngLastRow, 1).Value) Then Cells(lngLastRow, 1).Value = txtAdd.Text Else Cells(lngLastRow + 1, 1).Value = txtAdd.Text End If txtAdd.Text = "" End Sub   代码解析:在命令按钮的Click事件过程中,先从A列底部向上查找最后一个包含数据的单元格,将该单元格的行号赋值给lngLastRow变量。然后使用If Then语句判断由该行号和第1列组成的单元格是否为空,如果是,则将文本框中的内容添加到该单元格中;如果不是,则将行号加1,得到下一行的行号,然后将其与第一列组成的单元格作为输入内容的单元格,并将文本框中的内容添加到该单元格中。 9.3.4 放大显示在文本框中输入的每一个字符   运行本例用户窗体,在文本框中每次输入一个字符,都会在下方放大显示该字符,如图9-22所示。单击“重新输入”按钮,将删除文本框中的所有内容,并将焦点置于文本框中。 图9-22 验证在文本框中输入的字符   用户窗体中有3个控件,它们的属性如表9-5所示。 表9-5 各个控件的属性 控 件 类 型 Name Caption 标签 lblCheck / 文本框 txtCheck / 命令按钮 cmdClear 重新输入      下面的代码位于用户窗体模块中,代码分为以下两个部分: * 文本框的Change事件过程:每次在文本框中输入字符时,将该字符显示在标签中。为了在标签中显示更大的字符,需要为标签的字号设置一个较大的值。 * 命令按钮的Click事件过程:单击“重新输入”按钮时,为文本框的Text属性赋值一个零长度字符串,以删除文本框中的所有内容,并使用文本框的SetFocus方法将焦点置于文本框中。 Private Sub txtCheck_Change() lblCheck.Caption = txtCheck.Text lblCheck.Font.Size = 26 End Sub Private Sub cmdClear_Click() txtCheck.Text = "" txtCheck.SetFocus End Sub 9.4 数值调节钮和滚动条   数值调节钮由上、下两个箭头组成,单击上箭头或下箭头将增加或减少数值。数值调节钮常与文本框搭配使用,通过单击数值调节钮上的箭头,每次调整后的当前值显示在与其关联的文本框中。Change事件是数值调节钮的默认事件,单击数值调节钮的上箭头或下箭头时将触发该事件。数值调节钮的常用属性如表9-6所示。 表9-6 数值调节钮的常用属性 属 性 说 明 Max 设置数值调节钮可以容纳的最大值 Min 设置数值调节钮可以容纳的最小值 Orientation 设置数值调节钮的方向,有水平和垂直两种 SmallChange 每次单击数据调节钮的上箭头或下箭头时,数值递增或递减的量,默认为1 Value 数值调节钮的当前值      滚动条用于快速浏览或定位大范围的内容,其工作机制与数值调节钮类似,它们的很多属性都相同。Change事件是滚动条控件的默认事件,拖动滚动条上的滑块、单击滚动条两端的箭头、单击滑块与两端箭头之间的区域时,都将触发Change事件。滚动条的常用属性如表9-7所示。 表9-7 滚动条的常用属性 属 性 说 明 LargeChange 单击滑块与两端箭头之间的区域时,数值递增或递减的量,默认为1 Max 设置滚动条可以容纳的最大值 Min 设置滚动条可以容纳的最小值 Orientation 设置滚动条的方向,有水平和垂直两种 SmallChange 单击滚动条的上箭头或下箭头时,数值递增或递减的量,默认为1 Value 滚动条的当前值   下面通过几个示例介绍数值调节钮和滚动条的用法。 9.4.1 使用数值调节钮设置密码位数   运行本例用户窗体,单击数值调节钮上的箭头,将在上方的文本框中显示6~8的一个数字,它表示密码的位数。为了避免用户在上方的文本框中随意输入数字,将其设置为禁止编辑状态。单击“确定”按钮,将检查在下方的文本框中输入的密码的位数是否与上方文本框中显示的位数一致,不一致时会显示提示信息,并删除已输入的密码,如图9-23所示。 图9-23 使用数值调节钮设置密码位数   用户窗体中有6个控件,它们的属性如表9-8所示。 表9-8 各个控件的属性 控 件 类 型 Name Caption 标签 lblLength 密码位数: 标签 lblPassword 密码: 文本框 txtLength // 文本框 txtPassword // 数值调节钮 spnLength / 命令按钮 cmdOk 确定      下面的代码位于用户窗体模块中,代码分为3个部分: * 用户窗体的Initialize事件过程:显示用户窗体时,将上方文本框设置为禁止编辑,并设置数值调节钮的最小值、最大值和增量。 * 数值调节钮的Change事件过程:每次单击数值调节钮上的箭头时,在上方的文本框中会同步显示数值调节钮的当前值。 * 命令按钮的Click事件过程:使用If Then语句判断在下方的文本框中输入的密码的字符数是否等于上方文本框中的密码位数,如果不等于,则显示提示信息,并删除已输入的密码。 Private Sub UserForm_Initialize() txtLength.Enabled = False spnLength.Min = 6 spnLength.Max = 8 spnLength.SmallChange = 1 End Sub Private Sub spnLength_Change() txtLength.Text = spnLength.Value End Sub Private Sub cmdOk_Click() If Len(txtPassword.Text) <> txtLength Then MsgBox "密码不足" & txtLength.Text & "位,请重新输入" txtPassword.Text = "" txtPassword.SetFocus End If End Sub 9.4.2 使用数值调节钮指定在工作表中选择的行范围   运行本例用户窗体,单击两个数值调节钮上的箭头,将在两个文本框中显示1~100的某个数字,它们确定要选择的行范围的起始行号和终止行号。单击“选择区域”按钮,将选择从起始行号到终止行号之间的所有行,如图9-24所示。 图9-24 使用数值调节钮控制选择的行范围   用户窗体中有7个控件,它们的属性如表9-9所示。 表9-9 各个控件的属性 控 件 类 型 Name Caption 标签 lblFirstRow 首行行号: 标签 lblLastRow 尾行行号: 文本框 txtFirstRow / 文本框 txtLastRow / 数值调节钮 spnFirstRow / 数值调节钮 spnLastRow / 命令按钮 cmdSelectRows 选择指定的行      下面的代码位于用户窗体模块中,代码分为3个部分。 * 用户窗体的Initialize事件过程:显示用户窗体时,设置两个数值调节钮的最小值、最大值和增量,并设置两个文本框中默认显示的值。 * 两个数值调节钮的Change事件过程:每次单击数值调节钮上的箭头时,与其关联的文本框中的值自动同步更新。 * 命令按钮的Click事件过程:将两个文本框中的值赋值给两个变量,然后使用Cells属性引用这两个行号位于第一列的单元格,再使用Range对象引用由这两个单元格组成的单元格区域,最后使用EntireRow属性引用该单元格区域所在的整行。 Private Sub UserForm_Initialize() With spnFirstRow .Min = 1 .Max = 100 .SmallChange = 1 End With With spnLastRow .Min = 1 .Max = 100 .SmallChange = 1 End With txtFirstRow.Text = spnFirstRow.Min txtLastRow.Text = spnLastRow.Min End Sub Private Sub spnFirstRow_Change() txtFirstRow.Text = spnFirstRow.Value End Sub Private Sub spnLastRow_Change() txtLastRow.Text = spnLastRow.Value End Sub Private Sub cmdSelectRows_Click() Dim intFirstRow As Integer, intLastRow As Integer intFirstRow = txtFirstRow.Text intLastRow = txtLastRow.Text Range(Cells(intFirstRow, 1), Cells(intLastRow, 2)).EntireRow.Select End Sub 9.4.3 使用滚动条放大字符的显示比例   运行本例用户窗体,在文本框中输入一个或多个字符,然后调整滚动条的位置,滚动条右侧的字符将同步放大,如图9-25所示。单击“清空”按钮,将删除文本框中的字符和放大后的字符,滚动条恢复到最初状态。 图9-25 使用滚动条放大字符的显示比例   用户窗体中有4个控件,它们的属性如表9-10所示。    表9-10 各个控件的属性 控 件 类 型 Name Caption 标签 lblZoom / 文本框 txtOrigin / 滚动条 scrZoom / 命令按钮 cmdClear 清空      下面的代码位于用户窗体模块中,代码分为4个部分。 * 用户窗体的Initialize事件过程:显示用户窗体时,设置标签中的文本居中对齐,并设置滚动条的最小值、最大值和两个增量。 * 文本框的Change事件过程:每次在文本框输入字符时,它将同时显示在标签中。 * 滚动条的Change和Scroll两个事件过程:每次改变滚动条的当前值时,将标签中的字符的字号设置为文本框中的字符的字号与滚动条当前值的乘积。Change事件只在改变滑块在滚动条上的位置后才会触发,这意味着拖动滑块的过程中不会触发该事件。如果希望在拖动滑块的过程中可以同步显示放大后的字符,则需要在滚动条的Scroll事件过程中编写相同的代码,移动滑块时将触发Scroll事件。 * 命令按钮的Click事件过程:单击“清空”按钮,将删除文本框和标签中的内容,将焦点置于文本框中,并将滚动条的滑块置于最小值的位置。 Private Sub UserForm_Initialize() lblZoom.TextAlign = fmTextAlignCenter With scrZoom .Min = 1 .Max = 10 .SmallChange = 1 .LargeChange = 2 End With End Sub Private Sub txtOrigin_Change() lblZoom.Caption = txtOrigin.Text End Sub Private Sub scrZoom_Change() lblZoom.Font.Size = txtOrigin.Font.Size * scrZoom.Value End Sub Private Sub scrZoom_Scroll() lblZoom.Font.Size = txtOrigin.Font.Size * scrZoom.Value End Sub Private Sub cmdClear_Click() txtOrigin.Text = "" lblZoom.Caption = "" txtOrigin.SetFocus scrZoom.Value = scrZoom.Min End Sub 9.5 选项按钮和复选框   选项按钮通常成组出现,用于提供多个选项,但是只能从一组选项中选择其中之一。当用户窗体中包含多组选项时,可以使用框架为这些选项分组,各组选项之间互不影响。Click事件是选项按钮的默认事件,选中选项按钮或将选项按钮的Value属性设置为True时,将触发Click事件。选项按钮的常用属性如表9-11所示。 表9-11 选项按钮的常用属性 属 性 说 明 Alignment 设置选项按钮和标题的位置:选项按钮在左且标题在右,或者标题在左且选项按钮在右 AutoSize 设置选项按钮是否自动缩放以完整显示其中的内容,为True将自动缩放,为False将不自动缩放 GroupName 为选项按钮分组,将多个选项按钮的该属性设置为同一个名称,表示它们是同一组选项 Value 返回或设置选项按钮是否已被选中,为True表示已被选中,为False表示未被选中      复选框与选项按钮类似,也用于在一个或多个选项中做出选择,两者的很多属性都相同。与选项按钮不同的是,用户可以同时勾择多个复选框。Click事件是复选框的默认事件,勾选复选框或将复选框的Value属性设置为True时,将触发Click事件。复选框的常用属性如表9-12所示。 表9-12 复选框的常用属性 属 性 说 明 Alignment 设置复选框和标题的位置:复选框在左且标题在右,或者标题在左且复选框在右 AutoSize 设置复选框是否自动缩放以完整显示其中的内容,为True将自动缩放,为False将不自动缩放 Value 返回或设置复选框是否已被勾选,为True表示已被选中,为False表示未被选中      下面通过几个示例介绍选项按钮和复选框的用法。 9.5.1 使用选项按钮实现单项选择功能   运行本例用户窗体,从多个选项中选择一个,然后单击“确定”按钮,将在提示信息中显示选择的Excel版本,如图9-26所示。 图9-26 使用选项按钮实现单项选择功能   用户窗体中有8个控件,它们的属性如表9-13所示。 表9-13 各个控件的属性 控 件 类 型 Name Caption 标签 lblExcelVer 选择Excel版本 命令按钮 cmdOk 确定 选项按钮 opt2007 Excel 2007 选项按钮 opt2010 Excel 2010 选项按钮 opt2013 Excel 2013 选项按钮 opt2016 Excel 2016 选项按钮 opt2019 Excel 2019 选项按钮 opt2021 Excel 2021      下面的代码位于用户窗体模块中,单击“确定”按钮时,将执行该按钮的Click事件过程中的代码。使用Select Case语句检查哪个选项按钮的Value属性返回True,表示该选项按钮被选中。使用一个变量存储与每个选项按钮对应的表示Excel版本的字符串。最后使用Msgbox函数显示Value属性为True的选项按钮所对应的Excel版本。 Private Sub cmdOk_Click() Dim strVersion As String Select Case True Case opt2007.Value strVersion = "Excel 2007" Case opt2010.Value strVersion = "Excel 2010" Case opt2013.Value strVersion = "Excel 2013" Case opt2016.Value strVersion = "Excel 2016" Case opt2019.Value strVersion = "Excel 2019" Case opt2021.Value strVersion = "Excel 2021" End Select MsgBox "选择的Excel版本是:" & strVersion End Sub 9.5.2 使用多组选项按钮实现多项选择功能   运行本例用户窗体,分别选择一个Windows版本和一个Excel版本,然后单击“确定”按钮,将在提示信息中显示选择的Windows版本和Excel版本,如图9-27所示。 图9-27 使用多组选项按钮实现多项选择功能   用户窗体中有9个控件,它们的属性如表9-14所示。 表9-14 各个控件的属性 控 件 类 型 Name Caption 框架 fraWindowsVer 选择Windows版本 选项按钮 optWin7 Windows 7 选项按钮 optWin10 Windows 10 选项按钮 optWin11 Windows 11 框架 fraExcelVer 选择Excel版本 选项按钮 opt2016 Excel 2016 选项按钮 opt2019 Excel 2019 选项按钮 opt2021 Excel 2021 命令按钮 cmdOk 确定      下面的代码位于用户窗体模块中,单击“确定”按钮时,将执行该按钮的Click事件过程中的代码。由于使用两个框架将6个选项按钮分成两组,所以需要使用两个Select Case语句分别检查每一组中的哪个选项按钮的Value属性返回True,表示该选项按钮在该组中被选中。使用两个变量分别存储与选项按钮对应的Windows版本和Excel版本。使用选项按钮的Caption属性可以返回Windows版本和Excel版本的字符串。最后使用Msgbox函数显示选择的Windows版本和Excel版本。 Private Sub cmdOk_Click() Dim strWindowsVer As String, strExcelVer As String Dim strMsg As String Select Case True Case optWin7.Value strWindowsVer = optWin7.Caption Case optWin10.Value strWindowsVer = optWin10.Caption Case optWin11.Value strWindowsVer = optWin11.Caption End Select Select Case True Case opt2016.Value strExcelVer = opt2016.Caption Case opt2019.Value strExcelVer = opt2019.Caption Case opt2021.Value strExcelVer = opt2021.Caption End Select strMsg = "选择的Windows版本是:" & strWindowsVer & vbCrLf strMsg = strMsg & "选择的Excel版本是:" & strExcelVer MsgBox strMsg End Sub 9.5.3 使用复选框选择多项   运行本例用户窗体,在文本框中输入任意内容,然后勾选左侧的一个或多个复选框,将为文本框中的内容设置一种或多种字体格式,如图9-28所示。 图9-28 使用复选框选择多项   用户窗体中有6个控件,它们的属性如表9-15所示。 表9-15 各个控件的属性 控 件 类 型 Name Caption 标签 lblFont 选择字体格式 文本框 txtName / 命令按钮 cmdClearFormat 清除格式 复选框 chkBold 加粗 复选框 chkItalic 倾斜 复选框 chkUnderLine 下画线      下面的代码位于用户窗体模块中,代码分为两个部分。 * 3个复选框的Click事件过程:由于每个复选框的Value属性可以返回True或False,而字体格式中的加粗、倾斜和下画线等格式也需要使用True或False来设置,所以可以使用每个复选框的Value属性的返回值来设置这3种字体格式。 * 命令按钮的Click事件过程:单击“清除格式”按钮,将清除3个复选框的勾选标记,并清除为文本框中的内容设置的3种字体格式。 Private Sub chkBold_Click() txtName.Font.Bold = chkBold.Value End Sub Private Sub chkItalic_Click() txtName.Font.Italic = chkItalic.Value End Sub Private Sub chkUnderLine_Click() txtName.Font.Underline = chkUnderLine.Value End Sub Private Sub cmdClearFormat_Click() chkBold.Value = False chkItalic.Value = False chkUnderLine.Value = False txtName.Font.Bold = False txtName.Font.Italic = False txtName.Font.Underline = False End Sub 9.6 列表框和组合框   列表框用于在一个列表中显示多个选项,可以从中选择一个或多个。Click事件是列表框的默认事件,在列表框中选择某个选项时将触发该事件。组合框与列表框类似,它们拥有很多相同的属性和方法。组合框相当于将文本框和列表框组合在一起,不但可以在组合框的列表中选择选项,还可以在列表顶部的文本框中输入数据。Change事件是组合框的默认事件,当组合框顶部的文本框中的内容发生改变时将触发该事件。   列表框的常用属性如表9-16所示,组合框的常用属性如表9-17所示,列表框和组合框的常用方法如表9-18所示。 表9-16 列表框的常用属性 属 性 说 明 ColumnCount 设置选项在列表框中显示的列数 List 返回或设置列表框包含的选项,列表框中的第一个选项的索引号是0,第二个选项的索引号是1,以此类推,最后一个选项的索引号是选项总数减1 ListCount 返回列表框中的选项总数 ListIndex 返回列表框中当前选择的选项的索引号 ListStyle 设置列表框中的选项的样式,选项可以显示为选项按钮或复选框 MultiSelect 设置是否可以选择多个选项,可以通过鼠标反复单击来选择或取消选择选项,也可以使用Ctrl或Shift键并配合鼠标单击来选择指定范围内的选项 RowSource 将单元格区域中的数据添加到列表框中 Selected 在允许多项选择的情况下,该属性用于返回或设置指定选项的选中状态,如果为True则表示已选中,如果为False则表示未选中 Text 返回在列表框中选择的选项 TopIndex 返回或设置位于列表框中可见范围内的第一项的索引号,列表框中没有选项或未被显示时该属性返回-1 表9-17 组合框的常用属性 属 性 说 明 ColumnCount 设置选项在组合框中显示的列数 DropButtonStyle 组合框右侧的下拉按钮上显示的图标,默认显示下箭头 List 返回或设置组合框包含的选项,组合框中的第一个选项的索引号是0,第二个选项的索引号是1,以此类推,最后一个选项的索引号是选项总数减1 ListCount 返回组合框中的选项总数 ListRows 设置列表中显示的最大行数 ListIndex 返回组合框中当前选择的选项的索引号 续表 属 性 说 明 ListStyle 设置组合框中的选项的样式,选项可以显示为选项按钮或复选框 MatchEntry 设置组合框按照用户输入的内容进行搜索的方式 MatchFound 是否将在文本框中输入的文本与列表中的选项匹配 MaxLength 设置可输入的最大字符数,为0表示不受限制 RowSource 将单元格区域中的数据添加到组合框中 ShowDropButtonWhen 设置何时显示组合框右侧的下拉按钮 Style 设置组合框的样式 Text 返回在组合框中选择的选项 TextLength 返回以字符数表示的组合框的文本框中的文本长度 TopIndex 返回或设置位于组合框中可见范围内的第一项的索引号,组合框中没有选项或未被显示时该属性返回-1 表9-18 列表框和组合框的常用方法 方 法 说 明 AddItem 将新的选项添加到列表框或组合框中 Clear 删除列表框或组合框中的所有选项 RemoveItem 删除列表框或组合框中的特定选项      下面通过几个示例介绍列表框和组合框的用法。由于很多示例中的操作和代码对于列表框和组合框都是相同的,所以在这些示例中主要以列表框为例进行讲解。 9.6.1 将工作表中的单列数据添加到列表框或组合框中   如需将单元格区域中的数据添加到列表框或组合框中,可以使用列表框或组合框的RowSource属性或List属性。   1. 使用RowSource属性   运行本例用户窗体,单击“添加”按钮,将活动工作表中的A1:A8单元格区域中的数据添加到列表框中,如图9-29所示。单击“清空”按钮,将删除列表框中的所有内容。 图9-29 使用RowSource属性添加单列数据   用户窗体中有3个控件,它们的属性如表9-19所示。 表9-19 各个控件的属性 控 件 类 型 Name Caption 列表框 lstData / 命令按钮 cmdAdd 添加 命令按钮 cmdClear 清空      下面的代码位于用户窗体模块中,如需删除使用RowSource属性向列表框或组合框中添加的数据,需要将RowSource属性设置为零长度字符串,而不能使用Clear方法。 Private Sub cmdAdd_Click() lstData.RowSource = "A1:A8" End Sub Private Sub cmdClear_Click() lstData.RowSource = "" End Sub   如需添加非活动工作表中的数据,需要在单元格区域添加工作表的名称和一个感叹号。下面的代码将名为“2023”工作表中的A1:A8单元格区域的数据添加到列表框中。 lstData.RowSource = "2023!A1:A8"   在组合框中添加数据的效果如图9-30所示。 图9-30 在组合框中添加数据   2. 使用List属性   在列表框或组合框中添加数据还可以使用List属性,添加单列数据时,该属性接受一个水平数组。如果数据位于一列单元格区域中,则需要先将其转换为一行,然后再赋值给List属性。下面的代码是使用List属性将活动工作表中的A1:A8单元格区域中的数据添加到列表框中。 lstData.List = WorksheetFunction.Transpose(Range("A1:A8"))   如需添加非活动工作表中的数据,需要为Range对象添加对特定工作表的引用。下面的代码将名为“2023”工作表中的A1:A8单元格区域的数据添加到列表框中。 lstData.List = WorksheetFunction.Transpose(Worksheets("2023").Range("A1:A8"))   使用Clear方法可以删除使用List属性添加到列表框或组合框中的数据。 9.6.2 将工作表中的多列数据添加到列表框或组合框中   与添加单列数据类似,可以使用列表框或组合框的RowSource属性或List属性,将工作表中的多列数据添加到列表框或组合框中。   1. 使用RowSource属性   运行本例用户窗体,单击“添加”按钮,将活动工作表中的A1:B8单元格区域的数据添加到列表框中。为了让数据在列表框中也显示为两列,需要将列表框的ColumnCount属性设置为2,如图9-31所示。 图9-31 使用RowSource属性添加多列数据   下面的代码位于用户窗体模块中,用户窗体中的控件与9.6.1小节相同。 Private Sub cmdAdd_Click() lstData.ColumnCount = 2 lstData.RowSource = "A1:B8" End Sub   如果希望为ColumnCount属性设置的列数由程序自动检测,则可以将数据所在的单元格区域的地址赋值给一个变量,然后将该变量作为Range属性的参数,从而返回一个Range对象,再使用Range对象的属性返回单元格区域的列数。修改后的代码如下,虽然比上面的代码多了两行,但是不用将列数固定写入到程序中,提高了程序的灵活性。 Private Sub cmdAdd_Click() Dim strDataAddress As String strDataAddress = "A1:B8" lstData.ColumnCount = Range(strDataAddress).Columns.Count lstData.RowSource = strDataAddress End Sub   2. 使用List属性   使用List属性也可以将多列数据添加到列表框中,并在列表框中显示为多列。此时需要将Range对象的Value属性赋值给列表框的List属性。 Private Sub cmdAdd_Click() lstData.ColumnCount = 2 lstData.List = Range("A1:B8").Value End Sub 9.6.3 将未存储在工作表中的数据添加到列表框或组合框中   如果数据没有存储在工作表中,则需要使用AddItem方法将这些数据添加到列表框或组合框中。每次使用AddItem方法只能添加一项数据,如需添加多项数据,需要多次使用该方法。   下面的代码位于用户窗体模块中,运行该用户窗体,单击“添加”按钮,将5个名称添加到列表框中,如图9-32所示。 Private Sub cmdAdd_Click() lstData.AddItem "牛奶" lstData.AddItem "酸奶" lstData.AddItem "早餐奶" lstData.AddItem "核桃奶" lstData.AddItem "果汁" End Sub 图9-32 使用AddItem方法添加无规律的多项数据   如需删除使用AddItem方法添加的所有数据。可以使用Clear方法。本例中的“清空”按钮的Click事件过程的代码如下: Private Sub cmdClear_Click() lstData.Clear End Sub   如果数据有一定的规律,则可以在For Next语句中使用AddItem方法快速添加数据。下面的代码将数字1~10添加到列表框中,如图9-33所示。 Private Sub cmdAdd_Click() Dim intNumber As Integer For intNumber = 1 To 10 lstData.AddItem intNumber Next intNumber End Sub 图9-33 使用AddItem方法添加有规律的多项数据   注意:如果已经使用RowSource属性为列表框或组合框添加了数据,则使用AddItem方法将导致运行时错误,此时需要先将RowSource属性的值设置零长度字符串,然后再使用AddItem方法添加数据。   由于列表框或组合框的List属性接受一个数组,所以可以使用Array函数在列表框或组合框中添加多项数据。下面的代码是使用Array函数在列表框中添加本小节第一个示例中的5个名称。 lstData.List = Array("牛奶", "酸奶", "早餐奶", "核桃奶", "果汁")   如需将未存储在工作表中的多列数据添加到列表框中,需要将多列数据存储在一个二维数组中,然后将该数组赋值给列表框的List属性。下面的代码先使用Array函数创建两组数据,一组是名称,一组是与名称关联的价格。然后将两组数据存储到名为varData的动态数组中,再将该数组赋值给名为lstData的列表框的List属性。在用户窗体中单击“添加”按钮,将两组数据添加到列表框中,并在其中显示为两列,如图9-34所示。 Private Sub cmdAdd_Click() Dim varData() As Variant Dim varNames As Variant, varPrice As Variant Dim intNameIndex As Integer, intPriceIndex As Integer varNames = Array("牛奶", "酸奶", "早餐奶", "核桃奶", "果汁") varPrice = Array(2.5, 3, 2, 3.5, 5) ReDim varData(0 To UBound(varNames), 0 To 1) For intNameIndex = 0 To UBound(varNames) varData(intNameIndex, 0) = varNames(intNameIndex) varData(intNameIndex, 1) = varPrice(intNameIndex) Next intNameIndex lstData.ColumnCount = 2 lstData.List = varData End Sub 图9-34 将两列数据添加到列表框中 9.6.4 在列表框中选择多个选项   在列表框中默认只能选择一项,如需同时选择多项,需要将列表框的MultiSelect属性设置为fmMultiSelectMulti或fmMultiSelectExtended,它们的含义如下。 * fmMultiSelectMulti:使用鼠标多次单击可以选择多个选项。单击已选中的选项,将取消其选中状态。 * fmMultiSelectExtended:与在Windows资源管理器中选择多个文件夹的方法类似,使用Ctrl键或Shift键并配合鼠标单击,可以选择多个选项。如果在按住Ctrl键时单击已选中的选项,则将取消其选中状态。   如图9-35所示是在列表框中选择多个选项的效果。 图9-35 在列表框中选择多个选项   如需恢复单项选择功能,可以将MultiSelect属性设置为fmMultiSelectSingle。 9.6.5 修改在列表框中选中的选项   运行本例用户窗体,单击“添加”按钮,将活动工作表中的A1:A8单元格区域的数据添加到列表框中。在列表框中选择一项,然后在文本框中修改该项。单击“修改”按钮,使用文本框中的修改结果替换列表框中的对应项,并仍然保持该项的选中状态,如图9-36所示。 图9-36 修改在列表框中选中的选项   用户窗体中有4个控件,它们的属性如表9-20所示。 表9-20 各个控件的属性 控 件 类 型 Name Caption 列表框 lstData / 文本框 txtEdit / 命令按钮 cmdAdd 添加 命令按钮 cmdEdit 修改      下面的代码位于用户窗体模块中,代码分为3个部分。 * “添加”按钮的Click事件过程:单击“添加”按钮,将活动工作表中的A1:A8单元格区域的数据添加到列表框中。 * 列表框的Click事件过程:在列表框中当前选中的选项将显示在文本框中。 * “修改”按钮的Click事件过程:首先判断列表框的TopIndex属性是否返回-1,如果不是,说明列表框包含数据。然后判断ListIndex属性是否返回-1,如果是,则说明当前未在列表框中选择任何信息,此时会显示提示信息;如果不是,则说明已经选择一项,此时使用一个变量保存列表框中当前选中的选项的索引号。然后使用AddItem方法将文本框中修改后的内容添加到该索引号所在的位置。添加新数据后,原来的数据会向下移动一个位置,其索引号会比原来多1,然后使用RemoveItem方法将其删除,最后将保存索引号的变量赋值给列表框的ListIndex属性,以便选中新添加的数据(即对原数据修改后的数据),它在列表框中的位置还是原来的位置。 Private Sub cmdAdd_Click() lstData.List = Application.Transpose(Range("A1:A8")) End Sub Private Sub lstData_Click() txtEdit.Text = lstData.Text End Sub Private Sub cmdEdit_Click() Dim intListIndex As Integer If lstData.TopIndex <> -1 Then If lstData.ListIndex = -1 Then MsgBox "需要先选择一项" Else intListIndex = lstData.ListIndex lstData.AddItem txtEdit.Text, intListIndex lstData.RemoveItem intListIndex + 1 lstData.ListIndex = intListIndex End If End If End Sub 9.6.6 在列表框中移动选项的位置   运行本例用户窗体,单击“添加”按钮,将活动工作表中的A1:A8单元格区域的数据添加到列表框中。在列表框中选择一项,然后单击“上移”按钮或“下移”按钮,可以在列表框中向上或向下移动选中的选项,如图9-37所示。 图9-37 在列表框中移动选项的位置   如果列表框中没有数据或未选择任何选项,则单击“上移”按钮和“下移”按钮时都不进行移动。如果选中的是第一个选项,则单击“上移”按钮时不进行移动。如果选中的是最后一个选项,则单击“下移”按钮时不进行移动。   用户窗体中有4个控件,它们的属性如表9-21所示。 表9-21 各个控件的属性 控 件 类 型 Name Caption 列表框 lstData / 命令按钮 cmdAdd 添加 命令按钮 cmdMoveUp 上移 命令按钮 cmdMoveDown 下移      下面的代码位于用户窗体模块中,代码分为3个部分。 * “添加”按钮的Click事件过程:将活动工作表中的A1:A8单元格区域的数据添加到列表框中,然后将列表框中的数据存储到一个动态数组中。 * “上移”按钮的Click事件过程:首先判断选中的选项的索引号是否大于0,如果是,则说明列表框包含数据且选择了一项。此时使用一个变量保存该项的索引号,然后使用一个变量临时保存选中的选项,接着将该选项的上一个选项保存到与选中的选项索引号对应的数组元素中,再将变量中临时保存的选项赋值给数组中的上一个元素,相当于对调了两项数据的位置。最后将对调数据位置后的整个数组赋值给列表框的List属性,以便将该数组中的所有数据重新添加到列表框中,并选中向上移动一个位置后的数据,即最初要移动的数据。 * “下移”按钮的Click事件过程:与“上移”按钮的代码类似,主要区别在于初始判断条件不同,以及交换数据时需要将索引号加1而不是减1。“下移”按钮可以正常工作的前提条件是,列表框包含数据,且当前选择的选项不是最后一项。只要ListIndex属性的返回值大于或等于0,就说明列表框包含数据,因为列表框没有数据时该属性返回-1。列表框中最后一项的索引号可以使用Ubound函数获取List属性返回的数组的上限而得到。 Dim varList() As Variant, varTemp As Variant Dim intIndex As Integer, intSelectedIndex As Integer Private Sub cmdAdd_Click() lstData.List = Application.Transpose(Range("A1:A8")) ReDim varList(0 To UBound(lstData.List)) For intIndex = 0 To UBound(varList) varList(intIndex) = lstData.List(intIndex) Next intIndex End Sub Private Sub cmdMoveUp_Click() If lstData.ListIndex > 0 Then intSelectedIndex = lstData.ListIndex varTemp = lstData.List(intSelectedIndex) varList(intSelectedIndex) = varList(intSelectedIndex - 1) varList(intSelectedIndex - 1) = varTemp lstData.List = varList lstData.ListIndex = intSelectedIndex - 1 End If End Sub Private Sub cmdMoveDown_Click() If lstData.ListIndex >= 0 And lstData.ListIndex < UBound(lstData.List) Then intSelectedIndex = lstData.ListIndex varTemp = lstData.List(intSelectedIndex) varList(intSelectedIndex) = varList(intSelectedIndex + 1) varList(intSelectedIndex + 1) = varTemp lstData.List = varList lstData.ListIndex = intSelectedIndex + 1 End If End Sub 9.6.7 将列表框中的一项或多项数据添加到工作表中   运行本例用户窗体,自动在列表框中添加两列数据,其代码与9.6.3小节中的最后一个示例相同,只是此处将代码写入用户窗体的Initialize事件过程中。单击用户窗体中的“保存部分”按钮,将列表框中选中的所有数据添加到以用户指定的单元格作为起点的单元格区域中,如图9-38所示。 图9-38 将列表框中选中的所有数据添加到单元格区域中   单击用户窗体中的“保存全部”按钮,将列表框中的所有数据添加到以用户指定的单元格作为起点的单元格区域中,如图9-39所示。 图9-39 将列表框中的所有数据添加到单元格区域中   用户窗体中有3个控件,它们的属性如表9-22所示。 表9-22 各个控件的属性 控 件 类 型 Name Caption 列表框 lstData / 命令按钮 cmdSavePart 保存部分 命令按钮 cmdSaveAll 保存全部      下面的代码位于用户窗体模块中,代码分为两个部分。 * “保存部分”按钮的Click事件过程:由于起初无法确定在列表框中一共选中了多少项,所以需要创建一个动态数组,并声明一个变量用于保存当前找到的选项总数。每次找到一个选中的选项时,该变量的值加1,并将其用作重新定义动态数组时的上限。由于本例要在动态数组中存储二维数据,但是在定义动态数组时使用Preserve关键字,只能更改数组最后一维的上限。所以需要将始终发生变化的上限作为数组的第二维,相当于在动态数组的第一维存储列表框中的列数据,在第二维存储行数据。为了将正确的数据写入单元格区域的行和列中,需要在最后使用WorksheetFunction对象的Transpose方法对调动态数组中行列数据位置。 * “保存全部”按钮的Click事件过程:由于处理的是列表框中的所有数据,所以可以直接将列表框的List属性返回的包含所有数据的二维数组赋值给指定的单元格区域,该单元格区域的行数和列数由该二维数组的第一维和第二维的上限,并加上1后的值决定。将上限加1是因为列表框中的第一行和第一列的索引号都从0开始。 Private Sub cmdSavePart_Click() Dim strCellAddress As String, rngStart As Range Dim varList() As Variant, intCount As Integer Dim intIndex As Integer strCellAddress = InputBox("输入左上角单元格地址:") On Error Resume Next Set rngStart = Range(strCellAddress) If rngStart Is Nothing Then MsgBox "输入的单元格地址无效" Exit Sub End If For intIndex = 0 To UBound(lstData.List, 1) If lstData.Selected(intIndex) Then intCount = intCount + 1 ReDim Preserve varList(1 To 2, 1 To intCount) varList(1, intCount) = lstData.List(intIndex, 0) varList(2, intCount) = lstData.List(intIndex, 1) End If Next intIndex rngStart.Resize(UBound(varList, 1), UBound(varList, 2)).Value = WorksheetFunction. Transpose(varList) End Sub Private Sub cmdSaveAll_Click() Dim strCellAddress As String, rngStart As Range strCellAddress = InputBox("输入左上角单元格地址:") On Error Resume Next Set rngStart = Range(strCellAddress) If rngStart Is Nothing Then MsgBox "输入的单元格地址无效" Exit Sub End If rngStart.Resize(UBound(lstData.List, 1) + 1, UBound(lstData.List, 2) + 1).Value = lstData.List End Sub 9.7 图像   图像用于显示图片,该控件支持以下几种图片文件格式:.bmp、.jpg、.wmf、.gif、.ico和.cur。使用图像可以裁剪或缩放图片,但是不能编辑图片。Click事件是图像的默认事件,单击图像时将触发该事件。图像的常用属性如表9-23所示。 表9-23 图像的常用属性 属 性 说 明 Picture 为图像设置要显示的图片,在程序运行期间需要使用LoadPicture函数进行设置 PictureAlignment 设置图片在图像中的位置,包括左上角、右上角、居中、左下角、右下角 PictureSizeMode 设置图片在图像中的填充方式,可以等比例放大图片以填满图像,但是图像的边界可能会出现空白,或在图片可能变形的情况下填满图像,当图片较大时,可以裁剪掉超出图像的部分      下面通过几个示例介绍图像的用法。 9.7.1 显示指定的图片   运行本例用户窗体,单击“显示图片”按钮,将显示预先指定的图片,如图9-40所示。单击“隐藏图片”按钮,将图片隐藏起来。 图9-40 显示预先指定的图片   用户窗体中有3个控件,它们的属性如表9-24所示。 表9-24 各个控件的属性 控 件 类 型 Name Caption 图像 imgPicture / 命令按钮 cmdDisplay 显示图片 命令按钮 cmdHide 隐藏图片      下面的代码位于用户窗体模块中,代码分为两个部分: * “显示图片”按钮的Click事件过程:将要显示的图片的完整路径保存到一个变量中,然后使用VBA内置的Dir函数判断该路径是否有效,如果有效,则使用LoadPicture函数加载该图片,并将其赋值给图像的Picture属性。最后将PictureSizeMode属性设置为fmPictureSizeModeZoom,将图片等比例填充控件,确保图片不会变形。 * “隐藏图片”按钮的Click事件过程:使用零长度字符串作为LoadPicture函数的第一个参数,并将该函数的返回值赋值给Picture属性,将删除控件中的图片。 Private Sub cmdDisplay_Click() Dim strPicName As String strPicName = ThisWorkbook.Path & "\橙子.jpg" If Dir(strPicName) <> "" Then imgPicture.Picture = LoadPicture(strPicName) imgPicture.PictureSizeMode = fmPictureSizeModeZoom End If End Sub Private Sub cmdHide_Click() imgPicture.Picture = LoadPicture("") End Sub   为了将图片填满整个控件,需要将PictureSizeMode属性设置为fmPictureSizeModeStretch,此时图片会变形,如图9-41所示。解决该问题的一种方法是,在设计时增大控件的宽度或高度。 图9-41 填满控件时图片出现变形 9.7.2 由用户选择要显示的图片   在实际应用,通常由用户自己选择要显示的图片,而不是每次都显示固定的图片。运行本例用户窗体,单击“显示图片”按钮,在对话框中双击一张图片,如图9-42所示,将在用户窗体中显示该图片,如图9-43所示。 图9-42 选择要显示的图片 图9-43 显示所选图片   用户窗体中有两个控件,它们的属性如表9-25所示。 表9-25 各个控件的属性 控 件 类 型 Name Caption 图像 imgPicture / 命令按钮 cmdSelect 选择图片      下面的代码位于用户窗体模块中,声明一个FileDialog类型的对象变量,将代表“打开文件”对话框的FileDialog对象赋值给该变量。然后设置在对话框中显示的3种图片文件类型。使用Show方法显示对话框,由于只能选择一张图片,所以选中的图片的索引号是1,使用FileDialog对象的SelectedItems属性返回选中的图片的完整路径,然后将其作为LoadPicture函数的参数,即可将选中的图片显示在图像控件中,最后将图片填满整个控件。 Private Sub cmdSelect_Click() Dim fdl As FileDialog Set fdl = Application.FileDialog(msoFileDialogOpen) With fdl.Filters .Clear .Add "图片文件", "*.bmp;*.jpg;*.gif" End With If fdl.Show Then imgPicture.Picture = LoadPicture(fdl.SelectedItems(1)) imgPicture.PictureSizeMode = fmPictureSizeModeStretch End If End Sub       9.7.3 随机显示图片   运行本例用户窗体,每次单击“随机显示图片”按钮,都会在用户窗体中随机显示一张.jpg格式的图片,并在图片的右侧显示其名称,如图9-44所示。 图9-44 随机显示图片   用户窗体中有3个控件,它们的属性如表9-26所示。 表9-26 各个控件的属性 控 件 类 型 Name Caption 图像 imgPicture / 标签 lblPicName / 命令按钮 cmdRandom 随机显示图片      下面的代码位于用户窗体模块中,使用两个变量分别存储图片的路径和名称,图片的名称通过VBA内置的Dir函数获取。首先使用该函数在指定的路径中查找是否存在.jpg格式的图片文件,如果没有该类型的图片,则Dir函数返回一个零长度字符串。如果找到图片,则进入Do Loop循环,将记录图片文件数量的变量加1,并使用该数量重新定义动态数组的上限,然后将Dir函数返回的文件名赋值给动态数组中的第一个元素。接着使用不带参数的Dir函数查找下一个.jpg格式的图片文件,如果找到图片,则继续重复上述操作,将找到的下一个图片文件赋值给动态数组中的下一个元素;如果找不到图片,则退出Do Loop循环。   此时在动态数组中保存着找到的所有图片,为了可以从中随机抽取一个图片,需要使用VBA内置的Rnd函数获取一个随机整数,该整数的范围是1到图片文件的总数。将得到的随机整数作为动态数组元素的索引号,从而随机得到一个动态数组元素。最后,使用LoadPicture函数将随机抽取到的图片文件显示在图像控件中,并将该图片文件的名称显示在标签控件中。 Private Sub cmdRandom_Click() Dim varFiles() As Variant, intFileIndex As Integer Dim strFilePath As String, strFileName As String Dim intRandom As Integer strFilePath = ThisWorkbook.Path & "\" strFileName = Dir(strFilePath & "*.jpg") Do While strFileName <> "" intFileIndex = intFileIndex + 1 ReDim Preserve varFiles(1 To intFileIndex) varFiles(intFileIndex) = strFileName strFileName = Dir Loop intRandom = Int(intFileIndex * Rnd + 1) imgPicture.Picture = LoadPicture(strFilePath & varFiles(intRandom)) imgPicture.PictureSizeMode = fmPictureSizeModeStretch lblPicName.Caption = "名称:" & varFiles(intRandom) End Sub 9.8 控件综合应用——创建用户登录窗口   本例将创建一个带有注册功能的用户登录窗口,如图9-45所示,其中有6个控件,它们的属性如表9-27所示。 图9-45 用户登录窗口 表9-27 各个控件的属性 控 件 类 型 Name Caption 标签 lblUserName 用户名: 标签 lblPassword 密码: 组合框 cboUserName / 文本框 txtPassword / 复选框 chkNewUser 注册新用户 命令按钮 cmdOk 登录/注册      显示用户登录窗口后,可以从顶部的下拉列表中选择现有的某个用户,然后在下方的文本框中输入密码。如果密码正确,将显示欢迎信息并关闭登录窗口,否则立即关闭该工作簿,并禁止用户使用它,如图9-46所示。 图9-46 使用现有用户进行登录   如果勾选“注册新用户”复选框,则“登录”按钮将变为“注册”按钮,如图9-47所示。此时可以在顶部的文本框中输入新用户的名称,然后在下方的文本框中输入密码,单击“注册”按钮,将添加该用户。未勾选“注册新用户”复选框时,不能在顶部的文本框中输入除了现有用户之外的任何字符。添加新用户后,在顶部的下拉列表中会立即显示新增的用户名,如图9-48所示。 图9-47 添加新用户 图9-48 新增用户显示在下拉列表中   在本例的工作簿中有一个名为“U&P”的工作表,如图9-49所示,在登录窗口中使用的用户名和密码保存在该工作表的A列和B列中,注册新用户时新增的用户名和密码也会添加到这两列中。完成程序的编写和调试后,可以在VBE窗口中将该工作表设置为xlSheetVeryHidden,防止其他用户随意显示和修改该工作表。 图9-49 在一个工作表中存储用户名和密码   下面的代码位于用户窗体模块中,代码分为3个部分。 * 第一个部分包括前3行代码,在模块的顶部声明了3个模块级变量,该模块中的所有过程可以共享这几个变量的值。 * 第二个部分包括前3个Sub过程,用于实现3个功能,以便在后面的事件过程中进行调用,避免编写重复的代码,也可使代码的结构更清晰。 * 第三个部分包括后3个事件过程,第一个事件过程是在显示用户窗体时,调用前面几个Sub过程对用户窗体中的选项进行初始化设置;第二个事件过程是在勾选或取消勾选复选框时,更改组合框的样式和按钮的标题;第三个事件过程是在单击“登录”或“注册”按钮时,验证用户名和密码是否正确或者添加新用户。 Dim wks As Worksheet Dim blnNewUser As Boolean Dim lngDataLastRow As Long '在组合框中加载现有用户名 Sub LoadUserName() Set wks = Worksheets("U&P") lngDataLastRow = wks.Cells(wks.Cells.Rows.Count, 1).End(xlUp).Row If WorksheetFunction.CountA(wks.Columns(1)) <> 0 Then cboUserName.List = wks.Range(wks.Cells(1, 1), wks.Cells(lngDataLastRow, 1)).Value End If End Sub '根据复选框的勾选状态设置组合框的样式 Sub ChangeComboStyle() Select Case blnNewUser Case True: cboUserName.Style = fmStyleDropDownCombo Case False: cboUserName.Style = fmStyleDropDownList End Select End Sub '根据复选框的勾选状态切换按钮标题 Sub ChangeButtonCaption() Select Case blnNewUser Case True: cmdOk.Caption = "注册" Case False: cmdOk.Caption = "登录" End Select End Sub '显示窗体时初始化设置 Private Sub UserForm_Initialize() frmLogin.Caption = "用户登录" blnNewUser = chkNewUser.Value LoadUserName ChangeComboStyle ChangeButtonCaption End Sub '勾选或取消勾选复选框时更改组合框样式和按钮标题 Private Sub chkNewUser_Click() blnNewUser = chkNewUser.Value ChangeComboStyle ChangeButtonCaption End Sub '单击登录或注册按钮时验证密码是否正确或创建新用户 Private Sub cmdOk_Click() Dim rng As Range Select Case cmdOk.Caption Case "登录" On Error Resume Next Set rng = wks.Cells.Find(cboUserName.Value, wks.Cells(1, 1), xlValues, xlWhole, xlByColumns) If txtPassword.Text = rng.Offset(0, 1).Value Then MsgBox cboUserName.Text & ",欢迎您登录本系统" Unload frmLogin Else MsgBox "密码错误,拒绝登录" ThisWorkbook.Close False End If Case "注册" If cboUserName.ListIndex = -1 Then wks.Cells(lngDataLastRow + 1, 1).Value = cboUserName.Text wks.Cells(lngDataLastRow + 1, 1).Offset(0, 1).Value = txtPassword.Text End If lngDataLastRow = lngDataLastRow + 1 LoadUserName End Select End Sub       第10章 处理文件和文件夹   使用Excel对象模型和Office对象模型中的一些对象,可以编程控制在Excel中打开和保存工作簿。如需对计算机中的任意文件和文件夹执行重命名、移动、复制、删除等操作,则需要使用VBA内置的函数和语句,或者使用FSO对象模型。本章将介绍使用VBA内置的函数和语句,以及使用FSO对象模型操作文件和文件夹的方法,最后介绍在文本文件中读取和写入数据的方法。 10.1 使用VBA内置的函数和语句操作文件和文件夹   VBA自身提供了一些用于操作文件和文件夹的函数和语句,使用它们可以创建和删除文件夹、移动和复制文件、修改文件或文件夹的名称、删除文件等,本节将介绍使用这些函数和语句操作文件和文件夹的方法。还有一些VBA内置的函数和语句专门用于处理文本文件中的数据,这部分功能将在10.3节中详细介绍。 10.1.1 处理文件和文件夹的VBA内置函数和语句   处理文件和文件夹的VBA内置函数和语句如表10-1所示,可以在VBA中直接使用这些函数和语句,无须进行额外设置,而且它们通用于所有Excel版本。 表10-1 处理文件和文件夹的VBA内置函数和语句 函数和语句 说 明 ChDir语句 改变当前文件夹 ChDrive语句 改变当前驱动器 CurDir函数 返回当前文件夹的路径 Dir函数 返回与指定格式或文件属性相匹配的文件名或文件夹 EOF函数 判断是否到达文件的结尾 FileAttr函数 返回使用Open语句打开文件的方式 FileCopy语句 复制文件 FileDateTime函数 返回最后一次修改文件的日期和时间 FileLen函数 返回文件的大小,以字节为单位 FreeFile函数 返回下一个可供Open语句使用的文件号 GetAttr函数 返回文件的属性 Input语句 从打开的顺序文件中读出数据并将其指定给变量 Input函数 返回从打开的文件中读取的指定数量的字符 续表 函数和语句 说 明 Line Input语句 从打开的顺序文件中读取一行数据并将其指定给变量 LOF函数 返回使用Open语句打开文件的大小,以字节为单位 Kill语句 删除文件 MkDir语句 创建一个新的文件夹 Name语句 重命名文件或文件夹 Open语句 打开文件 Print语句 将格式化显示的数据写入顺序文件 RmDir语句 删除空文件夹 SetAttr语句 设置文件的属性 Tab函数 与Print语句一起使用,用于指定写入数据的位置 10.1.2 使用Dir函数判断文件和文件夹是否存在   如果指定的文件或文件夹不存在,则在VBA中处理它们时将出现运行时错误。为了避免错误,需要先使用Dir函数判断要处理的文件或文件夹是否存在。如果指定的文件或文件夹存在,则Dir函数返回该文件或文件夹的名称,否则返回一个零长度字符串。Dir函数的语法如下: Dir(pathname, attributes) * pathname(可选):文件或文件夹的完整路径,可以使用通配符匹配多个文件。 * attributes(可选):文件的属性,该参数的值如表10-2所示,可以将多个值相加以同时指定多个属性。 表10-2 attributes参数的值 常 量 值 说 明 vbNormal 0 指定无属性的文件,默认值 vbReadOnly 1 指定无属性的只读文件 vbHidden 2 指定无属性的隐藏文件 VbSystem 4 指定无属性的系统文件 vbVolume 8 指定卷标文件,如果指定了其他属性,则忽略该属性 vbDirectory 16 指定无属性文件及其路径和文件夹      下面的代码用于判断E盘的“测试数据”文件夹中的“总公司.xlsx”文件是否存在,如果存在,则在Excel中打开该文件,否则显示一条信息。 Sub 判断文件是否存在() Dim strFileName As String strFileName = "E:\测试数据\总公司.xlsx" If Dir(strFileName) <> "" Then Workbooks.Open strFileName Else MsgBox "文件不存在" End If End Sub   如需判断某个文件夹是否存在,需要将Dir函数的第二个参数设置为vbDirectory。下面的代码用于判断E盘中的“测试数据”文件夹是否存在,无论其是否存在,都显示一条信息。 Sub 判断文件夹是否存在() Dim strPath As String strPath = "E:\测试数据" If Dir(strPath, vbDirectory) <> "" Then MsgBox "【" & strPath & "】文件夹存在" Else MsgBox "【" & strPath & "】文件夹不存在" End If End Sub   如果在路径的末尾添加路径分隔符“\”,则无须为Dir函数指定第二个参数,也可实现相同的功能,代码如下: Sub 判断文件夹是否存在2() Dim strPath As String strPath = "E:\测试数据\" If Dir(strPath) <> "" Then MsgBox "【" & strPath & "】文件夹存在" Else MsgBox "【" & strPath & "】文件夹不存在" End If End Sub 10.1.3 列出指定文件夹中的所有文件   使用Dir函数和动态数组,可以查找指定文件夹中的所有文件,并可将找到的文件名添加到Excel工作表中。下面的代码用于在活动工作表的A、B两列中,列出E盘的“测试数据”文件夹的所有文件和文件的大小,如图10-1所示。 Sub 列出指定文件夹中的所有文件() Dim strPath As String, strFileName As String Dim intIndex As Integer, varFiles() As Variant strPath = "E:\测试数据\" strFileName = Dir(strPath & "*.*") Range("A1").Value = "文件名" Do While strFileName <> "" intIndex = intIndex + 1 ReDim Preserve varFiles(1 To intIndex) varFiles(intIndex) = strFileName strFileName = Dir Loop Range("A2").Resize(UBound(varFiles)).Value = WorksheetFunction.Transpose (varFiles) Range("A1").CurrentRegion.HorizontalAlignment = xlCenter Range("A1").CurrentRegion.Columns.AutoFit End Sub   代码解析:将“测试数据”文件夹的路径存储在strPath变量中,然后使用Dir函数在该路径中查找以“*.*”为名称的文件,第一个星号表示任意名称,第二个星号表示任意扩展名,它们组合在一起表示任意名称和扩展名的文件。在进入Do Loop循环时先检查Dir函数的返回值是否为零长度字符串,如果不是,说明至少找到了一个文件,此时进入Do Loop循环,将用作数组元素索引号的变量加1,并使用该变量定义动态数组的上限,然后将当前找到的文件名赋值给动态数组的第一个元素,再使用不带参数的Dir函数继续按照之前的条件查找下一个文件。反复执行上述操作,将每次找到的文件赋值给动态数组的下一个元素。最后将水平数组转换为垂直数组,并输入以A2单元格为起点的单元格区域中,区域的总行数由数组的上限决定。 图10-1 列出指定文件夹中的所有文件   如需同时列出指定文件夹中的文件名和文件大小,可以使用下面的代码,运行效果如图10-2所示。本例代码与上一个示例类似,只是需要定义一个二维动态数组,将文件名和文件大小分别存储在每一维元素中。 Sub 列出指定文件夹中的所有文件2() Dim strPath As String, strFileName As String Dim intIndex As Integer, varFiles() As Variant strPath = "E:\测试数据\" strFileName = Dir(strPath & "*.*") Range("A1:B1").Value = Array("文件名", "文件大小") Do While strFileName <> "" intIndex = intIndex + 1 ReDim Preserve varFiles(1 To 2, 1 To intIndex) varFiles(1, intIndex) = strFileName varFiles(2, intIndex) = FileLen(strPath & strFileName) & "字节" strFileName = Dir Loop Range("A2").Resize(UBound(varFiles, 2), 2).Value = WorksheetFunction.Transpose (varFiles) Range("A1").CurrentRegion.HorizontalAlignment = xlCenter Range("A1").CurrentRegion.Columns.AutoFit End Sub 图10-2 同时列出文件名和文件大小 10.1.4 使用MkDir语句创建文件夹   如需在计算机磁盘中创建新的文件夹,可以使用MkDir语句。该语句有一个参数,表示要创建文件夹的完整路径。下面的代码用于在E盘的“测试数据”文件夹中创建名为Excel的文件夹。 MkDir "E:\测试数据\Excel"   如果路径不存在,则会出现运行时错误。为了避免错误,可以使用Dir函数判断路径是否存在,如果存在,再创建新的文件夹。   如果在参数中省略驱动器的名称,则将在当前文件夹中创建新的文件夹。使用CurDir函数可以返回当前文件夹,使用ChDir语句可以更改当前文件夹。使用ChDrive语句可以更改当前驱动器。   假设当前文件夹是E盘的“测试数据”文件夹,则下面的代码将在该文件夹中创建名为Excel的文件夹。 MkDir "Excel"   如果当前文件夹不是E盘的“测试数据”文件夹,则可以使用下面的代码将当前文件夹更改为该文件夹。 ChDir "E:\测试数据" MkDir "Excel"   如果当前驱动器不是E,则需要先使用ChDrive语句将当前驱动器更改为E,然后使用ChDir语句更改当前文件夹,再使用MkDir语句在当前文件夹中创建新的文件夹。 ChDrive "E" ChDir "E:\测试数据" MkDir "Excel"   注意:如果在指定的位置已经存在要创建的文件夹,则在该位置创建同名的文件夹时将出现运行时错误。 10.1.5 使用RmDir语句删除文件夹   使用RmDir语句可以删除一个空文件夹。如果文件夹包含文件或子文件夹,需要先将其中的所有文件和子文件夹删除,然后再使用RmDir语句删除该文件夹,否则将出现运行时错误。   下面的代码用于删除E盘的“测试数据”文件夹中名为Excel的文件夹。 RmDir "E:\测试数据\Excel" 10.1.6 使用Name语句修改文件和文件夹的名称   使用Name语句可以修改文件和文件夹的名称,该语句的语法如下: Name oldpathname As newpathname * oldpathname(必需):原始文件的完整路径。 * newpathname(必需):修改名称后的文件的完整路径。   下面的代码用于将“测试数据”文件夹中的“总公司.xlsx”文件的名称修改为“总公司2023.xlsx”。 Sub 修改文件名() Dim strOldName As String, strNewName As String strOldName = "E:\测试数据\总公司.xlsx" strNewName = "E:\测试数据\总公司2023.xlsx" Name strOldName As strNewName End Sub   注意:如果修改名称的文件处于打开状态,则将出现运行时错误。   下面的代码将“测试数据”文件夹的名称修改为“测试”。 Sub 修改文件夹名() Dim strOldName As String, strNewName As String strOldName = "E:\测试数据\" strNewName = "E:\测试\" Name strOldName As strNewName End Sub 10.1.7 使用Name语句移动文件   除了可以使用Name语句修改文件和文件夹的名称之外,还可以使用该语句移动文件和文件夹,只需将Name语句的第二个参数设置为一个不同的路径即可。下面的代码用于将“测试数据”文件夹中的“总公司.xlsx”文件移动到C盘根目录中。 Sub 移动文件() Dim strOldName As String, strNewName As String strOldName = "E:\测试数据\总公司.xlsx" strNewName = "C:\总公司.xlsx" Name strOldName As strNewName End Sub   注意:如果目标路径中已经存在同名文件,则将出现运行时错误。   也可以在移动文件的同时修改其名称。下面的代码用于将“测试数据”文件夹中的“总公司.xlsx”文件移动到C盘根目录中,并将其名称改为“总公司2023.xlsx”。 Sub 移动文件并改名() Dim strOldName As String, strNewName As String strOldName = "E:\测试数据\总公司.xlsx" strNewName = "C:\总公司2023.xlsx" Name strOldName As strNewName End Sub 10.1.8 使用FileCopy语句复制文件   使用FileCopy语句可以复制一个文件,该语句的语法如下: FileCopy source, destination * source(必需):文件的完整路径。 * destination(必需):将文件复制到的目标位置的完整路径。   下面的代码用于将“测试数据”文件夹中的“总公司.xlsx”文件复制到C盘根目录中,并将复制后的文件命名为“总公司(备份).xlsx”。 Sub 复制文件() Dim strSouName As String, strDesName As String strSouName = "E:\测试数据\总公司.xlsx" strDesName = "C:\总公司(备份).xlsx" FileCopy strSouName, strDesName End Sub   注意:如果复制的文件处于打开状态,则将出现运行时错误。 10.1.9 使用Kill语句删除文件   使用Kill语句可以删除一个文件,被删除的文件不会进入回收站,所以无法对其进行恢复。删除文件前不会有任何提示信息,所以使用Kill语句时一定要谨慎。下面的代码用于删除“测试数据”文件夹中的“总公司.xlsx”文件。 Kill "E:\测试数据\总公司.xlsx"   注意:如果删除的文件不存在或处于打开状态,则将出现运行时错误。 10.2 使用FSO对象模型操作文件和文件夹   与使用Excel对象模型中的对象操作Excel应用程序类似,在VBA中还可以使用FSO对象模型操作文件和文件夹。与用于操作文件和文件夹的VBA内置函数和语句相比,FSO对象模型提供了更多的灵活性,并可获取有关文件和文件夹更丰富的信息。本节将介绍使用FSO对象模型操作文件和文件夹的方法。 10.2.1 了解FSO对象模型   FSO的全称是File System Object(文件系统对象)。与Excel对象模型类似,FSO对象模型提供了一套用于处理文件和文件夹的对象及相关的属性和方法,可以实现比VBA内置的函数和语句更丰富的功能,编写的代码也更清晰,具有更多的灵活性。   在FSO对象模型中有8个对象:FileSystemObject、Drives、Drive、Folders、Folder、Files、File和TextStream,以字母s结尾的对象是集合。   1. FileSystemObject对象   FileSystemObject对象是FSO对象模型中的顶层对象,其作用类似于Excel对象模型中的Application对象。使用FileSystemObject对象的一些方法可以返回FSO对象模型中的其他对象。例如,使用GetDrive方法将返回Drive对象,使用GetFolder方法将返回Folder对象,使用GetFile方法将返回File对象。   FileSystemObject对象的一些方法与FSO对象模型中的其他对象的方法具有相同的功能。例如,CopyFile方法用于复制文件,其功能与File对象的Copy方法相同。FileSystemObject对象包含大量的方法,使用这些方法可以完成所有与文件和文件夹相关的操作。然而,在处理文本文件时,FileSystemObject对象只提供了创建和打开文本文件的方法,而无法读取和写入文本文件中的数据,实现这些功能需要依靠TextStream对象的属性和方法。FileSystemObject对象的方法如表10-3所示。   FileSystemObject对象只有Drivers一个属性,用于返回计算机中所有驱动器的集合,从该集合中可以引用表示特定驱动器的Drive对象。然后使用Drive对象的相关属性返回表示文件夹的Folders集合,接着从该集合中引用表示特定文件夹的Folder对象。再使用Folder对象的相关属性返回表示文件的Files集合,最后从该集合中引用表示特定文件的File对象。       表10-3 FileSystemObject对象的方法 方 法 说 明 BuildPath 将名称添加到已存在的路径中 CopyFile 复制文件 CopyFolder 复制文件夹 CreateFolder 创建文件夹 CreateTextFile 创建文本文件 DeleteFile 删除文件 DeleteFolder 删除文件夹 DriveExists 确定指定的驱动器是否存在 FileExists 确定指定的文件是否存在 FolderExists 确定指定的文件夹是否存在 GetAbsolutePathName 返回绝对路径 GetBaseName 返回路径中最后部分的名称,不包括文件扩展名 GetDrive 返回表示指定路径中的驱动器的Drive对象 GetDriveName 返回指定路径中的驱动器的名称 GetExtensionName 返回路径中最后部件扩展名的字符串 GetFile 返回表示指定路径中的文件的File对象 GetFileName 返回指定路径中的最后名称 GetFolder 返回表示指定路径中的文件夹的Folder对象 GetParentFolderName 返回指定路径的父文件夹的名称 GetSpecialFolder 返回表示指定的特殊文件夹的Folder对象 GetTempName 返回随机产生的临时文件或文件夹的名称 MoveFile 移动文件 MoveFolder 移动文件夹 OpenTextFile 打开文本文件   2. Drives集合和Drive对象   Drives集合表示计算机中的所有驱动器,Drive对象表示特定的驱动器。Drive对象的属性如表10-4所示。 表10-4 Drive对象的属性 属 性 说 明 AvailableSpace 返回驱动器的可用空间 DriveLetter 返回驱动器的字母 DriveType 返回驱动器的类型 FileSyttem 返回驱动器的文件系统类型 FreeSpace 返回驱动器的剩余容量,通常与AvailableSpace的值相同 IsReady 确定驱动器是否已准备好 续表 属 性 说 明 Path 返回驱动器的路径 RootFolder 返回驱动器的根目录 SerialNumber 返回驱动器的卷标序列号 ShareName 返回驱动器的网络共享名 TotalSize 返回驱动器的总容量 VolumeName 返回驱动器的卷标名   3. Folders集合和Folder对象   Folders集合表示文件夹集合,Folder对象表示特定的文件夹。Folder对象有4个方法:Move、Copy、Delete和CreateTextFile,前3个方法分别用于移动、复制和删除文件夹,最后一个方法用于创建一个文本文件。Folder对象的属性如表10-5所示。 表10-5 Folder对象的属性 属 性 说 明 Attributes 返回或设置文件夹的属性 DateCreated 返回文件夹的创建日期和时间 DateLastAccessed 返回最后一次访问文件夹的日期和时间 DateLastModified 返回最后一次修改文件夹的日期和时间 Drive 返回文件夹所在的驱动器号 Files 返回表示指定文件夹中的所有文件的Files集合 IsRootFolder 确定文件夹是否为根目录 Name 返回或设置文件夹的名称 ParentFolder 返回表示指定文件夹的父文件夹的Folder对象 Path 返回文件夹的路径 ShortName 返回需要较早的8.3命名规则约定的程序所使用的短名称 ShortPath 返回需要较早的8.3命名规则约定的程序所使用的短路径 Size 返回以字节为单位的包含在文件夹中所有文件和子文件夹的大小 SubFolders 返回表示指定文件夹中的所有子文件夹的Folders集合 Type 返回文件夹类型的相关信息   如需返回Folders集合,可以使用Folder对象的SubFolders属性。如需返回驱动器根目录中的所有文件夹集合,需要先使用Drive对象的RootFolder属性返回Folder对象,然后使用该Folder对象的SubFolders属性返回Folders集合。   4. Files集合和File对象   Files集合表示特定文件夹中所有文件的集合,File对象表示特定的文件。如需返回Files集合,可以使用Folder对象的Files属性。File对象有4个方法:Move、Copy、Delete和OpenAsTextStream,前3个方法分别用于移动、复制和删除文件,最后一个方法用于打开一个文本文件。File对象的属性如表10-6所示。 表10-6 File对象的属性 属 性 说 明 Attributes 返回或设置文件的属性 DateCreated 返回文件的创建日期和时间 DateLastAccessed 返回最后一次访问文件的日期和时间 DateLastModified 返回最后一次修改文件的日期和时间 Drive 返回文件所在的驱动器号 Name 返回或设置文件的名称 ParentFolder 返回表示指定文件所在文件夹的Folder对象 Path 返回指定文件的路径 ShortName 返回需要较早的8.3命名规则约定的程序所使用的短名称 ShortPath 返回需要较早的8.3命名规则约定的程序所使用的短路径 Size 返回文件的大小,以字节为单位 Type 返回文件类型的相关信息   5. TextStream对象   TextStream对象专门用于在文本文件中读取和写入数据,TextStream对象的属性和方法如表10-7和表10-8所示。 表10-7 TextStream对象的属性 属 性 说 明 Line 返回文本文件中的当前行号 AtEndOfStream 确定是否达到文本文件的结尾 AtEndOfLine 确定是否达到文本文件中指定行的结尾 Column 返回文本文件中当前字符位置的列号 表10-8 TextStream对象的方法 方 法 说 明 ReadAll 读取并返回文本文件的所有内容 WriteLine 将指定内容和换行符写入文本文件 Read 读取并返回文本文件中指定数量的字符 Close 关闭已打开的文本文件 WriteBlankLines 将指定数量的换行符写入文本文件 Skip 在读取文本文件时跳过指定数量的字符 ReadLine 读取并返回文本文件中的一整行内容 SkipLine 读取文本文件时跳过下一行 Write 将指定内容写入文本文件      可以使用以下3种方法创建或打开一个TextStream对象: * 使用FileSystemObject对象的CreateTextFile方法或OpenTextFile方法,创建或打开一个文本文件。 * 使用Folder对象的CreateTextFile方法创建一个文本文件。 * 使用File对象的OpenAsTextStream方法打开一个文本文件。 10.2.2 创建FSO对象模型中的顶层对象   与使用字典对象类似,在VBA中使用FSO对象模型之前,也需要在VBE中引用Microsoft Scripting Runtime类型库,或者使用CreateObject函数通过后期绑定技术创建FSO对象模型中的顶层对象,然后才能使用该对象以及FSO对象模型中的其他对象操作文件和文件夹。   在VBE中引用Microsoft Scripting Runtime类型库的方法可参考5.5.1小节。添加该类型库的引用后,需要创建一个FileSystemObject类型的变量,然后使用New关键字将FileSystemObject对象的一个实例赋值给该变量,代码如下: Dim fso As FileSystemObject Set fso = New FileSystemObject   也可以将上面的两行代码合并为一行: Dim fso As New FileSystemObject   如果不想预先在VBE中引用Microsoft Scripting Runtime类型库,则可以使用CreateObject函数创建FSO对象模型中的顶层对象。此时需要先声明一个Object类型的变量,然后使用CreateObject函数为该变量赋值,代码如下: Dim fso As Object Set fso = CreateObject("Scripting.FileSystemObject")   注意:后面几个小节中的示例都使用的是第一种方法创建FileSystemObject对象。为了使这些示例中的代码正常运行,需要先在VBE中引用Microsoft Scripting Runtime类型库。 10.2.3 引用指定的驱动器、文件夹和文件   使用FileSystemObject对象的GetDrive、GetFolder和GetFile三个方法,可以分别引用指定的驱动器、文件夹和文件,并返回相应的Drive、Folder和File对象。   1. 引用指定的驱动器   使用FileSystemObject对象的GetDrive方法将返回一个Drive对象,它表示指定的驱动器。GetDrive方法有一个参数,该参数的值可以是以下几种形式: * 一个表示驱动器的字母,例如“E”。 * 一个表示驱动器的字母和一个冒号,例如“E:”。 * 一个表示驱动器的字母和一个冒号,并在冒号的右侧加上路径分隔符,例如“E:\”。 * 任何网络共享的路径。   下面的3行代码都返回一个引用驱动器E的Drive对象,并将该对象赋值给drv变量。 fso.GetDrive "E" fso.GetDrive "E:" fso.GetDrive "E:\"   2. 引用指定的文件夹   使用FileSystemObject对象的GetFolder方法将返回一个Folder对象,它表示指定的文件夹。GetFolder方法有一个参数,表示文件夹的路径。下面的代码返回一个引用E盘的“测试数据”文件夹的Folder对象。 fso.GetFolder "E:\测试数据\"   3. 引用指定的文件   使用FileSystemObject对象的GetFile方法将返回一个File对象,它表示指定的文件。GetFile方法有一个参数,表示文件的路径。下面的代码返回一个引用“测试数据”文件夹中的“总公司.xlsx”文件的File对象。 fso.GetFile "E:\测试数据\总公司.xlsx"   无论引用的是驱动器、文件夹还是文件,为了便于后续处理这些对象,通常会将引用的对象赋值给一个对象变量。下面的代码分别将指定的驱动器、文件夹和文件赋值给相应类型的对象变量。 Set drv = fso.GetDrive("E") Set fdr = fso.GetFolder("E:\测试数据\") Set fil = fso.GetFile("E:\测试数据\总公司.xlsx") 10.2.4 判断驱动器、文件夹和文件是否存在   使用FileSystemObject对象的DriveExists、FolderExists和FileExists三个方法,可以分别判断指定的驱动器、文件夹和文件是否存在,从而避免由于操作无效的对象而出现运行时错误。   下面的代码判断驱动器A是否存在,如果存在,则使用GetDrive方法引用该驱动器,并将其返回的Drive对象赋值给drv变量,否则显示一条信息。 Sub 判断指定的驱动器是否存在() Dim fso As FileSystemObject, drv As Drive Set fso = New FileSystemObject If fso.DriveExists("A") Then Set drv = fso.GetDrive("A") Else MsgBox "指定的驱动器不存在" End If End Sub   FolderExists和FileExists两个方法的用法与DriveExists类似。 10.2.5 列出指定文件夹中的所有文件   使用VBA内置的函数和语句列出指定文件夹中的所有文件时,由于预先不知道文件夹中的文件总数,所以需要使用动态数组,通过不断改变数组的上限来向其中添加每次找到的文件。   使用FSO对象模型完成这项工作将变得更简单,因为可以使用Files集合的Count属性返回文件总数,然后使用For Each语句逐一处理Files集合中的每一个File对象。   下面的代码用于在活动工作表中列出E盘的“测试数据”文件夹的每一个文件的名称和大小。 Sub 列出指定文件夹中的所有文件() Dim fso As FileSystemObject, fil As File Dim strPath As String, intRow As Integer Set fso = New FileSystemObject strPath = "E:\测试数据\" Range("A1:B1").Value = Array("文件名", "文件大小") intRow = 2 For Each fil In fso.GetFolder(strPath).Files Cells(intRow, 1).Value = fil.Name Cells(intRow, 2).Value = fil.Size & "字节" intRow = intRow + 1 Next fil Range("A1").CurrentRegion.HorizontalAlignment = xlCenter Range("A1").CurrentRegion.Columns.AutoFit End Sub   如果处理的数据量很大,为了加快程序的运行速度,也可以使用动态数组存储文件的相关信息,最后一次性将动态数组中的数据写入单元格区域。下面的代码使用动态数组实现相同的功能。 Sub 列出指定文件夹中的所有文件2() Dim fso As FileSystemObject, fil As File Dim strPath As String, intFileCount As Integer Dim varFiles() As Variant, intIndex As Integer Set fso = New FileSystemObject strPath = "E:\测试数据\" intFileCount = fso.GetFolder(strPath).Files.Count ReDim varFiles(1 To intFileCount, 1 To 2) Range("A1:B1").Value = Array("文件名", "文件大小") For Each fil In fso.GetFolder(strPath).Files intIndex = intIndex + 1 varFiles(intIndex, 1) = fil.Name varFiles(intIndex, 2) = fil.Size & "字节" Next fil Range("A2").Resize(UBound(varFiles, 1), 2).Value = varFiles Range("A1").CurrentRegion.HorizontalAlignment = xlCenter Range("A1").CurrentRegion.Columns.AutoFit End Sub 10.2.6 创建文件和文件夹   如需创建文件,可以使用FileSystemObject对象的CreateTextFile方法,还可以使用Folder对象的CreateTextFile方法和File对象的OpenAsTextStream方法创建文本文件。3种方法的用法类似,此处只介绍FileSystemObject对象的CreateTextFile方法,该方法的语法如下: CreateTextFile(filename, overwrite, unicode) * filename(必需):文本文件的完整路径。 * overwrite(可选):是否替换同名的文本文件,该参数为True表示替换,该参数为False表示不替换。省略该参数时默认为True。 * unicode(可选):文本文件的编码格式,该参数为True表示Unicode编码格式,该参数为False表示ASCII编码格式。省略该参数时默认为False。   下面的代码用于在E盘的“测试数据”文件夹中创建名为“测试.txt”的文本文件。由于省略了第二个参数,所以如果在目标位置存在同名文件,则使用新建的文件替换它。 fso.CreateTextFile "E:\测试数据\测试.txt"   如需创建一个文件夹,可以使用FileSystemObject对象的CreateFolder方法。下面的代码用于在E盘的“测试数据”文件夹中创建名为Excel的文件夹。 fso.CreateFolder "E:\测试数据\Excel"   如果文件夹已存在,则将出现运行时错误。为了避免错误,可以先使用FolderExists方法判断文件夹是否存在,如果不存在,再使用CreateFolder方法创建文件夹,代码如下: Sub 创建文件夹() Dim fso As FileSystemObject, strPath As String Set fso = New FileSystemObject strPath = "E:\测试数据\Excel" If fso.FolderExists(strPath) Then MsgBox "指定的文件夹已存在" Else fso.CreateFolder strPath End If End Sub 10.2.7 移动文件和文件夹   使用FileSystemObject对象的MoveFile方法可以移动一个文件,使用FileSystemObject对象的MoveFolder方法可以移动一个文件夹。两个方法都包含以下两个参数: * source(必需):文件或文件夹的完整路径。 * destination(必需):将文件或文件夹移动到目标位置的完整路径。   移动文件和移动文件夹的方法基本相同,下面以移动文件为例进行介绍。   下面的代码用于将名为“总公司.xlsx”的文件从“测试数据”文件夹移动到该文件夹中的“Excel”子文件夹中。 Sub 移动一个文件() Dim fso As FileSystemObject Dim strSou As String, strDes As String Set fso = New FileSystemObject strSou = "E:\测试数据\总公司.xlsx" strDes = "E:\测试数据\Excel\" fso.MoveFile strSou, strDes End Sub   注意:为destination参数设置的路径必须以路径分隔符“\”结尾,否则会在移动文件后,将其重命名为路径中最后一个路径分隔符之后的字符串且没有扩展名。   如需移动同类型的多个文件,可以在source参数中使用通配符。下面的代码用于将“测试数据”文件夹中的所有扩展名为.xlsx的Excel文件移动到该文件夹中的“Excel”子文件夹中。 Sub 移动多个文件() Dim fso As FileSystemObject Dim strSou As String, strDes As String Set fso = New FileSystemObject strSou = "E:\测试数据\*.xlsx" strDes = "E:\测试数据\Excel\" fso.MoveFile strSou, strDes End Sub   还可以只移动名称中包含特定字符的一系列文件。例如,下面的代码将source参数设置为只移动名称以“分公司”3个字结尾的所有.xlsx文件。 strSou = "E:\测试数据\*分公司.xlsx"   使用类似的方法也可以设置文件扩展名。如需移动文件夹中的所有文件,可以将source参数中的文件名设置为“*.*”。   移动文件时可以修改文件的名称,只需在destination参数的结尾输入希望的文件名,即可在移动文件后将其改为新的名称。下面的代码将移动后的“总公司.xlsx”文件的名称修改为“总公司2023.xlsx”。 Sub 移动一个文件并改名() Dim fso As FileSystemObject Dim strSou As String, strDes As String Set fso = New FileSystemObject strSou = "E:\测试数据\总公司.xlsx" strDes = "E:\测试数据\Excel\总公司2023.xlsx" fso.MoveFile strSou, strDes End Sub 10.2.8 复制文件和文件夹   使用FileSystemObject对象的CopyFile方法可以复制一个文件,使用FileSystemObject对象的CopyFolder方法可以复制一个文件夹。两个方法都包含3个参数,前两个参数与MoveFile方法和MoveFolder方法相同,第三个参数用于决定是否替换同名的文件或文件夹,为True表示替换,为False表示不替换,默认为True。   复制文件和文件夹的方法与移动文件和文件夹基本相同。下面的代码将“测试数据”文件夹的名为“总公司.xlsx”的文件复制到“测试数据”文件夹的“Excel”子文件夹中,并将复制后的文件名修改为“总公司(备份).xlsx”。 Sub 复制一个文件() Dim fso As FileSystemObject Dim strSou As String, strDes As String Set fso = New FileSystemObject strSou = "E:\测试数据\总公司.xlsx" strDes = "E:\测试数据\Excel\总公司(备份).xlsx" fso.CopyFile strSou, strDes End Sub   下面的代码将“测试数据”文件夹的名称以“分公司”3个字结尾的所有.xlsx文件复制到“测试数据”文件夹的“Excel”子文件夹中。 Sub 复制多个文件() Dim fso As FileSystemObject Dim strSou As String, strDes As String Set fso = New FileSystemObject strSou = "E:\测试数据\*分公司.xlsx" strDes = "E:\测试数据\Excel\" fso.CopyFile strSou, strDes End Sub   下面的代码将名为“Excel”的文件夹复制到其所在的文件夹中,并将复制后的文件夹的名称设置为“Excel(备份)”。 Sub 复制文件夹() Dim fso As FileSystemObject Dim strSou As String, strDes As String Set fso = New FileSystemObject strSou = "E:\测试数据\Excel" strDes = "E:\测试数据\Excel(备份)" fso.CopyFolder strSou, strDes End Sub 10.2.9 删除文件和文件夹   使用FileSystemObject对象的DeleteFile方法可以删除一个文件,使用FileSystemObject对象的DeleteFolder方法可以删除一个文件夹,两个方法都包含以下两个参数。 * filespec/folderspec(必需):文件或文件夹的完整路径。 * force(可选):是否删除具有只读属性的文件或文件夹,该参数为True表示删除,该参数为False表示不删除,默认为False。   下面的代码用于删除“测试数据”文件夹中名为“总公司.xlsx”的文件。 fso.DeleteFile "E:\测试数据\总公司.xlsx"   下面的代码用于删除“测试数据”文件夹中的所有文件。 fso.DeleteFile "E:\测试数据\*.*"   下面的代码用于删除“测试数据”文件夹中的“Excel”子文件夹,无论文件夹中是否有文件,都会将其删除,比VBA内置的RmDir语句要方便得多。 fso.DeleteFolder "E:\测试数据\Excel"   注意:删除的文件和文件夹不会进入回收站,所以无法恢复它们。 10.2.10 修改文件和文件夹的名称   使用File对象和Folder对象的Name属性可以修改文件或文件夹的名称。下面的代码用于将E盘的“测试数据”文件夹中的“总公司.xlsx”文件的名称修改为“总公司2023.xlsx”。 Sub 修改文件名() Dim fso As FileSystemObject, fil As File Set fso = New FileSystemObject Set fil = fso.GetFile("E:\测试数据\总公司.xlsx") fil.Name = "总公司2023.xlsx" End Sub   下面的代码用于将E盘的“测试数据”文件夹中的“Excel”子文件夹的名称修改为“Excel(备份)”。 Sub 修改文件夹名() Dim fso As FileSystemObject, fdr As Folder Set fso = New FileSystemObject Set fdr = fso.GetFolder("E:\测试数据\Excel") fdr.Name = "Excel(备份)" End Sub 10.3 在文本文件中读取和写入数据   使用VBA可以通过编程的方式将Excel工作表中的数据写入文本文件,或者将文本文件中的数据加载到工作表中,并为文本文件中的数据格式提供了更多的灵活性。本节将介绍使用VBA内置的函数和语句,以及FSO对象模型中的TextStream对象在文本文件中读取和写入数据的方法。 10.3.1 使用Open语句和Close语句打开和关闭文本文件   在一个文本文件中读取或写入数据之前,需要先使用VBA内置的Open语句打开该文本文件。Open语句的语法如下: Open pathname For mode [Access access] [lock] As [#]filenumber [Len=reclength] * pathname(必需):文本文件的完整路径。 * mode(必需):打开文件的访问方式,包括Append(顺序访问,将数据追加到文件末尾)、Binary(二进制访问)、Input(顺序访问,从文件中读取数据)、Output(顺序访问,向文件中写入数据)或Random(随机访问)几种,默认以Random方式打开文件。本小节和后续几个小节主要介绍以Input和Output两种访问方式打开文本文件并在其中读取和写入数据的方法。 * access(可选):打开文件后可以执行的操作,包括Read、Write或Read Write三种。 * lock(可选):限制其他进程打开文件后可以执行的操作,包括Shared、Lock Read、Lock Write和Lock Read Write几种。 * filenumber(必需):一个未被使用的文件号,范围是1~511,可以使用VBA内置的FreeFile函数自动获取一个可用的文件号。 * reclength(可选):一个小于或等于32767的数字,该参数在使用随机访问方式打开的文件中表示记录的长度,在使用顺序访问方式打开的文件中表示缓冲字符数。   在Open语句中以Input方式打开文本文件时,如果该文件不存在,则将出现运行时错误。在Open语句中以Output或Append方式打开文本文件时,如果该文件不存在,则将创建该文本文件并打开它。处理完一个文本文件后,需要使用Close语句将其关闭,以免丢失数据。   下面的代码以Input访问方式打开E盘的“测试数据”文件夹中名为“库存记录.txt”的文本文件,为了避免文件不存在时出现运行时错误,在执行Open语句之前,先使用Dir函数检查文件是否存在。如果文件存在,则先打开它,然后使用Close语句关闭它;如果文件不存在,则显示一条信息并退出程序。 Sub 打开文本文件() Dim intFileNumber As Integer, strFileName As String intFileNumber = FreeFile strFileName = "E:\测试数据\库存记录.txt" If Dir(strFileName) <> "" Then Open strFileName For Input As #intFileNumber Else MsgBox "无法打开不存在的文件" Exit Sub End If Close #intFileNumber End Sub   使用Close语句可以关闭已打开的一个或多个文本文件。下面的代码用于关闭与intFileNumber变量表示的文件号关联的文本文件。 Close #intFileNumber   下面的代码用于关闭文件号为1和2的两个文本文件。 Close #1, #2   在Close语句省略文件号,表示关闭使用Open语句打开的所有文本文件。 10.3.2 使用Write语句将数据写入文本文件   使用Write语句可以向文本文件中写入数据。如果写入的数据包含多列,各列数据之间自动以逗号分隔。Write语句还会为不同类型的数据添加相应的分界符,将字符串放置在一对双引号中,将日期放置在一对井号中,数字保持原样不变。Write语句的语法如下: Write #filenumber, outputlist * filenumber(必需):以Output访问方式打开的文本文件的文件号。 * outputlist(可选):写入文本文件的数据列表,列表中各个数据项之间以逗号分隔。如果要写入的数据包含多行和多列,则可以声明多个变量,每个变量代表每一列数据,变量的类型必须与其对应的列的数据类型匹配,否则可能会出现错误。   下面的代码以Output访问方式打开名为“库存记录.txt”的文本文件,如果该文件不存在则会创建它,然后使用Write语句将4项数据写入该文件。完成后的文本文件中的内容如图10-3所示。 Sub 使用Write语句写入数据() Dim strFileName As String, intFileNumber As Integer strFileName = "E:\测试数据\库存记录.txt" intFileNumber = FreeFile Open strFileName For Output As #intFileNumber Write #intFileNumber, DateValue("10月5日"), "牛奶", 2, 10 Close #intFileNumber End Sub 图10-3 写入数据后的文本文件   通常很少每次手动写入一行数据,而是将Excel工作表中的数据写入文本文件。下面的代码将如图10-4所示的A1:D6单元格区域中的第2~6行数据写入名为“库存记录.txt”的文本文件。 Sub 使用Write语句将Excel数据写入文本文件() Dim strFileName As String, intFileNumber As Integer Dim datDate As Date, strName As String Dim sngPrice As Single, intQuantity As Integer Dim intRow As Integer, intLastRow As Integer strFileName = "E:\测试数据\库存记录.txt" intFileNumber = FreeFile intLastRow = Range("A1").CurrentRegion.Rows.Count Open strFileName For Output As #intFileNumber For intRow = 2 To intLastRow datDate = Cells(intRow, 1).Value strName = Cells(intRow, 2).Value sngPrice = Cells(intRow, 3).Value intQuantity = Cells(intRow, 4).Value Write #intFileNumber, datDate, strName, sngPrice, intQuantity Next intRow Close #intFileNumber End Sub 图10-4 将Excel中的数据写入文本文件   代码解析:虽然本例代码的行数较多,但是本质上与上一个只写入一行数据的示例并无太大区别。本例与上一个示例的主要区别在于,本例使用For Next语句在数据区域中的第2行到最后一行之间逐行处理数据,依次将每一行中的4个值分别赋值给4个变量,然后使用Write语句将4个变量的值写入文本文件。   如需在文本文件中使用空行分隔各行数据,可以使用下面的代码,即在Write语句中省略第二个参数,但是保留它前面的逗号分隔符,如图10-5所示。 Write #intFileNumber, 图10-5 使用空行分隔数据 10.3.3 使用Print语句将数据写入文本文件   使用Write语句只能以固定的格式将数据写入文本文件,如需为写入文本文件中的数据提供灵活的格式,可以使用Print语句。使用Print语句写入文本文件中的各列数据之间默认以空格分隔,字符串和日期不再使用特定的符号标识。将10.3.2小节的最后一个示例中的Write语句改为Print语句,写入数据后的文本文件如图10-6所示。 图10-6 使用Print语句将数据写入文本文件   Print语句的语法如下: Print #filenumber, outputlist   Print语句的两个参数与Write语句相同,但是可以为Print语句的第二个参数进行更多的设置。将第二个参数展开后的语法如下: {Spc(n) | Tab[(n)]} expression charpos * Spc(n):在数据项之间插入指定数量的空格,n表示空格的个数。 * Tab(n):将放置数据项的插入点定位到指定的列号,n表示列号。如果省略n,则将插入点定位到下一个打印区的起始位置。 * expression:写入文本文件中的数据。 * charpos:写入下一个数据项的起始位置。将该参数设置为分号,表示将下一个数据项写入上一个数据项之后,两项数据之间只保留默认的空间,没有多余的空格。将该参数设置为Spc(n)或Tab(n),表示在下一个数据项之前插入空格或将起始位置设置为指定的列。省略该参数表示将下一个数据项写入下一行。   虽然Print语句默认使用空格分隔各项数据,但是可以通过将各项数据合并在一起,并在它们之间添加所需的符号,从而实现自定义分隔符的功能。下面的代码与10.3.2小节中的示例类似,但是使用分号作为各项数据之间的分隔符,并将日期设置为中文格式,还在每个表示金额的数字右侧添加了“元”字,使数字的含义更清晰,如图10-7所示。由于使用Format函数设置格式后,返回的是String类型,所以将存储日期的变量strDate声明为String类型。 Sub 使用Print语句将Excel数据写入文本文件() Dim strFileName As String, intFileNumber As Integer Dim strDate As String, strName As String Dim sngPrice As Single, intQuantity As Integer Dim intRow As Integer, intLastRow As Integer strFileName = "E:\测试数据\库存记录.txt" intFileNumber = FreeFile intLastRow = Range("A1").CurrentRegion.Rows.Count Open strFileName For Output As #intFileNumber For intRow = 2 To intLastRow strDate = Format(Cells(intRow, 1).Value, "m月d日") strName = Cells(intRow, 2).Value sngPrice = Cells(intRow, 3).Value intQuantity = Cells(intRow, 4).Value Print #intFileNumber, strDate & ";" & strName & ";" & sngPrice & "元;" & intQuantity Next intRow Close #intFileNumber End Sub 图10-7 使用Print语句自定义数据项的格式和分隔符 10.3.4 使用Input语句读取文本文件中的数据   Input语句主要用于从文本文件中读取使用Write语句写入的数据,Input语句的语法如下: Input #filenumber, varlist * filenumber(必需):以Input访问方式打开的文本文件的文件号。 * varlist(必需):存储读取出的各个数据项的一系列变量,各个变量之间以逗号分隔。各个变量的数据类型必须与文本文件中的各项数据匹配,否则将出现运行时错误。   下面的代码从10.3.2小节创建的文本文件中读取所有数据,并把这些数据添加到活动工作表中,并在该工作表中的第一行为各列数据添加标题。本例代码相当于使用Write语句写入数据的反向操作,使用EOF函数判断是否读取到文件的结尾,如果是,则说明已经读取了所有数据;如果不是,则继续读取下一项数据。 Sub 使用Input语句读取文本文件中的数据() Dim strFileName As String, intFileNumber As Integer Dim datDate As Date, strName As String Dim sngPrice As Single, intQuantity As Integer Dim intRow As Integer strFileName = "E:\测试数据\库存记录.txt" intFileNumber = FreeFile intRow = 2 Cells.Clear Open strFileName For Input As #intFileNumber Range("A1:D1").Value = Array("日期", "名称", "单价", "数量") Do While Not EOF(intFileNumber) Input #intFileNumber, datDate, strName, sngPrice, intQuantity Cells(intRow, 1).Value = Format(datDate, "m月d日") Cells(intRow, 2).Value = strName Cells(intRow, 3).Value = sngPrice Cells(intRow, 4).Value = intQuantity intRow = intRow + 1 Loop Range("A1").CurrentRegion.HorizontalAlignment = xlCenter Close #intFileNumber End Sub 10.3.5 使用Line Input语句读取文本文件中的数据   Line Input语句每次从文本文件中读取一整行数据,并将其赋值给一个变量,然后可以根据数据之间的分隔符类型,将一整行数据拆分为多个数据项。Line Input语句适合从文本文件中读取使用Print语句写入的数据。Line Input语句的语法如下: Line Input #filenumber, varname * filenumber(必需):以Input访问方式打开的文本文件的文件号。 * varname(必需):存储读取出的一整行数据的变量。   下面的代码从10.3.3小节创建的文本文件中读取所有数据,将这些数据添加到活动工作表中,并在该工作表中的第一行为各列数据添加标题。本例仍然使用EOF函数判断是否读取到文件的结尾,从而决定是继续读取数据还是退出Do Loop循环。为了单独处理读取中的整行数据中的每个数据项,需要使用VBA内置的Split函数按照数据项之前的分隔符对整行数据进行拆分,然后将拆分后的每个数据项分别输入工作表的不同列中。由于文本文件的第3列数据中的“元”字是在使用Print语句写入数据时自定义添加的,为了将数据读取到Excel中时去掉“元”字,可以使用Excel对象模型中的Range对象的Replace方法将其替换为零长度字符串,达到删除该字的目的。 Sub 使用LineInput语句读取文本文件中的数据() Dim strFileName As String, intFileNumber As Integer Dim strDataLine As String, intRow As Integer Dim varData As Variant strFileName = "E:\测试数据\库存记录.txt" intFileNumber = FreeFile intRow = 2 Cells.Clear Open strFileName For Input As #intFileNumber Range("A1:D1").Value = Array("日期", "名称", "单价", "数量") Do While Not EOF(intFileNumber) Line Input #intFileNumber, strDataLine varData = Split(strDataLine, ";") Cells(intRow, 1).Resize(1, UBound(varData) + 1).Value = varData intRow = intRow + 1 Loop With Range("A1").CurrentRegion .HorizontalAlignment = xlCenter .Columns(3).Replace "元", "" End With Close #intFileNumber End Sub 10.3.6 使用TextStream对象读取和写入文本文件中的数据   前几个小节介绍了使用VBA内置的函数和语句在文本文件中读取和写入数据的方法,最后再来介绍一下使用FSO对象模型中的TextStream对象读写文本文件数据的方法。   首先需要打开要处理的文本文件,10.2.1小节介绍了打开文本文件的两种方法,此处以FileSystemObject对象的OpenTextFile方法为例,介绍如何使用FSO对象模型打开文本文件。OpenTextFile方法的语法如下: OpenTextFile(filename, iomode, create, format) * filename(必需):文本文件的完整路径。 * iomode(可选):打开文件的访问方式,包括ForReading、ForWriting和ForAppending三种,表示读取数据、写入数据和追加数据3种方式。 * create(可选):文本文件不存在时是否创建该文件,该参数为True表示创建文件,该参数为False表示不创建文件,省略该参数时默认为False。 * format(可选):文件的编码格式,该参数为TristateTrue表示以Unicode编码格式打开文件,该参数为TristateFalse表示以ASCII编码格式打开文件。省略该参数时默认以ASCII编码格式打开文件。   下面的代码打开E盘的“测试数据”文件夹中名为“库存记录.txt”的文本文件,并在对话框中显示该文件中的所有数据,如图10-8所示。本小节中的所有示例都需要先在VBE窗口中引用Microsoft Scripting Runtime类型库。 Sub 显示文本文件中的所有数据() Dim fso As FileSystemObject Dim tsm As TextStream Dim strFileName As String Set fso = New FileSystemObject strFileName = "E:\测试数据\库存记录.txt" Set tsm = fso.OpenTextFile(strFileName, ForReading) MsgBox tsm.ReadAll End Sub 图10-8 显示文本文件中的所有数据   注意:如果文本文件中没有任何数据,则将出现运行时错误。如果本例使用ForWriting或ForAppending打开文本文件,也将出现运行时错误。使用ForWriting访问方式打开文件时,会自动删除其中的所有数据,而使用ForAppending访问方式打开文件则不会。   使用TextStream对象的Write方法和WriteLine方法可以将数据写入文本文件。Write方法用于每次写入一个字符串。WriteLine方法用于每次写入一个字符串,但是会自动在字符串的结尾添加一个换行符,相当于写入一行数据,下次输入的数据被放置到下一行的开头。Write方法和WriteLine方法的功能类似于VBA内置的Write语句和Print语句。   下面的代码与10.3.3小节中的示例类似,使用WriteLine方法将活动工作表的A1:D6单元格区域中的第2~6行数据写入名为“库存记录.txt”的文本文件。如果该文件不存在,则自动创建该文件并写入数据。 Sub 使用WriteLine方法将Excel数据写入文本文件() Dim fso As FileSystemObject, tsm As TextStream Dim strDate As String, strName As String Dim sngPrice As Single, intQuantity As Integer Dim intRow As Integer, intLastRow As Integer Dim strFileName As String Set fso = New FileSystemObject strFileName = "E:\测试数据\库存记录.txt" intLastRow = Range("A1").CurrentRegion.Rows.Count Set tsm = fso.OpenTextFile(strFileName, ForWriting, True) For intRow = 2 To intLastRow strDate = Format(Cells(intRow, 1).Value, "m月d日") strName = Cells(intRow, 2).Value sngPrice = Cells(intRow, 3).Value intQuantity = Cells(intRow, 4).Value tsm.WriteLine strDate & ";" & strName & ";" & sngPrice & "元;" & intQuantity Next intRow tsm.Close End Sub   提示:如需在文本文件中插入空行,可以使用不带参数的WriteLine方法。   下面的代码使用TextStream对象的Write方法实现相同的功能,但是需要手动在一行的结尾添加换行符。由于本例工作表中共有4列数据,所以使用Select Case检测当前正在处理哪一列数据,如果处理的是前3列数据,则在每个数据项的结尾添加分号,如果处理的是第4列数据,则在数据项的结尾添加回车换行符。 Sub 使用Write方法将Excel数据写入文本文件() Dim fso As FileSystemObject, tsm As TextStream Dim strFileName As String, strData As String Dim intRow As Integer, intCol As Integer Dim intLastRow As Integer Set fso = New FileSystemObject strFileName = "E:\测试数据\库存记录.txt" intLastRow = Range("A1").CurrentRegion.Rows.Count Set tsm = fso.OpenTextFile(strFileName, ForWriting, True) For intRow = 2 To intLastRow For intCol = 1 To 4 Select Case intCol Case 1 strData = Format(Cells(intRow, intCol).Value, "m月d日") & ";" tsm.Write strData Case 2, 3 strData = Cells(intRow, intCol).Value & ";" tsm.Write strData Case 4 strData = Cells(intRow, intCol).Value & vbCrLf tsm.Write strData End Select Next intCol Next intRow tsm.Close End Sub   如需读取文本文件中的数据,可以使用TextStream对象的Read方法、ReadLine方法和ReadAll方法。Read方法用于读取指定数量的字符,ReadLine方法用于读取一整行数据,ReadAll方法用于读取所有数据。   下面的代码与10.3.5小节中的示例类似,从使用TextStream对象的WriteLine方法创建的文本文件中读取所有数据,将这些数据添加到活动工作表中,并在该工作表中的第一行为各列数据添加标题。与VBA内置的EOF函数类似,使用TextStream对象的AtEndOfStream属性可以判断当前是否已经读取到文件的结尾。 Sub 使用ReadLine方法读取文本文件中的数据() Dim fso As FileSystemObject, tsm As TextStream Dim strFileName As String, intRow As Integer Dim strDataLine As String, varData As Variant Set fso = New FileSystemObject strFileName = "E:\测试数据\库存记录.txt" Set tsm = fso.OpenTextFile(strFileName, ForReading) intRow = 2 Cells.Clear Range("A1:D1").Value = Array("日期", "名称", "单价", "数量") Do While Not tsm.AtEndOfStream strDataLine = tsm.ReadLine varData = Split(strDataLine, ";") Cells(intRow, 1).Resize(1, UBound(varData) + 1).Value = varData intRow = intRow + 1 Loop With Range("A1").CurrentRegion .HorizontalAlignment = xlCenter .Columns(3).Replace "元", "" End With End Sub       第11章 VBA高级编程技术   本章将介绍使用VBA编程操作注册表和其他Office应用程序的方法,还将介绍如何创建和使用类。这3个主题之间没有必然的联系,但是使用VBA开发较为专业的程序时,通常会用到这几种技术。 11.1 在注册表中读取和写入数据   注册表是一个包含计算机系统中的硬件、软件和用户配置等各方面信息的数据库,在计算机中执行的各种操作都与注册表有关,例如启动系统、配置硬件、安装软件、加载用户个人数据等。使用VBA内置的函数和语句可以在注册表中读取和写入数据,为开发具有“记忆”功能的程序提供方便。本节首先介绍注册表的结构,然后介绍使用VBA编程操作注册表的方法。 11.1.1 注册表的结构   为了便于统一管理Windows操作系统,微软从Windows 95开始使用一种称为“注册表”的数据库,它将计算机中的各种软硬件资源和配置信息集中存储起来,以便更有效地管理操作系统。   操作系统为用户提供了一些用于修改注册表中数据的图形化工具,控制面板和组策略就是其中的两种工具,使用这些工具对系统的各个选项进行设置时,系统会将用户的设置结果写入注册表。如需对注册表进行更灵活、更全面的设置,可以使用Windows操作系统中的注册表编辑器,使用该工具可以在注册表中添加或删除数据、查找数据、导入或导出数据。   regedit.exe是启动注册表编辑器的可执行文件,该文件位于安装Windows操作系统的磁盘分区的Windows文件夹中,如图11-1所示。 图11-1 注册表编辑器的启动文件   提示:早期版本的Windows操作系统提供了两种注册表编辑器——regedit.exe和regedt32.exe,它们的大多数功能相同。从Windows XP操作系统开始,将两种注册表编辑器合并为一个,即regedit.exe。   双击regedit.exe文件,启动注册表编辑器,启动后将显示注册表分层式的组织结构,整个注册表由根键、子键和键值组成,如图11-2所示。注册表有5个根键,它们位于注册表的顶层。每个根键包含多个子键,每个子键可以再包含子键,组成多层嵌套的子键。   根键和子键都可以包含键值。键值是选择一个根键或子键后显示在右侧窗格中的一个或多个项目,每个键值由名称、数据类型和数据3个部分组成。每个子键可以包含零个或多个键值,在键值中存储不同类型的数据,例如REG_SZ、REG_DWORD和REG_BINARY等。 图11-2 注册表的结构   用户不能创建新的根键,也不能删除注册表的5个根键或修改它们的名称。5个根键的功能如下。 * HKEY_CLASSES_ROOT:存储文件扩展名与软件之间的关联,以及组件对象模型的相关信息。 * HKEY_CURRENT_USER:存储当前登录系统的用户账户的相关信息。 * HKEY_LOCAL_MACHINE:存储在操作系统中安装的硬件、软件和系统配置等信息。 * HKEY_USERS:存储操作系统中所有用户账户的相关信息。 * HKEY_CURRENT_CONFIG:存储硬件配置的相关信息。   根据Windows操作系统版本的不同,选中的根键或子键的完整路径将显示在注册表编辑器的顶部或底部,如图11-1所示。下面的路径表示位于HKEY_CURRENT_USER根键中的Control Panel子键的Desktop子键。 HKEY_CURRENT_USER\Control Panel\Desktop   VBA内置了几个用于操作注册表的函数和语句,使用它们可以在注册表中的VB and VBA Program Settings子键中读取和写入数据。如果该子键不存在,则在使用VBA向注册表中写入数据时将自动创建该子键。VB and VBA Program Settings子键的完整路径如下: HKEY_CURRENT_USER\SOFTWARE\VB and VBA Program Settings   用于操作注册表的VBA内置函数和语句如下。 * SaveSetting语句:在注册表中写入数据。 * GetSetting函数:从注册表中读取特定键值。 * GetAllSettings函数:从注册表中读取特定子键中的所有键值。 * DeleteSetting语句:从注册表中删除特定子键及其中的键值。 11.1.2 使用SaveSetting语句将数据写入注册表   SaveSetting语句用于将数据写入注册表,该语句的语法如下: SaveSetting appname, section, key, setting * appname(必需):在VB and VBA Program Settings子键中创建的子键的名称,通常将该参数设置为使用VBA开发的程序的名称。 * section(必需):在由appname参数表示的子键中创建的子键的名称,通常将该参数设置为VBA程序中的某类设置的名称。 * key(必需):在由section参数表示的子键中创建的键值的名称。 * setting(必需):在由key参数表示的键值中写入的数据。   下面的代码使用VBA内置的InputBox函数创建一个对话框,然后将用户输入的用户名和密码写入注册表,如图11-3和图11-4所示。 Sub 将数据写入注册表() Dim strUserName As String, strPassword As String strUserName = InputBox("输入用户名:") strPassword = InputBox("输入密码:") SaveSetting "信息管理系统", "用户登录信息", strUserName, strPassword End Sub 图11-3 用户输入的用户名和密码 图11-4 将用户名和密码写入注册表   提示:如果在执行上述代码之前已经打开了注册表编辑器,为了在注册表编辑器中显示新写入的数据,需要按F5键刷新注册表。 11.1.3 使用GetSetting函数读取特定键值   GetSetting函数用于读取注册表中的特定键值,该函数的语法如下: GetSetting(appname, section, key, default)   GetSetting函数的前3个参数与SaveSetting语句相同,最后一个default参数是可选的,用于为GetSetting函数的返回值指定默认值。如果读取的键值不包含数据,则GetSetting函数返回default参数的值,省略该参数时返回零长度字符串。   下面的代码从注册表中读取在11.1.2小节的示例中写入注册表中的数据,并将读取到的数据添加到活动工作表中,如图11-5所示。 Sub 读取注册表中的特定键值() Dim strApp As String, strSection As String Dim strKey As String, varValue As String strApp = "信息管理系统" strSection = "用户登录信息" strKey = "admin" varValue = GetSetting(strApp, strSection, strKey) Range("A1:B1").Value = Array("用户名", "密码") Cells(2, 1).Value = strKey Cells(2, 2).Value = varValue ActiveSheet.UsedRange.HorizontalAlignment = xlCenter End Sub 图11-5 读取特定键值   注意:如果在GetSetting函数中指定的子键在注册表中不存在,则将出现运行时错误,可以使用On Error Resume Next语句屏蔽运行时错误。 11.1.4 使用GetAllSettings函数读取特定子键中的所有键值   GetAllSettings函数用于从注册表中读取特定子键中的所有键值,返回一个包含所有键值的名称和数据的二维数组,每一维的下限是0。GetAllSettings函数的语法如下: GetAllSettings(appname, section)   GetAllSettings函数的两个参数的含义与前面介绍的两个函数的同名参数相同。   下面的代码将注册表中名为“用户登录信息”的子键中的所有键值的名称和数据添加到活动工作表中,如图11-6所示。 Sub 读取注册表中特定子键包含的所有键值() Dim strApp As String, strSection As String Dim varData As Variant strApp = "信息管理系统" strSection = "用户登录信息" varData = VBA.GetAllSettings(strApp, strSection) Range("A1:B1").Value = Array("用户名", "密码") Range("A2").Resize(UBound(varData, 1) + 1, 2).Value = varData ActiveSheet.UsedRange.HorizontalAlignment = xlCenter End Sub 图11-6 读取特定子键中的所有键值   注意:如果在GetAllSettings函数中指定的子键在注册表中不存在,则将出现运行时错误,可以使用On Error Resume Next语句屏蔽运行时错误。 11.1.5 使用DeleteSetting语句删除注册表中的键值   DeleteSetting语句用于从注册表中删除特定子键及其包含的键值,该语句的语法如下: DeleteSetting appname, section, key   DeleteSetting语句的3个参数的含义与SaveSetting语句的前3个参数相同,不过DeleteSetting语句的第3个参数是可选的。如果省略第3个参数,则删除由section参数表示的子键及其中的所有键值。如果指定第3个参数,则只删除由该参数表示的键值。   下面的代码由用户决定是删除特定键值还是所有键值。如果用户输入字母Y,则删除“用户登录信息”子键中的所有键值;如果用户输入字母N,则将显示第二个对话框,用户需要在该对话框中输入要删除的键值名称,单击“确定”按钮,将删除由用户指定的键值,如图11-7所示。 Sub 删除注册表中的数据() Dim strApp As String, strSection As String Dim strKey As String, lngAnswer As Long strApp = "信息管理系统" strSection = "用户登录信息" lngAnswer = MsgBox("是否删除子键及其中的所有键值?", vbYesNo + vbQuestion) On Error Resume Next Select Case lngAnswer Case vbYes DeleteSetting strApp, strSection Case vbNo strKey = InputBox("输入要删除的键值名称:") If strKey = "" Then Exit Sub DeleteSetting strApp, strSection, strKey End Select End Sub 图11-7 删除注册表中的键值   注意:如果在DeleteSetting语句中指定的子键和键值在注册表中不存在,则将出现运行时错误,可以使用On Error Resume Next语句屏蔽运行时错误。 11.1.6 在所有工作表中同步显示或隐藏网格线   工作簿中的每个工作表的网格线的显示状态是相互独立的,如果希望所有工作表都显示或都隐藏网格线,则需要对工作簿中的每一个工作表重复相同的设置。利用注册表,可以使一个工作簿中的所有工作表同步显示或隐藏网格线。   下面的代码位于VBA工程的标准模块中,用于将活动工作表中网格线的当前显示状态保存到注册表中,如图11-8所示。运行该代码将显示一个对话框,单击“是”按钮,将在工作表中显示网格线;单击“否”按钮,将在工作表中隐藏网格线。无论单击哪个按钮,都会将相应的状态信息写入注册表的“是否显示网格线”键值中。由于键值所在的路径信息会在另一个VBA过程中使用,所以将存储路径信息的常量声明为模块级的。 Public Const strApp As String = "Excel" Public Const strSection As String = "网格线设置" Public Const strKey As String = "是否显示网格线" Sub 在所有工作表中同步显示或隐藏网格线() Dim strDisplay As String, lngAnswer As Long lngAnswer = MsgBox("是否显示工作表的网格线?", vbYesNo + vbQuestion) Select Case lngAnswer Case vbYes strDisplay = "是" ActiveWindow.DisplayGridlines = True Case vbNo strDisplay = "否" ActiveWindow.DisplayGridlines = False End Select SaveSetting strApp, strSection, strKey, strDisplay End Sub 图11-8 在所有工作表中同步显示或隐藏网格线   为了让其他工作表能够同步显示或隐藏网格线,需要在工作簿的Workbook_SheetActivate事件过程编写下面的代码,当激活任意一个工作表时,将从注册表中读取网格线的状态信息,并将其应用到激活的工作表中。 Private Sub Workbook_SheetActivate(ByVal Sh As Object) Dim strDisplay As String strDisplay = GetSetting(strApp, strSection, strKey) Select Case strDisplay Case "是" ActiveWindow.DisplayGridlines = True Case "否" ActiveWindow.DisplayGridlines = False End Select End Sub 11.2 自动控制其他Office应用程序   除了使用VBA编程控制Excel应用程序之外,还可以在Excel中编程控制其他Office应用程序,例如Word、PowerPoint和Access。这种可以控制其他Office应用程序的技术称为OLE(Object Linking and Embedding,对象链接与嵌入)自动化,并在后来演变为其他一些技术形式,例如COM(Component Object Model,组件对象模型)。本节将介绍自动化的基本概念以及前期绑定和后期绑定,并以在Excel中编程控制Word为例,介绍在Excel中控制其他Office应用程序的方法。 11.2.1 自动化的基本概念   在自动化技术中,将用于控制其他应用程序的程序称为“自动化客户端”,将被控制的应用程序称为“自动化服务器”。例如,在Excel中编程控制Word时,Excel是自动化客户端,Word是自动化服务器。   在一个应用程序中控制另一个应用程序关键有以下两点: * 建立对另一个应用程序的连接,可以使用前期绑定或后期绑定。 * 掌握另一个应用程序的对象模型。   只要建立与另一个应用程序的连接,即可在VBA中使用该应用程序对象模型中的对象及其属性和方法,从而编程控制该应用程序。   在第5章和第10章介绍字典对象和FSO对象模型时,曾简要介绍并使用了前期绑定和后期绑定。本章接下来的两个小节将详细介绍使用这两种技术建立对外部应用程序的连接方法。 11.2.2 前期绑定   前期绑定是指在程序运行前建立对外部应用程序的连接。前期绑定有以下几个优点: * 可以将变量声明为外部应用程序对象模型中的特定对象类型。 * 可以在对象浏览器中查看外部应用程序对象模型中包含的所有对象、属性和方法。 * 可以使用外部应用程序对象模型中的所有内置常量和命名参数。 * 为外部应用程序中的对象设置属性和方法时,可以从自动成员列表中选择属性和方法,而无须手动输入。 * 代码的运行速度比后期绑定快。   下面以在Excel中建立对Word的连接为例,介绍前期绑定的方法,操作步骤如下:   (1)在Excel中打开VBE窗口,单击菜单栏中的“工具”|“引用”命令。   (2)打开“引用”对话框,在“可使用的引用”列表框中勾选Word应用程序的类型库的复选框,如图11-9所示。 图11-9 勾选Word应用程序的类型库   提示:只有将Word应用程序正确安装到操作系统中,才会在“引用”对话框中显示其类型库。   (3)单击“确定”按钮,关闭“引用”对话框。按F2键,打开对象浏览器,在“工程/库”下拉列表中选择“Word”,将在下方的“类”列表框中列出Word对象模型中的所有对象(实际上是类),如图11-10所示。 图11-10 在对象浏览器中查看Word对象模型中的所有对象   (4)关闭对象浏览器,在一个VBA过程中声明所需的变量,可以将变量声明为Word对象模型中的任何对象类型。下面的代码将名为wdApp的变量声明为Word中的Application对象。 Dim wdApp As Word.Application   (5)使用Set语句和New关键字,或者使用CreateObject函数,将对象的一个实例赋值给变量。 Set wdApp = New Word.Application 或 Set wdApp = CreateObject("Word.Application")   接下来就可以在代码中使用Word对象模型中的Application对象的属性和方法了。   由于不同的Office应用程序的对象模型中包含一些同名对象,所以在声明外部应用程序对象模型中的对象时,应该添加类型库的名称。例如,Word.Application中的Word表示类型库,Application表示该类型库中的对象。   如果在声明变量时不想添加类型库的名称,则可以在“引用”对话框中单击或按钮来调整类型库的优先级,位置越靠上的类型库具有更高的优先级。例如,如果Word类型库的优先级高于Excel类型库,则在将变量声明为Range对象时,如果不添加类型库的名称,则声明的是Word类型库中的Range对象。 11.2.3 后期绑定   后期绑定是指在程序运行后才能建立对外部应用程序的连接,所以在代码编写阶段无法获悉该应用程序的对象模型。正因为如此,前期绑定的优点正好是后期绑定的缺点。   然而,后期绑定有一个显著的优点是,当需要将编写的VBA程序分发给其他用户使用时,无须在VBE中提前引用特定应用程序的类型库,这个优点是前期绑定无法比拟的。因为它为程序提供了最大的自动化和灵活性。首先,目标用户无须手动进入VBE并添加对类型库的引用;其次,可以根据目标用户的计算机中已安装的应用程序版本来选择要连接的版本。   下面仍然以在Excel中建立对Word的连接为例,介绍后期绑定的方法,操作步骤如下:   (1)在一个VBA过程中输入下面的代码,声明一个Object类型的变量。 Dim wdApp As Object   (2)使用CreateObject函数将外部应用程序对象模型中的顶层对象的一个实例,赋值给步骤(1)创建的变量。 Set wdApp = CreateObject("Word.Application")   如果计算机中安装了多个Word版本,如需连接到特定的Word版本,则可以在Word.Application的结尾添加一个英文句点和表示Word版本号的数字,例如Word.Application.16。 11.2.4 启动Word的一个新实例并创建文档   使用11.2.2小节或11.2.3小节中的方法,都会启动Word的一个新实例。如需在Word中创建一个文档,可以使用下面的代码。本小节及后几个小节中的示例使用的都是前期绑定,所以在运行代码前,需要在VBE中引用Word类型库。 Sub 启动Word的一个新实例并创建文档() Dim wdApp As Word.Application Dim wdDoc As Word.Document Set wdApp = New Word.Application Set wdDoc = wdApp.Documents.Add wdApp.Visible = True End Sub   创建的Word应用程序默认处于隐藏状态,如需使其可见,需要将Word的Application对象的Visible属性设置为True。   程序结束后,会自动释放wdApp变量和wdDoc变量占用的内存。如果在程序的后面还有其他代码,则可以使用下面的代码主动释放两个变量占用的内存。 Set wdApp = Nothing Set wdDoc = Nothing   如需退出Word应用程序,可以使用Word对象模型中的Application对象的Quit方法。 11.2.5 在已启动的Word中创建文档   如果当前已经启动了Word应用程序,如需在当前的Word中创建文档,而不是在一个新的Word中创建文档,则可以使用VBA内置的GetObject函数,该函数的语法如下: GetObject(pathname, class) * pathname(可选):在由class参数指定的应用程序中打开的文件的完整路径。如果将该参数设置为零长度字符串,则将创建应用程序的一个新实例并返回对它的引用;如果省略该参数,则将引用一个已启动到内存中的应用程序的实例。 * class(可选):应用程序的类型库和对象(类)的名称。省略该参数时,将使用与pathname参数指定的文件关联的应用程序打开该文件。   下面的代码将在当前已启动的Word中创建一个文档,如果当前没有启动Word,则显示一条信息并退出程序。 Sub 在已启动的Word中创建文档() Dim wdApp As Word.Application On Error Resume Next Set wdApp = GetObject(, "Word.Application") On Error GoTo 0 If wdApp Is Nothing Then MsgBox "当前没有启动Word" Exit Sub End If wdApp.Documents.Add wdApp.Visible = True End Sub 11.2.6 在Word中打开文档   下面的代码在当前已启动的Word中打开名为“测试”的文档。如果当前没有启动Word,则启动Word并打开该文档。 Sub 在Word中打开文档() Dim wdDoc As Word.Document, strFileName As String strFileName = "E:\测试数据\Word\测试.doc" On Error Resume Next Set wdDoc = GetObject(strFileName) On Error GoTo 0 If wdDoc Is Nothing Then MsgBox "未找到文档" Exit Sub End If wdDoc.Application.Visible = True End Sub   代码解析:为了使打开的文档显示在Word窗口中,需要将Application对象的Visible属性设置为True。由于本例声明的wdDoc变量是Word中的Document对象,为了引用Application对象,需要使用Document对象的Application属性。 11.2.7 将Excel工作表中的数据写入新建的Word文档   下面的代码将Excel活动工作表中的A1:D6单元格区域的数据复制到剪贴板,然后将剪贴板中的数据以表格的形式粘贴到一个新建的Word文档中,并将表格在文档页面中水平居中对齐,最后在Word窗口中显示该文档,如图11-11所示。 Sub 将Excel工作表中的数据写入新建的Word文档() Dim wdApp As Word.Application Range("A1:D6").Copy Set wdApp = New Word.Application With wdApp.Selection .Paste .WholeStory .Tables(1).Rows.Alignment = wdAlignRowCenter .EndKey wdStory End With Application.CutCopyMode = False wdApp.Visible = True End Sub 图11-11 将Excel工作表中的数据写入新建的Word文档 11.3 创建和使用类   Excel对象模型包含大量的对象,对于大多数用户来说,在VBA中编程操作这些对象已经足以完成在Excel中需要执行的几乎所有任务。然而,用户仍然可以创建新的对象,以满足任何可能的编程需求。本节将介绍使用类模块创建新的类和对象的方法,还将介绍类在处理多个同类型的控件和捕获应用程序事件方面的实际应用。 11.3.1 了解类和类模块   在Excel中编写VBA程序大多数时间都是在处理各类对象,每个对象都属于某个特定的类。声明对象变量时,As关键字后面的部分就是类的名称。例如,下面的代码声明一个Worksheet类型的变量,As关键字后面的Worksheet就是对象的类。 Dim wks As Worksheet   类是对象的基础模型,通过“类”可以创建一系列相同类型的对象,为这些对象设置不同的属性,可以使它们具有不同的外观和状态,从而在同类型的多个对象之间加以区分。基于类创建的每一个对象都是类的实例。   通过在VBA工程中插入类模块,用户可以创建自己的类,通过在类模块中编写代码来为类创建属性和方法。属性用于改变对象的外观和状态,方法用于为对象执行特定的操作。VBA工程中的ThisWorkbook模块、工作表模块、用户窗体模块都是类模块,只不过它们与用户自己创建的类模块有些区别。   除了可以使用类模块创建新的类和对象之外,类模块还用于完成以下几个任务: * 同时处理多个同类型的控件。 * 捕获和使用应用程序事件。 * 捕获和使用嵌入图表事件。 * 创建可被其他VBA工程重用的组件。 * 封装复杂的代码,例如调用API函数的过程。 11.3.2 创建新的类及其属性和方法   本小节将以一个示例为主,介绍如何创建新的类及其属性和方法。本小节创建的类及其属性和方法的相关信息如表11-1所示。       表11-1 类及其属性和方法的相关信息 名 称 类 型 说 明 Product 类 类的名称 Name 属性 设置或返回产品的名称 Price 属性 设置或返回产品的价格 Quantitiy 属性 或设置或返回产品的数量 AmountPay 方法 计算产品的金额:价格×数量   1. 创建基础的类   首先创建名为Product的类,操作步骤如下:   (1)在VBA工程中右击任意一项,然后在弹出的菜单中选择“类模块”命令,在该工程中插入一个类模块。   (2)在工程资源管理器中选择步骤(1)创建的类模块,然后按F4键,在属性窗口中将“(名称)”属性的值修改为“Product”,如图11-12所示。   修改后的类模块在VBA工程中将显示为如图11-13所示。接下来就可以在类模块的代码窗口中为Product类创建属性和方法了。 图11-12 修改类模块的“(名称)”属性 图11-13 修改名称后的类模块   2. 创建属性   本例需要为Product类创建Name、Price和Quantity三个属性,由于这3个属性都可用于设置值或返回值,所以需要在类模块代码窗口中的顶部使用Public关键字将它们声明为模块级变量。Name变量的数据类型是String,Price变量的数据类型是Double,Quantity变量的数据类型是Long。 Public Name As String Public Price As Double Public Quantity As Long   3. 创建方法   本例需要为Product类创建AmountPay方法,用于计算价格和数量的乘积。为了可以在VBA程序中使用AmountPay方法的计算结果,需要在类模块的代码窗口中使用Function过程来创建AmountPay方法,并在Function过程的开头添加Public关键字,Function过程的名称就是方法的名称。如果使用Sub过程创建方法,则该方法没有返回值。   下面的代码为Product类创建AmountPay方法: Public Function AmountPay() As Double AmountPay = Price * Quantity End Function   现在已经为Product类创建好了3个属性和1个方法,接下来可以使用该类创建对象,并在代码中使用对象的属性和方法执行具体的操作。 11.3.3 使用类创建和使用对象   本小节将使用11.3.2小节创建的类来创建一个对象,通过3个属性为创建的对象设置名称、价格和数量,然后使用AmountPay方法计算对象的金额,操作步骤如下:   (1)在VBA工程中插入一个标准模块,将模块的名称修改为“使用类创建和使用对象”。   (2)打开步骤(1)创建的标准模块的代码窗口,在其中创建一个名为“计算产品金额”的Sub过程,然后使用Dim语句声明一个名为clsProduct 的变量,该变量的类型是Product。输入As关键字并按空格键后,在列表中将会显示Product。 Dim clsProduct As Product   (3)声明变量后,需要使用Set语句和New关键字将Product类的实例赋值给clsProduct变量。 Set clsProduct = New Product   (4)为clsProduct变量的Name、Price和Quantitiy三个属性赋值。输入变量名和英文句点后,可以在弹出的列表中选择属性,如图11-14所示。 clsProduct.Name = "牛奶" clsProduct.Price = 2 clsProduct.Quantity = 10   (5)使用AmountPay方法计算Price属性和Quantitiy属性的乘积,将返回的金额显示在对话框中,如图11-15所示。 MsgBox clsProduct.Name & "的金额是:" & clsProduct.AmountPay & "元" 图11-14 在弹出的列表中选择属性 图11-15 使用创建的对象执行操作   完整的代码如下所示: Sub 计算产品金额() Dim clsProduct As Product Set clsProduct = New Product clsProduct.Name = "牛奶" clsProduct.Price = 2 clsProduct.Quantity = 10 MsgBox clsProduct.Name & "的金额是:" & clsProduct.AmountPay & "元" End Sub   可以使用With语句简化上述代码: Sub 计算产品金额2() Dim clsProduct As Product Set clsProduct = New Product With clsProduct .Name = "牛奶" .Price = 2 .Quantity = 10 MsgBox .Name & "的金额是:" & .AmountPay & "元" End With End Sub 11.3.4 使用Property过程创建可灵活控制的属性   虽然使用Public关键字创建的属性简单易用,但是只能简单地为属性赋值,无法对所赋的值进行更多的控制。使用Property过程可以通过编写代码检查和计算属性的值,这样能够以更加灵活的方式控制属性的值。Property过程有以下3种形式: * Property Get:返回属性的值。创建Property Get过程时需要为其返回值指定数据类型。 * Property Let:设置属性的值,需要至少包含一个参数,用于接收用户为属性设置的值。该过程中参数的数据类型必须与Property Get过程的返回值的数据类型相同。 * Property Set:与Property Let过程类似,但是用于处理对象。   使用Property过程创建类的属性时,需要在类模块中使用Private关键字声明模块级变量,以便在不同的Property过程之间传递数据,但是不能被其他模块使用。如果同时使用Property Let过程和Property Get过程创建一个属性,则既可以为属性赋值,又可以读取该属性的值。如果只使用Property Get过程创建一个属性,则只能读取该属性的值,不能为其赋值。   仍以11.3.3小节中的示例进行介绍,假设当购买的数量在10个以上时,超出10个的部分的价格按照原价的5折计算。此时在为Quantity属性赋值时,需要判断数量是否大于10,如果大于10,则需要将超过10的部分的价格乘以0.5,而10以内的价格仍然按照原价计算。   为了适应上述需求,需要修改Product类模块中的代码。保持原来的Name和Price两个变量的声明,但是不再声明Quantity变量,而是将其创建在Property Let和Property Get过程中,以便对其值进行所需的处理。还需要使用Private关键字声明两个模块级变量,它们用于在类模块中的各个过程之间传递10以内的数量和超过10的数量。在类模块中还需要使用Property Get过程创建Quantity10Down和Quantity10Up两个只读属性,它们的值只能由程序根据用户输入的数量是否大于10自动计算得到,不能手动为这两个属性赋值。最后需要修改AmountPay方法的计算方式,根据是否大于10,使用不同的价格进行计算,并将两个计算结果加在一起。 Public Name As String Public Price As Double Private Qty10Down As Long Private Qty10Up As Long Property Let Quantity(qty As Long) Qty10Down = WorksheetFunction.Min(10, qty) Qty10Up = WorksheetFunction.Max(0, qty - 10) End Property Property Get Quantity() As Long Quantity = Qty10Down + Qty10Up End Property Property Get Quantity10Down() As Long Quantity10Down = Qty10Down End Property Property Get Quantity10up() As Long Quantity10Up = Qty10Up End Property Public Function AmountPay() AmountPay = Qty10Down * Price + Qty10Up * Price * 0.5 End Function   现在可以修改11.3.3小节中的示例,为Quantity属性设置不同的值,例如15,价格是2,此时显示的计算结果是25,如图11-16所示。15=10+5,10与原价2的乘积是20。超出10的部分是5,按照原价的5折计算,此时价格变成1,5×1=5,最终的金额是20+5=25。   下面的代码通过只读属性返回享受5折价格的产品数量,如图11-17所示。 Sub 计算产品金额3() Dim clsProduct As Product, strMsg As String Set clsProduct = New Product With clsProduct .Name = "牛奶" .Price = 2 .Quantity = 15 End With strMsg = strMsg & "总金额是:" & clsProduct.AmountPay & "元" & vbCrLf strMsg = strMsg & "产品的总数量是:" & clsProduct.Quantity & vbCrLf strMsg = strMsg & "享受5折优惠的数量是:" & clsProduct.Quantity10up MsgBox strMsg End Sub 图11-16 根据数量按照不同价格计算金额 图11-17 使用只读属性返回值 11.3.5 同时处理多个同类型的控件   利用类模块,可以为多个同类型的控件编写统一的事件处理程序,单击其中的任意一个控件时,将执行相同或相似的操作。否则,需要分别为每一个控件编写相同的事件处理程序。   如图11-18所示,在用户窗体中有3个按钮,单击任何一个按钮时,将显示该按钮的标题。   (1)在VBA工程中插入一个用户窗体,然后在其中添加3个“命令按钮”控件,修改它们的Caption属性,如图11-18所示。      (2)在VBA工程中插入一个类模块。在属性窗口中将类模块的名称设置为cmdEvents,如图11-19所示。 图11-18 创建用户窗体和控件 图11-19 设置类模块的名称   (3)在cmdEvents类模块的代码窗口中输入下面的代码,使用WithEvents关键字声明一个用户窗体中的“命令按钮”控件的对象,该声明相当于为类模块创建了一个属性。 Public WithEvents fmCmd As MSForms.CommandButton   提示:如需处理其他类型的控件,例如“文本框”控件,则可以将As关键字右侧的控件类型修改为MSForms.TextBox。   (4)在类模块的代码窗口顶部的左侧下拉列表中选择步骤(3)创建的fmCmd,在右侧下拉列表中选择Click,然后在Click事件过程中输入下面的代码: Private Sub fmCmd_Click() MsgBox "单击的按钮是:" & fmCmd.Caption End Sub   (5)在用户窗体的代码窗口顶部的左侧下拉列表中选择UserForm,在右侧的下拉列表中选择Initialize,然后编写Initialize事件过程的代码。在用户窗体模块顶部的声明部分声明一个名为cmdButtons的对象数组变量,该变量的数据类型是前面创建的类模块的名称cmdEvents。 Private cmdButtons() As New cmdEvents Private Sub UserForm_Initialize() Dim ctl As Control, intCount As Integer For Each ctl In Me.Controls If TypeName(ctl) = "CommandButton" Then intCount = intCount + 1 ReDim Preserve cmdButtons(1 To intCount) Set cmdButtons(intCount).fmCmd = ctl End If Next ctl End Sub   完成上述操作后,运行用户窗体,单击其中的任意一个按钮,将显示该按钮的标题,如图11-20所示。 图11-20 同时处理多个同类型的控件 11.3.6 捕获应用程序事件   第7章介绍了工作簿和工作表的事件,它们分别用于处理工作簿中的任意一个工作表或特定的工作表。实际上,利用类模块可以触发应用程序级别的事件,这意味着在Excel中打开的任意一个工作簿都会响应该级别的事件。   本例将实现在Excel中打开任意一个工作簿时,在对话框中显示该工作簿的完整路径。操作步骤如下:   (1)在VBA工程中插入一个类模块,将其名称修改为appEvents。   (2)打开步骤(1)创建的类模块的代码窗口,在模块顶部输入下面的代码,使用Public关键字和WithEvents关键字声明一个Application类型的变量xlsApp。WithEvents关键字用于引发与Application对象相关的事件。 Public WithEvents xlsApp As Application   (3)在类模块的代码窗口顶部的左侧下拉列表中选择步骤(2)创建的xlsApp变量,在右侧的下拉列表中选择WorkbookOpen事件,然后在该事件过程中编写代码。 Private Sub xlsApp_WorkbookOpen(ByVal Wb As Workbook) MsgBox Wb.FullName End Sub   (4)在VBA工程中插入一个标准模块,打开其代码窗口,在模块顶部输入下面的代码,声明一个appEvents类型的变量。 Public clsApp As appEvents   提示:与ThisWorkbook模块和工作表模块不同,用户创建的类模块默认无法自动响应用户的操作,所以需要先创建类的实例。   (5)在标准模块中创建一个Sub过程,将appEvents类的实例赋值步骤(4)创建的变量,然后将Application对象赋值给clsApp变量代表的appEvents对象的xlsApp属性。 Sub 捕获事件() Set clsApp = New appEvents Set clsApp.xlsApp = Application End Sub   (6)运行一次步骤(5)创建的Sub过程,在保持该工作簿一直打开的情况下,以后在Excel中打开任意一个工作簿时,将自动显示该工作簿的完整路径。       第12章 为程序设计功能区界面和快捷菜单   微软从Excel 2007开始使用全新的功能区代替在Excel早期版本中一直使用的菜单栏和工具栏。虽然在Excel 2007及Excel更高版本中仍然可以使用VBA创建菜单栏和工具栏,但是它们只能显示在功能区的“加载项”选项卡中。使用VBA创建快捷菜单的方法及其显示方式并未改变,仍然与Excel早期版本相同。如需为Excel 2007及Excel更高版本定制功能区界面,则需要了解和编写RibbonX代码。本章将介绍使用RibbonX定制功能区和使用VBA定制快捷菜单的方法。 12.1 功能区开发基础   本节将介绍定制功能区之前需要了解的基础知识,包括功能区的结构、Excel文件的内部结构、定制功能区的流程和工具、功能区中的控件类型、控件属性、控件回调等内容。 12.1.1 功能区的结构   功能区位于Excel窗口标题栏的下方,是一个与Excel窗口等宽的矩形区域。功能区由选项卡、组和命令3个部分组成,如图12-1所示。单击选项卡顶部的标签,可以显示不同的选项卡。每个选项卡中的命令按照功能分为多个组,各个组的名称显示在选项卡的底部。 图12-1 Excel功能区   功能区中的命令有多种类型,按钮、编辑框、复选框、切换按钮、下拉列表、组合框、库和垂直分隔条等。在某些组的右下角显示按钮,将该按钮称为“对话框启动器”。单击该按钮可以打开一个对话框,其中包括该按钮所在组中的选项。将出现在功能区中的各种对象称为控件。 12.1.2 Excel文件的内部结构   从Excel 2007开始,微软为Excel文件提供了新的文件格式,每个Excel工作簿实际上由一组XML文件组成,这些文件被压缩到Zip容器中。与标准的文本文件相比,XML文件采用父、子层次结构描述文件的结构和内容。   如需查看Excel文件的内部结构,可以将Excel文件的扩展名.xlsx或.xlsm修改为.zip,也可以在Excel文件的扩展名之后添加.zip,如图12-2所示。按Enter键,将显示如图12-3所示的确认信息,单击“是”按钮,完成扩展名的修改。双击将扩展名修改为.zip后的压缩文件,将显示Excel文件的内部结构,如图12-4所示。 图12-2 修改Excel文件的扩展名 图12-3 修改文件扩展名时的确认信息 图12-4 Excel文件的内部结构 12.1.3 定制功能区的整体流程和工具   可以将定制功能区的整体流程分为以下两个阶段。   1. 编写代码   该阶段包括以下两个部分: * 在Excel工作簿中编写用于实现功能区中的控件功能的VBA代码。 * 在文本编辑工具中编写RibbonX代码,用于定制在功能区中包含哪些控件,以及这些控件的位置和外观,将包含RibbonX代码的文件保存为customUI.xml。   2. 在Excel文件内部为代码和功能区建立关联   该阶段包括以下几个部分: * 将包含VBA代码的Excel文件的扩展名修改为.zip,然后双击该文件,在其内部创建名为customUI的文件夹。 * 将包含RibbonX代码的customUI.xml文件添加到customUI文件夹中。 * 打开ZIP文件内部的_rels文件夹中的.rels.xml文件,修改该文件的内容,为RibbonX代码和功能区建立关联。 * 将.zip扩展名删除,恢复原来的Excel文件。   为了能够收到RibbonX代码出现错误时的反馈信息,需要在“Excel选项”对话框的“高级”选项卡中勾选“显示加载项用户界面错误”复选框,如图12-5所示。   编写RibbonX代码可以使用任何文本编辑工具,Windows操作系统中的记事本程序就是其中之一。编写RibbonX代码更易于使用的工具是Custom UI Editor,如图12-6所示,该工具有以下几个优点: * 自动验证代码的有效性,及时发现并修改错误的代码,确保代码能够正常工作。 * 使用不同颜色标识代码中的不同元素。 图12-5 勾选“显示加载项用户界面错误”复选框 * 自动为RibbonX代码和功能区建立关联。在Custom UI Editor中编写好RibbonX代码,然后保存并关闭在Custom UI Editor中打开的Excel文件。在Excel中打开该文件,将看到功能区外观上的变化。 图12-6 在Custom UI Editor中编写RibbonX代码 12.1.4 控件类型   编写RibbonX代码时,可以在功能区中添加两类控件:基本控件和容器控件。基本控件用于执行特定的操作,例如按钮、编辑框和复选框。容器控件用于为基本控件提供容器,这意味着可以将基本控件添加到容器控件的内部,例如下拉列表和组合框。   1. 基本控件   基本控件如表12-1所示,可以将这些控件添加到功能区的自定义组或容器控件中。 表12-1 基本控件 控 件 类 型 说 明 控件样式示例 通用控件类型 / “按钮”控件,单击该控件可执行指定的操作,可以同时显示图像和标题    续表 控 件 类 型 说 明 控件样式示例 “切换按钮”控件,可在按下和弹起两种状态之间切换 “编辑框”控件,可在编辑框中输入内容 “复选框”控件,可在勾选和取消勾选两种状态之间切换 “库”控件,用于提供一个下拉列表,其中包含其他类型的控件 “标签”控件,用于为其他控件提供标题 “垂直分隔条”控件,用于分隔组中的控件 “水平分隔条”控件,用于分隔菜单中的菜单项 “弹出菜单”控件,运行时使用回调为其提供内容 / “对话框启动器”控件,位于组的右下角 “选项”控件,用于为下拉列表或组合框提供选项 /   2. 容器控件   容器控件如表12-2所示,可以将基本控件或容器控件添加到容器控件中。 表12-2 容器控件 控 件 类 型 说 明 可包含的控件类型 内容 控制其他控件的布局 可以包含任何其他类型的控件 内容 将包含在其中的控件显示为一个组