第2章 掌握VBA编程语言
本章将介绍通用于Microsoft Office应用程序的VBA编程语言的基本概念、结构及其包含的语言元素。在VBA中进行对象编程的内容将在第3章进行详细介绍。本章和第3章是使用Excel VBA进行编程的基础,有必要认真学习和掌握。
2.1 与VBA进行简单的交互
VBA提供了与用户进行简单交互的两种方法,一个是使用MsgBox函数在屏幕上显示信息,另一个是使用InputBox函数接收用户输入的信息。MsgBox和InputBox都是VBA的内置函数。
2.1.1 使用MsgBox函数输出信息
在代码中使用MsgBox函数可以产生一个对话框,其中显示由用户指定的内容,可用于在程序运行期间显示阶段性的运行结果,或显示需要用户确认的操作提示消息。MsgBox函数的语法格式如下:
MsgBox(prompt[, buttons] [, title] [, helpfile, context])
□ prompt:必选,在对话框中显示的内容。
□ buttons:可选,在对话框中显示的按钮和图标的类型,可以只显示按钮,也可以同时显示按钮和图标。该参数的值见表2-1。
□ title:可选,在对话框标题栏中显示的内容。
□ helpfile、context:可选,表示帮助文件和帮助主题。
提示:“必选”是指必须要为其提供值的参数,“可选”是指可以省略其值的参数。本书后面在介绍其他VBA语言元素的语法格式时都会使用这种表述方式。
表2-1 buttons参数的值
案例2-1 只使用一个参数的MsgBox函数
下面的代码显示如图2-1所示的对话框,只为MsgBox函数提供了prompt参数的值,省略了其他参数。显示该对话框时代码会中断运行,直到用户单击“确定”按钮。
Sub MsgBox函数() MsgBox "删除当前工作表吗?" End Sub
图2-1对话框的标题栏中显示的是Excel默认的“Microsoft Excel”,用户可以将其替换为自己的内容。
案例2-2 使用两个参数的MsgBox函数
下面的代码显示如图2-2所示的对话框,标题栏中显示由用户指定的内容“确认信息”而非默认的“Microsoft Excel”,此时为MsgBox函数同时提供了prompt和title两个参数的值。
Sub MsgBox函数() MsgBox "删除当前工作表吗?", , "删除工作表" End Sub
图2-1 MsgBox函数产生对话框
图2-2 自定义对话框的标题
读者可能已经注意到,在上面的代码中两个参数值之间有两个逗号,这是因为当前只为MsgBox函数提供了第一参数prompt和第三参数title的值,而省略了第二参数的值。由于没有按正确顺序依次指定每一个参数的值,因此必须为省略的参数保留一个额外的逗号。
为了避免输入额外的逗号,还可以使用另一种称为“命名参数”的方法为MsgBox函数指定参数值。这种方法需要在参数值的左侧加上参数的名称,并将原来的等号“=”改为冒号+等号“:=”的形式。在后面第3章还会详细为读者介绍通过命名参数为对象的方法设置参数值的内容。
案例2-3 在MsgBox函数中使用命名参数
下面的代码显示的对话框与图2-1相同,但是由于在代码中使用参数名称来指定参数的值,因此可以按照任意顺序输入参数。
Sub MsgBox函数() MsgBox Title:="删除工作表", Prompt:="删除当前工作表吗?" End Sub
可以根据表2-1中列出的buttons参数的值改变对话框中默认显示的按钮和图标。
案例2-4 改变MsgBox对话框中的默认按钮和图标
下面的代码显示如图2-3所示的对话框,使用“是”和“否”按钮代替原来的“确定”按钮,还显示了“询问信息”图标。
Sub MsgBox函数() MsgBox "删除当前工作表吗?", vbYesNo + vbQuestion, "删除工作表" End Sub
图2-3 指定对话框中显示的按钮和图标类型
当用户单击案例2-4中的“是”或“否”按钮时,VBA通过MsgBox函数的返回值来确定用户单击的是哪个按钮。与在Excel工作表中输入函数可以得到计算结果类似,在VBA中使用函数也可以返回计算结果。前面几个案例演示的MsgBox函数的用法只是显示了一个对话框而不包含返回值。
如果希望获取MsgBox函数的返回值,则需要将MsgBox函数赋值给一个变量,使用该变量保存MsgBox函数的返回值,此时必须将MsgBox函数的所有参数放置在一对圆括号中,否则会出现编译错误。之后可以使用If判断语句将包含返回值的变量与表2-2中列出的MsgBox函数的返回值进行比较,以判断用户单击的是哪个按钮,从而进一步执行所需的操作。
表2-2 MsgBox函数的返回值
案例2-5 判断用户单击的MsgBox对话框中的按钮
下面的代码对用户单击的按钮进行判断,并根据判断结果执行不同的操作。首先将MsgBox函数的返回值保存到iAnswer变量中,然后使用If语句判断iAnswer变量的值是否等于7,如果等于7则说明用户单击了对话框中的“否”按钮,此时不会执行任何后续操作并直接退出当前Sub过程。否则说明用户单击了对话框中的“是”按钮,将执行删除当前工作表的操作。
Sub MsgBox函数() Dim lngAnswer As Long lngAnswer = MsgBox("删除当前工作表吗?", vbYesNo + vbQuestion, "删除工作表") If lngAnswer = 7 Then Exit Sub ActiveSheet.Delete End Sub
注意:为了简化代码的复杂度,在上面的代码中没有包含判断工作表数量的代码。如果当前工作簿中只有一个工作表,运行上面的代码将会产生运行时错误。
如果愿意,也可以不将MsgBox函数的返回值指定给变量,而是直接将其与MsgBox函数的返回值列表进行比较,因此上面的代码也可以改为以下形式:
Sub MsgBox函数2() If MsgBox("删除当前工作表吗?", vbYesNo + vbQuestion, "删除工作表") = 7 Then Exit Sub ActiveSheet.Delete End Sub
只有对MsgBox函数的返回值列表非常熟悉的用户,才能理解前面案例中的数字7代表用户单击了对话框中的“否”按钮,否则可能很难明白数字7的含义。为了使代码更具可读性,可以使用表2-2第一列中的常量代替相应的数字值。比如前面案例中的数字7可以使用vbNo常量代替。
有时可能需要将对话框中的内容分多行显示,可以在代码中需要换行的位置插入vbCrLf或vbNewLine常量来实现此目的。
案例2-6 在对话框中将信息分多行显示
下面的代码显示如图2-4所示的对话框,内容分别显示在3行中,其中多次使用strMessage变量来存储不同行的内容,并将它们拼接在一起。“&”符号用于将两部分内容连接起来。
Sub MsgBox函数() Dim strMessage As String strMessage = "是否删除当前工作表?" & vbCrLf strMessage = strMessage & "删除请单击【是】按钮" & vbCrLf strMessage = strMessage & "不删除请单击【否】按钮" MsgBox strMessage, vbYesNo + vbQuestion, "删除工作表" End Sub
图2-4 将内容分多行显示
2.1.2 使用InputBox函数输入信息
使用VBA内置的InputBox函数可以产生一个允许用户输入内容的对话框,并以字符串的形式返回该内容。即使在对话框中输入的是数字,InputBox函数的返回值仍然是字符串类型。InputBox函数的语法格式如下:
InputBox(prompt[, title] [, default] [, xpos] [, ypos] [, helpfile, context])
□ prompt:必选,在对话框中显示的提示性内容,用于提醒用户需要输入什么样的内容。
□ title:可选,在对话框的标题栏中显示的内容。
□ default:可选,在接收输入的文本框中显示的默认值,如果用户不输入任何内容,则将返回该默认值。
□ xpos、ypos:可选,对话框左上角在屏幕上的坐标值。
□ helpfile、context:可选,帮助文件和帮助主题。
案例2-7 使用InputBox函数接收用户的输入
下面的代码显示如图2-5所示的对话框,要求用户在文本框中输入用户名。为了让程序可以处理用户输入的内容,需要将InputBox函数的返回值赋值给一个变量,然后在后面的代码中通过处理这个变量来操作用户输入的内容,本例是使用MsgBox函数在对话框中显示用户名。
Sub InputBox函数() Dim strInput As String strInput = InputBox("请输入用户名:") MsgBox "用户名是:" & strInput End Sub
图2-5 InputBox函数产生的对话框
如果在对话框中未输入任何内容而单击“确定”按钮,或直接单击“取消”按钮,InputBox函数都会返回一个零长度的字符串。可以使用If判断语句对InputBox函数的返回值进行检测来处理这种情况。下面的代码假设strInput变量中存储了InputBox函数的返回值,在If判断语句中检测该变量是否是零长度字符串(在双引号中不能包含空格),如果是则退出当前的Sub过程,这样后面的程序就不会对毫无意义的空字符串进行处理了。
If strInput = "" Then Exit Sub
提示:VBA内置的InputBox函数不能限制用户输入的数据类型,而Excel对象模型中的Application对象的InputBox方法则可以加以限制,第13章将对此进行详细介绍。
2.2 数据类型、变量和常量
我们在前面的案例中已经接触过变量和常量,它们是VBA代码的重要组成元素。变量和常量都用于在代码中存储数据,它们之间的主要不同之处在于,变量中存储的数据可以在代码运行过程中随时改变,而常量中存储的数据在代码运行过程中通常是固定不变的。由于数据可以分为不同的类型,而数据通常存储在变量和常量中,因此变量和常量也具有相应的数据类型。
2.2.1 VBA中的数据类型
Excel允许用户在工作表中输入不同类型的数据,比如整数“168”、小数“3.5”、中文字符“编程”、英文字符“Excel”、日期“2018年3月”、逻辑值“True”和“False”等。在VBA中同样可以处理这些类型的数据,而且还对数据类型进行了更细致的划分。
计算机以不同的方式存储不同类型的数据,存储文本的方式与存储数字不同,整数与小数的存储方式也不相同。不同类型的数据会占用不同大小的内存空间。表2-3列出了VBA支持的数据类型、取值范围以及占用的内存空间。
表2-3 VBA支持的数据类型、取值范围与占用的内存空间
表2-3中第一列的数据类型主要用于变量和常量的声明中,即在声明变量和常量时指明它们可以存储的数据类型。变量和常量的声明会在本章后面的内容中进行介绍。
2.2.2 声明变量
变量是一些位于计算机内存中已经命名的存储位置。在程序中使用变量可以存储随时可能发生变化的数据。VBA允许不事先声明变量就可以在程序中使用这个变量,此时变量的数据类型被默认指定为Variant,具有这种数据类型的变量可以存储任何类型的数据,缺点是比其他数据类型需要占用更多的内存空间,运行效率低。
如果知道要在变量中存储哪种类型的数据,那么应该在使用该变量之前预先将其声明为要使用的数据类型,这样可以让数据存储在与其匹配的具有适当内存大小的变量中,而不会浪费内存空间,而且也可以提高程序的运行效率。例如,在程序中要使用一个其值可能在100~10 000的数字,由于该范围位于Integer数据类型中,因此如果此时将存储该数字的变量声明为Long数据类型就会浪费内存空间。
在前面的案例中曾经遇到过声明变量的例子,它们以Dim关键字开头。下面的代码声明了一个名为strUserName的变量,该变量的数据类型是String,用于存储文本(字符串)。
Dim strUserName As String
提示:关键字用于标识VBA中的特定语言元素,比如语句名、函数名、运算符等,是VBA中的保留字,用户不能使用关键字作为变量的名称。
下面的代码声明了两个String数据类型的变量:
Dim strMyName As String Dim strYourName As String
为了减少代码的行数,可以在一条Dim语句中声明多个变量。无论这些变量的数据类型是否相同,各变量之间必须以逗号分隔。下面的代码在同一行声明了两个String数据类型的变量:
Dim strMyName As String, strYourName As String
下面的代码只将第二个变量声明为String数据类型,而第一个变量的数据类型是Variant。这个案例说明在同一行声明多个变量时,必须明确指定每个变量的数据类型。
Dim strMyName, strYourName As String
提示:除了可以使用Dim关键字声明变量之外,还可以使用Public、Private和Static关键字,它们的区别在于声明的变量具有不同的作用域和生存期。变量的作用域和生存期将在2.2.4节进行详细介绍。
虽然VBA允许用户直接使用变量而不需要预先声明,但是便捷的同时也容易出现问题。例如,在下面的代码中由于误将变量dRate拼写为dRata,因此程序返回了错误的结果。在包含大量代码的程序中,这种错误很容易出现并且难以发现。
Sub 变量名拼写错误() intTotal = 100 intTotal = intTotal + 10 MsgBox intTotel End Sub
在默认情况下,VBA会将程序中任何无法识别为关键字的单词看作是新的变量。为了避免由于拼写错误而导致意外地创建新的变量,可以让VBA强制变量声明。一旦检测到未经声明就直接使用的变量,VBA会自动显示编译错误的提示消息,并自动选中未声明的变量,如图2-6所示。
图2-6 VBA自动检测未声明的变量
可以使用以下两种方法让VBA强制变量声明:
□ 单击VBE窗口菜单栏中的“工具”|“选项”命令,打开“选项”对话框,在“编辑器”选项卡中选中“要求变量声明”复选框,如图2-7所示。
□ 将Option Explicit语句放置在模块顶部的声明部分,即模块中所有过程的最上方。
在上面两种方法中,第一种方法对已经存在的模块无效,此时必须手动将Option Explicit语句添加到已经存在的每个模块顶部的声明部分中。
声明后的每个变量都有一个初始值,不同数据类型的变量具有不同的初始值。Integer、Long、Single、Double等数值数据类型的变量的初始值是数字0,String数据类型的变量的初始值是空字符串、Boolean数据类型的变量的初始值是逻辑值False。
图2-7 选中“要求变量声明”复选框
只有将特定的数据存储到变量中,变量才变得有意义。将数据存储到变量的过程称为“为变量赋值”。要为一个变量赋值,需要先输入该变量的名称,然后在其右侧输入一个等号,再在等号的右侧输入要为其赋值的数据。下面的代码将数字100赋值给intCount变量。
intCount = 100
下面的代码将文本“销售数据”赋值给strFileName变量。
strFileName = "销售数据"
2.2.3 变量的命名规则
并不是所有内容都可以作为变量的名称,在为变量命名时需要遵守一些既定的规则,具体如下:
□ 变量的第一个字符必须使用英文字母或汉字。
□ 在变量名中可以使用数字和下画线,但是不能使用空格、句点、叹号等符号。
□ 变量名的字符长度不能超过255个字符。
□ 不能使用VBA中的关键字作为变量名。
除了上面列出的针对变量名自身的严格限定之外,为了使代码更具可读性,便于开发人员自己和其他开发人员理解,在为变量命名时还应该包含表示数据类型的前缀,这样通过变量名的前缀就可以快速了解变量的数据类型。可以从数据类型标识符中取1~3个字符来作为变量名的前缀,表2-4列出了建议的前缀及其对应的数据类型。
表2-4 用于表示数据类型的前缀
变量可以在一个过程、一个模块或整个工程中使用,变量的不同使用范围称为变量的作用域。为了可以通过变量名了解到变量的作用域,可以使用一个表示作用域的字母加在数据类型前缀的前面。使用字母g表示工程级,字母m表示模块级,不使用字母则表示过程级。例如,下面的代码分别声明了具有不同作用域的两个String数据类型的变量,除了第一个字母不同,名称中的其他部分相同。
Dim gstrFileName As String Dim mstrFileName As String
2.2.4 变量的作用域和生存期
变量的作用域决定了变量的可用范围,分为过程级、模块级、工程级3种。变量的生存期决定了数据在变量中能够保存多长时间。下面将分别介绍3种不同级别的变量声明方式、可用范围和保存数据的时长。
1.过程级变量
过程级变量是指在过程内部声明的变量。可以使用Dim或Static关键字声明过程级变量。过程级变量只能在其所在的过程内部使用,这意味着可以在不同的过程中声明具有相同名称的变量。过程运行结束后,过程级变量中保存的数据会被自动清空并恢复为初始值。
案例2-8 使用过程级变量
下面的代码说明了声明过程级变量的方式,以及过程运行期间对过程级变量中的数据的影响。这两个过程都包含同名变量intTotal。运行第一个过程后,intTotal变量的值为1,而第二个过程运行结束后,intTotal变量的值为5。这说明虽然两个变量的名称相同,但是由于它们是分别在不同的过程中声明的,因此它们只能在所属的过程内部使用,对其他过程无效。
Sub 过程级变量() Dim intTotal As Integer intTotal = intTotal + 1 End Sub Sub 过程级变量2() Dim intTotal As Integer intTotal = intTotal + 5 End Sub
上面两个过程无论运行多少次,两个变量的值始终都是1和5,这是因为过程在每次开始运行时,都会将变量的值初始化为0,这也意味着过程运行结束后,变量中的当前值不会被一直保留。如果希望过程运行结束后可以一直保留变量中的值,则需要使用Static关键字声明变量。
下面的两个过程使用Static关键字声明变量,第一次运行这两个过程时,其中的intTotal变量的值分别为1和5。再次运行这两个过程时,intTotal变量的值分别为2和10。这是因为第一次运行两个过程后,intTotal变量的值1和5被保留下来,并作为该变量在第二次运行的两个过程中的初始值。在工作簿被打开期间,使用Static关键字声明的过程级变量中存储的值会一直保留着。
Sub 过程级变量() Dim intTotal As Integer intTotal = intTotal + 1 MsgBox intTotal End Sub Sub 过程级变量2() Dim intTotal As Integer intTotal = intTotal + 5 MsgBox intTotal End Sub
2.模块级变量
如果在模块顶部的声明部分,即位于模块中的所有过程的最上方,使用Dim、Static或Private关键字声明的变量就是模块级变量。模块级变量可被其所在模块中的任意一个过程使用。在工作簿被打开期间,模块级变量中存储的值会一直保留着。
案例2-9 使用模块级变量
下面的代码声明了一个模块级变量,位于该模块中的两个过程都可以使用该变量。运行第一个过程后,intTotal变量的值为1。由于intTotal变量是模块级的,在运行第二个过程时该变量的当前值可以被直接使用,因此运行第二个过程后,intTotal变量的值为6而不是5。
Option Explicit Dim intTotal As Integer Sub 模块级变量() intTotal = intTotal + 1 MsgBox intTotal End Sub Sub 模块级变量2() intTotal = intTotal + 5 MsgBox intTotal End Sub
如果同时声明了两个名称相同的模块级变量和过程级变量,那么包含该过程级变量的过程只会使用该过程级变量,而不会使用模块级变量。
3.工程级变量
如果希望变量可以在当前工程的所有模块的所有过程中使用,则需要在模块顶部的声明部分使用Public关键字声明变量。下面的代码声明了一个工程级变量,变量中存储的数据在工作簿被打开期间始终可用。
Public strAppName As String
通常应该在VBA的标准模块中声明工程级变量。如果是在工作簿或工作表模块顶部的声明部分使用Public关键字声明变量,当在其他模块中使用该变量时,需要在变量前添加工作簿或工作表对象名称的引用。例如,在ThisWorkbook模块顶部的声明部分输入下面的代码声明一个变量:
Public strAppName As String
当在工作表模块或标准模块的任意过程中为该变量赋值时,需要使用下面的格式:
ThisWorkbook.strAppName = "工资管理系统"
图2-8 在VBA中引用其他工作簿
VBA工程中的工程级变量还可被其他工作簿使用,只需激活其他工作簿对应的VBA工程,然后单击VBE窗口菜单栏中的“工具”|“引用”命令,打开“引用”对话框,在列表框中选中包含要引用的变量所在的工作簿对应的VBA工程名的复选框,如图2-8所示。如果要引用的工作簿当前未打开,则需要单击“浏览”按钮找到并打开这个工作簿。在列表框中选择好要引用的VBA工程后,单击“确定”按钮。
2.2.5 使用常量
VBA有很多内置的固有常量,比如前面介绍MsgBox函数时使用过的vbYesNo和vbQuestion,这些VBA内置常量可以在整个VBA中使用。除了内置常量,用户还可以创建自己的常量,在常量中保存固定不变且需要在程序中频繁使用的数值或文本。声明常量需要使用Const关键字。下面的代码声明了一个名为Pi的常量,并将圆周率的值保存到该常量中。
Const Pi = 3.14159265
声明常量时也可以指定常量的数据类型。下面的代码声明了一个名为AppName的String数据类型的常量:
Const AppName As String = "工资管理系统"
常量的命名规则和作用域与变量相同,这里不再赘述。
2.3 表达式和运算符
表达式可以由变量、常量、函数、运算符等多种类型的内容组成,用于执行数学计算、处理文本或测试数据。下面是一个表达式的例子,它曾在前面的案例中出现过,该表达式返回intTotal变量的当前值与数字5之和,并将计算结果赋值给intTotal变量。
intTotal = intTotal + 5
表达式返回的结果分为多种类型,可以是一个数字,也可以是一个文本,还可以是一个日期或逻辑值。无论哪种类型的表达式,其中通常都会包含一个或多个运算符,用于连接表达式的各个部分,并决定着表达式要执行的运算类型和运算顺序。VBA包括以下4类运算符:
□ 连接运算符:连接两个或多个内容,从而组成一个包含复杂内容的字符串。
□ 算术运算符:进行常规的数学运算,比如加法、减法、乘法和除法等。
□ 比较运算符:对给定的两部分内容进行比较,由比较运算符组成的表达式返回的结果是逻辑值True或False。比较运算符主要在If判断语句的条件部分中使用。
□ 逻辑运算符:将多个由比较运算符组成的表达式组合在一起,以形成更复杂的判断条件。由逻辑运算符组成的表达式返回的结果是逻辑值True或False。
当一个表达式包含多个运算符时会涉及运算顺序的问题,运算顺序由运算符的优先级决定。当表达式包含不同类型的运算符时,首先计算算术运算符,然后计算比较运算符,最后计算逻辑运算符。表2-5列出了算术运算符、比较运算符和逻辑运算符,其中的算术运算符和逻辑运算符是按照优先级从高到低的顺序进行排列的,而所有的比较运算符具有相同的优先级。当表达式包含多个相同级别的运算符时,运算顺序按照表达式各部分的排列的位置从左到右进行。
表2-5 算术运算符、比较运算符和逻辑运算符
连接运算符的优先级位于所有算术运算符之后,并位于所有比较运算符之前。&和+是VBA中的两个连接运算符,在实际应用中最好使用&,因为+在用于连接字符串时有可能会执行加法操作而不是连接操作,具体执行哪种操作由待连接的两部分内容决定。
案例2-10 使用+运算符
下面的代码说明了+运算符的用法。第一个过程的运行结果是15而不是6,这是因为要连接的两部分内容都是String数据类型,虽然两部分内容都是数字,但是由于它们被包围在双引号中,因此VBA会将它们视为字符串,所以此时的+运算符起到字符串连接功能。第二个过程的运行结果是6,这是因为虽然数字5使用双引号包围起来会被VBA视为String数据类型,但是由于intNumber变量中的1没有被包围在双引号中,因此它是Integer数据类型,两部分使用+运算符将会执行加法操作,而不是单纯的字符串连接。
Sub 连接运算符() Dim intNumber intNumber = "1" MsgBox intNumber + "5" End Sub Sub 连接运算符2() Dim intNumber intNumber = 1 MsgBox intNumber + "5" End Sub
上面的案例说明了只要参与运算的两部分内容的其中之一是数值类型的数据,+运算符就会执行加法运算而非字符串连接。如果待连接的两部分内容中的其中之一是文本而不是数字,那么在使用+运算符进行连接时需要格外小心。如果其中一部分内容是文本,而另一部分是加了双引号的数字,那么+运算符可以正常连接它们,此时与使用&运算符具有相同的效果。但是如果一部分内容是文本,另一部分内容是没有加双引号的数字,那么在使用+运算符连接这两部分内容时将会出现类型不匹配的错误,正如下面的代码所示:
Sub 连接运算符3() Dim intNumber intNumber = 1 MsgBox intNumber + "hello" End Sub
虽然运算符有自己默认的优先级,但是可以通过使用圆括号强制改变运算符的默认优先级,以便优先计算表达式中低优先级的部分。运算时总会先执行圆括号中的运算,然后才会执行圆括号外的运算,位于圆括号中的运算仍然会按照运算符的优先级顺序进行计算。下面的代码使用圆括号改变了默认的运算顺序,此时会先计算圆括号中的加法,然后才会计算圆括号外的乘法,因此最终的计算结果为30,如果不使用圆括号则计算结果为26。
intTotal = (1 + 5) * 5
2.4 创建Sub过程
过程是组织和运行一组VBA代码的逻辑单元。VBA中包括Sub过程(子过程)、Function过程(函数过程)、事件过程和属性过程。事件过程实际上属于Sub过程,只不过它依附于特定的对象。本节和2.5节主要介绍创建Sub过程和创建Function过程,第11章将会介绍属性过程,第12章将会介绍事件过程。
2.4.1 声明Sub过程
Sub过程是VBA中最常使用的一类过程,在Excel中录制的宏就是Sub过程。使用过程的主要原因之一是为了简化大量代码堆积在一起导致的复杂程度。可以将实现一个程序中每个独立小功能的代码分别放入不同的过程中,确保每个过程只实现单一的简单功能,最后在一个过程中依次调用包含独立小功能的各个过程。这种利用过程来组织程序的方式使编写和调试代码变得更加简单高效。
一个Sub过程由Sub语句开始,End Sub语句结束,在这两个语句之间放置所需的VBA代码。声明Sub过程的语法格式如下:
[Private | Public] [Static] Sub name [(arglist)] [statements] [Exit Sub] [statements] End Sub
□ Private:可选,表示声明的是一个私有的Sub过程,只有在该过程所在的模块中的其他过程可以访问该过程,其他模块中的过程无法访问该过程。
□ Public:可选,表示声明的是一个公共的Sub过程,所有模块中的所有其他过程都可以访问该过程。如果在包含Option Private Module语句的模块中声明该过程,即使该过程使用了Public关键字,也仍然会变为私有过程。
□ Static:可选,Sub过程运行结束后保留过程中所使用的变量的值。
□ Sub:必选,表示Sub过程的开始。
□ name:必选,Sub过程的名称,与变量的命名规则相同。
□ arglist:可选,一对圆括号中可以包含一个或多个参数,这些参数用于向Sub过程传递数据以供Sub过程处理,各参数之间以逗号分隔。如果过程不包含任何参数,则必须保留一对空括号。
□ statements:可选,Sub过程中包含的VBA代码。
□ Exit Sub:可选,中途退出Sub过程。
□ End Sub:必选,表示Sub过程的结束。
模块中可以包含任意多个Sub过程,在每个Sub过程中放置用于完成不同功能的VBA代码。一些代码需要放置到模块顶部的声明部分中,即位于所有过程的最上方,这些代码包括模块级变量的声明、用户自定义数据类型、Option Base语句等。
我们可以使用两种方法声明一个Sub过程:手动输入和“添加过程”对话框,下面将分别进行介绍。
1.手动输入法声明Sub过程
打开VBE窗口,在指定的VBA工程中插入一个模块,打开该模块的代码窗口,然后输入关键字Sub和过程的名称如test,按Enter键后Excel会自动添加End Sub语句。创建好的Sub过程如下所示,接下来可以在Sub和End Sub之间添加所需的VBA代码。
Sub test()
End Sub
2.“添加过程”对话框法声明Sub过程
除了手动输入Sub和End Sub语句之外,还可以使用“添加过程”对话框来声明Sub过程,而且该方法还可用于创建Function过程和属性过程。单击VBE窗口菜单栏中的“插入”|“过程”命令,打开“添加过程”对话框,如图2-9所示。在“名称”文本框中输入Sub过程的名称,然后在“类型”区域中选择“子程序”以创建Sub过程,最后单击“确定”按钮。
图2-9 使用“添加过程”对话框创建Sub过程
2.4.2 Sub过程的作用域
Sub过程的作用域与变量类似,但是只分为模块级和工程级两种。对于Sub过程而言,可以将模块级的Sub过程称为私有过程,将工程级的Sub过程称为公有过程。区分Sub过程是私有过程还是公有过程的最直接方法,是在Sub语句之前是否包含Public或Private关键字,以Public关键字开头的Sub过程是公有过程,以Private关键字开头的Sub过程是私有过程。如果既没有Public关键字也没有Private关键字,而是直接以Sub关键字开头,那么该过程是公有过程。
在下面的三个过程中,前两个过程是公有过程,第三个过程是私有过程。
Public Sub 公有过程() MsgBox "这是一个公有过程" End Sub Sub 公有过程2()
MsgBox "这也是一个公有过程" End Sub
Private Sub 私有过程() MsgBox "这是一个私有过程" End Sub
如果使用2.4.1节介绍的第二种方法声明Sub过程,则可以在“添加过程”对话框的“范围”区域中选择过程的作用域。在该对话框中选择“把所有局部变量声明为静态变量”选项相当于使用Static关键字声明过程,“局部变量”指的就是过程级变量。
VBA中的大多数过程都是公有过程,用户在标准模块中创建的Sub过程通常都是公有过程,录制的宏也是公有过程,而在诸如ThisWorkbook、Sheet等类模块中的工作簿和工作表的事件过程都是私有过程。公有过程可以被其所在的工程中的任何模块中的任何过程调用,私有过程只能被其所在的模块中的其他过程调用,而不能被其他模块中的过程调用。
提示:如果不希望让创建的Sub过程显示在“宏”对话框中,而只想在VBA代码中进行调用,则需要将该Sub过程创建为私有过程,或者在创建该Sub过程时为其提供参数。
2.4.3 在VBA中调用Sub过程
将功能复杂的程序分解成包含多个功能相对独立的Sub过程,可使程序的结构更清晰,也会使代码的编写和维护更容易。可能会有这样一些Sub过程,它们完成的是一些通用的操作,这些操作会在其他多个Sub过程所完成的任务中用到,比如打开文件的操作。此时在这些Sub过程中只要调用包含打开文件这一操作的Sub过程,就可以实现打开文件的操作,而不必重复编写打开文件的VBA代码。可以使用以下几种方法调用Sub过程:
□ 直接输入过程的名称。如果过程包含参数,则需要输入过程的名称及其参数。如果过程包含多个参数,则需要在参数之间使用逗号进行分隔。
□ 输入Call关键字,然后输入过程的名称。如果过程包含参数,则需要输入过程的名称及其参数,并将所有参数放置到一对圆括号中,参数之间以逗号分隔。
□ 使用Excel中的Application对象的Run方法运行过程,过程的名称以字符串的形式作为Run方法的参数,过程的参数与过程的名称之间需要使用逗号进行分隔,所有参数不需要放置到一对圆括号中。
为了避免表述混乱,可以将调用某个Sub过程的过程称为主调过程,将被其他过程调用的Sub过程称为被调过程。
案例2-11 调用过程
下面的代码使用第一种方法调用名为“确认退出”的Sub过程,其中名为“主过程”的Sub过程是主调过程,名为“确认退出”的Sub过程是被调过程。
Sub 主过程() 确认退出 End Sub
Sub 确认退出() MsgBox "是否退出程序?", vbYesNo + vbQuestion, "退出程序" End Sub
可以使用Call语句来调用Sub过程,与直接使用过程名的调用方法具有相同的效果,如下所示:
Call 确认退出
案例2-12 调用包含参数的过程
下面的代码说明了调用包含参数的Sub过程的方法。假设前面案例中的“确认退出”过程包含两个参数,分别用于指定对话框的标题和内容,而对话框中的按钮类型已在名为“确认退出”的被调过程中指定。可以看出,为Sub过程提供参数可以为用户提供更大的灵活性,在本例中可以让用户自定义在对话框中显示的提示消息和标题的具体内容。
Sub 主过程() 确认退出 "退出吗?", "退出" End Sub
Sub 主过程2() Call 确认退出("退出吗?", "退出") End Sub
Sub 确认退出(varPrompt, varTitle) MsgBox varPrompt, vbYesNo + vbQuestion, varTitle End Sub
案例2-13 使用Application.Run方法调用过程
下面的代码使用Application.Run方法代替Call语句调用Sub过程:
Sub 主过程() Application.Run "确认退出", "退出吗?", "退出" End Sub
Sub 确认退出(varPrompt, varTitle) MsgBox varPrompt, vbYesNo + vbQuestion, varTitle End Sub
前面介绍的过程调用方式都是主调过程与被调过程位于同一个模块的情况,这种调用方式同样适用于它们位于不同模块的情况。但是以下两种例外情况需要注意。
(1)主调过程与被调过程位于同一个模块中,在另一个模块中还存在一个同名的被调过程。
在这种情况下,如果使用前面介绍的方法调用被调过程,那么调用的是与主调过程在同一个模块中的被调过程。如果想要调用的是位于另一个模块中具有相同名称的被调过程,则需要在调用时输入模块名以限定被调过程的来源。
案例2-14 调用位于不同模块中的过程
工程中包含名为“模块1”和“模块2”的两个模块,在模块1和模块2中都有一个名为“确认退出”的被调过程,模块1中还有一个名为“主过程”的主调过程。如果要在主调过程中调用模块2中的被调过程,则需要先输入被调过程所在的模块名,然后输入一个句点,再输入被调过程的名称或从自动弹出列表中选择被调过程。也可以使用Call语句进行调用,但是后续输入方法相同。如图2-10所示显示了代码窗口中的过程调用情况。
Sub 主过程() 模块2.确认退出 End Sub
图2-10 通过使用模块名来限定被调过程的来源
(2)主调过程与被调过程位于不同的模块中,但它们具有相同的名称。
在这种情况下调用被调过程将会出现运行时错误,此时需要使用与第(1)种情况相同的方法来处理,即在调用被调过程时为其添加模块名以明确指定其来源。
以上介绍的方法调用的都是位于同一个工程中的Sub过程。也可以调用位于其他工程中的过程,此时可以添加对包含被调过程的工作簿的引用,方法与2.2.4节介绍的跨工作簿引用工程级变量类似。通过VBE窗口菜单栏中的“工具”|“选项”命令,在打开的“引用”对话框中选择包含被调过程的工程,工程名称默认会显示为VBAProject,可以在工程属性对话框中修改工程名称。如果工程对应的工作簿当前未被打开,则需要单击“浏览”按钮打开这个工作簿。
建立引用后就可以调用所引用的工程中的Sub过程了。为了可以准确调用指定的过程,应该使用下面的格式:
工程名.模块名.过程名
如图2-11所示显示了一个实际调用外部工作簿的案例。外部工作簿使用的是上一个例子用到的工作簿。在当前工作簿中建立对该外部工作簿的引用后,在当前工作簿的模块1中的test过程中使用了下面的代码来调用位于外部工作簿中的模块1中名为“主过程”的过程。外部工作簿对应的工程被命名为MyPorject而没有使用默认的VBAProject,在工程资源管理器中可以看到该名称。
MyProject.模块1.主过程
图2-11 调用其他工作簿中的过程
2.4.4 向Sub过程传递参数
所有录制的宏都是公有的Sub过程,这些过程不包含任何参数。为了让Sub过程具有更大的灵活性,可以通过向Sub过程传递参数,从而允许用户为过程提供需要处理的数据,而不是将要处理的数据固定写入到过程内部。包含参数的Sub过程只能被其他过程调用,而不能直接运行,也不会显示在“宏”对话框中。
可以将变量、常量、对象等不同类型的内容作为参数传递给过程,一个过程可以包含一个或多个参数,这些参数可以是必选参数,也可以是可选参数。必选参数是指在调用过程时必须要为该参数提供值,可选参数是指在调用过程时不一定非要为该参数提供值。还可以指定参数的数据类型,方法与为变量指定数据类型相同。
在声明Sub过程时,在过程名右侧的圆括号中输入所需的一个或多个参数,各参数之间以逗号分隔。如果需要,可以使用As关键字为参数指定数据类型。
案例2-15 在欢迎信息中显示指定的用户名
下面的代码声明的Sub过程(名为Greeting)包含一个参数(名为UserName),该参数的数据类型是String,该参数用于指定要在问候消息中显示的用户名。然后在另一个过程中调用Greeting过程,并使用具体的人名作为参数值提供给该过程。
Sub Greeting(UserName As String) MsgBox "你好" & UserName & ",欢迎登录" End Sub
Sub 主过程() Greeting "John" End Sub
在上面的主调过程中,也可以声明一个变量,然后为变量赋值,再在调用过程时使用变量作为参数的值进行传递,如下所示:
Sub 主过程2() Dim strMyName As String strMyName = "John" Greeting strMyName End Sub
在向过程传递参数时可以采用传址和传值两种方式,默认为传址。参数的传址或传值是通过在声明过程时使用ByRef或ByVal关键字指定的,使用ByRef或省略该关键字表示参数是传址的,使用ByVal关键字表示参数是传值的。传址是指传递到过程内部的是变量本身,过程中的代码对变量的修改不只局限于过程内,而且还会影响过程外的代码,相当于过程内、过程外共享这个变量。传值是指传递到过程内部的是变量的副本,过程中的代码对变量的修改只限于过程内,而不会对过程外的其他过程有任何影响。
案例2-16 参数的传址与传值
下面的代码说明了参数的传址与传值的区别。在“求和”过程中使用ByRef将Number1指定为传址,使用ByVal将Number2指定为传值,因此在主调过程中将声明的两个变量intNum1和intNum2赋值后传递给“求和”过程时,intNum1是传址的,intNum2是传值的。“求和”过程对传递进来的两个变量都执行加1运算,由于第一个变量是传址的,因此加1后的变量结果会改变原变量intNum1的值。而第二个变量由于是传值的,因此加1后的变量结果不会改变原变量intNum2的值。可以在主调过程中使用MsgBox函数在对话框中验证两个变量的值,第一个变量的值为2,第二个变量的值仍为1,如图2-12所示。
图2-12 参数的传址和传值
可以在立即窗口中随时验证过程的运行结果。首先需要在VBE窗口中通过单击菜单栏中的“视图”|“立即窗口”命令显示出立即窗口,然后需要在代码中使用Debug.Print语句代替MsgBox函数,以便将变量的值输出到立即窗口中。接下来只需在立即窗口中输入过程名称并按Enter键,即可显示变量的结果,如图2-13所示。
图2-13 在立即窗口中验证过程的运行结果
提示:通常会在Function过程中更频繁地使用参数,因此在后面2.5节介绍Function过程时会对参数进行更多的介绍。在Function过程中包含不同类型参数的具体方法会在第16章进行详细介绍。
2.4.5 Sub过程的递归
递归是指过程调用其自身。任何一个过程都可以递归,但是递归可能会导致内存耗尽而产生运行时错误。在某些情况下也可以利用递归来完成任务。
案例2-17 验证用户登录
下面的代码运行后会显示一个输入对话框让用户输入用户名,然后检测用户名是否正确,如果不正确则会再次显示该对话框,直到用户输入正确的用户名为止。如果正确则会显示登录成功的消息并立刻结束该过程。由于使用If语句设置了终止递归的判断条件,因此本例中的过程递归不会陷入无限循环。
Sub 用户登录() Dim strUserName As String strUserName = InputBox("请输入用户名:") If strUserName = "admin" Then MsgBox "登录成功!" Exit Sub Else MsgBox "用户名错误,请重新输入!" 用户登录 End If End Sub
2.5 创建Function过程
除了Sub过程,用户还可以在VBA中创建Function过程(函数过程),它与Sub过程有很多相似之处,比如声明方式、调用方式、过程包含的参数声明和传递方式等,Sub过程涉及的很多概念和操作方法也同样适用于Function过程。本节将介绍声明与调用Function过程的基本方法,还会介绍VBA内置函数的使用方法。第16章将会详细介绍开发自定义函数的方法和更多案例。
2.5.1 Function过程与Sub过程的区别
尽管Function过程和Sub过程在很多方面都具有相同或相似的特性,但是二者之间存在一个重要而明显的区别:Function过程可以返回一个值,而Sub过程不能返回任何值。Function过程类似于Excel内置的工作表函数。在VBA中创建的Function过程主要有以下两个用途:
□ 在工作表公式中使用,弥补Excel内置函数无法实现的计算功能,简化公式的复杂度。
□ 在VBA中被其他过程调用,或者作为表达式的一部分参与运算。
2.5.2 声明Function过程
声明Function过程的语法格式与声明Sub过程类似,但是由于Function过程有返回值,因此在格式声明的某些部分与Sub过程有所区别。声明Function过程的语法格式如下:
[Public | Private] [Static] Function name [(arglist)] [As type] [statements] [name = expression] [Exit Function] [statements] [name = expression] End Function
□ Private:可选,表示声明的是一个私有的Function过程,只有在该过程所在的模块中的其他过程可以访问该过程,其他模块中的过程无法访问该过程。
□ Public:可选,表示声明的是一个公共的Function过程,所有模块中的所有其他过程都可以访问该过程。如果在包含Option Private Module语句的模块中声明该过程,即使该过程使用了Public关键字,也仍然会变为私有过程。
□ Static:可选,Function过程运行结束后保留过程中所使用的变量的值。
□ Function:必选,表示Function过程的开始。
□ name:必选,Function过程的名称,与变量的命名规则相同。
□ arglist:可选,一对圆括号中可以包含一个或多个参数,这些参数用于向Function过程传递数据以供Function过程处理,各参数之间以逗号分隔。如果过程不包含任何参数,则必须保留一对空括号。
□ type:可选,Function函数的返回值的数据类型。
□ statements:可选,Function过程中包含的VBA代码。
□ expression:可选,Function过程的返回值。
□ Exit Function:可选,中途退出Function过程。
□ End Function:必选,表示Function过程的结束。
与声明Sub过程的方法类似,可以使用两种方法声明Function过程。如果使用“添加过程”对话框声明Function过程,则需要在该对话框的“类型”区域中选择“函数”选项,其他选项的设置与Sub过程类似。
如果想要手动声明Function过程,则需要在代码窗口中输入Function关键字和Function过程的名称,按Enter键后Excel会自动添加End Function语句,如下所示,接下来可以在Function和End Function之间添加所需的VBA代码。
Function MyTime()
End Function
还可以在Function过程名右侧的圆括号中输入一个或多个参数,各参数以逗号分隔,如下所示:
Function GetSum(varNumber1, varNumber2)
End Function
提示:在创建Function过程时可以为其添加不同形式的参数,关于这方面的具体内容将在第16章进行详细介绍。
案例2-18 创建与使用Function过程
下面的代码声明了一个用于计算两个数字之和的Function过程,该过程包含两个参数,它们表示要参与计算的数字。在另一个过程中调用了这个Function过程,在对话框中显示了用户指定的两个数字之和,如图2-14所示。本例中的Function过程是作为表达式的一部分使用的。
Function GetSum(varNumber1, varNumber2) GetSum = varNumber1 + varNumber2 End Function
Sub test() MsgBox "两个数字之和是:" & GetSum(1, 2) End Sub
图2-14 一个简单的Function过程
2.5.3 调用Function过程
与调用Sub过程类似,可以在其他过程中调用指定的Function过程,具体可以调用哪些Function过程以及调用的方式,由Function过程的作用域决定。Function过程的作用域所遵循的规则与Sub过程相同。如果在Function过程的开头使用或省略了Public关键字,那么该Function过程是公有过程。如果在Function过程的开头使用了Private关键字,那么该Function过程是私有过程。
公有的Function过程可以被同一个工程中的所有模块中的所有过程调用。如果要在外部工作簿中调用该Function过程,需要建立对包含该Function过程的工作簿的引用,方法与前面介绍的引用外部工作簿中的Sub过程相同。上一个案例说明了在VBA中调用Function过程的方法,在一个对话框中显示了使用GetSum函数对两个数字求和的计算结果。如果希望在后面的代码中使用Function过程的返回值,则需要将返回值赋值给一个变量,之后可以在代码中处理这个变量。
案例2-19 在程序中使用函数的返回值
下面是对上一个案例中的代码修改后的版本,其中声明了一个varSum变量,用于保存GetSum函数的返回值,然后在If判断语句中测试这个变量是否小于10,如果是则显示“总和太小”的提示消息。
Function GetSum(varNumber1, varNumber2) GetSum = varNumber1 + varNumber2 End Function
Sub test() Dim varSum varSum = GetSum(1, 2) If varSum < 10 Then MsgBox "总和太小" End Sub
公有的Function过程还可以在工作表公式中使用,就像使用Excel内置的工作表函数一样。这里仍然使用前面案例中创建的GetSum函数,该函数计算A1和B1两个单元格中的数字之和,如图2-15所示。
图2-15 在工作表公式中使用Function过程
如果只想在VBA中调用Function过程,不希望在工作表公式中使用该过程,那么需要在声明Function过程的开头使用Private关键字。这样该Function过程将变为私有过程,它只能被Function过程所在模块中的任意过程调用,而不能被其他模块中的过程调用,也不能在工作表公式中使用。
2.5.4 使用VBA内置函数
VBA内置函数是VBA自身提供的用于实现特定功能的Function过程,它们可用于完成不同类型的计算和文本处理任务。VBA内置函数与Excel工作表函数类似。例如,名为Ucase的VBA内置函数用于将文本中的英文字母转换为大写形式,该函数的功能与Excel工作表函数UPPER相同。需要注意的是,如果某个VBA内置函数与某个Excel工作表函数实现相同的功能(就像上面提到的Ucase和UPPER),那么该工作表函数就不能在VBA中使用,否则会出现错误。
如果用户创建的Function过程与VBA内置函数同名,在VBA中调用该Function过程时,VBA会认为用户希望使用自己创建的这个Function过程,而不是同名的VBA内置函数。此时如果希望使用同名的VBA内置函数,则需要先输入VBA和一个句点,然后在弹出的自动成员列表中选择所需的VBA内置函数(以绿色标记开头),如图2-16所示,使用方向键选择某个函数,然后按下Tab键将函数输入到代码窗口中。在不知道都有哪些VBA内置函数时,也可以使用这种方法快速获得函数列表。
图2-16 在代码中使用VBA内置函数
2.6 控制程序的运行流程
正常情况下,过程中的代码按照从上到下的顺序运行,但是可以在过程中加入控制结构来改变程序的运行流程。
2.6.1 If Then判断结构
If Then结构用于根据条件的判断结果是否成立来选择执行不同的代码。如果判断结果为逻辑值True,则说明条件成立,如果为False则说明条件不成立。If Then判断结构可以分为以下3种形式:
□ 只处理条件成立情况的If Then结构。
□ 可以处理条件不成立情况的If Then结构。
□ 可以处理多个条件的If Then结构。
1.只处理条件成立情况的If Then结构
If Then结构的最简单形式是单行的If Then语句,用于在条件成立时执行指定的代码。要检测的条件位于If关键字之后,要执行的代码位于Then关键字之后,格式如下:
If 要检测的条件 Then 条件成立时执行的代码
案例2-20 If条件成立时执行单条语句
下面的代码判断输入的用户名是否是“Admin”,如果是则显示欢迎信息。
Sub 显示欢迎信息() Dim strUserName As String strUserName = InputBox("请输入用户名:") If strUserName = "Admin" Then MsgBox "hello " & strUserName End Sub
在上例中,只有输入大小写完全相同的“Admin”才会显示欢迎消息,这是因为VBA默认使用二进制方式对字符串进行比较。如果希望无论输入大小写形式的Admin都能被If语句判断为条件成立,则可以将If条件判断部分的代码改为以下两种形式之一,其中的Lcase和Ucase是VBA的两个内置函数,前者用于将文本转换为小写形式,后者用于将文本转换为大写形式。
If LCase(strUserName) = "admin" If UCase(strUserName) = "ADMIN"
如果希望模块中的所有过程都使用不区分大小写形式的文本比较方式,则可以在模块顶部的声明部分输入下面的代码:
Option Compare Text
如果在条件成立时需要执行多行代码,则可以使用下面的If Then格式。此时在Then关键字之后没有任何代码,而将条件成立时要执行的代码放在If语句下面的一行或多行中,最后使用End If语句作为If Then结构的结束。不需要手动输入End If语句,只需先输入好If Then这条语句,然后按Enter键,Excel会自动添加End If语句。
If 要检测的条件 Then 条件成立时执行的代码 End If
案例2-21 If条件成立时执行多条语句
下面的代码是对上一个案例修改后的版本,如果用户输入正确的用户名,将在对话框中显示欢迎信息,并会记录用户的登录次数。声明intLogin变量时使用的是Static关键字,目的是在工作簿打开期间多次执行该过程时,intLogin变量的值可以持续累加,而不是在每次过程运行结束后自动归零。
Sub 显示欢迎信息() Dim strUserName As String Static intLogin As Integer strUserName = InputBox("请输入用户名:") If strUserName = "Admin" Then MsgBox "hello " & strUserName intLogin = intLogin + 1 End If End Sub
实际上单行的If Then结构也可以在满足条件时执行多条语句,只要使用冒号分隔多条语句即可,如下所示:
If strUserName = "Admin" Then MsgBox "hello " & strUserName: intLogin = intLogin + 1
2.同时处理条件成立和不成立两种情况的If Then结构
如果希望在条件成立和不成立时都执行相应的代码,则需要在If Then结构中使用Else子句。如果在条件成立和不成立时各执行一行代码,则可以使用单行的If Then Else结构,格式如下:
If 要检测的条件 Then 条件成立时执行的代码 Else 条件不成立时执行的代码
案例2-22 同时处理If条件成立与不成立两种情况并执行单条语句
下面的代码判断输入的用户名是否是“Admin”,如果是则显示登录成功的信息,如果不是则显示出错信息。代码中使用strMessage变量存储在条件成立和不成立时在对话框中显示的不同信息,最后使用MsgBox函数显示该信息。
Sub 验证用户名() Dim strUserName As String, strMessage As String strUserName = InputBox("请输入用户名:") If strUserName = "Admin" Then strMessage = "登录成功" Else strMessage = "用户名错误" MsgBox strMessage End Sub
如果在条件成立和不成立时要分别执行多行代码,则需要使用多行的If Then Else结构,格式如下:
If 要检测的条件 Then 条件成立时执行的代码 Else 条件不成立时执行的代码 End If
案例2-23 同时处理If条件成立与不成立两种情况并执行多条语句
下面的代码判断输入的用户名是否是“Admin”,如果是则显示登录成功的信息,如果不是则记录用户登录的次数,并显示包括已登录次数在内的登录失败的信息。
3.处理多个条件的If Then结构
使用If Then结构还可以对多个条件进行判断,并根据判断结果执行不同的代码。处理多个条件的If Then结构可以将普通的If Then结构嵌套在另一个If Then结构中,可以嵌套任意数量的If Then结构,从而构成多层嵌套的If Then结构,如下所示,每个If Then结构都必须有对应的End If语句。
案例2-24 多个If Then Else结构嵌套
下面的代码说明了If多条件判断第一种结构的用法,根据用户输入的不同内容而显示不同的信息。如果用户输入的是Admin,则显示“你好,管理员”;如果输入的是User,则显示“你好,普通用户”;否则会显示Else子句中的信息,即用户输入的实际用户名。
还可以使用下面的If Then结构来处理多个条件,可以添加任意数量的ElseIf子句。
案例2-25 使用If Then ElseIf处理多个条件
下面的代码使用If Then ElseIf结构进行了重新编写,实现与上一个案例相同的功能。
无论使用的是单条件判断还是多条件判断的If Then结构,都可以在If条件部分使用逻辑运算符组合多个条件,以实现同时满足多个条件才执行指定的代码。
案例2-26 使用逻辑运算符实现多条件判断
下面的代码检查用户输入的是否是“Admin”,以及登录次数是否未超过3次。由于在If条件部分使用了And逻辑运算符,因此只有同时满足两个条件,才会显示登录成功的信息,否则只要有一个条件不满足,就会显示用户名错误或已超过登录次数的信息。
2.6.2 Select Case判断结构
当需要依次检测一个表达式的多个值,并根据每个值来执行不同的操作时,Select Case可以提供更清晰的结构。Select Case结构的格式如下:
案例2-27 使用Select Case结构处理多个条件
下面的代码使用Select Case结构对案例2-24和案例2-25进行了重新编写。
如果在每个Case下面只执行一条语句,为了使代码结构更紧凑,可以将执行的语句与Case语句合并为一行,两个语句之间使用分号分隔,如下所示:
Select Case结构中的每个Case语句都可以对多个值进行检测,各个值之间以逗号分隔。
案例2-28 验证Excel程序版本号
下面的代码检测当前Excel程序的版本,如果是Excel 2007/2010/2013/2016,则显示“Excel 2003之后的版本”的提示信息,如果是Excel 2003或更早版本,则显示“Excel 2003或更早版本”的提示信息。本例中用到了Excel对象模型中的Application对象,它代表Excel程序本身。Version是Application对象的一个属性,Application.Version表示Excel程序的版本号。
还可以在Case语句中使用To关键字表示要检测的值的范围,或者使用Is关键字与指定的值进行比较。
案例2-29 计算折扣率
下面的代码首先检测用户输入的内容是否是数字,如果不是则会显示一条提示信息,然后自动退出当前程序。Exit Sub语句用于在满足条件时退出当前Sub过程。如果输入的是数字,则会使用Select Case结构检测该数字的大小,并返回其所在数值范围内对应的折扣率。在Case语句中使用了Is和To关键字来指定不同的数值范围。
Select Case结构也可以嵌套使用,即在一个Select Case结构中包含另一个Select Case结构。
案例2-30 查询商品定价
下面的代码可以实现根据用户输入的食品的类别和名称来显示对应的食品定价。如果输入的内容不在指定范围内,则会显示“输入的内容无效”的提示信息。本例使用了嵌套的Select Case结构,外层的Select Case结构用于查找食品的类别,内层的Select Case结构用于查找具体的食品名称,每个Case语句返回相应食品的定价,最后使用MsgBox函数在对话框中显示找到的食品的名称和定价。
2.6.3 For Next循环结构
计算机最擅长的工作之一就是处理需要不断重复进行的操作。VBA支持两种最主要的循环结构:For Next和Do Loop,本节主要介绍For Next循环结构,下一节会详细介绍Do Loop循环结构。
如果预先知道操作要重复的次数,那么可以使用For Next循环结构,其语法格式如下:
For counter = start To end [Step step] [statements] [Exit For] [statements] Next [counter]
□ counter:必选,用做循环计数器的数值变量,该变量不能是Boolean或数组元素。该值在循环期间会不断递增或递减。
□ start:必选,counter计数器的初始值。
□ end:必选,counter计数器的终止值。
□ Step:可选,counter计数器的步长,未指定该值则默认为1。如果指定步长值,则需要按“Step步长值”的格式输入,其中的“步长值”几个字替换为实际值。
□ statements:可选,For Next结构中包含的VBA代码,它们将被执行指定的次数。
□ Exit For:可选,中途退出For Next循环。
使用For Next循环结构需要将一个变量指定为计数器(counter),然后为该变量提供一个初始值(start)和一个终止值(end),通过步长值(step)使计数器从初始值递增或递减到终止值。当计数器的值超过终止值或初始值时,结束For Next循环并继续执行后面的代码。
案例2-31 计算1到100之间的所有整数之和
下面的代码计算1到100之间的所有整数之和。在该For Next结构中,起始值为1,终止值为100。由于参与计算的是连续范围内的所有整数,因此步长值为1。
案例2-32 计算1到100之间的所有偶数之和
下面的代码计算1到100之间的所有偶数之和,由于偶数是2、4、6、8这样的数字,两个相邻偶数之间的增量为2,因此本例中需要将步长值设置为2,同时需要将初始值改为0,这样才能将数字2包含在计算范围之内。其他代码与上例相同。
还可以将For Next循环结构与If Then或Select Case判断结构嵌套使用。
案例2-33 计算指定范围内的所有整数之和
下面的代码计算从数字1到用户指定的数字之间的所有整数之和。由于用户在InputBox对话框中输入的内容有可能不是数字,因此需要先使用If语句检测用户输入的内容,如果是数字才会执行For Next结构中的代码。将用户输入的数字赋值给intNumber变量,然后将该变量中的值作为For Next结构中的终止值,这样就实现了从1到用户指定的数字之间的整数范围。
实际上不一定必须完成所有预定次数的循环,而是可以在满足特定条件时中途退出循环。为了实现这个目的,通常需要在For Next结构中嵌入If Then结构,并在If Then结构中使用Exit For语句。
案例2-34 达到指定值时结束累加
下面的代码计算数字1到10之间的所有整数之和,但是当累加的总和大于或等于20时就停止累加,并显示达到该值时累加到的那个数字。本例中的intSum变量存储累加后的当前总和,在If语句中判断该变量的值是否大于等于20,如果是则执行Exit For语句退出当前的For Next循环。最后在对话框中显示退出For Next循环时的循环计数器的当前值,该值保存在intCounter变量中。
2.6.4 Do Loop循环结构
如果无法预先获悉操作要循环的次数,但是知道在什么情况下开始或停止循环,那么可以使用Do Loop循环结构。Do Loop循环结构分为Do While和Do Until两种形式,下面将分别对这两种形式进行介绍。
1.Do While循环
Do While用于不知道要循环的次数,但是知道在什么条件下开始循环的情况。当条件为True时执行循环,条件为False时终止循环。Do While分为以下两种形式。
形式一:
Do While 要检测的条件 条件成立时执行的代码 Loop
形式二:
Do 条件成立时执行的代码 Loop While 要检测的条件
Do While两种形式的区别在于在循环开始之前,是否先对条件进行一次判断。
案例2-35 使用Do While结构
下面两段代码检测用户输入的用户名是否是“Admin”,如果不是则会重新显示输入对话框,如果是则显示欢迎信息。两段代码实现的功能相同,但写法不同。
如果希望在满足指定条件时退出Do While循环,可以使用Exit Do语句。
案例2-36 使用Exit Do退出循环
在上面的案例中,只有输入Admin(大小写形式均可)才会退出循环,即使单击对话框中的“取消”按钮也无法退出循环。正常情况下应该允许用户在单击“取消”按钮时关闭对话框并退出程序,因此应该在Do While循环中加入检测InputBox函数的返回值是否为空的判断条件,如果返回值为空,则使用Exit Do语句退出Do While循环。
提示:如果在对话框中未输入任何内容并单击“确定”按钮,也会执行Exit Do语句退出循环。
2.Do Until循环
Do Until用于不知道要循环的次数,但是知道在什么条件下停止循环的情况。当条件为False时执行循环,条件为True时终止循环。Do Until分为以下两种形式:
形式一:
Do Until 要检测的条件 条件不成立时执行的代码 Loop
形式二:
Do 条件不成立时执行的代码 Loop Until 要检测的条件
Do Until两种形式的区别在于在循环开始之前,是否先对条件进行一次判断。
案例2-37 使用Do Until结构
下面的两段代码是对前面介绍的Do While结构的两个案例的修改版,这里使用了Do Until结构。
与在Do While结构中使用Exit Do语句的作用类似,也可以使用Exit Do语句在满足指定条件时退出Do Until循环,这里不再赘述。
2.7 使用数组
普通变量只能存储一个数据,如果希望在一个变量中同时存储多个数据,则需要使用数组。数组是一种特殊类型的变量,其中可以包含多个数据,这些数据称为数组元素,它们共享同一个变量名,通过使用不同的索引号来识别和引用每个数组元素。根据不同的划分方式可以将数组分为不同的类型,按维数划分可以将数组分为一维数组、二维数组和多维数组,按使用方式划分可以将数组分为静态数组和动态数组。本节将介绍数组的声明、赋值等基本操作,还介绍了动态数组的用法。
2.7.1 数组的维数
数组的维数可以是一维、二维或多维。可以将一维数组想象成排列在一行或一列中的数据。数据排列在一行中的数组称为一维水平(横向)数组,如图2-17所示,数组元素之间以逗号分隔。下面的一维水平数组包含6个数组元素,分别为数字1、2、3、4、5、6。
{1,2,3,4,5,6}
如果数组元素是文本,则需要为每个数组元素添加一对双引号。下面的一维水平数组包含3个文本类型的数组元素。
{"星期一","星期二","星期三"}
数据排列在一列中的数组称为一维垂直(纵向)数组,如图2-18所示,数组元素之间以分号分隔。下面的一维垂直数组包含6个数组元素,分别为数字1、2、3、4、5、6。
={1;2;3;4;5;6}
图2-17 一维水平数组
图2-18 一维垂直数组
二维数组中的数据同时排列在行和列中,水平方向上的数组元素以逗号分隔,垂直方向上的数组元素以分号分隔。可以将二维数组看作是由行和列构成的表,表中的每一个单元格由行和列的编号组成,表中包含的单元格数量就是数组包含的元素个数,也可以由表中的行数与列数的乘积得出。下面的二维数组由2行3列组成,第一行包含数字1、2、3;第二行包含数字4、5、6,该数组一共包含6个数组元素。
={1,2,3;4,5,6}
三维或更多维的数组很少用到,因此本书不做过多介绍。
2.7.2 声明一维数组
声明一维数组的方法与声明普通变量类似,可以使用Dim Static、Private、Public关键字,这些关键字的区别在于使数组具有不同的作用域。与声明普通变量不同的是,需要在声明的数组名称的右侧包含一对圆括号,并在其中输入表示数组上界的数字,上界是数组可以使用的最大索引号,即最后一个数组元素的索引号。下面的代码声明了一个名为Numbers的数组,其上界为2。由于没有明确指定数组的数据类型,因此默认为Variant类型。
Dim Numbers(2)
数组中的所有元素可以是同一种数据类型,也可以是不同的数据类型。如果像上面那样将数组声明为Variant数据类型,那么数组中的元素可以是不同类型的数据。也可以在声明数组时明确指定一种数据类型,这样数组中的元素都将是该种数据类型。下面的代码将上面的数组声明为Integer数据类型:
Dim Numbers(2) As Integer
也可以像声明普通变量那样,为数组添加表示数据类型的前缀。为了与普通变量区分,可以在数据类型前缀之前使用小写字母a来表示变量是数组,下面的数组名称中的字母a表示数组(Array),int表示Integer数据类型。
Dim aintNumbers(2) As Integer
要引用一个数组元素,只需使用数组名+索引号。下面的代码引用数组中索引号为1的数组元素。
aintNumbers(1)
默认情况下,数组元素的索引号从0开始而不是1,因此前面声明的aintNumbers(2)数组包括以下3个元素:
aintNumbers(0) aintNumbers(1) aintNumbers(2)
如果希望数组元素的索引号从1开始,可以使用以下两种方法:
□ 声明数组时使用To关键字,显式指定数组的下界和上界,如下所示:
Dim aintNumbers(1 To 3) As Integer
□ 在模块顶部的声明部分输入下面的语句,在该模块的任意过程中声明的数组的下界默认都变为1。
Option Base 1
可以使用VBA内置的LBound和UBound函数自动检查数组的下界和上界。如果模块顶部的声明部分没有使用Option Base 1语句,在声明aintNumbers数组时只是指定了上界,那么下面的代码将会检测到数组的下界为0,上界为2:
Sub 检查数组的上下界() Dim aintNumbers(2) As Integer MsgBox LBound(aintNumbers) MsgBox UBound(aintNumbers) End Sub
如果模块顶部的声明部分使用了Option Base 1语句,在声明aintNumbers数组时仍然只是指定了上界,那么下面的代码将会检测到数组的下界为1,上界为2:
Option Explicit Option Base 1
Sub 检查数组的上下界() Dim aintNumbers(2) As Integer MsgBox LBound(aintNumbers) MsgBox UBound(aintNumbers) End Sub
使用LBound和UBound函数可以自动计算出一个数组包含的元素总数,而不必在乎是否使用了Option Base 1语句,以及声明数组时是否同时指定了上界、下界。
案例2-38 计算数组包含的元素总数
下面的代码用于计算aintNumbers数组包含的元素总数,通过使用LBound和UBound函数可以避免由Option Base 1语句和数组声明方式带来的上界、下界的不确定性。可以任意改变这个数组的声明方式,包括改变数组的上界、使用To关键字显式指定上下界、在模块顶部的声明部分添加Option Base 1语句,该段代码都能正确计算出数组包含的元素总数。
Sub 计算数组包含的元素总数() Dim aintNumbers(2) As Integer Dim intItems As Integer intItems = UBound(aintNumbers) - LBound(aintNumbers) + 1 MsgBox intItems End Sub
2.7.3 声明二维数组
声明二维数组的方法与声明一维数组类似,但是比一维数组多了一个维度,因此在声明数组时,需要在数组名称右侧的圆括号中输入表示两个维度上界的数字,第一个数字表示数组第一维的上界,第二个数字表示数组第二维的上界,两个数字之间以逗号分隔。
下面的代码声明了一个二维数组,数组第一维的上界是2,第二维的上界是3。如果没有在模块顶部的声明部分使用语句,那么该数组两个维度的下界都是0,该数组共包含3×4=12个元素。
Dim aintNumbers(2, 3) As Integer
在声明二维数组时也可以使用To关键字,显式指定数组每一维的上界、下界,如下所示:
Dim aintNumbers(1 To 3, 1 To 6) As Integer
当引用二维数组中的元素时,需要同时使用两个维度上的索引号,与使用x坐标和y坐标坐标定位一个点类似。可以将二维数组中的第一维想象成行,将第二维想象成列。下面的代码将数字100赋值给aintNumbers数组中位于第1行第2列的元素,这里假设在模块顶部的声明部分没有使用Option Base 1语句。
aintNumbers(0, 1) = 100
与一维数组类似,也可以使用LBound和UBound函数检查二维数组的下界和上界。由于二维数组包含两个维度,因此必须在LBound和UBound函数的第二参数中指定要检查的是哪个维度。
案例2-39 检查二维数组每一维的上下界
下面的代码在对话框中显示了aintNumbers数组第一维和第二维的下界和上界,运行结果如图2-19所示。
图2-19 检查二维数组每一维的上下界
2.7.4 为数组赋值
为数组赋值与为普通变量赋值类似,需要在等号的左侧输入数组名称,在等号的右侧输入要赋的值。与普通变量不同的是,由于数组包含多个元素,因此在赋值时需要单独为数组中的每一个元素分别赋值。下面的代码为aintNumbers数组中的每一个元素赋值了一个数字:
Sub 为数组赋值() Dim aintNumbers(2) As Integer aintNumbers(0) = 1 aintNumbers(1) = 2 aintNumbers(2) = 3 End Sub
由于数组元素是通过连续的索引号来识别和引用的,因此可以利用循环结构批量为数组元素赋值。对于上面的案例而言,可以使用For Next循环结构来为数组批量赋值。当数组中包含大量元素时,利用循环可以明显加快赋值的速度。
案例2-40 利用循环结构批量为数组赋值
下面的代码是对上一个案例进行修改后的版本,使用VBA内置函数LBound和UBound自动检查数组的下界和上界,以便作为For Next循环计数器的初始值和终止值,从而实现批量赋值的目的。
为数组赋值还可以使用VBA内置的Array函数。使用该函数可以创建一个数据列表,并将该列表赋值给一个Variant数据类型的变量,从而创建一个包含列表中所有数据的数组并自动完成赋值操作。下面的代码使用Array函数将表示文件名的3个文本赋值给aFileNames变量,该变量的数据类型为Variant。
aFileNames = Array("一季度", "二季度", "三季度")
注意:如果直接使用Array函数,它所创建的数组的下界受Option Base 1语句的限制。但是如果使用类型库来限定Array函数,比如以VBA.Array这种形式输入Array函数,此时由该函数创建的数组则不受Option Base 1语句的限制。
使用Array函数创建并赋值一个数组后,可以使用For Next循环结构操作这个数组。
案例2-41 操作Array函数创建的数组
下面的代码首先使用Array函数创建了一个包含3个文件名的数组,然后在For Next循环结构中,通过Dir函数检查指定路径中的这3个文件是否存在。如果Dir函数的返回值为空字符串,则说明指定文件不存在,此时会在对话框中显示文件不存在的提示信息。
注意:如果声明的数组要使用Array函数进行赋值,那么该数组必须声明为Variant数据类型,而且在数组名称右侧不能包含一对圆括号及上界。
2.7.5 使用动态数组
前面介绍的数组在声明时就指定了数组元素的个数,这种数组是静态数组,在程序运行期间不能改变静态数组的大小。但是在很多实际应用中,在程序运行前无法确定数组包含的元素个数,此时就需要使用动态数组技术。
动态数组与静态数组在声明方式上的唯一区别,是在声明动态数组时不需要在圆括号中指定数组的上界,但是必须保留一对空括号。下面的代码声明了一个名为aintNumbers的动态数组。
Dim aintNumbers() As Integer
在程序运行过程中根据实际情况,使用ReDim语句指定动态数组的上界。
案例2-42 创建动态数组
下面的代码首先声明了一个没有上界的动态数组,在程序运行后对数组重新定义,将用户输入的数字指定为动态数组的上界。使用IsNumeric函数检查用户输入的内容是否是数字,以避免出现运行时错误。
Sub 创建动态数组() Dim aintNumbers() As Integer, strUBound As String strUBound = InputBox("请输入数组的上界:") If IsNumeric(strUBound) Then ReDim aintNumbers(strUBound) MsgBox "重新定义后的数组上界是:" & UBound(aintNumbers) End If End Sub
如果在程序中需要多次使用ReDim语句定义数组的大小,那么在下一次使用ReDim语句时会自动清除数组中包含的数据。如果需要在重新定义数组的大小时保留数组中的数据,则可以在ReDim语句中使用Preserve关键字。
案例2-43 重新定义动态数组时保留原有数据
下面的代码在程序开头声明了一个动态数组,在程序运行后定义该数组的上界为2,然后使用For Next循环结构为数组中的元素赋值,之后在对话框中显示索引号为1的数组元素的值,此时为2。再次使用ReDim Preserve语句定义该数组的上界为3,重新在对话框中显示索引号为1的数组元素的值,仍为原来的值2。
如果将上面代码中第二次使用ReDim语句中的Preserve关键字删除,那么第二次在对话框中显示索引号为1的数组元素的值时则变为默认值0而不是原来的2,说明在不使用Preserve关键字重新定义数组时会清除数组中原有的数据。
对于二维数组或多维数组而言,在ReDim语句中使用Preserve关键字只能改变数组最后一维的大小,并且不能改变数组的维数。
2.8 错误处理
即使再熟悉VBA编程的开发人员,也无法避免在编写的代码中不包含任何错误。为了在错误发生时可以为最终用户提供有指导意义的信息,开发人员应该预先考虑到在程序运行期间,由于客观条件或用户人为原因导致程序可能出现的问题,然后编写应对这些问题的错误处理程序。本节将介绍VBA包含的三种错误类型、代码调试工具以及处理运行时错误的方法。
2.8.1 错误类型
VBA包含的错误分为三类:编译错误、运行时错误、逻辑错误。编译错误通常在运行代码前就能被VBA检测出来,运行时错误只有在运行代码的过程中才能被发现,而逻辑错误可能很难被发现。本节将对这三种错误类型进行详细介绍。
1.编译错误
编译错误是指代码中存在不符合VBA语法规则的部分。比如将VBA的关键字、语句、函数写错,又比如将Dim语句写成Din,或者在使用If Then判断结构时没有输入Then关键字或End If语句。在按下Enter键后,VBA会自动弹出编译错误的提示信息,如图2-20所示。只要将错误的部分改为符合语法规则的正确形式,就可以解决编译错误。
图2-20 编译错误
2.运行时错误
运行时错误是指代码的编写符合VBA的语法规则,但是在程序运行时出现的各种问题,它是程序出现的最主要的错误类型。导致运行时错误的主要原因是程序试图执行无效的操作。下面列出了一些导致运行时错误的常见原因:
□ 将0作为除法运算中的分母。
□ 将Null值传递给无法处理它的函数。在从文件或数据库等不同来源获取来的数据中可能会包含无效的Null值,在将包含Null值的数据作为参数传递给VBA的某些内置函数时,将会产生运行时错误。
□ 对不存在或当前处于不可用状态的对象执行操作。例如,对不存在的文件或文件夹执行打开、移动、复制、删除等操作,或者对不可用的驱动器执行操作,都会产生运行时错误。如图2-21所示是在删除一个不存在的文件时显示的运行时错误提示信息。
图2-21 删除不存在的文件时产生的运行时错误
可以在程序中预先编写防御性代码,以便在出现这些问题时具备相应的处理能力。下面给出了针对以上3种情况的解决方法:
□ 对于第一种情况,可以在执行除法运算前,先使用If Then判断结构检查除数是否为0,如果不为0则执行除法计算,否则向用户发出类似“除数不能为零”的提示信息。
□ 对于第二种情况,可以使用VBA的内置函数IsNull检查作为参数传递的数据是否包含Null值,如果是则向用户发出警告信息,如果不是则将其作为参数传递给指定的函数。
□ 对于第三种情况,需要先使用VBA的错误处理语句尝试捕获错误,如果发生了错误,则使用错误处理程序解决发生的错误。
3.逻辑错误
与前两类错误不同,逻辑错误在程序运行过程中通常不会产生错误提示信息,因此并不会影响程序的正常运行,但是却会导致不正确的程序运行结果。逻辑错误具有一定的隐蔽性,通常只有仔细检查和测试代码,才能找到并解决逻辑错误。
2.8.2 调试代码
如果程序运行过程中产生运行时错误,则会弹出运行时错误的对话框,其中显示了错误编号和简要说明。如果单击“调试”按钮,VBA会将导致错误的代码标记为黄色,代码行的左侧还会显示一个箭头,如图2-22所示。VBE窗口顶部的标题栏会显示“中断”字样,程序此时处于中断模式,可在该模式下修改导致问题的代码。
修正错误的代码后,可以单击“标准”或“调试”工具栏中的“继续”按钮或按F5键,从出错的代码处继续运行程序,以检查是否已经解决问题。也可以单击“标准”或“调试”工具栏中的“重新设置”按钮,再单击“运行子过程/用户窗体”按钮或按F5键从头开始运行程序。
如果怀疑某条语句可能是导致问题的原因,则可以将这条语句设置为断点,在程序运行后,会自动在断点位置停止并进入中断模式。要设置断点,只需在代码窗口中单击要设置断点的语句的左侧位置,设置了断点的语句会自动高亮显示,并在其左侧显示一个圆点,如图2-23所示。
图2-22 在中断模式下修复错误的代码
图2-23 设置断点
提示:可以在代码中使用Stop语句实现断点功能,程序运行到Stop语句会自动进入中断模式。
为了监视程序运行过程中的某个变量、表达式或属性的值,可以单击菜单栏中的“调试”|“添加监视”命令,打开如图2-24所示的“添加监视”对话框,在“表达式”文本框中输入要监视的内容。
图2-24 “添加监视”对话框
单击“确定”按钮,监视窗口出现在VBE窗口的底部,如图2-25所示。在程序运行过程中,监视窗口中的该表达式的值会发生变化,通过观察其变化,可以有助于找到出错的原因。
图2-25 使用监视窗口帮助查找代码出错原因
在调试代码时,还可以使用F8键在当前过程中逐行执行每一条语句,以观察程序的运行状况并尽快发现出错的地方。
2.8.3 处理运行时错误
错误处理的主要任务是处理运行时错误,需要预先考虑到程序运行过程中可能会发生的错误,然后编写解决这些错误的代码并将其嵌入到正常的程序中。VBA提供了以下几个错误处理工具:
□ On Error Goto Line:该语句用于在错误发生时将程序的执行转到由Line指定的位置,并开始执行错误处理程序。换句话说,Line标志着错误处理程序的起点。Line表示一个标签,可以是任何有效的字符串,标签的右侧必须包含一个冒号。
□ On Error GoTo 0:该语句用于关闭之前的错误处理程序,这意味着如果在该语句之后的代码中出现运行时错误,VBA会正常显示运行时错误的提示信息并停止程序的运行。
□ On Error Resume Next:该语句用于关闭错误捕获监控功能,这意味着在该语句之后出现的所有运行时错误都会被忽略,而不会显示任何提示信息。使用该语句后,当程序中出现运行时错误时,程序会忽略这个错误,并在出错语句的下一条语句继续执行。
□ Err对象:Err对象存储着最近一次出现的运行时错误的相关信息,可以通过检查该对象中的内容来判断程序是否出现了运行时错误。Err对象的Number属性包含一个错误号,可以通过检查该属性的值来确定是否发生了特定类型的运行时错误。如果希望获得错误的描述信息,可以使用Err对象的Description属性,但是开发人员通常会在错误处理程序中自定义错误的提示信息。由于Err对象是全局范围的固有对象,因此在使用前不需要创建该对象的实例。
可以使用以下步骤创建错误处理程序:
(1)在可能导致错误的语句之前添加On Error Goto Line语句,使用实际的标签替换Line。
(2)在可能导致错误的语句之后添加On Error Goto 0语句,关闭之前处于活动状态的错误处理程序。
(3)在当前过程中的正常程序的末尾添加Exit Sub或Exit Function语句,以免在未发生错误时仍然执行错误处理程序。
(4)在Exit Sub或Exit Function语句的下一行添加一个标签,标签右侧必须包含一个冒号,标签名必须与前面的On Error Goto Line语句中设置的Line相同。
(5)在标签的下一行编写错误处理程序的代码。可以使用VBA支持的流程控制结构如If Then来判断不同的情况,以便执行不同的错误处理操作。在错误处理程序中通常需要根据希望实现的操作来使用Resume Next或Resume语句:如果希望在错误处理完成后重新执行出错的那条语句,则需要使用Resume语句;如果要在错误处理完成后继续执行出错语句之后的下一条语句,则需要使用Resume Next语句。如果希望在错误处理完成后执行特定的语句,而不是出错语句或出错语句的下一条语句,那么可以使用Resume Line语句,Line指明了要执行的特定语句的标签。
注意:如果在程序中添加了多个错误处理程序,那么必须确保每个错误处理程序都以Resume Next、Exit Sub或Exit Function语句结束,以免当前的错误处理程序执行结束后,又继续执行其后的错误处理程序。
下面通过几个案例讲解错误处理程序的实际应用。
案例2-44 捕获并处理运行时错误
下面的代码使用用户在对话框中输入的数字作为数组的上界来创建动态数组。如果用户输入的不是一个有效的数字,那么在使用ReDim语句定义动态数组时将会出现运行时错误,此时会被错误处理程序捕获,从而执行ErrTrap标签位置上的错误处理代码。由于使用了Resume语句,因此会在发出重新输入的提示信息后,重新显示InputBox对话框要求用户重新输入数组的上界。只有输入了有效数字,才会执行第一个MsgBox函数所在的语句并结束当前过程,否则会一直重复显示输入对话框。
案例2-45 使用Err对象的Number属性判断是否出现运行时错误
下面的代码是上一案例的另一种处理方式,它没有使用错误处理标签,而是在忽略了所有错误之后,通过判断Err对象的Number属性是否为0,来判断是否出现运行时错误。如果Err.Number不等于0,则说明程序出错,从而给出提示信息并退出过程。
案例2-46 在错误处理中使用On Error Resume Next语句
下面的代码使用On Error Resume Next语句忽略了所有运行时错误,即使要删除的文件不存在而导致运行时错误,仍会毫无提示地显示提示信息对话框,因为文件是否存在并不重要,毕竟这才是该有的逻辑。
Sub 错误处理() Dim strFile As String strFile = "C:\销量情况.txt" On Error Resume Next Kill strFile MsgBox "文件不存在或已被删除!" End Sub
2.9 规范化编写代码
为了让编写的VBA代码易于阅读和理解,我们应该有意识地使代码规范化。规范化代码包括多个方面,例如使用变量之前必须先进行声明、使用流程控制结构时应该保持缩进格式、为代码添加注释等。编写规范化的代码还可以减少很多不必要的错误发生。本节将介绍规范化编写代码需要遵循的4条规则。
2.9.1 强制变量声明
变量名拼写错误会导致很多不易察觉的问题,如果在使用变量前先进行声明,则可以避免很多不必要的错误。为了在工程中的任何位置都强制变量声明,需要将下面的语句添加到每个模块顶部的声明部分。以后在使用未声明的变量时,将会自动显示“变量未定义”的提示信息。
Option Explicit
2.9.2 使用缩进格式
在编写代码时使用适当的缩进格式,可以让代码易于阅读和理解,也便于检查和排除代码中的错误。尤其在代码中互相嵌套判断结构和循环结构时,正确的缩进格式可以让不同层次的结构变得清晰直观。在下面的代码中包含了两个嵌套的If Then结构,使用正确的缩进格式可以很容易分辨出哪些代码属于内层的If Then结构。
如果不使用缩进格式,那么代码可能像下面这样,很难分辨每个If Then结构的开始和结束位置,使代码变得混乱而复杂。
Sub 验证用户名() Dim strUserName As String strUserName = InputBox("请输入用户名:") If strUserName = "Admin" Then MsgBox "你好,管理员" Else If strUserName = "User" Then MsgBox "你好,普通用户" Else MsgBox "你好" & strUserName End If End If End Sub
2.9.3 将长代码分成多行
如果代码窗口中的某行代码过长而超出了窗口的宽度,那么需要拖动窗口底部的水平滚动条才能看到位于窗口外的代码部分,给查看和编写代码带来了不便。为了让所有代码都能完整地显示在代码窗口的可视区域中,可以对较长的代码进行分行处理。
将插入点定位到代码中要准备换行的位置,输入一个空格和一条下画线,然后按下Enter键,插入一个VBA可以识别的续行标记,此标记之后的代码会自动移入下一行。两部分代码虽然位于两行,但它们仍属于同一条语句,在运行时不会产生语法错误。如图2-26所示包含UBound和LBound函数的两行代码就是经过分行处理后的效果,它们实际上是同一条语句。
图2-26 将长代码分成多行
2.9.4 为代码添加注释
为了在时隔多日之后还能理解以前编写的代码所要实现的功能以及编写思路,应该养成为代码添加注释的习惯,不仅方便自己,也为其他开发人员了解和维护代码提供方便。运行VBA代码时会自动忽略注释内容。可以使用以下几种方法为代码添加注释:
□ 将单引号(')放置在注释内容的开头。
□ 将Rem关键字放置在注释内容的开头。
□ 选择要转换为注释的一行或多行内容,然后单击“编辑”工具栏中的“设置注释块”按钮。如果要取消一行或多行注释内容,使它们变成可被VBA识别的有效代码,则可以单击“编辑”工具栏中的“解除注释块”按钮。
无论使用哪种方法,注释内容都可以单独占据一行,也可以放在一行代码的右侧。在使用Rem关键字添加注释时,根据注释的位置需要使用不同的格式:
□ 如果将注释放在代码行的上方,需要在Rem关键字和注释内容之间保留一个空格。
□ 如果将注释放在代码行的右侧,需要在Rem关键字之前输入一个冒号,然后输入Rem和注释内容,Rem和注释内容之间保留一个空格。
提示:如果输入的内容被VBA正确识别为注释,那么这些内容的字体颜色默认会变为绿色。
下面的代码分别使用了添加注释的两种方法,并将注释放在了不同位置。在实际应用中通常不需要为每一行代码都添加注释,这里主要是为了说明添加注释的不同方法。