VB與Windows API 間的呼叫技巧 (入门必看)

类别:VB语言 点击:0 评论:0 推荐:

VB與Windows API 間的呼叫技巧



















   一般會使用WINDOW API的情況,實在是因為VB本身不提供某些功能,但是,程式所需又不得不然,例如:讀取Registry內的資料,VB只提供SaveSetting、Getsetting 等系列的指令,但是它只能讀取特定地區的值,要讀、刪、更動其他區域的值時,就無法仔細看一看Combo Box的Events,其中沒有MouseMove,但這是我們經常用上的一個Event,那該如何呢?是的,那只有透過Winodow API。而VB呼叫Window API一般不都使用API檢視員,直接將相對應的API  COPY到我們的程式中就好,那還用什麼技巧嗎?其實不然,因為VB資料格式的問題,又加上VB本身沒有指標,在許多地方需要一些小技巧才能解決,而且我們經常因應不同的需求,將API 檢視員的宣告COPY過來後再做一些修改,最重要的,如果有一個.DLL檔,它不在API 檢視員中定義,那時,就只有自己想辦法啦。











一、 整數參數











Windows

Int,  INT
UNIT,  DWORD
BOOL
WPARAM,  LPARAM,  LRESULT
Handle(如HKEY)
WORD,  ATOM,  SHORT
BYTE, CHAR





API32位元VB

ByVal  Long
ByVal  Long
ByVal  Long    ture時為1
ByVal  Long
ByVal  Long
ByVal  Long
ByVal  Integer
ByVal  Byte








Eg.
-----------------------------------------------------------------------------
Windows API 宣告

  SHORT GetKeyState( int  nVirtKey )

對應的VB宣告

 Declare Function GetKeyState Lib "user32" (ByVal nVirtKey As Long) As Integer
--------------------------------------------------------------------------------------------------------------

    這個API 可用來檢視某些KEY (如Insert鍵、Num Lock、CapsLock等)是on/off。程式如下:這個例子應該可十分楚的看到各個整數間的宣告對應。
----------------------------------------------------------------------------------------------------------------
Dim InsertMode as Integer
InsertMode = GetKeyState(vbKeyInsert) And vbShiftMask
If InsertMode = 1 then
  Debug.print "表示 Insert Mode"
Else
  Debug.print "表示 OverWrite Mode"
End If











二、 指向整數的指標











Windows API

LPINT
LPUNIT
LPBOOL
LPDWORD
LPHANDLE (如:PHKEY)
LPWORD
LPSHORT
LPBYTE






32位元VB

(ByRef )  Long
(ByRef )  Long
(ByRef )  Long
(ByRef )  Long
(ByRef )  Long
(ByRef )  Integer
(ByRef )  Integer
(ByRef )  Byte








VB內定是使用傳址呼叫,所以ByRef 可以省略,也就是說
   Func(ByRef param1 as type)
     與
   Func(param1 as  type)
是相同的,使用傳址呼叫的方式,不外乎想將參數傳給API 後將結果傳回來。然而LONG型態的傳址呼叫在VB中又佔了相當大的份量,因為32位元的指標都是LONG的型態,而字串、自定型態的Structure在Windows API中是以指標來傳遞的,而指標的傳遞事實上也是Long值的傳遞,只不過傳過去的LONG值,於WIN API中會將之當成Address,而再配合指標運作而得指標所指的內容,這個觀念在後面會很重要。

例如:
-----------------------------------------------------------------------------
LONG RegOpenKeyEx(
    HKEY        hKey,           // handle of open key
    LPCTSTR     lpszSubKey,     // address of name of subkey to open
    DWORD       dwReserved,     // reserved
    REGSAM      samDesired,     // security access mask
    PHKEY       phkResult       // address of handle of open key
   );
