第十五话关于公用系统DLL的版本
用API写程序,经常少不了对Windows公用组件的调用,比如打开、保存对话框、选择文件夹对话框等,都是多数程序都使用的作为打开、保存文件时的标准对话框,因为这些组件是由Windows自己提供的,除了方便,还可以让自己的程序和其它Windows程序有相同的风格,方便用户使用。但是由于这些组件是由Windows提供,随着Windows的升级、系统软件的更新,这些组件可能也被更新了,就可能出现各台计算机上文件版本不同。一般地,软件针对低版本公用系统文件的设计,在高版本中使用是没问题,但是反过来就不一定,因为可能针对高版本的设计中使用到低版本中没有的功能,所以在设计程序的时候,根据文件的版本来做适当调整是很有需要的。这一话我要向你介绍一个获得公用组件版本的办法——使用DllGetVersion()。DllGetVersion并不是一个一定可以使用的API。微软在自己的新版本的公用系统DLL中使用了这个函数,但是早期版本的系统DLL中并没有,而这个函数并不是一个获得其它DLL的版本的函数,而是由DLL自己告诉你它的版本,所以你不可以把它当成一个一般API来看待。那么怎么使用它呢?为了得知要调用的DLL是否有这个函数,我找
到了这个方法:用GetProcAddress()。GetProcAddress()能访问一个DLL,返回一个指定的函数的入口地址。使用这个函数,还需要另外两个API:
LoadLibrary()和FreeLibrary()。以下是声明:
PrivateDeclareFunctionLoadLibraryLib"kernel32"Alias
"LoadLibraryA"(ByVallpLibFileNameAsString)AsLongPrivateDeclareFunctionFreeLibraryLib"kernel32"(ByValhLibModuleAsLong)AsLong
PrivateDeclareFunctionGetProcAddressLib"kernel32"(ByValhModuleAsLong,ByVallpProcNameAsString)AsLong
比如你要获得COMCTL32.DLL的版本(如果是其它系统DLL的话,也应该每个都声明),就要按下面这样声明(虽然你不知道是否有这个函数在该DLL中):
PrivateDeclareFunctionDllGetVersionLib"COMCTL32"(pdvi
AsDLLVERSIONINFO)AsLong
这个函数使用到了一个DLLVERSIONINFO的用户定义类型:
PrivateTypeDLLVERSIONINFO
cbSizeAsLong
dwMajorAsLong
dwMinorAsLong
dwBuildNumberAsLong
dwPlatformIDAsLong
EndType
DLLVERSIONINFO类型是由微软规定,无论你要访问哪个系统DLL,如果它有DllGetVersion这个函数,那么它的参数也是这个类型。
接下来就可以这么做:
DimhmodAsLong
DimlRAsLong
DimlptrDLLVersionAsLong
DimtDVIAsDLLVERSIONINFOhmod=LoadLibrary("comctl32.dll")If(hmod<>0)Then
lptrDLLVersion=GetProcAddress(hmod,"DllGetVersion")
If(lptrDLLVersion<>0)Then
tDVI.cbSize=Len(tDVI)
lR=DllGetVersion(tDVI)
If(lR=&H0)Then
Debug.Print"Major:"&tDVI.dwMajor,"Minor:"&
tDVI.dwMinor,"Build:"&tDVI.dwBuildNumber
EndIf
Else
Debug.Print"DllGetVersionFailed"
EndIf
FreeLibraryhmod
EndIf
上面这一段就是整个过程。首先用LoadLibrary把系统DLL装载进内存,如果成功就返回非0,然后对返回的句柄执行GetProcAddress,这时是在假设DLL中有DllGetVersion这个函数的情况下调用的,如果成功,GetProcAddress返回非0,如果失败(包括找不到函数),则返回0,所以我们就可以知道DllGetVersion()函数可不可以调用。
调用DllGetVersion()时,需要为DLLVERSIONINFO类型参数设置好cbSize,它告诉DLL你的参数占用多少内存。如果调用DllGetVersion成功,返回值是0,这时dwMajor、dwMinor和dwBuildNumber分别是主、副版本号和编译号,而dwPlatformID是DLL的使用平台:NT
(DLLVER_PLATFORM_NT)还是WIN9X
(DLLVER_PLATFORM_WINDOWS)。其中,DLLVER_PLATFORM_NT值是&H2,DLLVER_PLATFORM_WINDOWS值是&H1,这是API浏览器中找不到的,记下来吧。
最后注意要用FreeLibrary把系统DLL从内存中释放,不然它将一直占用系统资源。
第十六话最后的礼物
这是《细水长流话API》的最后一节,从最早的《从消息说起》到上一期的《回调》,我们一起度过了许多时光,用到了许多可以说是使用API的必备知识,现在我们就来完成一个综合的题目,作为本次连载的压轴。
图1是大家经常看过的选择文件夹对话框,

它是常用的Windows公用组件之一,当然,这里吸引你的并不只是如何显示这个对话框,还有图2,

