VBA 类模块系列之十五——结语

VBA类模块系列文章终于写完了。第一次填完一个坑,先给自己一点掌声^_^。

本系列,从类的概念和思想谈起,逐一介绍了类对象的属性,方法和事件。当然也谈到了类中的常量和错误处理以及类对象的生命周期。作为一个入门级的系列,该谈的内容都差不多谈完了。

在介绍类的自定义事件时,为了示例的效果,引入了稍微复杂一点的“心脏起搏器”的做法,这里本质上,其实是属于类对象之间的关系范畴(一个类对象包含了另一个类对象)。有了这样一个有自主意识的类对象,我们可以有更多的想象空间,比如,做成一个谈话机器人。当然这个方向就有点跑偏了,大多数人使用Access VBA并不是要搞这个。

关于VB/VBA类模块,其实还有很多内容没有谈到,比如:集合类,类对象之间的关系,子类化问题,多态(多接口),接口继承等等。这些等将来有兴致,再开一个高级类模块的系列谈谈。

最后,向广大的群友们致谢。没有你们的支持和鼓励,意见和建议,按我的一贯作风,这个系列肯定是半途夭折^_^。特别鸣谢:tmtony(王站),河北-SQL Designer,滋阴壮阳冬瓜汤。

 

VBA 类模块系列之十四——在类中定义和触发自己的事件

现在是时候解锁两个新技能了——在类中定义和触发自己的事件(Event)。

作为一个有一定经验的Access VBA开发人员,你没少写过事件的响应程序。比如:Form_Load(),Command0_Click(),Text0_Enter()等等。总是使用别人定义好的事件,有没有觉得乏味和沮丧?

现在是时候解锁两个新技能了——在类中定义触发自己的事件(Event)。这两个技能,再加上上一篇介绍的WithEvents,这三项技能一旦合体,就会变形成一支笔,神笔马良的笔,凡是用这只笔创建的类对象,就立刻有了自主意识。

前面几篇一直要说给Person类创建一个生日提醒事件,终于吹过的牛逼要兑现了^_^,所以我们在Person类的头部写如下代码:

'Person 类代码
Option Compare Database
Option Explicit
 
'
'以上代码省略

Public Event Birthday()
'
'以下代码省略

第8行就是我们定义的生日(Birthday)事件。是不是出奇的简单?^_^Public表示生日事件是公有的,事件是用来往外转播的,私有事件没有任何意义。所以,这里必须是Public,你可以试试将它改成Private,编译就会报错。Event是VBA的又一个关键字,专门用来定义事件。Birthday就是我们的事件名,后面有一对括号,我们可以在括号里面添加参数,当往外传播事件时,连同参数一起传递出去。我们先弄简单一点,不写任何参数。

生日事件定义好了,但是,在Person类中,我们该怎么来触发这个事件呢?

还记得上一篇给Person类安装的心脏起搏器吗?它每隔0.7秒就能获得一次脉冲,让她有机会思考一下今天是不是她的生日。如果是,就触发生日事件。我们修改一下Person类的Timer事件响应子程序如下:

Private Sub mfrm_Timer()
'    Debug.Print "我是" & Me.Name & ",现在时间是:" & Now() & ",我有脉搏和心跳了!"
    If Format(mdatDOB, "MMDD") = Format(Date, "MMDD") Then
        RaiseEvent Birthday
    End If
End Sub

第3行代码就是她的思考过程,比较一下当前日期(Date)的月和日与她的出生日期的月和日的值是否一致,如果一致,就执行第4行代码。

第4行代码就是触发生日事件的代码!RaiseEvent是与事件相关的第3个关键字(前2个分别是监听事件的WithEvents和定义事件的Event)RaiseEvent字面意思是“发起事件”。Birthday就是刚才我们定义的生日事件名称。

