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 类模块系列之十三——给类对象安装心脏起搏器

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

VBA 类模块系列之十二——揭开事件的真相以及使用WithEvents

上篇介绍到,当类的使用者在给类的属性赋值的时候,如果赋的值不符合类的设计者规定的范围,那么作为类的设计者,为了 … 继续阅读“VBA 类模块系列之十二——揭开事件的真相以及使用WithEvents”

上篇介绍到,当类的使用者在给类的属性赋值的时候,如果赋的值不符合类的设计者规定的范围,那么作为类的设计者,为了类的健壮性,必须让类主动抛出一个异常(运行时错误)给到类的使用者。

你对“类主动”3个字有何种感受?

还记得我们为我们的第一个类Person设计了姓名(Name),性别(Gender),出生日期(DOB)属性吗?这些属性,我们既可以读取属性值,又能重新赋予它们新的值。这个过程,是谁在主动?我们看到,给这些属性赋值,以及读取这些属性值的代码,都在模块1中。模块1是Person类的使用者。所以,这个过程,是类的使用者主动在调用属性,类的使用者还能主动调用类的方法(例如Speak)。

我为什么对“类主动”还是“类的使用者主动”这个问题这么有兴趣?谈过恋爱的人都知道,如果你心仪某个女孩子,你肯定先想方设法要到她的微信,然后发消息虚寒问暖(比如,你叫什么名字呀?赵冰冰。你是什么性别啊?女性。出生日期呢?X年X月。)如果对方都是这样,有一句答一句,从不主动向你发送任何消息,你肯定是垂头丧气的。突然有一天,她主动发来消息:“下周我过生日”。你估计会欣喜若狂一晚上。这就是主动发送消息较之被动回应的不同之处。

在《系列之二——什么是类》中,我们谈到了类的接口是由属性,方法,事件组成。属性和方法,都是类的被动元素,你什么时候问我,我就什么时候告诉你,你不问,我也懒得说。而事件(Events)却恰好相反,作为类的使用者,你冷不丁的会突然收到类发过来一句消息:“下周我过生日”。有了事件,这场恋爱谈起来就有意思了,否则,她跟充气娃娃有什么区别?^_^

接下来是要给我们的第一个类Person添加一个能主动发送“下周我过生日”消息的事件吗?呃,还没到时候。

本篇讲到现在,我们只是讲清了事件的真相之一:事件的传递方向(类 -> 类的使用者)。(而属性和方法的传递方向:类的使用者 ->类)。我们还需要了解其他几个真相。

先从大家最熟悉的开始:作为一个有一定经验的Access VBA开发人员,你肯定给某个窗体(窗体1)上的命令按钮(Command0)编写过单击事件响应(Click)代码,比如:

'窗体1
Option Compare Database
Option Explicit


Private Sub Command0_Click()
    MsgBox "你好,我是窗体1"
End Sub

这里,我们清点一下:有2个对象:窗体1,命令按钮Command0,窗体1使用命令按钮Command0来接受用户鼠标的单击信息,从而执行一段预定的代码,完成一定的功能。命令按钮是Access的一个内置控件,它是一个类。窗体(窗体1)是命令按钮(Command0)类的使用者。命令按钮(Command0)这个类能向它的使用者(窗体1)发送很多消息事件(Events),比如,单击(Click),双击(DblClick),获得焦点(GotFocus)等。

稍等,你说获得焦点(GotFocus)这个事件我怎么这么陌生?我怎么从来没用过呢?你是怎么知道命令按钮还有这个事件呢?命令按钮还有哪些事件?呃,如果你有这样的问题,那我得给你补补Access VBA初级的内容了。

Alt+F11进到IDE界面,双击进入窗体1的代码模块,在代码模块窗口的左上角和右上角,分别都有一个下拉框,先选择左上角的下拉框,如图下图所示:

这个下拉框中列出了当前窗体类模块(Form_窗体1)中,可以接收到事件的对象。我们选择Command0。然后点开右上角的下拉框,如下图所示:

这个下拉框中,列出了Command0这个对象能够发送出来的消息事件(Events)。你不熟悉的“GotFocus”也在其中。

