本文中,大部分内容源于书籍《Excel 2007 VBA 参考大全》,ISBN:9787115311696。感谢原书第一作者及译者。
工作表模块、图表工作表模块、工作簿模块和用户窗体模块都是类模块。不过,这些模块都是特定类型的类模块,其行为与自己创建的类模块稍有不同。
这些特定的模块专门设计来支持与它们相关联的对象,提供对该对象的事件过程的访问,并且如果不删除与之相关的对象,就不能删除相应的对象模块。
1 问题背景
创建一个 Employee 对象。希望在该对象中存储雇员的姓名、每周工作时间和薪水等级,然后利用这些信息计算雇员每周的薪水。
可创建带有3个属性和1个方法的Employee对象,属性用来包含必需的数据,方法用来计算每周的薪水。
2 创建自己的对象
在类模块中:
- 公共变量 都表现为类对象的 属性
- 公共函数 或 子过程 都表现为类对象的方法。函数是能产生返回值的方法,而子过程是没有返回值的方法。
创建一个名为 CEmployee
的类模块。声明了3个公共变量(属性):Name
、HoursPerWeek
和 Rate
,一个公共函数(方法) WeeklyPay
。
Public Name As String
Public HoursPerWeek As Double
Public Rate As Double
Public Function WeeklyPay() As Double
WeeklyPay = HoursPerWeek * Rate
End Function
创建一个名为 modExamples 的标准模块,插入如下代码:
Sub EmployeePay()
Dim clsEmployee As CEmployee
Set clsEmployee = New CEmployee
clsEmployee.Name = "Mary"
clsEmployee.Rate = 15
clsEmployee.HoursPerWeek = 35
MsgBox clsEmployee.Name & "每周可挣得$" & clsEmployee.WeeklyPay & "/wk"
End Sub
- 代码从 类模块
CEmployee
中生成一个Employee
对象。 - 该模块声明 变量
clsEmployee
为CEmployee
类型。 - Set 语句 将
CEmployee
的一个新实例赋给 变量clsEmployee
。也就是说,Set语句创建了这个新对象。 - 以上代码给对象的3个属性赋值,然后产生显示在所示的消息框中的消息。消息中使用了
Employee
对象的Name
属性并执行了该对象的WeeklyPay
方法。
当仅需要创建对象变量的 单个实例 时,设置标准代码模块的另一种方法如下:
Dim Employee As New CEmployee
Sub EmployeePay()
Employee.Name = "Mary"
Employee.Rate = 15
Employee.HoursPerWeek = 35
MsgBox Employee.Name & "每周可挣得$" & Employee.WeeklyPay & " /wk"
End Sub
这里,在声明行中使用关键字 New。本例中,当在代码里第一次引用 Employee 对象时,将自动创建该对象。
3 属性过程
如果通过 声明公共变量定义属性,则为可读/写属性,能直接访问并直接赋新值,正如在上一节中所看到的。
如果需要在属性中执行检查或计算,那么应在类模块中使用 Property Let
过程和 Property Get
过程定义属性,而不是使用公共变量。
Property Get
过程允许类模块控制访问属性的方式Property Let
过程允许类模块控制给属性赋值的方式- 也可以使用
Property Set
过程,其作用与Property Let
过程相似,但用于处理对象而不是值。
例如:
假设希望将雇员的 工作时间分为正常时间和加班时间,超过35小时都属于加班时间。
需要一个 HoursPerWeek
属性,包含正常时间和加班时间,可读取并可赋新值。
要使类模块将工作时间分成正常时间和加班时间,则应设置 NormalHours
和 OverTimeHours
属性,可读取但不可直接赋新值。此时,在 CEmployee
类模块 中设置的代码为:
Public Name As String
Public Rate As Double
Private dNormalHrs As Double
Private dOverTimeHrs As Double
Public Function WeeklyPay() As Double '返回每周的薪水
WeeklyPay = dNormalHrs * Rate + dOverTimeHrs * Rate * 1.5
End Function
Property Let HoursPerWeek(dHours As Double) '将输入的工作时间转换为正常时间和加班时间
dNormalHrs = WorksheetFunction.Min(35, dHours)
dOverTimeHrs = WorksheetFunction.Max(0, dHours - 35)
End Property
Property Get HoursPerWeek() As Double '返回每周总的工作时间
HoursPerWeek = dNormalHrs + dOverTimeHrs
End Property
Property Get NormalHours() As Double '返回正常工作时间
NormalHours = dNormalHrs
End Property
Property Get OverTimeHours() As Double '返回加班时间
OverTimeHours = dOverTimeHrs
End Property
HoursPerWeek
不再作为变量在声明部分进行声明,而是添加了两个新的私有变量:dNormalHrs
和 dOverTimeHrs
。
此时,通过 Property Let
过程定义 HoursPerWeek
,用来处理赋值给 HoursPerWeek
属性时的输入值。该过程将工作时间分成正常时间和加班时间。当访问 HoursPerWeek
属性的值时,Property Get
过程为该属性返回正常时间和加班时间之和。
仅仅通过 Property Get
过程定义 NormalHours
和 OverTimeHours
,分别返回私有变量dNormalHrs
和 dOverTimerHrs
的值。这使得 NormalHours
属性和 OverTimeHours
属性都是只读属性,除了通过 HoursPerWeek
属性外,没有办法给这两个属性赋值。
用更新的 WeeklyPay
函数来计算薪水,正常时间以标准薪水等级计算,加班时间以1.5倍的标准薪水等级计算。可以将标准模块的代码修改如下
Sub EmployeePay()
Dim clsEmployee As CEmployee
Set clsEmployee = New CEmployee '创建CEmployee对象的实例
clsEmployee.Name = "Mary" '定义属性
clsEmployee.Rate = 15
clsEmployee.HoursPerWeek = 45
'显示属性
MsgBox clsEmployee.Name & "每周可挣得$" _
& clsEmployee.WeeklyPay & "/wk" _
& ",包括" & clsEmployee.OverTimeHours _
& "小时的加班时间"
End Sub
4 创建集合
4.1 Collection
对象创建集合
此时,已经有了一个 Employee
对象,如果希望有许多 Employee
对象,那么除了在集合里组织这些对象外,还有更好的方法吗?VBA有一个 Collection
对象,可在 标准模块 中使用,如下面的代码所示:
Dim mcolEmployees As New Collection '包含Employee对象的集合
Sub AddEmployees()
Dim clsEmployee As CEmployee
Dim lCount As Long
For lCount = 1 To mcolEmployees.Count '确保集合是空的
mcolEmployees.Remove 1
Next lCount
Set clsEmployee = New CEmployee '定义Employee对象
clsEmployee.Name = "Mary"
clsEmployee.Rate = 15
clsEmployee.HoursPerWeek = 45
mcolEmployees.Add clsEmployee, clsEmployee.Name '添加Employee对象到集合中
Set clsEmployee = New CEmployee '定义Employee对象
clsEmployee.Name = "Jack"
clsEmployee.Rate = 14
clsEmployee.HoursPerWeek = 35
mcolEmployees.Add clsEmployee, clsEmployee.Name '添加Employee对象到集合中
MsgBox "雇员数=" & mcolEmployees.Count '显示集合中的数据
MsgBox "mcolEmployees(2).Name = " & mcolEmployees(2).Name
MsgBox "mcolEmployees(""Jack"").Rate = " & mcolEmployees("Jack").Rate
For Each clsEmployee In mcolEmployees '处理所有的雇员
MsgBox clsEmployee.Name & "每周挣得$" & clsEmployee.WeeklyPay
Next clsEmployee
End Sub
在标准模块的顶部,声明变量 mcolEmployees
为一个新集合。
AddEmployees
过程在 For…Next
循环内使用该集合的 Remove
方法 删除所有现有的对象。
语句 .Remove 1
始终删除集合中的第一个对象,因为只要删除了集合中的第一个对象,第二个对象会自动成为第一个,依此类推。
正常情况下,可省掉这个步骤,因为初始化后的集合为空。这里仅仅是为了演示 Remove
方法,同时也允许多次运行本过程而不必担心集合中的项目越来越多。
AddEmployees
过程创建第一个雇员 Mary,并使用该集合的 .Add
方法将 Mary 对象添加到集合中。
Add
方法的 第一个参数是对对象自身的引用;第二个参数为可选参数,是一个标识关键字,用于在后面引用该对象。
本例中,使用 Employee
对象的 Name
属性作为关键字。在该过程中用相同的方式创建 Jack 对象。
如果为集合中的每个成员都提供了一个关键值,则该值必须是唯一的。
当试图添加一个新成员到集合中,而其关键值与已经使用的成员的关键值相同时,会发生运行时错误。不建议使用人名作为关键值,因为不同的人可能会有相同的名字。建议使用一个唯一的标识符,例如社会保障号(Social Security number)。
MsgBox
语句说明,可采用与引用Excel内置集合相同的方式引用所创建的集合。
例如,Employees
集合有 Count
属性,能通过位置或关键值(如果已输入了关键值)引用集合成员。
4.2 在类模块中创建集合
同样可在类模块中创建集合,但这样有优点也有缺点。
- 优点是可更好地控制与集合的交互,可防止直接访问集合,而且代码被封装在单个的模块中,更易传输,也更易维护。
- 缺点是需要采取更多的步骤创建集合,并失去了引用集合本身及其成员的一些快捷方式。
类模块 CEmployees
中的代码为:
Private mcolEmployees As New Collection '包含Employee实例的集合
Public Function Add(clsEmployee As CEmployee) '添加雇员到集合中的方法
mcolEmployees.Add clsEmployee, clsEmployee.Name
End Function
Public Property Get Count() As Long '返回Count属性
Count = mcolEmployees.Count
End Property
Public Property Get Items() As Collection '返回集合
Set Items = mcolEmployees
End Property
Public Property Get Item(vItem As Variant) As CEmployee '返回该集合的成员
Set Item = mcolEmployees(vItem)
End Property
Public Sub Remove(vItem As Variant) '删除该集合的成员
mcolEmployees.Remove vItem
End Sub
当集合处于自己的类模块中时,在标准模块中不再能够直接使用集合的4个方法(Add
、Count
、Item
和 Remove
),而需要在类模块中创建自己的方法和属性,即便不打算修改该集合的方法。
另一方面,可以完全控制是选择作为方法来执行,还是选择作为属性来修改。
在类模块 CEmployees
中,Function Add
、Sub Remove
、Property Get Item
和 Property Get Count
过程传递了该集合的方法的大多数功能。在 Property Get Items
过程中有一个新功能。Property Get Item
返回对集合中单个成员的引用,而 Property Get Items
返回对整个集合的引用,因而能在 For Each…Next
循环中使用。
此时,标准模块中的代码如下:
Sub AddEmployees()
Dim clsEmployees As CEmployees
Dim clsEmployee As CEmployee
Dim lCount As Long
Dim vNames, vRates, vHours
Dim sText As String
vNames = Array("Mary", "Jack", "Anne", "Harry") '输入数据
vRates = Array(15, 14, 20, 17)
vHours = Array(45, 35, 40, 40)
Set clsEmployees = New CEmployees '初始化集合
For lCount = LBound(vNames) To UBound(vNames) '定义和添加雇员到集合中
Set clsEmployee = New CEmployee
clsEmployee.Name = vNames(lCount)
clsEmployee.Rate = vRates(lCount)
clsEmployee.HoursPerWeek = vHours(lCount)
clsEmployees.Add clsEmployee
Set clsEmployee = Nothing
Next lCount
MsgBox "雇员数=" & clsEmployees.Count '显示集合中的数据
MsgBox "Employees.Item(2).Name = " & clsEmployees.Items(2).Name
MsgBox "Employees.Item(""Jack"").Rate = " & clsEmployees.Items("Jack").Rate
For Each clsEmployee In clsEmployees.Items
sText = sText & clsEmployee.Name & "挣得$" & clsEmployee.WeeklyPay & vbCrLf
Next clsEmployee
MsgBox sText
End Sub
将变量 clsEmployees
声明为 CEmployees
类型,接下来的代码定义了3个数组,以便区分使用的数据。
初始化 Employees
集合后,创建 Employee
对象的实例并将它们添加到集合中。作为一处不大的便利,当使用 Employees
集合的 Add
方法时不再需要指定关键值,clsEmployees
中的 Add
方法完成了这个工作。
第二、第三和第四个MsgBox
语句显示了引用该集合及其成员所需的新属性,使用 Item
属性引用成员,使用 Items
属性引用整个集合。
5 封装
类模块允许封装代码和数据。在这种方式下,代码和数据的使用与共享都极为容易,维护也变得简单多了。
用户不必知道代码是如何工作的,只需要知道类模块代表的对象,以及与对象相关的属性和方法。当必须调用Windows API(应用程序接口)来执行正常的VBA代码不可能完成的任务时,这是特别有用的。示例将演示如何封装非常复杂的代码和创建非常有用的对象。
类模块提供了封装代码的一种机制,可以在其他工作簿中使用这些代码,或者与其他程序员共享这些代码,从而缩短开发时间。可以很容易地复制类模块到另一个工作簿中。在工程资源管理器窗口中,在工程之间能直接拖放类模块。
右键单击工程资源管理器中的模块并选择“导出文件”,可创建一个能够复制到另一台PC中的文本文件,从而将类模块中的代码导出到文件中。要把该文件再导入到另一个工作簿中,只需要右键单击工程资源管理器中另一个工作簿的工程并选择“导入文件”。
至此,本章已经从普通编程设计的角度分析了类模块。接下来介绍如何使用类模块更好地控制Excel。