现在,我们在Person类中定义好了事件Birthday,也在Person类中,适当的地方,适当的时候,发起了这个事件。Person类(事件源 Event Source)中该做的事件都做完了,剩下的就是测试一下,看看事件监听者(Event Sink)能否监听到这个事件。第十二篇讲过,事件监听者必须是类对象,所以这个测试不能在模块1(标准模块)中来进行了。我们使用窗体4(窗体模块,也是一种类模块)来做测试。

上一篇我们在窗体4代码窗口的头部定义了一个objPerson对象变量,现在,我们还要监听objPerson对象指向的Person类对象的Birthday事件,所以,我们需要给objPerson添加WithEvents修饰符。如下代码所示:

'窗体4
Option Compare Database
Option Explicit

Private WithEvents objPerson As Person
'
'以下代码省略

有了WithEvents,就可以在窗体4的代码窗口的左上角的下拉框中,找到对象变量objPerson,然后为它的Birthday事件编写事件响应程序。如下代码所示:

'窗体4
Option Compare Database
Option Explicit

Private WithEvents objPerson As Person

Private Sub Form_Load()
    Set objPerson = New Person
    objPerson.Name = "赵冰冰"
    objPerson.Gender = Female
    objPerson.DOB = #2/3/1995#
    
End Sub

Private Sub Form_Unload(Cancel As Integer)
    Set objPerson = Nothing
End Sub

Private Sub objPerson_Birthday()
    MsgBox "今天是" & objPerson.Name & "的生日"
End Sub

事件监听者的代码也写好了,我们打开窗体4。等着一个消息窗口谈出来,告诉我们“今天是赵冰冰的生日”。

可是等了好久都没出现什么消息窗口。什么原因?是不是事件监听失败了?呃,不是的,因为今天不是她的生日,你要等到明年2月3日,才会弹出这个消息窗口。呃,要等半年多时间,有点太久了,大家都这么忙的,我们把“生日”,改成“生秒”。比如说,你是1995年2月3日8时30分25秒出生的,那么任何时点的第25秒,比如8点31分25秒,你的“生秒”就到了。所以每分钟你都能过一个“生秒”。我们修改Person类中“心脏起搏器的脉冲”响应代码:

Private Sub mfrm_Timer()
'    Debug.Print "我是" & Me.Name & ",现在时间是:" & Now() & ",我有脉搏和心跳了!"
'    If Format(mdatDOB, "MMDD") = Format(Date, "MMDD") Then
    If Format(mdatDOB, "SS") = Format(Now, "SS") Then
        RaiseEvent Birthday
    End If
End Sub

也调整一下窗体4的代码如下:

'窗体4
Option Compare Database
Option Explicit

Private WithEvents objPerson As Person

Private Sub Form_Load()
    Set objPerson = New Person
    objPerson.Name = "赵冰冰"
    objPerson.Gender = Female
'    objPerson.DOB = #2/3/1995#
    objPerson.DOB = #2/3/1995 8:30:25 AM#
    
End Sub

Private Sub Form_Unload(Cancel As Integer)
    Set objPerson = Nothing
End Sub

Private Sub objPerson_Birthday()
'    MsgBox "今天是" & objPerson.Name & "的生日"
    MsgBox "现在时间是" & Time & ",是" & objPerson.Name & "的生秒"
End Sub

然后打开窗口4,最多等待1分钟,就能看到弹出窗口:

我们完美的完成了生日事件的监听测试。

这个生日事件,有点不足之处,就是不知道她是多少岁的生日,叫我怎么为她选择数字蜡烛呢?

现在,让我们给事件加上参数。重新定义Birthday事件。

'Person 类代码
'
'以上代码省略

'Public Event Birthday()
Public Event Birthday(lintAge As Integer)
'
'以下代码省略

第6行代码,为生日事件添加了一个年龄参数lintAge。那么当你触发这个事件,向外部传播之前,需要给给这个参数赋值。

我们修改一下触发事件的代码:

Private Sub mfrm_Timer()
'    Debug.Print "我是" & Me.Name & ",现在时间是:" & Now() & ",我有脉搏和心跳了!"
'    If Format(mdatDOB, "MMDD") = Format(Date, "MMDD") Then
    If Format(mdatDOB, "SS") = Format(Now, "SS") Then
'        RaiseEvent Birthday
        RaiseEvent Birthday(Age)
    End If
End Sub

这里直接使用现成的类属性Age作为实参向外传递。

然后我们同步修改一下窗体4中的事件监听响应代码如下:

'
'以上代码省略
Private Sub objPerson_Birthday(lintAge As Integer)
    MsgBox "现在时间是" & Time & ",是" & objPerson.Name & "的" & lintAge & "岁生秒"
End Sub

再次打开窗体4,等待1分钟,弹出如下消息窗:

这就是带参数的事件的定义,触发,和响应。

总结一下。事件看起来神秘莫测,实际上,它的定义和触发都是非常简单的。在类中定义什么样的事件?在类的什么地方触发它?在什么时候触发它?这3个问题,是设计事件时,需要通盘考虑的主要问题。事件定义好以后,就可以在各个地方监听和响应事件。

最后,将我的Access文件贴在这里,供大家下载。VBA 类模块系列之十四——在类中定义和触发自己的事件

 

 

 

 

VBA 类模块系列之十三——给类对象安装心脏起搏器

原计划本篇应该谈谈如何定义自己的事件的,比如给我们创建的第一个类Person定义一个生日提醒事件。然而,我发现 … 继续阅读“VBA 类模块系列之十三——给类对象安装心脏起搏器”

原计划本篇应该谈谈如何定义自己的事件的,比如给我们创建的第一个类Person定义一个生日提醒事件。然而,我发现,我们的Person类对象从一出生(Set objperson = New Person)到最后死亡(Set objperson = Nothing),中间是没有呼吸和脉搏的。

你在说什么?什么是呼吸和脉搏?

Sub Test()
'
'以上代码省略
    objperson.Name = "赵冰冰"
    objperson.Gender = 3
    objperson.Speak
'
'以下代码省略
End Sub

上面的3句代码不是类对象的呼吸和脉搏吗?

呃,你说的没错,这3句是类对象的呼吸和脉搏,但它的心脏就只跳动了3下而已。因为外界就只刺激了它3下,这3句代码执行完,我们的类对象的心脏就停止了跳动。

一个人,虽然活着,却需要外界刺激才能反应,这个人其实跟植物人没什么两样。一个植物人,你怎么能期望她主动告诉你:“下周我过生日”呢?

所以,在给我们创建的第一个类Person定义事件之前,我们得先给她安装一个心脏起搏器,要让她有一定的主观能动性才行。

心脏起搏器是个什么东西?它具体长什么样,我也不知道,但是你可以捂住你的胸口感受一下自己的心跳,你就会知道,它是一个非常有规律的脉冲,比如0.7秒跳动一下。我们从哪里去给我们的类Person找一个每0.7秒跳动一下的东西?

作为一个有一定经验的Access VBA开发人员,不知道你使用过窗体的Timer事件没有?没有没关系,我们一起来创建一个窗体3,在窗体3的属性表的事件选项卡中,将“计时器间隔”设置为一个大于0的数(比如说2000),然后为“计时器触发事件”编写一段响应程序,那么,每隔2000毫秒(2秒)时间,这段响应程序就会自动执行一次。如下图所示:

'窗体3
Option Compare Database
Option Explicit

Private Sub Form_Timer()
    Debug.Print "每隔2秒,这里就自动执行一次"
End Sub

你保存一下窗体3,然后打开它,进到IDE界面,你会看到立即窗口中,每隔2秒,就会有一行文字“每隔2秒,这里就自动执行一次”。

如果将2000改为700,就是心脏跳动的频率了。这就是我们要给Person类安装的心脏起搏器。