相對應的VB 宣告
Declare Function RegOpenKeyEx Lib "advapi32.dll" Alias "RegOpenKeyExA" _
        (ByVal hKey As Long, _
         ByVal lpSubKey As String, _
         ByVal ulOptions As Long,  _
         ByVal samDesired As Long, _
         phkResult As Long)  As Long   '//最後一個參數是ByRef之宣告
-----------------------------------------------------------------------------

    我們經常會想要用程式來讀取Registry中的資料,例如:我們想得知Win95的Product ID該如何做呢?這裡有幾個觀念要先清楚:首先:ProductId在何處呢?在
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVerson下的ProductId。

我們要取得的便是

KEY       為 HKEY_LOCAL_MACHINE
SUBKEY    為 SOFTWARE\Microsoft\Windows\CurrentVerson
ValueName 為 ProductId 的value

    然而要取得ProductId的value可沒那麼直接,要先取得SubKey的KeyHandle而KeyHandle的取得便是利用RegQueryKeyEx的API 。程式部份在介紹Win API字串傳遞時再一併介紹。
 










三、 字串參數









    凡是所有字串參數指標都以 ByVal 參數名稱 As String 傳。如RegOpenKeyEx()的第二參數 ByVal lpSubKey As String,便是一例。或許會問,這個例子是把subkey值傳給 Win API所以用ByVal,沒什麼大不了,其實不然,要Win API傳回字串時,也一定要用ByVal的宣告。這是VB5字串格式(BSTR)與WIN API標準字串格式(LPSTR)不同的因素。
LPSTR 字串格式是NULL Terminate的字串,若有一字串"HaHa !OK!",則格式如下:


-----------------------------------------------------------------------------
Address  0  1  2  3  4  5  6  7  8  9
         -- -- -- -- -- -- -- -- -- --
內容     H  a  H  a  !     O  K  !  \0

而BSTR則在字串的前面還有一個LONG值存字串長度,格式如下:

Address  0.. 3  4  5  6  7  8  9  10 11 12 13
         ------ -- -- -- -- -- -- -- -- -- --
內容        9   H  a  H  a  !     O  K  !  \0
----------------------------------------------------------------------------------------------

    所以了字串以ByVal的方式來傳像不像指到BSTR中第4個位置,如此一來,不就和LPSTR 可以相容了嗎?我想也正因為如此以ByVal的方式來傳String可以取得Win API的傳回值,(就算不是如此,至少這麼想比較記得住String要用ByVal的方式傳)。現在又有一個問題,Window95 API的字串使用的是ASCII Code但VB是用Unicode,Unicode佔兩個位元組,那麼能和WinAPI的字串相?所幸我們可以先不用管它,因為vb本身做了轉換,即vb傳給api時,轉了一次,傳回時又轉回 Unicode,所以如果我們用的是Byte Array來傳字串,也可以但是要自己去轉碼。
。然而32位元的VB 中,字串有種格式,一個是BSTR,另一個是HLSTR,如果我們宣告的串是非固定長度者,就會是BSTR,反之則為HLSTR。

        DIM  BSTR5   AS  STRING         ?BR>         DIM  HLSTR5  AS  STRING(255)    ?BR>
    VB5中WIN32 API的呼叫請多多使用BSTR,因為使用HLSTR的結果是,VB還得做HLSTR-> BSTR的轉換來呼叫WIN API若有傳回STRING而後再做BSTR->HLSTR的工作。然而使用BSTR來工作時,若處理有傳回值的STRING參數,則還要有額外的動作:

    1.先給定字串的初值,且字串的長度要夠放傳回值。
    2.傳回後,去除傳回值中多餘的字元。

    或
例如:
-----------------------------------------------------------------------------
int GetWindowText(
    HWND        hWnd,           // handle of window or control with text
    LPTSTR      lpString,       // address of buffer for text
    int         nMaxCount       // maximum number of characters to copy
   );
   該 API 取得WINDOW  Title Bar的文字,而傳回值是放入lpString的character個數。
VB的宣告如下:

Decl are Function GetWindowText Lib "user32" Alias "GetWindowTextA" _
       (ByVal hwnd As Long,  _
        ByVal lpString As String,  _
        ByVal cch As Long)  As Long
範例一

*****************************************************************************
Dim CharCnt As Long
Dim lpString As String
Dim tmpstr As String
Dim NullPos As Long

Form1.Caption = "這是一個test"
lpString = String(255, 0) '設定初值
CharCnt = GetWindowText(Me.hwnd, lpString, 256) 'CharCnt = 12
tmpstr = Left(lpString, CharCnt) '如此做會有一些問題
Debug.Print Len(tmpstr)   '得12
Label1.Caption = Left(lpString, CharCnt)
Debug.Print Len(Label1.Caption) '得8
*****************************************************************************

    以範例一的例子來看,設定lpString= String(255,0)的目的,是設定255個字元的空間給 lpString(加上最後的null一共256),CharCnt的值是12,明眼者可看到len("這是一個test") 會是8,但CharCnt是12, 所以直接使用Left()函數來取得子字串會有問題,這是UniCode與ANSI String間的關係,所以了,當您看到有些書的範例用這種方法取子字串,是不太完善的,所以改用範例二的方式,比較正確。

範例二

*****************************************************************************
Form1.Caption = "這是一個test"
lpString = String(255, 0)  '設定初值
CharCnt = GetWindowText(Me.hwnd, lpString, 256) 'CharCnt = 12
NullPos = InStr(1, lpString, Chr(0), vbBinaryCompare)
tmpstr = Left(lpString, NullPos - 1)
lable1.Caption = tmpstr
*****************************************************************************


四、 Null 值的傳遞


    我們再回到求ProductId的問題,我們已知使用RegOpenKeyEx()來取得subkey的Handle值,緊接著便是用RegQueryValueEx()來取值。

-----------------------------------------------------------------------------
LONG RegQueryValueEx(
    HKEY     hKey,              // handle of key to query
    LPTSTR   lpszValueName,     // address of name of value to query
    LPDWORD  lpdwReserved,      // reserved
    LPDWORD  lpdwType,          // address of buffer for value type
    LPBYTE   lpbData,           // address of data buffer
    LPDWORD  lpcbData           // address of data buffer size
   );
VB的宣告(由API檢視員中Copy下來者)
Declare Function RegQueryValueEx Lib "advapi32.dll" Alias "RegQueryValueExA" _
    (ByVal hKey As Long, _
     ByVal lpValueName As String, _
     ByVal lpReserved As Long, _
    lpType As Long,  _
    lpData As Any, _
    lpcbData As Long) As Long
-----------------------------------------------------------------------------
    仔細看一下第三個參數,WIN API中是LPDWORD可是VB中麼會是用ByVal的方式傳遞呢?原因在於 lpReserved一定要傳Null進去,VB在呼叫時便在 這參數的位置上填0(見範例三)。為何傳Null就得這做?我們可以這麼想,我們 在程式中下指令,告訴VB要以ByVal 的方式傳0出去,而WIN API裡,它可不管VB是ByVal或ByRef,API 認定我們傳進來的就是它需要的,所以了,第三個參數在API中認定我們傳進的是一個Address,而VB傳0進去,那代表API若去取得它的內容,便會取得Address 0 的內容,或許Window的Null值便是指向Address 0呢!另一個作法比較直接,將VB宣告的第三個參數宣告由ByVal lpReserved As Long改成 ByVal lpReserved as String而使用時固定傳vbNullString 進去也可以。這裡在一個觀念,那就是VB對Win API的宣告,純粹是給VB自己看的,在API中定義了一個指標的參數,Api檢視員會將之宣告成ByRef的方式(字串除外),但我們可隨需要而更動它,一個原始應為ByRef的參數宣告,我們可以將之改為ByVal的方式,只要我們能取得參數的位址,而將這型態為Long的位址以ByVal傳出去,Win API 端根本不知道VB端是用什麼方式傳,反正只要我們傳了一Long值進去,Win API就會以這個Long值當作是Address來運作。

    問題還沒有解決,RegQueryValueEx()的第四個參數lpType若為REG_SZ(= 1)那代表lpData是Null Terminate的String,若為REG_DWORD ( = 4)那代表lpData是Long值,正是因為沒有辦法事先知道lpData的真正型態,所以VB就使用 ASAny的型態,它要VB放棄型態的檢查,傳什麼值進去都可以,但是在這裡有一些問題,如果lpType是REG_DWORD那麼lpData以ByRef的方式沒有問題,但是如果lpType 是REG_SZ,STRING是要以ByVal的方式來宣告,所以會有衝突,而解決的方式就是改寫API檢視員Copy進來的宣告。

