⑴ VB 如何獲得DLL導出函數在其他進程中的地址
關於公用系統DLL的版本用API寫程序,經常少不了對Windows公用組件的調用,比如打開、保存對話框、選擇文件夾對話框等,都是多數程序都使用的作為打開、保存文件時的標准對話框,因為這些組件是由Windows自己提供的,除了方便,還可以讓自己的程序和其它Windows程序有相同的風格,方便用戶使用。但是由於這些組件是由Windows提供,隨著Windows的升級、系統軟體的更新,這些組件可能也被更新了,就可能出現各台計算機上文件版本不同。一般地,軟體針對低版本公用系統文件的設計,在高版本中使用是沒問題,但是反過來就不一定,因為可能針對高版本的設計中使用到低版本中沒有的功能,所以在設計程序的時候,根據文件的版本來做適當調整是很有需要的。這一話我要向你介紹一個獲得公用組件版本的辦法--使用DllGetVersion().
DllGetVersion並不是一個一定可以使用的API.微軟在自己的新版本的公用系統DLL中使用了這個函數,但是早期版本的系統DLL中並沒有,而這個函數並不是一個獲得其它DLL的版本的函數,而是由DLL自己告訴你它的版本,所以你不可以把它當成一個一般API來看待。那麼怎麼使用它呢?為了得知要調用的DLL是否有這個函數,我找到了這個方法:用GetProcaddress().
GetProcAddress()能訪問一個DLL,返回一個指定的函數的入口地址。使用這個函數,還需要另外兩個API:
LoadLibrary()和 FreeLibrary().以下是聲明:
Private Declare Function LoadLibrary Lib 「kernel32」Alias「LoadLibraryA」(ByVal lpLibFileName As String)As LongPrivate Declare Function FreeLibrary Lib 「kernel32」(ByValhLibMole As Long)As LongPrivate Declare Function GetProcAddress Lib 「kernel32」(ByValhMole As Long,ByVal lpProcName As String)As Long比如你要獲得 COMCTL32.DLL 的版本(如果是其它系統DLL 的話,也應該每個都聲明),就要按下面這樣聲明(雖然你不知道是否有這個函數在該DLL 中):
Private Declare Function DllGetVersion Lib 「COMCTL32」(pdviAs DLLVERSIONINFO)As Long這個函數使用到了一個DLLVERSIONINFO 的用戶定義類型:
Private Type DLLVERSIONINFOcbSize As LongdwMajor As LongdwMinor As LongdwBuildNumber As LongdwPlatformID As LongEnd TypeDLLVERSIONINFO 類型是由微軟規定,無論你要訪問哪個系統DLL,如果它有DllGetVersion 這個函數,那麼它的參數也是這個類型。
接下來就可以這么做:
Dim hmod As LongDim lR As LongDim lptrDLLVersion As LongDim tDVI As DLLVERSIONINFOhmod =LoadLibrary(「comctl32.dll」)If (hmod <>0)ThenlptrDLLVersion =GetProcAddress(hmod,「DllGetVersion」)If (lptrDLLVersion <>0)ThentDVI.cbSize =Len(tDVI)lR =DllGetVersion(tDVI)If (lR =&H0)ThenDebug.Print 「Major:」&tDVI.dwMajor,「Minor:」&tDVI.dwMinor,「Build:」&tDVI.dwBuildNumberEnd IfElseDebug.Print 「DllGetVersion Failed」
End IfFreeLibrary hmodEnd If上面這一段就是整個過程。首先用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和用戶定義類型:
Public Declare Function SHBrowseForFolder Lib 「shell32」(lpbiAs BrowseInfo)As LongPublic Declare Function SHGetPathFromIDList Lib 「shell32」(ByValpidList As Long,ByVal lpBuffer As String)As LongPublic Type BrowseInfohWndOwner As LongpIDLRoot As LongpszDisplayName As LonglpszTitle As LongulFlags As LonglpfnCallback As LonglParam As LongiImage As LongEnd TypeSHBrowseForFolder調用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和部分Windows NT的API使用ANSI字元集,但是VB使用UNICODE.這個情況下,「Look!」在內存中表示成16進制是6C 00 6F 00 6F 00 21 00,API用NULL(即0)表示字元串結束,這就是原因,所以我們需要找一個讓lpszTitle指向ANSI字元集的辦法。
你可能見過一些人(包括許多國外程序員寫的示例)使用這樣一種方法:lstrcat(「Look!」,「」),沒錯,lstrcat把兩個字元串連接起來,並返回指針,因為字元串傳遞給API時已經由VB轉換為ANSI字元了,所以它看起來似乎沒問題。但是我要建議你不要這樣做,因為它可以讓你的指針成為無效指針,不信你試試在它返回指針之後,先用Debug.Print列印一次指針(或用其它方法訪問這個變數一次)後再使用這個指針,你的指針的返回結果會變成沒用的。那麼你應該怎麼做呢?
Dim szTitle() As ByteszTitle=StrConv(「Look!」,vbFromUnicode)lpszTitle=VarPtr(szTitle(0))上面就是我的辦法。我用數組存放,並用StrConv把字元串轉換為ANSI的。
在設置好所需要的值給BrowseInfo類型的變數後,就可以調用SHBrowseForFolder了。當選擇對話框顯示過後(你按了「確定」或「取消」),SHBrowseForFolder會返回一個值(取消則返回0),它又是一個指針,這個指針指向你選擇的路徑,不過不要試圖用CopyMemory把這個指針指向的內容當成字元串般復制下來,你應該使用SHGetPathFromIDList取回(類似函數我已經講過使用方法,這里不再說明).
用這種方法調用的對話框,就如圖1,如果你需要初始化路徑,那麼你要用到回調。lpfnCallback就是指向函數入口的指針。不過不幸的是,AddressOf是關鍵字,所以你無法把AddressOf得到的值直接賦給變數,怎麼辦?
Public Function MyAddressOf(AddressOfX As Long)AsLongMyAddressOf =AddressOfXEnd Function上面是MyAddressOf 函數,我們變通一下,把AddressOf 的得到的值傳遞給自己的一個函數,再由這個函數返回AddressOf 的結果。通過MyAddressOf(AddressOf functionA)就可以得到 AddressOf functionA 的結果了。
那麼接下來是SHBrowseForFolder 所要求的回調函數的形式:
Public Function BrowseForFoldersProc(ByVal hWnd As Long,ByVal uMsg As Long,ByVal lParam As Long,ByVal lpData AsLong)As LonghWnd 是對話框的句柄,uMsg 是消息,lParam 隨著uMsg 的不同,有時有作用,有時沒有,而lpData,如以前所說過的一些回調函數,也是由你的程序來定,API把它傳給回調函數,與之相對應的是BrowseInfo 中的lParam .
這個回調函數在瀏覽文件夾對話框發送不同消息時被調用,BFFM_INITIALIZED 消息在對話框初始化完成時被發送,在這個時候,我們就可以為對話框設置選擇哪個文件夾:用 SendMessage .
SendMessage hWnd,BFFM_SETSELECTIONA,1,ByVallpData我事先為BrowseInfo 類型變數中的lParam 設置好要初始化的文件夾的路徑(ANSI 的),當回調函數被調用並且是發生BFFM_INITIALIZED 消息時,我向對話框發送BFFM_SETSELECTIONA 消息,使對話框選擇某個文件夾。當然如果你不使用lpData,你也可以在要發送時再把路徑的指針作為參數發送,它們沒什麼不同的。
最後,如果API 成功返回了一個路徑,我建議你調用一下CoTaskMemFree:
Public Declare Sub CoTaskMemFree Lib 「ole32.dll」(ByValhMem As Long)CoTaskMemFree 的參數是指向一段內存的指針,由於SHBrowseForFolder 返回了一個指向路徑的指針,即是說它可能在內存中保留了一部分資源,CoTaskMemFree則可以把這資源回收。雖然如果你不這么做可能不會馬上發現問題,但是小心一點總是好的。
好了,除去聲明,我把我的整個調用過程列出如下:
Const MAX_PATH =260Dim lpIDList As LongDim sBuffer As StringDim tBrowseInfo As BrowseInfoDim szTitle()As ByteDim sPath()As ByteszTitle =StrConv(「你要選擇哪個文件夾?」&vbNullChar,vbFromUnicode)sPath =StrConv(「C:\ 」&vbNullChar,vbFromUnicode)With tBrowseInfo.hWndOwner =Me.hWnd.lpszTitle =VarPtr(szTitle(0)).ulFlags =BIF_RETURNONLYFSDIRS OrBIF_DONTGOBELOWDOMAIN Or BIF_EDITBOX.l p f n C a l l b a c k =M y A d d r e s s O f (A d d r e s s OfBrowseForFoldersProc).lParam =VarPtr(sPath(0))End WithlpIDList =SHBrowseForFolder(tBrowseInfo)If (lpIDList)ThensBuffer =Space(MAX_PATH)SHGetPathFromIDList lpIDList,sBufferCoTaskMemFree lpIDListsBuffer =Left(sBuffer,InStr(sBuffer,vbNullChar)-1)MsgBox sBufferEnd If以下是標准模塊中的回調函數:
Public Function BrowseForFoldersProc(ByVal hWnd As Long,ByValuMsg As Long,ByVal lParam As Long,ByVal lpData As Long)As LongSelect Case uMsgCase BFFM_INITIALIZEDSendMessage hWnd,BFFM_SETSELECTIONA,1&,ByVallpDataCase BFFM_SELCHANGED'Selection changedEnd SelectEnd Function注意看我使用字元串的那兩個地方,記得結尾加上vbNullChar 以表示字元串結束。
好了,關於這一個的內容我就只講這么多,有不明白的地方,可以參考上面我的調用過程。一些地方我沒有講,我相信你已經有能力自己進一步去探討了。關於這個對話框的更復雜的使用,我想MSDN 應該是最好的輔助工具書了。
由於API 瀏覽器中查不到我給出的常量的值,所以我把這個API 常用到的一些值幫你列在下面:
瀏覽文件夾的常量:
BIF_RETURNONLYFSDIRS =1BIF_DONTGOBELOWDOMAIN =2BIF_STATUSTEXT =4BIF_RETURNFSANCESTORS =8BIF_EDITBOX =&H10BIF_VALIDATE =&H20BIF_BROWSEFORCOMPUTER =&H1000BIF_BROWSEFORPRINTER =&H2000BIF_BROWSEINCLUDEFILES =&H4000對話框發出的消息:
BFFM_INITIALIZED =1BFFM_SELCHANGED =2BFFM_VALIDATEFAILEDA =3BFFM_VALIDATEFAILEDW =4發給對話框的消息:
WM_USER =&H400BFFM_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 的見證。例如正當許多人正不知從何入手製作Office XP 風格的菜單的時候,我在去年年末就用VB 為NaviEdit 寫出了這種風格的菜單(見圖3),我靠的是API,同時也是靠一系列的方法。是誰說VB 就不好呢?我就很喜歡用。如果你想得心應手的使用VB,那麼API 是必不可少的。雖然關於GDI 方面,我未能在本次連載中講到,但我相信你已經有獨立進入更深一層研究的堅實基礎。記住這些基礎,繼續學習,並應用到實際中去,加油吧!再見!