命令按钮是Access内置的控件,它也是一个类,但是我们看不到它的类代码,所以

  1. 我们不清楚命令按钮(Command0)是怎样将一个事件(比如Click)发送(Raise)出来
  2. 我们也不清楚窗体1是怎样接收到这个事件的

我们唯一清楚的是,可以为Command0的单击事件编写响应代码子程序(Private Sub Commaond0_Click()),当单击事件被Command0对象发送出来的时候,窗体1接收到这个事件,然后响应该事件的子程序自动被执行。

事件的发起方(比如Command0),我们称之为事件源(Event Source),事件的接收方(比如窗体1),我们称之为事件监听者(Event Sink)(Sink这个词的本意是下沉,沉没,很难翻译成容易理解的中文,所以我就意译为事件监听者)这就是事件的真相之二:事件包含两方:事件发起方(事件源),事件接收方(事件监听者)。

我们可以拿电台收音机来类比一下。事件源就好比一个电台,它向外发送电波,事件监听者就好比收音机,可以接收到电台发出来的电波,收听到音乐新闻等。一个事件源发送出来的电波,可以被很多台收音机接收到。这是一个生活常识,但是,事件源发送出来的事件,能被多个事件监听者接收到吗?

我直接给出答案:可以。这就是事件的真相之三:一个事件源发送的事件可以被多个事件监听者接收到。能演示一下吗?必须的。

我们新建一个窗体2,让窗体2也能接收到窗体1上的命令按钮Command0发送出来的单击(Click)事件。

为了让窗体2能建立与窗体1上的按钮Command0之间的联系,我们要在窗体2的头部定义一个命令按钮对象变量mCmd。然后想办法将这个对象变量指向Command0。

'窗体2
Option Compare Database
Option Explicit

Private mCmd As CommandButton

如何让mCmd指向Command0?我们知道,Access中所有被打开的窗体,都会被自动添加到一个叫做Forms的集合中。所以当窗体1被打开后,我们可以用Forms(“窗体1”)来引用到内存中的窗体1,然后用Forms(“窗体1”).Command0,或者Forms(“窗体1”).Controls(“Command0”)来引用到窗体1上的Command0。这件事最好是在窗体2加载的时候完成,所以有了下面的代码:

'窗体2
Option Compare Database
Option Explicit

Private mCmd As CommandButton

Private Sub Form_Load()
    Set mCmd = Forms("窗体1").Controls("Command0")
End Sub

当然这段代码要想执行成功,必须得先将窗体1打开加载到内存才行。先不管错误处理代码。

现在我们有了一个指向窗体1上的命令按钮Command0的对象变量mCmd。我们就可以随心所欲的来调用Command0这个对象的任何属性和方法,但是必须由mCmd主动发起。

但是,Command0发送出来的事件,能窗体2中的mCmd接收到吗?

能不能我不知道,但是我知道,如果能,那么窗体2的代码模块窗口左上角的下拉框中,如果有mCmd这个对象变量,那么就说明mCmd可以接收到事件,就可以为它接收到的事件编写响应子程序。你点一下左下拉框看看,mCmd并不在其中。

这里我们要向大家介绍VBA中的一个新的关键字:WithEvents。(字面意思是携带事件)它是专门用来解决我们目前遇到的问题的:现在mCmd已经指向了一个命令按钮对象Command0,如果要向让mCmd能接收到Command0发送出来的事件,就可以在定义mCmd的时候,将WithEvents关键字添加到mCmd前面。我们修改mCmd的定义如下:

'窗体2
Option Compare Database
Option Explicit

Private WithEvents mCmd As CommandButton

Private Sub Form_Load()
    Set mCmd = Forms("窗体1").Controls("Command0")
End Sub

然后我们再来看看左下拉框中的选项,如下图所示:

在mCmd之前加了关键字“WithEvents”之后,左上下拉框中就出现了mCmd选项,说明我们可以为mCmd对象变量编写事件响应子程序了。我们在下拉框中选择mCmd,然后点开右下拉框,如下图所示:

右下拉框与窗体1中Command0的右下拉框列示的内容是一样的。说明mCmd现在可以接收它所指向的对象(Command0)发送出来的所有事件了。我们只对单击事件感兴趣,所以编码如下:

'窗体2
Option Compare Database
Option Explicit

Private WithEvents mCmd As CommandButton

Private Sub Form_Load()
    Set mCmd = Forms("窗体1").Controls("Command0")
