感悟 Visual Basic (九)
第十四话回调
《细水长流话API》系列写到现在已经第十四话了,不知不觉许多API知识都已经讲过,由于读者的要求,我们现在进入更深一层的学习:回调。
第一部分:回调的概念
什么是回调?它是干什么用的?相信初次接触回调的读者都会这样问。在一个程序中,当我们要访问一个外部函数时(指函数不在我们的程序中,一般称之为DLL函数),我们可以主动的访问;如果我们需要外部函数主动访问我们程序中的函数时,一般是做不到的。但是在某些情况下我们需要和DLL函数之间进行信息的相互传递,或者我们的函数需要接收DLL函数连续发回的多个数据,又该怎么办呢?回调就是为此而设计的。
我们可以把我们程序中的某个函数公开给DLL函数来调用,而不是由我们自己调用,这样的函数就是回调函数,而DLL对此函数的调用,就是回调(参考图1)。回调的应用很广,比如子类。其实子类也是一种回
调,是系统DLL对我们程序回调。又比如计时器(API中的计时器),是每隔一定时间的回调,又或者API中的枚举函数,都是广泛应用到回调的。
第二部分:建立简单的回调函数
其实建立回调函数并不难,它只是一种高级的技术而已,而不是高难度的技术。下面我就用一个最简单不过的例子给你看看如果建立一个回调函数。看图2,这是一个显示桌面上所有窗口(不只是可见窗口)的句柄和它们的标题的程序,这个程序用到了三个API:
PublicDeclareFunctionEnumWindowsLib"user32"(ByVallpEnumFunc
AsLong,ByVallParamAsLong)AsBoolean
PublicDeclareFunctionGetWindowTextLib"user32"Alias
"GetWindowTextA"(ByValhwndAsLong,ByVallpStringAsString,ByValcchAsLong)AsLong
PublicDeclareFunctionGetWindowTextLengthLib"user32"Alias
"GetWindowTextLengthA"(ByValhwndAsLong)AsLong
其中,EnumWindows是最简单的一个使用回调函数的API,作用是枚举桌面上所有可见和不可见的窗口句柄,GetWindowText用来得到窗口的标题,GetWindowTextLength用来得到窗口标题的长度。
建立一个回调函数,首先你必须根据要进行回调的DLL函数写一个相符合的函数在公共模块里,如EnumWindows所要求的回调函数形式是这样的:
FunctionEnumWindowsProc(ByValhwndAsLong,ByVallParamAs
Long)AsLong所以你应该在一个公共模块里写这样一个函数:PublicFunctionEnumWindowsProc(ByValhwndAsLong,ByVal
lParamAsLong)AsLong
这个函数将要成为回调函数,作为一个原则,你不应该自己调用它,而应该把它留给DLL。接下来,在你需要调用EnumWindows时,把这个函数告诉EnumWindows:
EnumWindowsAddressOfEnumWindowsProc,ByVal0&
这里有几点要说明,一是EnumWindowsProc这个函数不必一定是这个函数名,可以是funcABC等之类的名字,但在传递给EnumWindows(或其它需要回调函数的API时),AddressOf之后的函数名一定要用相应的函数名
(如函数名是funcABC,则这里必须是AddressOffuncABC),DLL函数不是靠函数名来识别你程序中哪个函数是回调函数,而是靠你给DLL函数的入口地址(即该函数在内存中的地址)来识别的。二是AddressOf,这个不是函数,它只是VB新加入的标识符,你不可以用“AddressOf
(EnumWindowsProc)”或者其它形式,而必须是“AddressOfEnumWindowProc”(两者之间一个空格),作用是取得某函数的入口地址,而且该函数一定要在公共模块里。三是这个回调函数的类型:AsLong,不必一定是Long(用了也VB也不会说你错,但我建议总是用Long),API中回调基本上都是用长型传递数据,如果你用其它类型,你必须确定当API取回其值时能够取到正确的值,在我的示例中我用的是Boolean而不是Long,但运行仍正常,因为我知道在这里用Boolean也一定不会出错。
当你执行了上面调用EnumWindows的API之后,EnumWindows得到EnumWindowsProc的入口地址,这时EnumWindowsProc就成为回调函数,每当EnumWindows找到一个窗口,它就来调用一次EnumWindowsProc,并把相应的值赋给EnumWindowsProc的两个参数(是按顺序,而不是按参数名,所以不可随意颠倒),在这个函数里,我们要对得到的参数做何用途,则是我们的事了。那么,我的示例利用这两个参数(其实只用到一个)得到
了EnumWindows找到的窗口的标题。
PublicFunctionEnumWindowsProc(ByValhwndAsLong,ByVallParam
AsLong)AsLong
DimsSaveAsString,RetAsLong
Ret=GetWindowTextLength(hwnd)
sSave=Space(Ret)
GetWindowTexthwnd,sSave,Ret 1
Form1.List1.AddItemhwnd&""&sSave
’注意这里:
EnumWindowsProc=1
EndFunction
首先,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
回调举一个例子。
图3是显示系统中字体的程序,用的是EnumFonts:
PublicDeclareFunctionEnumFontsLib"gdi32"Alias"EnumFontsA"(ByValhDCAsLong,ByVallpszAsString,ByVallpFontEnumProcAsLong,
ByVallParamAsLong)AsLong EnumFonts第一个参数是对象的DC,可以是Form的DC,也可以是PictureBox的DC,其实无论是用Form的DC还是用PictureBox的DC结果都没有什么区别,不过这却是不可缺少的;第二个参数是字体的家族名
(FamilyName),比如找属于Arial家族的,或是其它家族的,如果传递NULL,则是所有字体;第三个参数就是回调函数的入口地址;第四个函数和上一个例子一样,是由你的程序来指定的,结果也是由你的程序自行处理,EnumFonts只负责把它原封不动的传递给回调函数。
EnumFonts的回调函数形式是这样的:
FunctionEnumFontProc(ByVallplfAsLong,ByVallptmAsLong,ByValdwTypeAsLong,ByVallpDataAsLong)AsLong
当EnumFonts调用该函数时,lplf是指向一个LOGFONT结构(自定义类型)的指针,lptm是指向一个TEXTMETRIC结构的指针,dwType是字体的类型,lpData则是你传递给EnumFonts时的lParam的值。
我们还需要一个LOGFONT的自定义类型,其声明是这样的:
PrivateConstLF_FACESIZE=32
PrivateTypeLOGFONT
lfHeightAsLong
lfWidthAsLong
lfEscapementAsLong
lfOrientationAsLong
lfWeightAsLong
lfItalicAsByte
lfUnderlineAsByte
lfStrikeOutAsByte
lfCharSetAsByte
lfOutPrecisionAsByte
lfClipPrecisionAsByte
lfQualityAsByte
lfPitchAndFamilyAsByte
lfFaceName(LF_FACESIZE)AsByte
EndType
LOGFONT结构记录了一个字体的各种信息:高度、宽度、风格、名字(叫FaceName)、字符集等。
调用EnumFonts可以这么写:
PrivateSubForm_Load()
EnumFontsMe.hDC,vbNullString,AddressOfEnumFontProc,0
EndSub
下面是回调函数:
FunctionEnumFontProc(ByVallplfAsLong,ByVallptmAsLong,ByValdwTypeAsLong,ByVallpDataAsLong)AsLong
DimLFAsLOGFONT,FontNameAsString,ZeroPosAsLong
CopyMemoryLF,ByVallplf,LenB(LF)
FontName=StrConv(LF.lfFaceName,vbUnicode)
ZeroPos=InStr(1,FontName,Chr$(0))
IfZeroPos>0ThenFontName=Left$(FontName,ZeroPos-1)
Form1.List1.AddItemFontName
EnumFontProc=1
EndFunction
我为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结构是这样的:
PublicTypeTEXTMETRIC
tmHeightAsLong
tmAscentAsLong
tmDescentAsLong
tmInternalLeadingAsLong
tmExternalLeadingAsLong
tmAveCharWidthAsLong
tmMaxCharWidthAsLong
tmWeightAsLong
tmOverhangAsLong
tmDigitizedAspectXAsLong
tmDigitizedAspectYAsLong
tmFirstCharAsByte
tmLastCharAsByte
tmDefaultCharAsByte
tmBreakCharAsByte
tmItalicAsByte
tmUnderlinedAsByte
tmStruckOutAsByte
tmPitchAndFamilyAsByte
tmCharSetAsByte
EndType
它记录了一个字体更详细的信息。取回这个结构的数据的部分就留给读者自己去完成吧。
文中程序在Win98/Win2000 VB6下调试通过。