VBA 面向对象编程

VBA 其实是可以(有限地)支持面向对象编程的。在 VBA 里有一些内置的类:Collection, Workbook, Worksheet 等等,但我们可以用 Class Modules 来构建自己需要的类。

创建一个类时,insert 后面选 Class Module, 然后在属性窗口修改默认的命名(如 CTest)。然后在第一行声明一个变量:

1
2
'Class Module: CTest
Public Name As String

现在在普通的 Module 里就可以引用这个类了:

1
2
3
4
5
6
7
Dim clsSomeObject As New CTest

Sub PrintTestString()
Dim clsSomeObject As New CTest
clsSomeObject.Name = "Hello, world"
Debug.Print clsSomeObject.Name
End Sub

Class Module 的组成

一个典型的 Class Module 由 4 个部分组成:

  1. Methods – functions or subs
  2. Member variables – variables
  3. Properties– 类别属于 functions 或者 subs 但在类中却像 variables 一样
  4. Events – 被某些事件触发的一类 subs

一个简单的 Class Module 示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
'Class Module: CAccount
'Member variable
Private fBalance As Double

'Properties
Property Get Balance() As Double
Balance = fBalance
End Property

Property Let Balance(value As Double)
fBalance = value
End Property

'Event - triggered when class created
Private Sub Class_Initialize()
fBalance = 100
End Sub

' Methods
Public Sub Withdraw(amount As Double)
fBalance = fBalance - amount
End Sub

Public Sub Deposit(amount As Double)
fBalance = fBalance + amount
End Sub

然后引用这个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
Sub PrintAccount()
Dim clsAccount As New CAccount

Debug.Print "At first you have: " & clsAccount.Balance

clsAccount.Deposit 50

Debug.Print "After deposit you have: " & clsAccount.Balance

clsAccount.Withdraw 100

Debug.Print "After withdraw you have: " & clsAccount.Balance
End Sub
1
2
3
At first you have: 100
After deposit you have: 150
After withdraw you have: 50

Class Module Methods

在 VBA 里类方法是类的 subs 或 functions. 和类变量一样,类方法的属性可以是 Public 或 Private.

举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
'Class Module: CExample

Public Sub PrintText(text As String)
Debug.Print text
End Sub

Public Function Calculate(amount As Double) As Double
Calculate = amount - GetDeduction
End Function

'Private 的类方法只能在类内调用
Private Function GetDeduction() As Double
GetDuction = 2.78
End Function

然后调用这个类:

1
2
3
4
5
6
7
8
Public Sub ClassMembers()
Dim oSimple As New CExample
oSimple.PrintText "Hello"

Dim total As Double
total = oSimple.Calculate(22.44)
Debug.Print total
End Sub

Class Module Member Variables

类变量和 VBA 中普通的变量非常相似,唯一的区别是在定义是用 Private 或 Public 而不是 Dim.

1
2
3
'Class Module: CBankAccount
Private fBalance As Double
Public sName As String

用 Public 关键字定义的变量意味着我们可以在类外访问这个变量:

1
2
3
4
5
6
7
8
9
10
'这段 code 会报错
Sub DemoBankAccount()
Dim oAccount As New CBankAccount

'Account 是可以访问的
oAccount.sName = "Trump"

'但 fBalance 不行
oAccount.fBalance = 100
End Sub

上面的例子中 fBalance 无法再类外被访问,因为在声明是这个变量是 Private 的.

Class Module Properties

  1. Get - 从类中返回一个 object 或 value
  2. Let - 在类中设置一个 value
  3. Set - 在类中设置一个 object

VBA Property 的格式

1
2
3
4
5
6
7
8
Public Property Get () As Type
End Property

Public Property Let (varname As Type )
End Property

Public Property Set (varname As Type )
End Property

Property 其实就是类的一种 sub. Property 的目的是在类外访问或者修改类内的私有变量. 那为什么不直接在类内用 Public 变量呢?参考下面的 例子. 假设我们用一个 class 来维护一个国家列表,我们可以把这个列表用 array 存起来:

1
2
3
4
5
6
'Class Module: CCountry
Public arrCountries as Variant

Private Sub Class_Initialize()
ReDim arrCountries(1 To 1000)
End Sub

在类外如果用户想得到这个列表的 count 的话需要用以下的方法:

1
2
3
4
5
Sub GetNumCountries()
Dim clsCountry As CCountry
NumCountries = UBound(clsCountry.arrCountries) - LBound(clsCountry.arrCountries)
Debug.Print NumCountries
End Sub

那么问题来了,挖掘机技术哪家… :

  • 为了得到国家的数量你需要先知道这个 list 是以什么方式存储的(这个 case 里是 array)
  • 如果我们把 Array 变成 Collection, 在我们的代码里所有引用这个 Array 的地方都需要更新

为了避免上述问题在类里我们可以这样返回国家的数量:

1
2
3
4
5
6
'Class Module: CCountry
Private arrCountries() As String

Public Function Count() As Long
Count = UBound(arrCountries) + 1
End Function

然后在类外这样写:

1
2
3
4
Sub GetNumCountries()
Dim clsCountry As New CCountry
Debug.Print "Number of countries is " & clsCountry.Count
End Sub

当我们要把 Array 改成用 Collection 来存储国家列表时,只需要在类定义里修改如下,而在类外的代码无需改变:

1
2
3
4
5
6
'Class Module: CCountry
Private collCountries() As Collection

Public Function Count() As Long
Count = collCountries.Count
End Function

这样在类外引用时只需要引用 Count 函数就可以了,无须指导类内国家列表时如何定义的。

在上面的例子中我们用了一些 sub 和 function 来处理需求,但 Property 还有更优雅的解决方案。

Using a Property instead of a Function/Sub

我们可以创建一个 Count Property 而不是 Count 函数:

1
2
3
Property Get Count() As Long
Count = UBound(arrCountries) - LBound(arrCountries) + 1
End Property

用 Property 与用 Function 还是有所不同的。通常我们还会写一个 Let Property:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
'Class Module: CPropertyTest
Private fTotalCost As Double

Property Get TotalCost() As Long
TotalCost = fTotalCost
End Property

Property Let TotalCost(value As Long)
fTotalCost = value
End Property

Private Sub Class_Initialize()
fTotalCost = 100
End Sub

通过 Let 我们可以向用一个变量一样用这个 Property:

1
2
3
4
5
Sub TestProperty()
Dim clsTProperty As New CPropertyTest
clsTProperty.TotalCost = 50
Debug.Print clsTProperty.TotalCost
End Sub

通过 Let 和 Get 我们可以用同样的 name 来引用 Let 或 Get Property, 从而达到像使用变量一样使用 Property. 如果使用 sub 或 function 我们可能要把代码写成这样:

1
2
clsTProperty .SetTotalCost 50
value = clsTProperty.GetTotalCost

第三个 VBA Property 是 Set. Set 的用法和 Let 类似但是 Set 用来对 object 赋值。

1
2
3
4
5
6
7
8
9
10
'Class Module: CPropertyTest
Private collPrices As Collection

Property Get prices() As Collection
Set prices = collPrices
End Property

Property Set prices(newPrices As Collection)
Set collPrices = newPrices
End Property

在类外我们可以这样用这个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Option Explicit

Sub TestLetSet()
Dim Prices As New Collection
Prices.Add 21.23
Prices.Add 22.12
Prices.Add 20.12

Dim clsCurrency As New CCurrency

Set clsCurrency.Prices = Prices

Dim PricesCopy As Collection

Set PricesCopy = clsCurrency.Prices

PrintCollection Prices, "Prices"
PrintCollection PricesCopy, "Copy"
End Sub

Sub PrintCollection(c As Collection, name As String)

Debug.Print vbNewLine & "Printing " & name & ":"

Dim item As Variant
For Each item In c
Debug.Print item
Next item

End Sub

有一个需要注意的点事当我们用 Set 时我们其实在引用同一个 collection. Set 并没有创造 collection 的副本。

Class Module Events

Class Module 有两个 Events:

  1. Initialize – 当类有新的对象创建时触发
  2. Terminate – 当类的对象被删除时触发

Events 有点像 C++ 中的构造函数和析构函数。在大多数语言中,我们可以传递参数给构造函数但是在 VBA 中并不能。

Initialize

1
2
3
4
5
6
7
8
9
10
11
12
'Class Module: CSimple
Private Sub Class_Initialize()
MsgBox "Class is being initialized"
End Sub

Private Sub Class_Terminate()
MsgBox "Class is being terminated"
End Sub

Public Sub PrintHello()
Debug.Print "Hello"
End Sub

在下面的 case 里, clsSimple 在我们第一次引用时才会被创建出来:

1
2
3
4
5
6
Sub ClassEventsInit()
Dim clsSimple As New CSimple

' Initialize occurs here
clsSimple.PrintHello
End Sub

用 Set 还是 New 来创建新对象是还是有区别的。在下面的 case 里用 Set 来创建新对象:

1
2
3
4
5
6
7
8
Sub ClassEventsInit2()

Dim clsSimple As CSimple

' Initialize occurs here
Set clsSimple = New CSimple
clsSimple.PrintHello
End Sub

正如之前所说, 我们无法给 Initialize 传递参数。如果确实要做的话我们需要首先用一个函数来创建一个对象:

1
2
3
4
'Class Module: CSimple
Public Sub Init(Price As Double)

End Sub

在类外部调用时:

1
2
3
4
5
6
7
8
9
10
11
Public Sub test()
Dim clsSimple As CSimple
Set clsSimple = CreateSimpleObject(199.99)
End Sub

Public Function CreateSimpleObject(Price As Double) As clsSimple
Dim clsSimple As New CSimple
clsSimple.Init Price

Set CreateSimpleObject = clsSimple
End Function

Terminate

当我们将类的对象设置为 Nothing 时, Terminate 也会被触发:

1
2
3
4
5
6
Sub ClassEventTerminate()
Dim clsSimple As CSimple
Set clsSimple = New CSimple

Set clsSimple = Nothing
End Sub

参考