End Sub

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

Private Sub mCmd_Click()
    MsgBox "你好,我是窗体2"
End Sub

当然我们在窗体2卸载的时候,将mCmd指向Command0的指针清除。要养成主动释放指针的好习惯。

窗体2的代码写好了,我们来测试一下,看看窗体1上的命令按钮Command0发送的单击事件,是否也能被窗体2中的mCmd接收到。

先打开窗体1,然后打开窗体2。最后单击窗体1上的Command0按钮。显示的截图如下:

这样就验证了窗体2中的mCmd也接收到了Command0的单击事件!

窗体1和窗体2都可以接收到Command0的单击事件,让我们有一种错觉,似乎所有的模块都可以接收到事件。其实不是的,模块1(标准模块)就无法接收到事件。我们试一下看看先,在模块1中头部位置写如下代码:

'模块1 测试代码
Option Compare Database
Option Explicit

Private WithEvents mCmd As CommandButton

'以下代码省略
'

我们编译一下跳出错误:

WithEvents被高亮选中,编译提示:仅在模块中有效。标准模块不是模块?这里提示翻译得不准确,应该是:仅在类模块和窗体模块(或报表模块)中有效。而类模块和窗体模块都是类模块。所以这里,我们从试错的角度给大家揭示事件的真相之四:事件接收方(监听者)必须是类对象

为什么事件的接收方(事件监听者)必须是类对象?

这个与事件传播到事件监听者(可能有多个)的底层实现有关。“传播”这个词是从结果上来描述的,在代码层面,只有一个词:“调用”。那么事件是如何从事件源传播到所有的事件监听者那里的?原来,事件源(Event Source)含有一个连接点容器(Connection Point Container),该容器中都是连接点对象(Connection Point Object),每一个连接点对象都指向一个事件监听者(Event Sink),当事件源对象中某个事件被触发(Raise),传播这个事件给每个事件监听者,就变成了遍历连接点容器中的所有对象,然后调用每个对象所指向的事件监听者的事件响应程序,于是,事件得以在全部事件监听者中传播。

了解了事件的底层实现机制,事件监听者(Event Sink)必须是类对象就很好理解了。因为事件源的连接点容器中的连接点对象,是指向事件监听者的,什么东西能被一个对象(变量)所指向?很简单,这个东西肯定是内存中的一个对象。所以事件监听者必须是类对象。WithEvents关键字只能出现在类模块中(包含窗体模块和报表模块)

最后总结一下本篇的要点:

  • 事件的真相之一:事件的传递方向(类 -> 类的使用者)(而属性和方法的调用方向:类的使用者 ->类)
  • 事件的真相之二:事件包含两方:事件发起方(事件源),事件接收方(事件监听者)。
  • 事件的真相之三:一个事件源发送的事件可以被多个事件监听者接收到。
  • 事件的真相之四:事件监听者必须是类对象。
  • 定义对象变量的时候,使用WithEvents修饰关键字,能接收对象变量指向的对象发送的事件。

作为一个有一定经验的Access VBA开发者,你早就知道如何为对象的事件编写事件响应程序。通过本篇的介绍,你应该对事件有更深入的了解,并且学会了如何接收其他类对象发送出来的事件(使用WithEvent关键字),那么关于事件,你就只剩下两块内容:在类中定义自己的事件,和在类中发起你定义的事件。下一篇将介绍该内容。

最后,按惯例,将最终全部代码贴给大家。

'窗体1
Option Compare Database
Option Explicit

Private Sub Command0_Click()
    MsgBox "你好,我是窗体1"
End Sub
'窗体2
Option Compare Database
Option Explicit

Private WithEvents mCmd As CommandButton

Private Sub Form_Load()
    Set mCmd = Forms("窗体1").Controls("Command0")
End Sub

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

Private Sub mCmd_Click()
    MsgBox "你好,我是窗体2"
End Sub

注意操作顺序:先打开窗体1,然后窗体2,最后点窗体1按钮Command0。

后记:有群友(河北-SQL Designer)建议将类的术语统一起来,与其他面向对象编程的语言相一致。非常感谢他的建议。我仔细斟酌之后,发现使用统一的术语,反而让意思更加晦涩难懂。所以就还是保持原样了。敬请谅解。