📄 感悟visualbasic(9).txt
字号:
感悟VisualBasic(9)
--------------------------------------------------------------------------------
广东 张鸿 时间:2003-11-28 10:09:35
第十四话回调
《细水长流话API》系列写到现在已经第十四话了,不知不觉许多API 知识都已经讲过,由于读者的要求,我们现在进入更深一层的学习:回调。
第一部分:回调的概念
什么是回调?它是干什么用的?相信初次接触回调的读者都会这样问。在一个程序中,当我们要访问一个外部函数时(指函数不在我们的程序中,一般称之为DLL 函数),我们可以主动的访问;如果我们需要外部函数主动访问我们程序中的函数时,一般是做不到的。但是在某些情况下我们需要和DLL函数之间进行信息的相互传递,或者我们的函数需要接收DLL函数连续发回的多个数据,又该怎么办呢?回调就是为此而设计的。
我们可以把我们程序中的某个函数公开给DLL函数来调用,而不是由我们自己调用,这样的函数就是回调函数,而D L L 对此函数的调用,就是回调(参考图1 )。
回调的应用很广,比如子类。其实子类也是一种回调,是系统DLL对我们程序回调。又比如计时器(API 中的计时器) ,是每隔一定时间的回调,又或者AP I 中的枚举函数,都是广泛应用到回调的。
第二部分:建立简单的回调函数
其实建立回调函数并不难,它只是一种高级的技术而已,而不是高难度的技术。下面我就用一个最简单不过的例子给你看看如果建立一个回调函数。看图2,这是一个显示桌面上所有窗口(不只是可见窗口)的句柄和它们的标题的程序,这个程序用到了三个API:
Public Declare Function EnumWindows Lib "user32" (ByVal lpEnumFunc As Long, ByVal lParam As Long) As Boolean
Public Declare Function GetWindowText Lib "user32" Alias "GetWindowTextA" (ByVal hwnd As Long, ByVal lpString As String, ByVal cch As Long) As Long
Public Declare Function GetWindowTextLength Lib "user32" Alias "GetWindowTextLengthA" (ByVal hwnd As Long) As Long
其中,EnumWindows 是最简单的一个使用回调函数的API,作用是枚举桌面上所有可见和不可见的窗口句柄,GetWindowText用来得到窗口的标题,GetWindowTextLength 用来得到窗口标题的长度。
建立一个回调函数,首先你必须根据要进行回调的DLL 函数写一个相符合的函数在公共模块里,如EnumWindows所要求的回调函数形式是这样的:
Function EnumWindowsProc(ByVal hwnd As Long, ByVal lParam As Long) As Long
所以你应该在一个公共模块里写这样一个函数:
Public Function EnumWindowsProc(ByVal hwnd As Long, ByVal lParam As Long) As Long
这个函数将要成为回调函数,作为一个原则,你不应该自己调用它,而应该把它留给DLL。接下来,在你需要调用EnumWindows时,把这个函数告诉EnumWindows:
EnumWindows AddressOf EnumWindowsProc, ByVal 0&
这里有几点要说明,一是EnumWindowsProc这个函数不必一定是这个函数名,可以是funcABC 等之类的名字,但在传递给EnumWindows(或其它需要回调函数的API时),AddressOf 之后的函数名一定要用相应的函数名( 如函数名是funcABC,则这里必须是AddressOffuncABC),DLL函数不是靠函数名来识别你程序中哪个函数是回调函数,而是靠你给DLL函数的入口地址(即该函数在内存中的地址)来识别的。二是AddressOf,这个不是函数,它只是VB新加入的标识符,你不可以用“AddressOf(EnumWindowsProc)”或者其它形式,而必须是“AddressOfEnumWindowProc”(两者之间一个空格),作用是取得某函数的入口地址,而且该函数一定要在公共模块里。三是这个回调函数的类型:As Long,不必一定是Long(用了也VB也不会说你错,但我建议总是用Long),API中回调基本上都是用长型传递数据,如果你用其它类型,你必须确定当API取回其值时能够取到正确的值,在我的示例中我用的是Boolean而不是Long,但运行仍正常,因为我知道在这里用Boolean也一定不会出错。
当你执行了上面调用EnumWindows 的API 之后,EnumWindows得到EnumWindowsProc的入口地址,这时EnumWindowsProc就成为回调函数,每当EnumWindows找到一个窗口,它就来调用一次EnumWindowsProc,并把相应的值赋给EnumWindowsProc 的两个参数(是按顺序,而不是按参数名,所以不可随意颠倒),在这个函数里,我们要对得到的参数做何用途,则是我们的事了。
那么,我的示例利用这两个参数(其实只用到一个)得到了EnumWindows找到的窗口的标题。
Public Function EnumWindowsProc(ByVal hwnd As Long, ByVal lParam As Long) As Long
Dim sSave As String, Ret As Long
Ret = GetWindowTextLength(hwnd)
sSave = Space(Ret)
GetWindowText hwnd, sSave, Ret + 1
Form1.List1.AddItem hwnd & " " & sSave
" 注意这里:
EnumWindowsProc = 1
End Function
首先,EnumWindows 找到了第一个窗口,它就调用我这个函数,并把这个窗口的句柄赋传递了hwnd 参数,我用GetWindowTextLength 得到窗口标题的长度,再用Space为sSave 字串分配足够多的内存,然后GetWindowText取回窗口的标题(类似这两个函数的调用形式前面讲过, 这里就不浪费口水了), 然后我给EnumWindowsProc 返回了1(可以是“非零”—— 0 以外的值),EnumWindows 得到我的返回值,知道我还想继续枚举,所以找到第二个窗口,并再次调用我这个函数..直到我给EnumWindowsProc返回0或者EnumWindows把所有窗口都找过了才结束。
在这个例子里,lParam 一直没有用到,而EnumWindows的第二个参数是作什么用的呢?其实这两个参数都没什么大用处,如果你调用EnumWindows 时,给lParam传递了某个值,那么EnumWindows调用你的回调函数时,会把这个值原封不动的传回来,赋给lParam。最多也就是让你可以判别当前的调用是在自己的程序哪个地方,比如在两个地方调用EnumWindows,一个给lParam传递1,一个传递2,在这里就可以知道是哪个地方在调用而作出不同处理,而不必为每一次调用都写一个回调函数。
第三部分:回调和指针的应用
以前我讲指针时,说过API 中经常遇到取回一个长型值时实际是另一个对象的指针,这里就结合回调举一个例子。
图3 是显示系统中字体的程序,用的是EnumFonts:
Public Declare Function EnumFonts Lib "gdi32" Alias "EnumFontsA" (ByVal hDC As Long, ByVal lpsz As String, ByVal lpFontEnumProc As Long, ByVal lParam As Long) As Long
EnumFonts第一个参数是对象的DC,可以是Form的DC,也可以是PictureBox 的DC,其实无论是用Form 的DC还是用PictureBox的DC结果都没有什么区别,不过这却是不可缺少的; 第二个参数是字体的家族名(FamilyName),比如找属于Arial 家族的,或是其它家族的,如果传递NULL,则是所有字体;第三个参数就是回调函数的入口地址;第四个函数和上一个例子一样,是由你的程序来指定的,结果也是由你的程序自行处理,EnumFonts只负责把它原封不动的传递给回调函数。
EnumFonts 的回调函数形式是这样的:
Function EnumFontProc(ByVal lplf As Long, ByVal lptm As Long,ByVal dwType As Long, ByVal lpData As Long) As Long
当EnumFonts 调用该函数时,l p l f 是指向一个LOGFONT 结构(自定义类型)的指针,lptm 是指向一个TEXTMETRIC 结构的指针,dwType 是字体的类型,lpData则是你传递给EnumFonts时的lParam的值。
我们还需要一个LOGFONT的自定义类型,其声明是这样的:
Private Const LF_FACESIZE = 32
Private Type LOGFONT
lfHeight As Long
lfWidth As Long
lfEscapement As Long
lfOrientation As Long
lfWeight As Long
lfItalic As Byte
lfUnderline As Byte
lfStrikeOut As Byte
lfCharSet As Byte
lfOutPrecision As Byte
lfClipPrecision As Byte
lfQuality As Byte
lfPitchAndFamily As Byte
lfFaceName(LF_FACESIZE) As Byte
End Type
LOGFONT 结构记录了一个字体的各种信息:高度、宽度、风格、名字(叫FaceName)、字符集等。
调用EnumFonts 可以这么写:
Private Sub Form_Load()
EnumFonts Me.hDC, vbNullString, AddressOf EnumFontProc, 0
End Sub
下面是回调函数:
Function EnumFontProc(ByVal lplf As Long, ByVal lptm As Long,ByVal dwType As Long, ByVal lpData As Long) As Long
Dim LF As LOGFONT, FontName As String, ZeroPos As Long
CopyMemory LF, ByVal lplf, LenB(LF)
FontName = StrConv(LF.lfFaceName, vbUnicode)
ZeroPos = InStr(1, FontName, Chr$(0))
If ZeroPos > 0 Then FontName = Left$(FontName, ZeroPos - 1)
Form1.List1.AddItem FontName
EnumFontProc = 1
End Function
我为EnumFonts 的第二个参数传递了vbNullString,可以让EnumFonts枚举所有字体。在EnumFontProc中,我又用CopyMemory通过指向一个LOGFONT结构的指针把数据复制到了一个新定义的LOGFONT结构中,这里是这一部分的重点。要注意lplf之前的ByVal。至于LenB,在这里也可以用Len,因为只有Long和Byte型的数据,不必担心一些VB本身带来的问题,虽然这里用LenB得64,用Len得61,但实际上的确是61,只是VB 内部用了一种称“内存对齐”的技术,这段程序按VB 的处理,保留了64 字节给它,所以LenB 得到它实际占用内存是64。一般对只包含Long、Integer 和Byte(或变长String)类型的数据,我们可以用Len,一般是不会有问题的,如果有定长String或更复杂的数据,还是应优先使用LenB,出错的机会较少(我也不敢肯定LenB 永远都是正确的)。
取回了lplf 指向的对象的数据后,lfFaceName就存储了字体的名字。由于VB 内部使用了UNICODE,而Windows95、Windows98以及部分WindowsNT/2000的API都是使用ANSI字符集,所以我们需要通过StrConv()把ANSI 字符转换成UNICODE 才能得到正确的文字。ZeroPos查找第一个NULL字符,它代表字体名字的结尾,如果不取回这前面的一段,我们得到的字体名字会很长,结尾有许多个NULL字符(数量视乎lfFaceName的长度而定),这是一个细节的问题,其实你不想去掉NULL 也不是不可以啦。
至于dwType,可以让我们判别该字体是何种字体:DEVICE_FONTTYPE ( 设备相关字体) 、RASTER_FONTTYPE( 光栅字体) 和TRUETYPE_FONTTYPE(TrueType 字体,Windows 中广泛使用的与设备无关的字体)。
函数最后为EnumFontProc 返回1 仍和上一个例子一样,当EnumFonts发现你的回调函数返回0或所有字体都已经枚举过,它将结束运行。
这里我保留了lptm,它和lplf 一样是一个指针,所指向的对象是一个TEXTMETRIC结构,TEXTMETRIC结构是这样的:
Public Type TEXTMETRIC
tmHeight As Long
tmAscent As Long
tmDescent As Long
tmInternalLeading As Long
tmExternalLeading As Long
tmAveCharWidth As Long
tmMaxCharWidth As Long
tmWeight As Long
tmOverhang As Long
tmDigitizedAspectX As Long
tmDigitizedAspectY As Long
tmFirstChar As Byte
tmLastChar As Byte
tmDefaultChar As Byte
tmBreakChar As Byte
tmItalic As Byte
tmUnderlined As Byte
tmStruckOut As Byte
tmPitchAndFamily As Byte
tmCharSet As Byte
End Type
它记录了一个字体更详细的信息。取回这个结构的数据的部分就留给读者自己去完成吧。
文中程序在Win98/Win2000+VB6下调试通过。源程序下载地址是:http://www.cfan.net.cn/qikan/cxg/0209gwv.zip。
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -