工作需要今日捣腾VB6.0中的事件、回调等。竟然在VS6.0的MSDN中发现这些很经典、很幽默,很具有程序员特色的话。特收藏一下。
何时使用事件或回调通知
主题“使用回调的异步通知”和“使用事件的异步通知”演示表明回调所要实现的工作比事件多。但不应只是根椐工作量来决定使用哪一种方式。回调和事件代表不同的通讯方式,应选择最适合需要的。
可将事件和回调之间的差别特征化:事件象匿名广播,而回调象一次握手。
由此可知,引发事件的部件对其客户端一无所知,而进行回调的部件却知之甚详。
对于开发人员意味着:
- 客户端引用一个引发事件的对象,对这个客户端,它可将引用放置在 WithEvents
变量中来处理那些对象。引发事件的对象没有有关其客户端的信息。它向未知数目的听众进行广播, 剧院中可能一个观众都没有。
与此相对,进行回调的部件必须引用它将调用的每一个对象。它必须准确了解有多少对象。
- 引发事件的对象不会控制接收事件的客户端次序。(应小心避免与可能要遵守的次序发生关系。)
与此相对,进行回调的部件可以控制回调客户程序的次序。例如,可以给某些客户程序以更高优先权。
- 当对象引发事件时,其所有客户端都在引发事件的对象再次获得控制之前处理该事件。
与此相对,进行回调的部件在每次调用客户程序之后进行控制。
- 如果事件包含 ByRef
参数,则该参数可被任何处理事件的客户程序改变。只有最后的客户端进行的改变才对引发事件的对象可见,因为(如上所述),直到所有客户端都处理该事件之前,引发事件的对象不会再度获得控制。
与此相对,进行回调的部件在每次调用客户端之后可检查 ByRef 参数的变化。
- 如果在客户端的处理程序中发生未处理的错误,则引发事件的对象就不能接收错误。如果对象是由在客户端地址空间中运行的进程内部件提供的,则客户端和部件都会由于未处理的错误而终止。
与此相对,进行回调的部件将接收回调方法中发生的错误,并且必须准备处理它们。
注意 事件与回调的另一差别在于,事件不能具有可选参数、命名参数或 ParamArray 参数。
- 部件可用回调提供一些通知,也可用事件提供一些通知。通知的特点决定要使用什么方式。当下列所有命题为真时,应使用事件来提供通知:
- 可匿名广播通知。
- 客户端接收通知的次序不重要。
- 直到知道所有客户端接收到通知之前,部件都不必再获得控制。
- 如果通知涉及 ByRef 参数,且在每个客户端接收到通知之后,部件不必测试这些参数的值。部件只需知道最后一个指定值。(可安排客户端在使用 ByRef
参数时进行合作,例如,一旦将 Cancel 参数设置成 True,客户端就不能改变它;但无法硬性这样做。)
- 部件不必知道客户端中发生的错误。
- 可匿名广播通知。
如果上述命题中有一为假,则应做额外工作使用回调方法来提供通知
当执行任务性能极为关键时,也可使用回调方法来做额外工作。使用 Implements 命题将回调接口添加到客户程序的回调对象上,就可得到具有回调方法的 vtable 绑定。事件是不会被 vtable 绑定的。(对提供事件或回调的进程内部件,这点尤为显著。)
将函数指针传递到 DLL 和类型库
熟悉 C 语言的程序员一定会熟悉函数指针的概念。对于不熟悉 C 语言的读者,有必要对此进行一番解释。函数指针是一种约定,程序员可以用它将一个自定义的函数的地址作为参数传递到另一个函数。后面一个函数可以不是自己编写的,但是已经进行了声明,所以可以在应用程序中使用。利用函数指针,可以调用 EnumWindows 等函数列出系统中打开的窗口,利用 EnumFontFamilies 列出所有的当前字体。利用函数指针还可以访问 Win32 API 中的其它许多函数,早期的 Visual Basic 没有提供对它们的支持。
在 Visual Basic 5.0 中,使用函数指针时仍然存在若干限制。详细信息,请参阅本帮助主题后面的“函数指针的局限与风险”。
有关函数指针的知识
使用例子可以很好地说明函数指针的用法。首先,看一看 Win32 API 中的 EnumWindows 函数:
Declare Function EnumWindows lib "user32" _
(ByVal lpEnumFunc as Long, _
ByVal lParam as Long ) As Long
EnumWindows 是一个枚举函数,它能够列出系统中每一个打开的窗口的句柄。EnumWindows 的工作方式是重复地调用传递给它的第一个参数(lpEnumFunc,函数指针)。每当 EnumWindows 调用函数,EnumWindows 都传递一个打开窗口的句柄。
在代码中调用 EnumWindows 时,可以将一个自定义函数作为第一个参数传递给它,用来处理一系列的值。例如,可以编写一个函数将所有的值添加到一个列表框中,将 hWnd 值转换为窗口的名字,以及其它任何操作!
为了表明传递的参数是一个自定义函数,在函数名称的前面要加上 AddressOf 关键字。第二个参数可以是合适的任何值。例如,如果要把 MyProc 作为函数参数,可以按下面的方式调用 EnumWindows:
x = EnumWindows(AddressOf MyProc, 5)
在调用过程时指定的自定义函数被称为回调函数。回调函数(通常简称为“回调”)能够对过程提供的数据执行指定的操作。
回调函数的参数集必须具有规定的形式,这是由使用回调函数的 API 决定的。关于需要什么参数,如何调用它们,请参阅 API 文档。
使用 AddressOf 关键字
如果代码要调用 Visual Basic 5.0 的函数指针,则必须将该代码放到标准的 .BAS 模块中,不可以将其放到类模块中,也不能将其附加到窗体上。在使用 AddressOf 关键字声明函数时,必须注意下列事项:
- AddressOf 只能紧接在是参数列表中的参数前;该参数可以是自定义的过程、函数或者属性的名字。
- 写在 AddressOf 后面的过程、函数、属性必须与有关的声明和过程在同一个工程中。
- AddressOf 只能用于自定义的过程、函数和属性,不能将其用于 Declare 语句声明的外部函数,也不能将其用于类型库中的函数。
- 在声明的 Sub、Function 或自定义的类型定义中,可以将函数指针传递到 As Any 或 As Long 类型的参数。
注意 可创建用 Visual C++ (或类似的工具)编译的 DLL 中的回调函数原型。要使用 AddressOf 时,原型必须使用 __stdcall 调用约定。不能将缺省调用约定与 AddressOf 并用
在变量中存储函数指针
在某些情况下,在将函数指针传递到 DLL 之前需要将其存储在一个中间变量中。如果需要将函数指针从一个 Visual Basic 传递到另一个,这种做法是很有用的。例如,在调用 RegisterClass 之类的函数时就需要用结构 (WndClass) 的成员来传递函数指针。
要将一个函数指针赋予结构中的一个成员,需要编写一个包装函数(wrapper)。例如,下面创建的 FnPtrToLong 就是一个包装函数,使用它可以将函数指针放入任何结构中:
Function FnPtrToLong (ByVal lngFnPtr As Long) As Long
FnPtrToLong = lngFnPtr
End Function
要使用该函数,首先需要声明类型,然后再调用 FnPtrToLong。AddressOf 加上回调函数的名字作为函数的参数。
Dim mt as MyType
mt.MyPtr = FnPtrToLong(AddressOf MyCallBackFunction)
子类派生
利用子类派生技术,可以截取发送到控件或窗体的消息。通过截取这些消息,可以编写自己的代码来改变或者扩展对象的行为。类的派生技术比较复杂,对它的全面讨论将超出本书的范围。下例只能提供该技术的大致轮廓。
重点 当 Visual Basic 处于中断模式时,不允许调用 vtable 方法或 AddressOf 函数。为了保证安全,Visual Basic 仅仅将 0 返回到 AddressOf 函数的调用者。对于子类派生情况,这意味着 WindowProc 将 0 返回到 Windows。Windows 要求它的许多消息返回非 0 值,因此返回的常数 0 将导致 Windows 与 Visual Basic 之间的死锁,从而迫使进程终止。
在下例中,应用程序包括一个简单的窗体,其中只有两个命令按钮。代码的作用是截取发送到窗体的 Windows 消息,并在“立即”窗口中打印出这些消息的值。
代码的第一部分是声明部分,包括 API 函数声明,常数声明和变量声明:
Declare Function CallWindowProc Lib "user32" Alias _
"CallWindowProcA" (ByVal lpPrevWndFunc As Long, _
ByVal hwnd As Long, ByVal Msg As Long, _
ByVal wParam As Long, ByVal lParam As Long) As Long
Declare Function SetWindowLong Lib "user32" Alias _
"SetWindowLongA" (ByVal hwnd As Long, _
ByVal nIndex As Long, ByVal dwNewLong As Long) As Long
Public Const GWL_WNDPROC = -4
Global lpPrevWndProc As Long
Global gHW As Long
下一步,使用两个例程“钩入”消息流。第一个过程 (Hook) 调用了 SetWindowLong 函数,它使用了 GWL_WNDPROC 索引来创建窗口类的子类,窗口类是用来创建窗口的。然后它使用 AddressOf 关键字和回调函数 (WindowProc) 来截取消息并在“立即”窗口中打印消息的值。第二个过程 (Unhook) 关闭了子类,重新使原来的 Windows 过程成为回调函数。
Public Sub Hook()
lpPrevWndProc = SetWindowLong(gHW, GWL_WNDPROC, _
AddressOf WindowProc)
End Sub
Public Sub Unhook()
Dim temp As Long
temp = SetWindowLong(gHW, GWL_WNDPROC, _
lpPrevWndProc)
End Sub
Function WindowProc(ByVal hw As Long, ByVal uMsg As _
Long, ByVal wParam As Long, ByVal lParam As Long) As _
Long
Debug.Print "Message: "; hw, uMsg, wParam, lParam
WindowProc = CallWindowProc(lpPrevWndProc, hw, _
uMsg, wParam, lParam)
End Function
最后,窗体的代码设置了 hWnd 的初始值,按钮的代码仅仅调用了上面的两个例程:
Private Sub Form_Load()
gHW = Me.hwnd
End Sub
Private Sub Command1_Click()
Hook
End Sub
Private Sub Command2_Click()
Unhook
End Sub
声明、引发以及处理事件的总结
为了将某个事件添加到一个类中,然后使用该事件,必须这样做:
- 在定义类的类模块声明部分,用 Event 语句来声明事件—该事件带有希望它带有的任何参数。事件总是 Public。
注意 事件不能有命名的参数、可选的参数、或 ParamArray 参数。事件没有返回值。
- 在类模块代码中的合适地方,用 RaiseEvent 语句来引发事件,并提供所需要的参数。
- 在将要处理事件的模块声明部分,使用 WithEvents 关键字,添加该类类型的变量。它必须是一个模块级的变量。
- 在代码窗口左边的下拉菜单上,选择声明为 WithEvents 的变量。
- 在代码窗口右边的下拉菜单上,选择希望处理的事件。(可以为类声明多个事件。)
- 使用所提供的参数,将代码添加到事件过程中。
详细信息 在“向类中添加事件”中提供了细节和代码示例。
向类中添加事件
现在,假定已经用 Stegosaur、Triceratops 和 Tyrannosaur 类创建了一个恐龙模拟。最后要完成的是,想让 Tyrannosaur 咆哮,并且当它这样做的时候,想让这个模拟中的其它每只恐龙都突然警觉起来。
如果 Tyrannosaur 类有一个 Roar 事件,就可以在所有其它恐龙类中处理该事件。本节主题讨论的就是,在类模块中事件的声明和处理。
注意 不要在家里试验这些,至少不要用许多恐龙来作试验。若是用事件将每只恐龙跟其它每一只连接起来,可能会使得恐龙非常之慢,以至于哺乳类对象将统治这个模拟。
有人说属性和方法属于入端接口,因为它们是从对象外面调用的。相对而言,事件被叫做出端接口,因为它们是在对象里边产生,在其它地方处理。
下面讨论的主题,是用实例来描述声明、产生和处理对象的过程。
- 事件的声明与引发 跟过程一样,事件(以及它们的参数)也必须首先声明,为了让声明的事件出现,对象必须引发它。
- 处理对象的事件 对象所引发的事件,可以被其它对象用变量来处理,这些变量是用
WithEvents 关键字来声明的。
- WithEvents
与窗体上控件事件的比较 与 WithEvents
变量相关联的事件过程,和与窗体上的控件相关联的事件过程,比较它们之间的相似之处和不同之处。
- 将事件添加到窗体 将自定义事件添加到窗体的简短步骤示例。
- 事件的声明、引发以及处理总结 对于在类中使用事件的处理过程的总结。
详细信息 随专业版和企业版一起提供的《部件工具指南》中的“创建 ActiveX 部件”,讨论了设计自己的软件部件时事件的使用问题。
关于处理恐龙问题的更好办法的讨论,请参阅本章后面的“多态”。
使用函数指针的局限与风险
使用函数指针是有风险的。每当调用 DLL 的时候,就失去了 Visual Basic 开发环境的稳定性,使用函数指针的危险性就更大了,因为它很容易导致应用程序失败并因此而丢失已完成的工作。在工作的时候必须经常地保存和备份工作成果。下面列出了使用函数指针的一些注意事项:
- 调试。在中断模式下,如果应用程序引发了回调函数,那么回调函数将被正常执行,但是断点和单步设置将被忽略。如果回调函数产生了异常,则可以捕捉到它并返回当前值。在中断模式下,如果堆栈上有回调函数,那么禁止复位。
- 形实转换程序。形实转换是 Windows
实现代码重定位的手段。如果在中断模式下删除回调函数,它的形实转换程序将被修改返回
0。该值通常总是正确的,但是也有意外的情况。如果在中断模式下先删除了一个回调函数,然后又再次将其加入,那么有些被调用者可能对新的函数地址一无所知。在 .exe
中不使用形实转换程序,指针被直接传递到入口点。
- 被传递的函数的签名有误。如果函数的参数个数与调用者期望的不一致,或者在调用某个参数时错误地使用 ByRef 或
ByVal,则应用程序可能会失败。因此,在传递函数时签名一定要正确。
- 将函数传递给不存在的 Windows 过程。在对某个窗口进行子类派生的时候,需要将一个函数指针作为 Windows 过程
(WindowProc) 传递给 Windows。但是,在 IDE 中运行应用程序时,在调用 WindowProc
时下一层函数可能已经被破坏了。这可能导致一般性保护错误,并使 Visual Basic 开发环境遭到破坏。
- 不支持“Basic 到 Basic”的函数指针。在 Visual Basic 的内部不能传递指向 Visual Basic
函数的指针。目前,只支持从 Visual Basic 到 DLL 函数的指针。
- 回调过程中包含错误。回调过程中的任何错误都不应回传最初调用它的外部过程,这是很重要的。可以通过在回调过程的开始加上 On Error Resume Next 语句来实现。
- 声明、引发以及处理事件的总结
-
为了将某个事件添加到一个类中,然后使用该事件,必须这样做:
- 在定义类的类模块声明部分,用 Event 语句来声明事件—该事件带有希望它带有的任何参数。事件总是 Public。
注意 事件不能有命名的参数、可选的参数、或 ParamArray 参数。事件没有返回值。
- 在类模块代码中的合适地方,用 RaiseEvent 语句来引发事件,并提供所需要的参数。
- 在将要处理事件的模块声明部分,使用 WithEvents 关键字,添加该类类型的变量。它必须是一个模块级的变量。
- 在代码窗口左边的下拉菜单上,选择声明为 WithEvents 的变量。
-
向类中添加事件
现在,假定已经用 Stegosaur、Triceratops 和 Tyrannosaur 类创建了一个恐龙模拟。最后要完成的是,想让 Tyrannosaur 咆哮,并且当它这样做的时候,想让这个模拟中的其它每只恐龙都突然警觉起来。
如果 Tyrannosaur 类有一个 Roar 事件,就可以在所有其它恐龙类中处理该事件。本节主题讨论的就是,在类模块中事件的声明和处理。
注意 不要在家里试验这些,至少不要用许多恐龙来作试验。若是用事件将每只恐龙跟其它每一只连接起来,可能会使得恐龙非常之慢,以至于哺乳类对象将统治这个模拟。
有人说属性和方法属于入端接口,因为它们是从对象外面调用的。相对而言,事件被叫做出端接口,因为它们是在对象里边产生,在其它地方处理。
下面讨论的主题,是用实例来描述声明、产生和处理对象的过程。
- 事件的声明与引发 跟过程一样,事件(以及它们的参数)也必须首先声明,为了让声明的事件出现,对象必须引发它。
- 处理对象的事件 对象所引发的事件,可以被其它对象用变量来处理,这些变量是用
WithEvents 关键字来声明的。
- WithEvents
与窗体上控件事件的比较 与 WithEvents
变量相关联的事件过程,和与窗体上的控件相关联的事件过程,比较它们之间的相似之处和不同之处。
- 将事件添加到窗体 将自定义事件添加到窗体的简短步骤示例。
- 事件的声明、引发以及处理总结 对于在类中使用事件的处理过程的总结。
详细信息 随专业版和企业版一起提供的《部件工具指南》中的“创建 ActiveX 部件”,讨论了设计自己的软件部件时事件的使用问题。
关于处理恐龙问题的更好办法的讨论,请参阅本章后面的“多态”。
- 在代码窗口右边的下拉菜单上,选择希望处理的事件。(可以为类声明多个事件。)
- 使用所提供的参数,将代码添加到事件过程中。
详细信息 在“向类中添加事件”中提供了细节和代码示例。