-----------------------------------------------------------------------------
Declare Function RegQueryLong Lib "advapi32.dll" Alias "RegQueryValueExA" _
     (ByVal hKey As Long, _
      ByVal lpValueName As String, _
      ByVal lpReserved As Long, _
      lpType As Long,  _
      lpData As Long, _
      lpcbData As Long) As Long

Declare Function RegQueryString Lib "advapi32.dll" Alias "RegQueryValueExA" _
     (ByVal hKey As Long, _
      ByVal lpValueName As String, _
      ByVal lpReserved As Long, _
      lpType As Long,  _
      Byval lpData As String, _
      lpcbData As Long) As Long
-----------------------------------------------------------------------------
    使用兩個宣告來解決這個問題,依不同的lpType呼叫不同的函式,即lpType= REG_DWORD時,呼叫RegQueryLong, lpType = REG_SZ時則為RegQueryString這也可以讓我了解為何VB API的宣告為什麼要有Alias的存在。

範例三

*****************************************************************************
Declare Function RegCloseKey Lib "advapi32.dll" (ByVal hKey As Long)  _
        As Long
Declare Function RegOpenKeyEx Lib "advapi32.dll" Alias "RegOpenKeyExA"
  (ByVal hKey As Long, ByVal lpSubKey As String, ByVal ulOptions As Long, _
   ByVal samDesired As Long, phkResult As Long) As Long
Declare Function RegQueryString Lib "advapi32.dll" Alias _
   "RegQueryValueExA" (ByVal hKey As Long,  _
   ByVal lpValueName As String, ByVal lpReserved As Long, _
   lpType As Long, ByVal lpData As String, lpcbData As Long) As Long
Const REG_EXPAND_SZ = 2
Const HKEY_CLASSES_ROOT = &H80000000
Const READ_CONTROL = &H20000
Const STANDARD_RIGHTS_READ = (READ_CONTROL)
Const KEY_QUERY_VALUE = &H1
Const KEY_ENUMERATE_SUB_KEYS = &H8
Const KEY_NOTIFY = &H10
Const SYNCHRONIZE = &H100000
Const KEY_READ = ((STANDARD_RIGHTS_READ Or _
      KEY_QUERY_VALUE Or KEY_ENUMERATE_SUB_KEYS Or _
      KEY_NOTIFY) And (Not SYNCHRONIZE))

Dim key5 As String, ValueName as String, strBuff as String, ResultStr as String
Dim leng1 As Long, resul As Long, hkey As Long
Dim tp As Long, i As Long

key5 = " SOFTWARE\Microsoft\Windows\CurrentVerson "
resul = RegOpenKeyEx(HKEY_CLASSES_ROOT, key5, 0, KEY_READ, hkey)
   'hkey便是subkey (key5)的KeyHandle,先取得它才能存取Subkey內的ValueName
ValueName= "ProDuctId "
tp = REG_SZ
strBuff = String(255, 0)
leng1 = Len(strBuff) + 1
resul = RegQueryString(hkey, ValueName, 0, tp, strBuff, leng1)
   '注意,第三個參數傳0,leng1傳回copy 到strBuff的字元個數(anci)
