VBA 类模块理解和使用总结
目 录
- VBA 类模块理解和使用总结
- 一、类的概念
- 二、类的定义
- 三、类详细定义
- 关于封装
- 关于多态
- 关于异常
- 关于自定义事件
- 四、结论:
VBA 类模块理解和使用总结
一、类的概念
记得有人总结,VBA是基于面向对象(OOP)的编程语言,而 java是完全面向对象的编程语言,为了更好地理解面向对象编程,去学习了一下 java基础知识,现在回过来看VBA的面向对象特性就比较好理解,只是两者语法相差甚远。
刚开始时,如何理解“对象”这个概念也是个问题,在面向对象编程中使用的单词是“object”,查字典发现其含义是 “物体;物品;东西;(极欲得到、研究、注意等的)对象;宗旨;目的;目标”,才知道对象就是物体、东西或研究的对象,我们中文翻译成对象。
要理解对象,还必须要了解“类”这个概念。我们可以从生活中理解一下,如果我们到超市购买一些饼干,可能是圆形或星型等各种形状,上面有许多好看的花纹或图案,这在流水线上生产时肯定有相应的模具的才能生成这种饼干,这个模具就是类。
其实面向对象编程中的类就是对某种一个个具体对象进行一定抽象而形成的概念。例如,现实世界上有各种各样的人——男人、女人、白人、有色人、大人、小孩等等,通过一定的抽象为人类,如果往更高一层抽象可以是动物。同样的还有狗类,汽车类……,这些类的概念就是面向对象编程中的需要定义的类,然后可以利用该类创建具体的对象,其属性值可以相同或不同。我们在抽象成一个类时,编程所关注的无非是对象的部分属性和方法,例如学生类(Student),我们关注的属性可能有:学号、姓名、出生日期、籍贯、班级等,还有很多属性我们可能不需要关注而已,例如是否双眼皮、头上是否有两个旋等。
在VBA中,已经定义了许许多多的类和对象供大家使用,例如Excel中的工作簿(workbook)、工作表(worksheet)、单元格(range或cell)、行(row)等等。一般我们可能不需要自定义类就可以实现所需的功能,但是如果理解类和对象,并加以运用,可能会更好。
二、类的定义
类的定义在不同的语言中是不同的,这里先给出java语言中类的定义:
java 类定义的示例:
public class Person {
public string name; // 声明字符串类型的变量name,用于保存姓名,是对象的姓名属性
private int age; // 声明int类型的变量age,用于保存年龄,是对象的年龄属性,int就是VBA中的Integer
public void setAge(int age){ // 间接对属性age赋值的方法,并且进行一些控制
if(age <= 0 || age > 130){
throw new RuntimeException("年龄范围不合法");
}
this.age= age;
}
public int getAge(){ // 间接获取age的值
return this.age;
}
// 定义方法 speak() 说话
public void speak() {
System.out.println("我姓名是" + name + "今年" + age + "岁了!");
}
}
class是关键字,其中文含义就是“类”,Person就是类名,业内约定首字母大写以表示类,从而与其他名称相区别。这个类中声明了两个属性和一个方法。其中name属性前面使用了修饰符public,其含义是公共的,外面可以直接访问该成员,起不到封装作用;而age属性前面使用了 private(私有的)修饰符,这样外面不能直接访问,所以要定义 setter、getter方法,提供对该属性进行赋值或获取,也可以在方法内进行一些控制,起到了类的封装作用;当然也可以只提供getter方法,就能实现只读功能。
一般类具有三大特征:封装、继承和多态。
关于封装,上面的示例主要体现了封装特性,就好像我们买来的电视机,外面都是有材料包裹的,你看不到内部细节,但是会留出几个接口供你使用,你可以使用遥控器或直接按压按钮操作接口。
关于继承,可以这样理解,你父亲有些财产(属性),也有一些秘诀(方法),你都能继承(如果不是父类私有的),这时父亲就是父类,你就是子类,子类继承父类。在面向对象编程中,始终有一个公共的父类object,任何系统提供的类和自定义的类都会默认继承这个object类,这点VBA中也是如此。
关于多态,子类继承了父类,那么,在子类基层上创建的对象都可以赋值给父类,就好像父亲可以代表子女处理他们的事件。具体代码在此不表。
VBA中类的定义
在VBA中也有类似的概念,用面向对象编程的术语,类为基于它创建的所有对象定义了属性(Properties),方法(methods)和事件(Events),其中的属性、方法和事件统称为类成员。到目前为止,我使用过vba中的类的封装和多态,还没有特别使用过vba的类的继承,是可以继承的!下面自定义类的两个事件就是继承而来的。
注意在VBA中要定义类比较特殊,首先需要插入一个类模块,这个类模块就是一个类,此时出现名称为“类1”的模块,我们把“类1”的名称更改为 Student(就是类名),在右侧的代码区就可以声明属性,定义方法等。这样就相当于java类的定义了。
例如: Excel中的“学生明细”表的数据格式(为了简单,字段和记录少点,实际工作上可能较多)如下:
这时,在VBE中可以插入一个类模块,并把名称改为 Student:
在右侧代码区输入:
' 类模块代码区
Public id As Integer ' 唯一代表学生的学号,因为姓名可能会重名, 声明 id 为整数
Public name As String ' 姓名,声明name 为字符串,public代表公共的,表示创建的对象这个属性可以直接访问和修改
Private mBirthday As Date ' 出生日期,声明为日期, m表示me,由于没有this.birthday这种形式,只能在变量名上做文章
Public Property Let birthday(aBirthday As Date) ' aBirthday中第一字符a表示为参数argument
If aBirthday < DateAdd("yyyy", -130, Date) Or aBirthday >= Date Then Then
Err.Raise 5555, "Student Class module", "年龄范围不合法" ' 如果不符合要求,抛出一个自定义异常
Else
mBirthday = aBirthday
End If
End Property
Public Property Get birthday() As Date ' 属性的getter方法,Property 中文含义是 “属性”
birthday = mBirthday
End Property
Public Property Get age() As Integer ' 属性的getter方法(这里没有使用set ,如果要赋值的是对象的,把Get换成Set)
age = DateDiff("yyyy", mBirthday, Date)
End Property
Public Sub speak() ' 这个方法在后面没有使用,类中可以定义而已
MsgBox "我姓名是" & name & "今年" & age & "岁了!"
End Sub
这样就在VBA中定义了学生类,其中实现了类的部分封装特性。对于需要封装的属性,需要对多个变量名进行区分,例如:name、mName和aName,这点在java或python比较方便。
属性方法有三个:
Public Property Let|Set 方法名 ( 参数 as 类型)
Public Property Get 方法名() As 返回类型
'其中Let和Set选一个,依据是传递过来的参数类型是否是对象,如果是使用Set,否则,使用Let。
然后,插入模块(普通模块)进行测试,由于以下示例中使用了字典对象,想要使用该工具需要通过菜单 工具–> 引用后才可用,可以参考 Excel VBA 中使用集合和字典对比总结 :
代码区输入如下代码,进入调试模式,可以看到所有的学生对象已经进入字典中:
在VBA编程中,对于处理这种多条记录的,我一般会在类模块中先定义一个类,用以保存表中各条记录的各个字段内容,类属性名对应于excel表中的字段名,不想直接使用数组编程,而是利用数组获取表中的记录,然后把各条记录的内容逐个赋值给基于定义类的创建的对象属性中,然后把对象逐个保存到字典中后使用。这是因为直接采用数组,在编程中那个位置对应什么属性不直观,使用对象的属性来记录数据直观,利于理解和今后的维护。例如在编程中看到 student.name 与 arrStudent(i,2) 两种不同的表示形式,喜欢哪一个?!
' 普通模块
Public Sub testClass()
Dim i As Integer
Dim iLastRow as Integer ' 保存最后一行的行号,i 表示 integer
Dim arrStudent As Variant ' 保存所有学生记录的数组,arr 表示 array
Dim objStudent As Student ' 学生对象,就是Student类的实例,obj 表示 object
Dim dicStudent As New Dictionary ' 字典,保存所有学生对象, dic 表示dictionary
'Dim dicStudent as Object ' 这样声明也是可以的, 然后可以把实例化的字典对象的引用赋值给这个变量,就是使用了vba中类的多态!
With Sheets("学生明细")
iLastRow = .Range("A" & .Cells.Rows.Count).End(xlUp).Row ' 假设一定有学生记录, 找到最后一行的行号
arrStudent = .Range("A2:C" & iLastRow ).Value ' 把所有的学生记录保存到数组中
End With
For i = 1 To UBound(arrStudent)
Set objStudent = New Student ' 创建一个Student类的对象,又称类的实例化,并把进行引用赋值,注意要使用set关键字!
objStudent.id = arrStudent(i, 1) ' 把对应的学号赋值给对象的属性id上
objStudent.Name = arrStudent(i, 2) ' 把对应的姓名赋值给对象的属性name上
objStudent.birthday = arrStudent(i, 3) ' 把对应的出生日期赋值给对象的属性birthday 上,其实这时对象的age属性也有了
set dicStudent(objStudent.id) = objStudent ' 把这个对象存入字典这样的集合中
Next i
End Sub
' 备注:
' 我认为利用 对象 来保存 记录,然后全部存入 字典 中,这样的联合使用比较好。
' Dim dicStudent As New Dictionary , Set objStudent = New Student 这两条语句中第一语句是在声明同时进行字典的实例化,
' 而后一句,是实例化学生对象,然后把对象的引用(地址)赋值给 objStudent 变量, 注意对象引用的赋值需要set关键字!
' 这样在后续编程中,可以遍历字典,取出学生对象就可以使用其属性,例如: objStudent.id, objStudent.name 等。
三、类详细定义
前面我们已经看到了类的一些基本定义,下面详细了解一下类定义中的细节。下面把大部分属性设置成私有的,需要通过属性方法赋值和获取,实现类的封装特性。
在我们插入类模块时,VBA默认继承了2个事件:初始化事件(Class_Initialize),销毁事件(Class_Terminate)。我们可以从代码区上侧看到:
Class_Initialize初始化事件(构造方法),顾名思义,是在类实例化的时候自动触发,就在执行set objStudent = New Student这条语句的时候触发。类的初始化事件是我们初始化类属性的最佳场合,例如可以在该方法内,设置一些默认值。而销毁事件(Class_Terminate)用于在销毁对象时,把一些资源予以清理。但是这种事件不能传递参数,即不能在实例化时传递参数。
下面给出比较详细的定义:
'Student 类代码
Option Explicit '变量要求显示声明才能使用
' 类模块代码区
Public id As Integer ' 唯一代表学生的学号,因为姓名可能会重名, 声明 id 为整数
Private mName As String ' 姓名,声明name 为字符串,public代表公共的,表示创建的对象这个属性可以直接访问和修改
Private mBirthday As Date ' 出生日期,声明为日期, m表示me,由于没有this.birthday这种形式,只能在变量名上做文章
Private mGender As enuGender ' 性别,设置为枚举类型
Public Enum enuGender ' 对性别进行枚举类型(Enumeration)声明
Female = 0 ' 女
Male = 1 ' 男
End Enum
Private Sub Class_Initialize() ' 构造方法,就是在实例化时该方法会被触发
Debug.Print "对象创建初始化!" ' 这种特殊的方法是事件,不能传递参数
mGender = enuGender.Male ' 可以设置默认值,例如性别默认为男
End Sub
Private Sub Class_Terminate() ' 析构时间,对象被销毁时,该方法会被触发
Debug.Print "对象被销毁了!"
End Sub
Public Property Let name(aName As String) ' 姓名name属性的setter方法,可以控制赋值过程
If Len(aName) <= 4 Then
mName = aName
Else
Err.Raise 5555, "Student Class module", "姓名不能超过4个字符"
End If
End Property
Public Property Get name() As String ' 姓名属性的getter方法,
name = mName
End Property
Public Property Let birthday(aBirthday As Date) ' aBirthday中第一字符a表示为参数argument
If aBirthday < DateAdd("yyyy", -130, Date) Or aBirthday >= Date Then
Err.Raise 5555, "Student Class module", "年龄范围不合法" ' 如果不符合要求,抛出一个自定义异常
Else
mBirthday = aBirthday
End If
End Property
Public Property Get birthday() As Date ' 生日属性的getter方法
birthday = mBirthday
End Property
Public Property Get age() As Integer ' 生日属性的getter方法(这里没有使用set ,如果要赋值的是对象的,把Get换成Set)
age = DateDiff("yyyy", mBirthday, Date)
End Property
Public Property Get gender() As enuGender
gender = mGender
End Property
Public Property Let gender(aGender As enuGender)
Static blnFlag As Boolean
If blnFlag = False Then
blnFlag = True
mGender = aGender
Else
Err.Raise 5555, "Student Class module", "性别一旦设定,就无法修改"
End If
End Property
Public Sub speak() ' 类中定义普通方法
MsgBox "我姓名是 " & mName & " 今年 " & age & " 岁了!"
End Sub
关于封装
以上Student类中除了id属性外,其他属性都予以封装。性别属性使用了枚举类型,便于编程时可以自动提示,更加方便和专业。同时,为了防止对性别任意修改,只允许赋值一次,这里使用了 static 修饰符,能够确保在excel未关闭的前提下,不能进行第二次修改。姓名属性要求输入的字符个数必须大于4个,否则,抛出一个自定义异常。出生日期属性要求输入的日期必须符合一定的要求,使产生的年龄在1~130之间。正是有了属性方法,才能实现对赋值的检查,以及实现输入出生日期,就可以得到年龄属性值。对象编程中的一个思想是,对象本身的属性或方法,由对象自身来完成,例如年龄的计算在类中完成。
用于封装的属性方法是比较特殊的:按理说,既然是方法,那么在使用时应该使用方法调用,例如:
objStudent.speak ' vba中普通方法的调用可以不加小括号,后面加空格后直接提供参数(如果有参数时)
而属性方法的使用就不一样,就好像不是方法,而是属性,例如:
objStudent.gender = male
关于多态
前面提到了字典对象的使用,在声明时可以使用以下方式,先把变量dic定义为对象的老祖宗 object 类型,再使用CreateObject方式创建字典对象,然后把字典的引用赋值给变量,在这个过程中,其实是使用了对象的多态特性。object类型的变量被赋值了具体的对象的引用。
dim dic as object ' 声明dic为object类型
set dic = CreateObject("scripting.dictionary") ' 创建一个字典对象,并赋值给 变量 dic
前面使用了这样的声明:
Dim arrStudent() As Variant ' 声明一个数组
这里的 Variant 是一种数据类型,是未明确声明为某一其他类型(例如,integer、string等)的所有变量的数据类型。Variant 是一个特殊数据类型,它包含除固定长度 String 数据以外的任何类型的数据(包含现在支持用户定义的类型)。Variant 还可以包含特殊值 Empty、Error、Nothing 和 Null。 可以使用 VarType 函数或 TypeName 函数来确定如何对待 Variant 中的数据。这种方法的使用是否有点类似于多态,不过VBA不使用这种说法。
关于异常
大家应该遇到过,例如出现 2 / 0 ,就会抛出一个除数是0的异常。自定义异常非常简单,具体提示为:
Err.Raise Number:=vbObjectError + 513, Source:="Student Class module", Description:="年龄范围不合法"
解释:
- Err 异常对象, .Raise 异常对象的方法,借用java概念就是抛出
- Number: 异常的编号。给错误编号为vbObjectError+513,为什么弄这么一个奇怪的编号?因为vbObjectError+512之前的号码都被占用了。vbObjectError是一个系统常量,通过语句 Debug.Print vbObjectError 打印出来的是 -2147221504 。
- Source: 异常来源于哪里
- Description: 异常描述
测试:
' 普通模块
Public Sub testStu()
Dim objStudent As New Student ' 声明一个学生类对象
' Dim objStudent As Student ' 使用这样声明可以
' Set objStudent = New Student ' 实例化对象
objStudent.birthday = #1/1/1966# ' 给对象的出生日期属性赋值
objStudent.id = 1001 ' 给对象的学号赋值
objStudent.name = "john" ' 给对象的姓名赋值
objStudent.speak ' 调用对象的普通方法
End Sub
运行这段代码,就会跳出一个:
在调试模式下,可以看到对象的属性值:
关于自定义事件
前面示例了继承自object类的两个事件,下面演示如何进行自定义事件,从代码上看是出乎意料的简单。在VBE中插入一个类模块,并更名为 TimerState。
在右侧代码区域输入:
Option Explicit
Public Event UpdateTime(ByVal dblJump As Double) ' 声明自定义事件UpdateTime
Public Event ChangeText() ' 声明自定义事件ChangeText
Public Sub TimerTask(ByVal Duration As Double) ' 时间任务方法,参数是 用时
Dim dblStart As Double ' 开始时间
Dim dblSoFar As Double ' 到目前的时间
dblStart = Timer ' 记住开始的时间值
dblSoFar = dblStart ' 保存到目前的时间值
Do While Timer < dblStart + Duration ' 当前时间小于开始时间加上需要的用时,执行以下代码
If Timer - dblSoFar >= 1 Then ' 如果当前时间与过去的时间相差大于定于1
dblSoFar = dblSoFar + 1 ' 到目前的时间加1
RaiseEvent UpdateTime(Timer - dblStart) ' 触发 UpdateTime 更新时间的方法,参数是当前时间与开始时间的差值
End If
Loop
RaiseEvent ChangeText ' 触发 ChangeText 改变文体的方法
End Sub
从语法上看,自定义事件的关键字是 Event,以上代码定义了两个事件,其基本语法:
[ Public ] Event procedurename [ (arglist) ]
public 是可选的,procedurename 必需的,是事件的名称(过程名),后面是参数列举(如果有参数的话)。
然后,插入用户窗体Form1,并在上面设置一个标签Label1,两个文本框,Text1和Text2,一个命令按钮Cammand1,以上控件需要在属性窗口中的(名称)中设置成以上的名称。
右键查看代码,输入以下代码:
Option Explicit
Private WithEvents mText As TimerState ' mText 声明为 Timerstate 事件类型
Private Sub Command1_Click() ' 命令按钮点击事件
Text1.Text = "跑步用时" ' 第一个文本框 显示 信息
Text2.Text = "0" ' 第二个文本框 显示 信息
Call mText.TimerTask(9.84) ' 调用对象mText相应的时间任务方法,传入 9.84 秒参数
End Sub
Private Sub UserForm_Initialize() ' 窗体初始化
Command1.Caption = "点击开始计时" ' 命令按钮上显示文字
Text1.Text = "" ' 两个文本框初始化为空字符串
Text2.Text = ""
Label1.Caption = "最快100米跑步用时(单位:秒):" ' 标签上显示的文字
Set mText = New TimerState ' 创建TimerState类型的对象并赋值引用(地址)给mText
End Sub
Private Sub mText_ChangeText() ' mText对象的文本改变事件
Text1.Text = "计时结束"
Text2.Text = "9.84"
End Sub
Private Sub mText_UpdateTime(ByVal dblJump As Double) ' mText对象的更新时间事件
Text2.Text = Str(Format(dblJump, "0")) ' 格式化时间为字符串,并显示在第二文本框中
DoEvents ' 交出执行控制权,以便操作系统能够处理其他事件
End Sub
以上语句中(Private WithEvents mText As TimerState)就是进行变量类型声明,这里使用了 WithEvents 关键字,其含义是同时带上事件,如果没有使用该关键字,TimerState类中的事件不能使用。
Private WithEvents mText As TimerState ' mText 声明为 Timerstate 事件类型
如果在声明时不带关键字 WithEvents ,则没有相应的事件。
以上代码使用了关键字 DoEvents ,其含义是 交出执行控制权,以便操作系统能够处理其他事件
DoEvents ' 交出执行控制权,以便操作系统能够处理其他事件
启动程序后,出现以下界面:
四、结论:
VBA的类的定义和使用对于编程帮助较大,建议予以使用。只是各种语言的语法各异,这里也是给自己进行记录,便于今后查询使用。