那怎么安装到我们的Person类中呢?我们先来分析一下我们的需求。我们的Person类需要每隔0.7秒获得一个脉冲,而窗体3,它每隔0.7秒就能发起一个Timer事件,所以,如果我们在Person类中能够监听窗体3的Timer事件的话,我们的目的就达到了。

所以,我们在Person类的头部,定义一个与窗体3一样类型的窗体变量mfrm,因为Person类还要监听窗体变量mfrm所指向的窗体3的Timer事件,所以要添加WithEvents修饰符:

'Person 类代码
Option Compare Database
Option Explicit
 
Private mstrName As String
Private menumGender As pGender
Private mstrSituation As String
Private mdatDOB As Date

Private WithEvents mfrm As Access.Form

Public Enum pGender
    Female = 0
    Male = 1
End Enum
'
'以下代码省略

到目前位置,只是定义好了窗体变量mfrm,它还需要指向窗体3。套用上一篇的方法,在Person类的实例化事件Class_Initialize()中(对应着上一篇的窗体2的Form_Load()事件),将mfrm指向窗体3(Set mfrm = Forms(“窗体3”))。

当然这样做是没有问题的,需要注意的一点是,代码执行前,先要打开窗体3,否则Forms(“窗体3”)就报错。

窗体3是Person类的心脏起搏器,现在你把它打开了,呃,人人都看得到Person类的心脏起搏器,一个人,她的心脏长在外面,吓人就算了,你发现她的心脏右上角还有一个关闭按钮,那就真的有点说不过去了。一不小心人把她的心脏给关闭了可咋整?(当然,应该是关不掉的,原因暂按不表)

所以我们这里,换一种方法,如下:

Private Sub Class_Initialize()
    Debug.Print "我出生了!"
    Situation = "一般"
    
    Set mfrm = New Form_窗体3

End Sub

第5行代码,有没有让你惊掉下巴^_^?其实很好理解,Form_窗体3是一个类的名字,所以 New Form_窗体3 就是创建一个Form_窗体3的类的对象,让mfrm这个窗体变量指向这个类对象。于是窗体3就在内存中被创建,但是,它不会显示出来,除非你设置它的Visible属性等于True。使用这种方式打开的窗体3,不会出现在Forms()中,所以你引用Forms(“窗体3”)是会报错的。

如果你问我,我们在定义mfrm的时候,指定的类型是Access.Form,为什么我们不给mfrm指定“Form_窗体3”类型?如果你能有这种疑问,说明你是带着脑子在看我的文章^_^。这里也先按下不表。因为涉及到面向对象的多态的概念,以后再谈。

好了,内存中创建了窗体3对象,mfrm也指向了窗体3。现在我们可以用指向窗体3的mfrm对象变量,任意的调用窗体3的属性和方法了。又由于mfrm前面加了关键字WithEvents,Person类对象可以监听窗体3的任何事件了!为什么任意任何被我加粗了?我想强调的是一种自由,一种想干嘛就可以干嘛的自由。通过mfrm,窗体3现在是你的提线木偶,一切听你的指示,做上帝的感觉又来了,有木有?^_^

现在可以监听窗体3的Timer事件了吗?呃,要看情况。要看什么情况?我们知道窗体的Timer事件的触发,是需要条件的,什么条件?窗体的“计时器触发”属性和“计时器间隔”属性必须要设置正确,Timer事件才能正常被触发。我们的第一个图中,“计时器触发”属性的值是“[事件过程]”,“计时器间隔”属性的值是“2000”,都已经设置好了呀?

是的,你没有看错,上面的截图确实是这样设置好了的。但是,心脏这么重要的事情,你能交给别人去帮你设置这2个重要属性的值吗?如果万一某个人打开窗体3,把这2个属性值给改了,怎么办?所以要确保万无一失,我们使用木偶的线mfrm来设置窗体3的这2个属性:

Private Sub Class_Initialize()
    Debug.Print "我出生了!"
    Situation = "一般"
    
    Set mfrm = New Form_窗体3
    mfrm.OnTimer = "[事件过程]"
    mfrm.TimerInterval = 700