leng1 = InStr(1, strBuff, Chr(0), vbBinaryCompare) '重新算個數(UniCode)
ResultStr = Left(StrBuff,leng1-1)   '這便是ProductId的值
*****************************************************************************
    在這裡有另外一件事要特別說明,範例三程式中有一行leng1=Len(strBuffer)+1,這行可省不得,很奇怪吧,為什麼明明是一個傳回值,卻一定要設定給它一個strBuff的大小呢?這是因為許多WIN API 不會聰明到找strBuff的Null Char在哪裡,所以需要程式傳進去,而後它再依這個欄位傳回填入strBuff 的數目。


五、Array參數的傳遞

    我們知道Win API 的陣列傳遞是傳陣列的起始位址,所以了,在VB中唯一要注意的是起始位置的寫法。以另一個取得Window目錄所在路徑的API為例:
-----------------------------------------------------------------------------
UINT GetWindowsDirectory(
    LPTSTR     lpBuffer,       // address of buffer for Windows directory
    UINT       uSize           // size of directory buffer
   );                          // 若成功,則傳回目錄的字元數
VB的宣告(API檢視員)
Declare Function GetWindowsDirectory Lib "kernel32" Alias  _
   "GetWindowsDirectoryA" (ByVal lpBuffer As String, ByVal nSize As Long) _
   As Long
我們將之更改為
Declare Function GetWindowsDirectory Lib "kernel32" Alias  _
  "GetWindowsDirectoryA" ( lpBuffer As Byte, ByVal nSize As Long) As Long

-----------------------------------------------------------------------------
範例四

*****************************************************************************
Dim  n  as  Long
Dim  Buff() as Byte
Dim  StrA  as  String

Buff = space(256)
n=GetWindowsDirectory(Buff(0), 256)
Buff = Leftb(Buff, n)
StrA = StrConv(Buff, vbUniCode)  'StrA便是Windows所在目錄
*****************************************************************************

    在範例四中,GetWindowsDirectory()傳入的第一個參數Buff(0)便是這陣列的起始Byte ,因VB 宣告成lpBuffer As Byte,故傳過去的是ByRef  Buff(0)的位址,當然了,你也可以呼叫成n=GetWindowsDirectory(Buff(1), 256),只是傳回值是填在Buff(1)  to  Buff(n),而Buff(0)則仍為起始的Space Character(32),因為該API傳回值是字元個數,再加上存於Buff中的是Byte Array故,使用Leftb()去除多出的byte,再用StrConv將Byte Array轉成Unicode的字串。比照範例二的作法,我們也可以將ByteArray 改成以String的方式來做,二者可做一比較,誰比較好或比較順暢,那見人見智,不過可以肯定的是,如果傳的值是Binary的值,那麼使用Byte Array來做才對,因用String來傳的話,會經過轉換成UniCode的步驟,這中間會發生什麼事,沒人知道。


六、CallBack Function的作法

    VB的使用者通常對於這個名詞有著多多少少的疑惑,或稱之為"哭爸"Function,而VB5使用手冊使用Window Procedure來說明,除非對Window 系統有一些了解,否則可能令人更不知所云;我使用另一個例子來說明,那便是KeyBoard Hook。什麼是KeyBoardHook 呢,簡言之便是按鍵盤時,便會自動執行某一段Function的功能,就好比Dos時代的攔截中斷向量一般。讓我們先看一下設定Hook的宣告吧。

-----------------------------------------------------------------------------
HHOOK SetWindowsHookEx(
    int         idHook,         // type of hook to install
    HOOKPROC    hkprc,          // address of hook procedure
    HINSTANCE   hMod,           // handle of application instance
    DWORD       dwThreadID      // identity of thread to install hook for
   );

