淘先锋技术网

首页 1 2 3 4 5 6 7


本文中,大部分内容源于书籍《Excel 2007 VBA 参考大全》,ISBN:9787115311696。感谢原书第一作者及译者。

工作表模块、图表工作表模块、工作簿模块和用户窗体模块都是类模块。不过,这些模块都是特定类型的类模块,其行为与自己创建的类模块稍有不同。
这些特定的模块专门设计来支持与它们相关联的对象,提供对该对象的事件过程的访问,并且如果不删除与之相关的对象,就不能删除相应的对象模块。

1  问题背景

创建一个 Employee 对象。希望在该对象中存储雇员的姓名、每周工作时间和薪水等级,然后利用这些信息计算雇员每周的薪水。
可创建带有3个属性和1个方法的Employee对象,属性用来包含必需的数据,方法用来计算每周的薪水。

2  创建自己的对象

在类模块中:

  • 公共变量 都表现为类对象的 属性
  • 公共函数子过程 都表现为类对象的方法。函数是能产生返回值的方法,而子过程是没有返回值的方法。

创建一个名为 CEmployee 的类模块。声明了3个公共变量(属性):NameHoursPerWeekRate,一个公共函数(方法) 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 对象。
  • 该模块声明 变量 clsEmployeeCEmployee 类型。
  • 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 属性,包含正常时间和加班时间,可读取并可赋新值。
要使类模块将工作时间分成正常时间和加班时间,则应设置 NormalHoursOverTimeHours 属性,可读取但不可直接赋新值。此时,在 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 不再作为变量在声明部分进行声明,而是添加了两个新的私有变量:dNormalHrsdOverTimeHrs

此时,通过 Property Let 过程定义 HoursPerWeek,用来处理赋值给 HoursPerWeek 属性时的输入值。该过程将工作时间分成正常时间和加班时间。当访问 HoursPerWeek 属性的值时,Property Get 过程为该属性返回正常时间和加班时间之和。

仅仅通过 Property Get 过程定义 NormalHoursOverTimeHours,分别返回私有变量dNormalHrsdOverTimerHrs 的值。这使得 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个方法AddCountItemRemove),而需要在类模块中创建自己的方法和属性,即便不打算修改该集合的方法。

另一方面,可以完全控制是选择作为方法来执行,还是选择作为属性来修改。

在类模块 CEmployees 中,Function AddSub RemoveProperty Get ItemProperty 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。