End Sub

这里,直接将计时器间隔(TimerInterval)2000的原始值,覆盖为700。

好了,现在可以为mfrm监听到的窗体3的Timer事件写事件响应代码了:

Private Sub mfrm_Timer()
    Debug.Print "我是" & Me.Name & ",现在时间是:" & Now() & ",我有脉搏和心跳了!"
End Sub

现在窗体3的Timer事件,有了2个监听者,一个是Person类对象,通过mfrm监听,另一个是窗体3本身,还记得窗体3中的 Form_Timer()代码吧?窗体3的Timer事件,只需要Person类对象来监听就好了,我们将窗体3中的Form_Timer()代码注释掉。

'窗体3
Option Compare Database
Option Explicit

'Private Sub Form_Timer()
'    Debug.Print "每隔2秒,这里就自动执行一次"
'End Sub

最后,在Person类的销毁事件中,将mfrm对象变量的指针释放掉:

Private Sub Class_Terminate()
    Set mfrm = Nothing
    Debug.Print "我挂了!"
End Sub

好了,Person类对象的心脏起搏器安装完毕。我们在模块1中写如下测试代码:

'模块1 测试类对象
Option Compare Database
Option Explicit

Sub Test()
On Error GoTo ErrHandler
    Dim objPerson As Person
    Set objPerson = New Person
    
    objPerson.Name = "赵冰冰"
    objPerson.Gender = Female
    objPerson.DOB = #2/3/1995#
    
    While True
        DoEvents
    Wend
'    objperson.Speak
'
'    MsgBox "修改前性别:" & objperson.Gender
'    objperson.Gender = Male
'    MsgBox "修改后性别:" & objperson.Gender
    
'    Debug.Print objperson.DOBtoString       '立即窗口值:1995年02月03日
'    Debug.Print objperson.Age               '立即窗口值:23
'    objperson.DOB = #6/10/1995#
'    Debug.Print objperson.DOBtoString       '立即窗口值:1995年02月03日
ExitProcedure:
    On Error Resume Next
    Set objPerson = Nothing
Exit Sub
ErrHandler:
    MsgBox "错误代码:" & Err.Number & VBA.vbCrLf & _
           "错误描述:" & Err.Description & VBA.vbCrLf & _
           "错误来源: " & Err.Source, _
           vbCritical, _
           "Test 报错"
    Resume ExitProcedure
End Sub

第14到16行代码,写了一个无限循环的语句。目的是为了让Test中定义的objPerson对象一直存活下去。DoEvents是释放线程控制权的意思。运行Test代码,你会在立即窗口中看到如下的消息:

这段测试代码,你不干预,它会永远运行下去,所以,当你听厌了赵冰冰的呼喊声后,可以点停止按钮强行停止执行。

对代码有洁癖的人,是不会就此罢休的。所以,再创建一个窗体4,在窗体4的代码窗口写如下测试代码:

'窗体4
Option Compare Database
Option Explicit

Private objPerson As Person

Private Sub Form_Load()
    Set objPerson = New Person
    objPerson.Name = "赵冰冰"
    objPerson.Gender = Female
    objPerson.DOB = #2/3/1995#
End Sub

Private Sub Form_Unload(Cancel As Integer)
    Set objPerson = Nothing
End Sub

使用窗体4做类的代码测试的好处是,只要窗体4打开后不关闭,objPerson对象就一直存活在内存中,你就可以一直听到赵冰冰的心跳和呼喊声,直到关闭窗体4。^_^

这回的出生入死的记录就完整了^_^

最后,就不按惯例贴出所有代码了。因为有点长,窗体也比较多。直接将我的Access文件贴在这里,供大家下载。VBA 类模块系列之十三——给类对象安装心脏起搏器

下一篇应该不会跳票了,开讲自定义事件和触发自定义事件。敬请期待^_^