Declare Function SetWindowsHookEx Lib "user32" Alias SetWindowsHookExA" _
        (ByVal idHook As Long,  _
 ByVal lpfn As Long,  _
 ByVal hmod As Long,  _
 ByVal dwThreadId As Long) As Long

-----------------------------------------------------------------------------
    Hook有很多種,如KeyBoard  Hook, Mouse Hook, JournalRecord Hook等,所以第一個參數指明了要哪一種Hook,第二個參數便是Hook Procedure所在,也就是方才所說"自動執行某一段Function的功能"中的那一個Function,這個Function的名稱可以隨意給定,但有一定的參數傳遞規則,例如:
       hnexthookproc = SetWindowsHookEx(WH_KEYBOARD,  _
                     AddressOf  MyKBHFunc, App.Hinstance, 0)
    如此設定則每當按任一個鍵時,程式自動會去執行 MyKBHFunc。這個Hook Function是由我們所定義,但是它是由Window自動去呼叫,而不是由我們的程式呼叫,這類的Function就叫CallBack Function。其實,有個更直覺的例子,那就是事件(Event);比如說Form產生時會有一個Form_Load事件,在這個事件內我們可以寫很多程式,但這些程式不是給我們其他的Procedure呼叫的,而是Form在Load進來時,由Window去呼叫的,這便是CallBack Function。

以上面的例子來說,這個CallBack Function定義如下:
-----------------------------------------------------------------------------
Public Function MyKBHFunc(ByVal iCode As Long, _
    ByVal wParam As Long, ByVal lParam As Long) As Long
  MyKBHFunc = 0
  If iCode < 0 Then
    MyKBHFunc = CallNextHookEx(hnexthookproc, iCode, wParam, lParam)
    Exit Function
  End If
 '偵測 有沒有按到PrintScreen鍵
 If wParam = vbKeySnapshot Then
   MyKBHFunc = 1
   Debug.Print "haha"
 End If
End Function
-----------------------------------------------------------------------------

    這個KeyBoard Hook Function的目的主要是想攔截有沒有按到Print Screen這個鍵,這個鍵不會在Form的KeyDown, KeyPress, KeyUp Event中作用,所以只好透過KeyBoard Hook去攔截。而CallBack Function放的位置有規定,一個是要與呼叫SetWindowsHookEx() 的地方在同樣的一個Project,另外,它只能存在於.BAS檔,不能放在其他地方。KeyBoard Hook的程式於範五。

範例五

*****************************************************************************
'以下程式於Hook.bas
Declare Function SetWindowsHookEx Lib "user32" Alias  _
"SetWindowsHookExA"  (ByVal idHook As Long, ByVal lpfn As Long, _
ByVal hmod As Long, ByVal dwThreadId As Long) As Long
Declare Function UnhookWindowsHookEx Lib "user32"  _
    (ByVal hHook As Long) As Long
Declare Function CallNextHookEx Lib "user32" (ByVal hHook As Long,  _
   ByVal ncode As Long, ByVal wParam As Long, lParam As Any) As Long

Public hnexthookproc As Long
Public Const HC_ACTION = 0
Public Const WH_KEYBOARD = 2

Public Sub UnHookKBD()
If hnexthookproc <> 0 Then
   UnhookWindowsHookEx hnexthookproc
   hnexthookproc = 0
End If
End Sub
Public Function EnableKBDHook()
If hnexthookproc <> 0 Then
   Exit Function
End If
hnexthookproc = SetWindowsHookEx(WH_KEYBOARD, AddressOf  _
            MyKBHFunc, App.Hinstance, 0)
If hnexthookproc <> 0 Then
   EnableKBDHook = hnexthookproc
End If
End Function
Public Function MyKBHFunc(ByVal iCode As Long, _
    ByVal wParam As Long, ByVal lParam As Long) As Long
  '這三個參數是固定的,不能動,而MyKBHFunc這個名稱只要和
  'SetWindowsHookex()中 AddressOf後的名稱一樣便可,不一定叫什麼
  MyKBHFunc = 0
  If iCode < 0 Then
    MyKBHFunc = CallNextHookEx(hnexthookproc, iCode, wParam, lParam)
    Exit Function
  End If
If wParam = vbKeySnapshot Then  '偵測 有沒有按到PrintScreen鍵
   MyKBHFunc = 1
   Debug.Print "haha"
 End If
End Function
'以下程式於Form
Private Sub Form_Load()
Call EnableKBDHook
End Sub

Private Sub Form_Unload(Cancel As Integer)
Call UnHookKBD
End Sub
*****************************************************************************


七、自訂型態的傳遞

   因這只要用ByRef的方式來做就沒有什麼大的問題,故不做說明。


八、綜合應用

    我們再以一個實例來說明Win API在VB5中呼叫的技巧。有一個函式叫CopyMemory的宣告如下:

-----------------------------------------------------------------------------
Declare Sub CopyMemory Lib "KERNEL32"  Alias "RtlMoveMemory" ( _
   lpvDest As Any,  lpvSource As Any, ByVal cbCopy as Long)
-----------------------------------------------------------------------------

    這個函式可以將 lpvDest的momory copy 到lpvSource上去,cbCopy則代表要copy多少個byte。有了這個函式,我們可以知道一個Double值存在Memory中的各個byte到底是多少。

-----------------------------------------------------------------------------
Dim  dbl  as  Double
Dim  bte(0 to 7) as Byte
Dbl = 168.256
CopyMemory dbl,  byt(0),  8
-----------------------------------------------------------------------------

    如此檢視bte陣列便可以知道這Double值的各個byte是多少。再以另一個JournalRecord Hook為例來說明:

範例六

*****************************************************************************
' 以下在Hook.bas
Const WM_MOUSELAST = &H209
Const WM_MOUSEFIRST = &H200
Public Const WM_KEYLAST = &H108
Public Const WM_KEYFIRST = &H100
Public Const WH_JOURNALRECORD = 0
Type EVENTMSG
        message As Long
        paramL As Long
        paramH As Long
        time As Long
        hwnd As Long
End Type
Declare Function SetWindowsHookEx Lib "user32" Alias  _
   "SetWindowsHookExA"  (ByVal idHook As Long, ByVal lpfn As Long,  _
   ByVal hmod As Long, ByVal dwThreadId As Long) As Long
Declare Function UnhookWindowsHookEx Lib "user32" _
   (ByVal hHook As Long) As Long
Declare Function CallNextHookEx Lib "user32" (ByVal hHook As Long,  _
   ByVal nCode As Long, ByVal wParam As Long, lParam As Any) As Long
Declare Sub CopyMemory Lib "KERNEL32" Alias "RtlMoveMemory"  _
  (lpvDest As Any, ByVal lpvSource As Long, ByVal cbCopy As Long)
Public hNxtHook As Long   ' handle of Hook Procedure
Public msg As EVENTMSG

Sub EnableHook()
   hNxtHook = SetWindowsHookEx(0, AddressOf HookProc, App.hInstance, 0)
End Sub
Sub FreeHook()
    Dim ret As Long
    ret = UnhookWindowsHookEx(hNxtHook)
End Sub
Function HookProc(ByVal code As Long, ByVal wParam As Long,  _
                ByVal lParam As Long) As Long
    CopyMemory  msg,  lParam,  Lenb(msg)
If (msg.message >= WM_KEYFIRST  _
  And msg.message <= WM_KEYLAST) Then
      Debug.Print msg.message, msg.paramH
    End If
    HookProc = CallNextHookEx(hNxtHook, code, wParam, lParam)
End Function
'以下程式於Form1
Private Sub Form_Load()
Call EnableHook
End Sub

Private Sub Form_Unload(Cancel As Integer)
Call FreeHook
End Sub
*****************************************************************************

    詳細的流程不多做說明,我們只把重點放在HookProc這個Hook Procedure,如果我們查JournalRecord Hook的Hook Procedure可得定義如下:

-----------------------------------------------------------------------------
LRESULT CALLBACK JournalRecordProc(
    int  code,                  // hook code
    WPARAM  wParam,     // undefined
    LPARAM  lParam      // 為一個EVENTMSG Structure的address值
);

這個JournalRecordProc 對應到我們的HookProc便是

Function HookProc(ByVal code As Long, ByVal wParam As Long,  _
                ByVal lParam As Long) As Long

-----------------------------------------------------------------------------
有沒有注意到第三個參數它是一個 ByVal的Long,指的是存放某一個EVENTMSG的位址,而先前我們提過,自定型態的參數傳遞要使用ByRef的方式才能解決,天啊!它用ByVal的方式來做,如果是C語言,那不成問題,只要如下:

-----------------------------------------------------------------------------
EVENTMSG  *p;
P = (EVENTMSG *) lParam;
-----------------------------------------------------------------------------
如此便可以用 *p->message  之方式來取得內容,但VB呢?這裡便要用些小技巧了,試想,如果我們能依lParam所指的位址,一個Byte一個Byte的Copy到一個EVENTMSG的變數上面,不就可以了嗎?所以了, CopyMomory這個函式派上用場了,但是 CopyMomory的原始宣告如下,前面兩個參數都是ByRef的方式,但目前對我們有的是lParam的內容(假設是lParam = 25600, Address of lParam = 100100),如果我們使用底下的宣告,而去呼叫
-- 宣告一 ----------------------------------------------------------------------
 Declare Sub CopyMemory Lib "KERNEL32" Alias "RtlMoveMemory" ( _
   lpvDest As Any,  lpvSource As Any, ByVal cbCopy as Long)

 CopyMomory msg ,  lParam,  Lenb(msg)
-----------------------------------------------------------------------------
那麼WinAPI  RtlMoveMemory會得到第二個參數值=100100,而使指標指到100100的位址,那麼就得不到想要的資料了 (因資料在25600的位址上)。所以我們改變原始宣告,將之變成宣告二的樣子,如此VB 第二個參數的作法會傳出25600(因為ByVal嘛)給RtlMoveMemory,那不就成功了嗎?

----宣告二 ---------------------------------------------------------------------
Declare Sub CopyMemory Lib "KERNEL32" Alias "RtlMoveMemory" ( _
   lpvDest As Any,  ByVal lpvSource As Long, ByVal cbCopy as Long)

CopyMomory msg ,  lParam,  Lenb(msg)
-----------------------------------------------------------------------------
   或許這RtlMoveMemory您在許多地方都會用上,前兩個參數時而要ByRef, 時而需ByVal,那是否就要定義四個宣告來因應不同之需,其實也不用,上面的例子中,只要宣告成宣告一的樣子,但是呼叫時改成:

    CopyMemory msg, ByVal lParam, Lenb(msg)

在第二個參數前加上ByVal這樣這可以了啦。

    這裡還有另外一個做法,那就是從Hook Procedure的宣告著手,別忘了,Hook Procedure是Window所呼叫的,所以它傳給我們定義的HookProc()時,第三個參數以先前的舉例來說便是傳入25600,那麼,我們將HookProc()改定義成:

-----------------------------------------------------------------------------
Function HookProc(ByVal code As Long, ByVal wParam As Long,  _
                 lParam As Long) As Long
-----------------------------------------------------------------------------
    第三個參數變成 ByRef的方式傳入,所以了,用msg = lParam來取代CopyMemory的作法, 嘛可以通啦!即如下:

-----------------------------------------------------------------------------
Function HookProc(ByVal code As Long, ByVal wParam As Long,  _
                 lParam As Long) As Long  'lParam改成ByRef
    msg = lParam
   

本文地址:http://com.8s8s.com/it/it6407.htm