在图1的基础上为它初始化了选择的文件夹,并在上面添加了一个可以让你直接输入文件夹路径的文本框。它是怎么做的?相信这是许多人迫切想知道的。
最初我们需要用到以下的API和用户定义类型:
PublicDeclareFunctionSHBrowseForFolderLib"shell32"(lpbi
AsBrowseInfo)AsLong
PublicDeclareFunctionSHGetPathFromIDListLib"shell32"(ByValpidListAsLong,ByVallpBufferAsString)AsLong
PublicTypeBrowseInfo
hWndOwnerAsLong
pIDLRootAsLong
pszDisplayNameAsLong
lpszTitleAsLong
ulFlagsAsLong
lpfnCallbackAsLong
lParamAsLong
iImageAsLong
EndType
SHBrowseForFolder调用shell32.dll中的函数,它不同于CommonDialog等在comctl.dll中的函数,而是外壳函数,主要执行系统已经存在的一些功能。我很愿意向你逐一解释BrowseInfo的所有成员的作用,但是我觉得已经没有必要了,因为你已经学到了这里,是时候自己领悟一下了,所以我只说明示例中我认为需要说明的:
hWndOwner设置所属窗体句柄,它将一直在该窗体之上
lpszTitle指向标题的指针
ulFlags设置对话框的参数,例如:BIF_BROWSEFORCOMPUTER只返回计算机,选择其它文件夹将无法点击确定按钮
BIF_BROWSEFORPRINTER只返回打印,选择其它文件夹将无法点击确定按钮
BIF_EDITBOX添加一个输入路径的文本框,需要shell32.dll版本在
4.71以上
BIF_RETURNONLYFSDIRS只返回本系统的文件夹,选择的文件夹如果不在本机,将无法点击确定按钮
BIF_DONTGOBELOWDOMAIN不显示网络文件夹
我需要说明lpszTitle应如何使用。一般情况下,你可以通过我以前讲的办法给它传递一个字符串的指针,但是你马上会发现,假如你正确的使用StrPtr("Look!"),你却得到了错误的结果:只有一个“L”字母。更糟的是,如果你使用中文,它显示的是一堆乱码。
这是为什么?我说过,Windows9x和部分WindowsNT的API使用ANSI字符集,但是VB使用UNICODE。这个情况下,“Look!”在内存中表示成16进制是6C00
6F006F002100,API用NULL(即0)表示字符串结束,这就是原因,所以我们需要找一个让lpszTitle指向ANSI字符集的办法。
你可能见过一些人(包括许多国外程序员写的示例)使用这样一种方法:lstrcat("Look!",""),没错,lstrcat把两个字符串连接起来,并返回指针,因为字符串传递给API时已经由VB转换为ANSI字符了,所以它看起来似乎没问题。但是我要建议你不要这样做,因为它可以让你的指针成为无效指针,不信你试试在它返回指针之后,先用Debug.Print打印一次指针(或用其它方法访问这个变量一次)后再使用这个指针,你的指针的返回结果会变成没用的。那么你应该怎么做呢?
DimszTitle()AsByte
szTitle=StrConv("Look!",vbFromUnicode)
lpszTitle=VarPtr(szTitle(0))
上面就是我的办法。我用数组存放,并用StrConv把字符串转换为ANSI的。
在设置好所需要的值给BrowseInfo类型的变量后,就可以调用SHBrowseForFolder了。当选择对话框显示过后(你按了“确定”或“取消”),SHBrowseForFolder会返回一个值(取消则返回0),它又是一个指针,这个指针指向你选择的路径,不过不要试图用CopyMemory把这个指针指向的内容当成字符串般复制下来,你应该使用SHGetPathFromIDList取回(类似函数我已经讲过使用方法,这里不再说明)。
用这种方法调用的对话框,就如图1,如果你需要初始化路径,那么你要用到回调。lpfnCallback就是指向函数入口的指针。不过不幸的是,AddressOf是关键字,所以你无法把AddressOf得到的值直接赋给变量,怎么办?PublicFunctionMyAddressOf(AddressOfXAsLong)As
Long
MyAddressOf=AddressOfXEndFunction
上面是MyAddressOf函数,我们变通一下,把AddressOf的得到的值传递给自己的一个函数,再由这个函数返回AddressOf的结果。通过MyAddressOf(AddressOffunctionA)就可以得到AddressOffunctionA的结果了。那么接下来是SHBrowseForFolder所要求的回调函数
的形式:
PublicFunctionBrowseForFoldersProc(ByValhWndAsLong,ByValuMsgAsLong,ByVallParamAsLong,ByVallpDataAsLong)AsLong
hWnd是对话框的句柄,uMsg是消息,lParam随着uMsg的不同,有时有作用,有时没有,而lpData,如以前所说过的一些回调函数,也是由你的程序来定,API把它传给回调函数,与之相对应的是BrowseInfo中的lParam。
这个回调函数在浏览文件夹对话框发送不同消息时被调用,BFFM_INITIALIZED消息在对话框初始化完成时被发送,在这个时候,我们就可以为对话框设置选择哪个文件夹:用SendMessage。
SendMessagehWnd,BFFM_SETSELECTIONA,1,ByVallpData
我事先为BrowseInfo类型变量中的lParam设置好要初始化的文件夹的路径(ANSI的),当回调函数被调用并且是发生BFFM_INITIALIZED消息时,我向对话框发送BFFM_SETSELECTIONA消息,使对话框选择某个文件夹。当然如果你不使用lpData,你也可以在要发送时再把路径的指针作为参数发送,它们没什么不同的。
最后,如果API成功返回了一个路径,我建议你调用一下CoTaskMemFree:
PublicDeclareSubCoTaskMemFreeLib"ole32.dll"(ByValhMemAsLong)
CoTaskMemFree的参数是指向一段内存的指针,由于SHBrowseForFolder返回了一个指向路径的指针,即是说它可能在内存中保留了一部分资源,CoTaskMemFree则可以把这资源回收。虽然如果你不这么做可能不会马上发现问题,但是小心一点总是好的。
好了,除去声明,我把我的整个调用过程列出如下:
ConstMAX_PATH=260
DimlpIDListAsLong
DimsBufferAsString
DimtBrowseInfoAsBrowseInfo
DimszTitle()AsByte
DimsPath()AsByte
szTitle=StrConv("你要选择哪个文件夹?"&vbNullChar,vbFromUnicode)
sPath=StrConv("C:\"&vbNullChar,vbFromUnicode)
WithtBrowseInfo
.hWndOwner=Me.hWnd
.lpszTitle=VarPtr(szTitle(0))
.ulFlags=BIF_RETURNONLYFSDIRSOr
BIF_DONTGOBELOWDOMAINOrBIF_EDITBOX
.lpfnCallback=MyAddressOf(AddressOf
BrowseForFoldersProc)
.lParam=VarPtr(sPath(0))EndWith
lpIDList=SHBrowseForFolder(tBrowseInfo)If(lpIDList)Then
sBuffer=Space(MAX_PATH)
SHGetPathFromIDListlpIDList,sBuffer
CoTaskMemFreelpIDList
sBuffer=Left(sBuffer,InStr(sBuffer,vbNullChar)-1)
MsgBoxsBuffer
EndIf
以下是标准模块中的回调函数:
PublicFunctionBrowseForFoldersProc(ByValhWndAsLong,ByValuMsgAsLong,ByVallParamAsLong,ByVallpDataAsLong)AsLong
SelectCaseuMsg
CaseBFFM_INITIALIZED
SendMessagehWnd,BFFM_SETSELECTIONA,1&,ByVallpData
CaseBFFM_SELCHANGED
’Selectionchanged
EndSelect
EndFunction
注意看我使用字符串的那两个地方,记得结尾加上符串结束。好了,关于这一个的内容我就只讲这么多,有不明白的地方,可以参考上面我的调用过程。一些地方我没有讲,我相信你已经有能力自己进一步去探讨了。关于这个对话框的更复杂的使用,我想MSDN应该是最好的辅助工具书了。
由于API浏览器中查不到我给出的常量的值,所以我把这个API常用到的一些值帮你列在下面:
浏览文件夹的常量:
BIF_RETURNONLYFSDIRS=1
BIF_DONTGOBELOWDOMAIN=2
BIF_STATUSTEXT=4
BIF_RETURNFSANCESTORS=8
BIF_EDITBOX=&H10
BIF_VALIDATE=&H20
BIF_BROWSEFORCOMPUTER=&H1000
BIF_BROWSEFORPRINTER=&H2000
BIF_BROWSEINCLUDEFILES=&H4000对话框发出的消息:BFFM_INITIALIZED=1
BFFM_SELCHANGED=2
BFFM_VALIDATEFAILEDA=3
BFFM_VALIDATEFAILEDW=4发给对话框的消息:WM_USER=&H400
BFFM_SETSTATUSTEXTA=(WM_USER 100)BFFM_ENABLEOK=(WM_USER 101)BFFM_SETSELECTIONA=
(WM_USER 102)BFFM_SETSELECTIONW=(WM_USER 103)BFFM_SETSTATUSTEXTW=(WM_USER 104)
第十七话再见
很高兴你一直看到这里,但愿我在这次连载中没有出什么大差错。我也相信你开始喜欢上了API。API可以帮助VB完成许多别人认为不可能的事情。我不禁又要提起自己的NaviEdit,不仅我用它赚钱(笑),我更重视的是,它是我从初次接触API到熟练使用API的见证。例如正当许多人正不知从何入手制作OfficeXP风格的菜单的时候,我在去年年末就用VB为NaviEdit写出了这种风格的菜单(见图3),我靠的是API,同时也是靠一系列的方法。是谁说VB就不好呢?我就很喜欢用。如果你想得心应手的使用VB,那么API是必不可少的。虽然关于GDI方面,我未能在本次连载中讲到,但我相信你已经有独立进入更深一层研究的坚实基础。记住这些基础,继续学习,并应用到实际中去,加油吧!再见!