驅動級模擬驅動級模擬:直接讀寫鍵盤的硬件端口!
有一些使用DirectX接口的游戲程序,它們在讀取鍵盤操作時繞過了windows的消息機制,而使用DirectInput.這是因為有些游戲對實時性控制的要求比較高,比如賽車游戲,要求以最快速度響應鍵盤輸入。而windows消息由於是隊列形式的,消息在傳遞時會有不少延遲,有時1秒鍾也就傳遞十幾條消息,這個速度達不到游戲的要求。而DirectInput則繞過了windows消息,直接與鍵盤驅動程序打交道,效率當然提高了不少。因此也就造成,對這樣的程序無論用PostMessage或者是keybd_event都不會有反應,因為這些函數都在較高層。對於這樣的程序,只好用直接讀寫鍵盤端口的方法來模擬硬件事件了。要用這個方法來模擬鍵盤,需要先了解一下鍵盤編程的相關知識。
在DOS時代,當用戶按下或者放開一個鍵時,就會產生一個鍵盤中斷(如果鍵盤中斷是允許的),這樣程序會跳轉到BIOS中的鍵盤中斷處理程序去執行。打開windows的設備管理器,可以查看到鍵盤控制器由兩個端口控制。其中&H60是數據端口,可以讀出鍵盤數據,而&H64是控制端口,用來發出控制信號。也就是,從&H60號端口可以讀此鍵盤的按鍵信息,當從這個端口讀取一個字節,該字節的低7位就是按鍵的掃描碼,而高1位則表示是按下鍵還是釋放鍵。當按下鍵時,最高位為0,稱為通碼,當釋放鍵時,最高位為1,稱為斷碼。既然從這個端口讀數據可以獲得按鍵信息,那么向這個端口寫入數據就可以模擬按鍵了!用過QbASIC4.5的朋友可能知道,QB中有個OUT命令可以向指定端口寫入數據,而INP函數可以讀取指定端口的數據。那我們先看看如果用QB該怎么寫代碼:
假如你想模擬按下一個鍵,這個鍵的掃描碼為&H50,那就這樣
OUT &H64,&HD2 '把數據&HD2發送到&H64端口。這是一個KBC指令,表示將要向鍵盤寫入數據
OUT &H60,&H50 '把掃描碼&H50發送到&H60端口,表示模擬按下掃描碼為&H50的這個鍵
那么要釋放這個鍵呢?像這樣,發送該鍵的斷碼:
OUT &H64,&HD2 '把數據&HD2發送到&H64端口。這是一個KBC指令,表示將要向鍵盤寫入數據
OUT &H60,(&H50 OR &H80) '把掃描碼&H50與數據&H80進行或運算,可以把它的高位置1,得到斷碼,表示釋放這個鍵
好了,現在的問題就是在VB中如何向端口寫入數據了。因為在windows中,普通應用程序是無權操作端口的,於是我們就需要一個驅動程序來幫助我們實現。在這里我們可以使用一個組件WINIO來完成讀寫端口操作。什么是WINIO?WINIO是一個全免費的、無需注冊的、含源程序的WINDOWS2000端口操作驅動程序組件(可以到http://www.internals.com/上去下載)。它不僅可以操作端口,還可以操作內存;不僅能在VB下用,還可以在DELPHI、VC等其它環境下使用,性能特別優異。下載該組件,解壓縮后可以看到幾個文件夾,其中Release文件夾下的3個文件就是我們需要的,這3個文件是WinIo.sys(用於win xp下的驅動程序),WINIO.VXD(用於win 98下的驅動程序),WinIo.dll(封裝函數的動態鏈接庫),我們只需要調用WinIo.dll中的函數,然后WinIo.dll就會安裝並調用驅動程序來完成相應的功能。值得一提的是這個組件完全是綠色的,無需安裝,你只需要把這3個文件復制到與你的程序相同的文件夾下就可以使用了。用法很簡單,先用里面的InitializeWinIo函數安裝驅動程序,然后就可以用GetPortVal來讀取端口或者用SetPortVal來寫入端口了。好,讓我們來做一個驅動級的鍵盤模擬吧。先把winio的3個文件拷貝到你的程序的文件夾下,然后在VB中新建一個工程,添加一個模塊,在模塊中加入下面的winio函數聲明:
Declare Function MapPhysToLin Lib "WinIo.dll" (ByVal PhysAddr As Long, ByVal PhysSize As Long, ByRef PhysMemHandle) As Long
Declare Function UnmapPhysicalMemory Lib "WinIo.dll" (ByVal PhysMemHandle, ByVal LinAddr) As Boolean
Declare Function GetPhysLong Lib "WinIo.dll" (ByVal PhysAddr As Long, ByRef PhysVal As Long) As Boolean
Declare Function SetPhysLong Lib "WinIo.dll" (ByVal PhysAddr As Long, ByVal PhysVal As Long) As Boolean
Declare Function GetPortVal Lib "WinIo.dll" (ByVal PortAddr As Integer, ByRef PortVal As Long, ByVal bSize As Byte) As Boolean
Declare Function SetPortVal Lib "WinIo.dll" (ByVal PortAddr As Integer, ByVal PortVal As Long, ByVal bSize As Byte) As Boolean
Declare Function InitializeWinIo Lib "WinIo.dll" () As Boolean
Declare Function ShutdownWinIo Lib "WinIo.dll" () As Boolean
Declare Function InstallWinIoDriver Lib "WinIo.dll" (ByVal DriverPath As String, ByVal Mode As Integer) As Boolean
Declare Function RemoveWinIoDriver Lib "WinIo.dll" () As Boolean
' ------------------------------------以上是WINIO函數聲明-------------------------------------------
Declare Function MapVirtualKey Lib "user32" Alias "MapVirtualKeyA" (ByVal wCode As Long, ByVal wMapType As Long) As Long
'-----------------------------------以上是WIN32 API函數聲明-----------------------------------------
再添加下面這個過程:
Sub KBCWait4IBE() '等待鍵盤緩沖區為空
Dim dwVal As Long
Do
GetPortVal &H64, dwVal, 1
'這句表示從&H64端口讀取一個字節並把讀出的數據放到變量dwVal中
'GetPortVal函數的用法是GetPortVal 端口號,存放讀出數據的變量,讀入的長度
Loop While (dwVal And &H2)
End Sub
上面的是一個根據KBC規范寫的過程,它的作用是在向鍵盤端口寫入數據前等待一段時間,后面將會用到。
然后再添加如下過程,這2個過程用來模擬按鍵:
Public Const KBC_KEY_CMD = &H64 '鍵盤命令端口
Public Const KBC_KEY_DATA = &H60 '鍵盤數據端口
Sub MyKeyDown(ByVal vKeyCoad As Long)
'這個用來模擬按下鍵,參數vKeyCoad傳入按鍵的虛擬碼
Dim btScancode As Long
btScancode = MapVirtualKey(vKeyCoad, 0)
KBCWait4IBE '發送數據前應該先等待鍵盤緩沖區為空
SetPortVal KBC_KEY_CMD, &HD2, 1 '發送鍵盤寫入命令
'SetPortVal函數用於向端口寫入數據,它的用法是SetPortVal 端口號,欲寫入的數據,寫入數據的長度
KBCWait4IBE
SetPortVal KBC_KEY_DATA, btScancode, 1 '寫入按鍵信息,按下鍵
End Sub
Sub MyKeyUp(ByVal vKeyCoad As Long)
'這個用來模擬釋放鍵,參數vKeyCoad傳入按鍵的虛擬碼
Dim btScancode As Long
btScancode = MapVirtualKey(vKeyCoad, 0)
KBCWait4IBE '等待鍵盤緩沖區為空
SetPortVal KBC_KEY_CMD, &HD2, 1 '發送鍵盤寫入命令
KBCWait4IBE
SetPortVal KBC_KEY_DATA, (btScancode Or &H80), 1 '寫入按鍵信息,釋放鍵
End Sub
定義了上面的過程后,就可以用它來模擬鍵盤輸入了。在窗體模塊中添加一個定時器控件,然后加入以下代碼:
[table=80%][tr][td]Private Sub Form_Load() If InitializeWinIo = False Then
'用InitializeWinIo函數加載驅動程序,如果成功會返回true,否則返回false
MsgBox "驅動程序加載失敗!"
Unload Me
End If
Timer1.Interval=3000
Timer1.Enabled=True
End Sub
Private Sub Form_Unload(Cancel As Integer)
ShutdownWinIo '程序結束時記得用ShutdownWinIo函數卸載驅動程序
End Sub
Private Sub Timer1_Timer()
Dim VK_A as Long = &H41
MyKeyDown VK_A
MyKeyUp VK_A '模擬按下並釋放A鍵
End Sub
[/quote]
運行上面的程序,就會每隔3秒鍾模擬按下一次A鍵,試試看,怎么樣,是不是對所有程序都有效果了?
需要注意的問題:
要在VB的調試模式下使用WINIO,需要把那3個文件拷貝到VB的安裝目錄中。
鍵盤上有些鍵屬於擴展鍵(比如鍵盤上的方向鍵就是擴展鍵),對於擴展鍵不應該用上面的MyKeyDown和MyKeyUp過程來模擬,可以使用下面的2個過程來准確模擬擴展鍵:
Sub MyKeyDownEx(ByVal vKeyCoad As Long) '模擬擴展鍵按下,參數vKeyCoad是擴展鍵的虛擬碼
Dim btScancode As Long
btScancode = MapVirtualKey(vKeyCoad, 0)
KBCWait4IBE '等待鍵盤緩沖區為空
SetPortVal KBC_KEY_CMD, &HD2, 1 '發送鍵盤寫入命令
KBCWait4IBE
SetPortVal KBC_KEY_DATA, &HE0, 1 '寫入擴展鍵標志信息
KBCWait4IBE '等待鍵盤緩沖區為空
SetPortVal KBC_KEY_CMD, &HD2, 1 '發送鍵盤寫入命令
KBCWait4IBE
SetPortVal KBC_KEY_DATA, btScancode, 1 '寫入按鍵信息,按下鍵
End Sub
Sub MyKeyUpEx(ByVal vKeyCoad As Long) '模擬擴展鍵彈起
Dim btScancode As Long
btScancode = MapVirtualKey(vKeyCoad, 0)
KBCWait4IBE '等待鍵盤緩沖區為空
SetPortVal KBC_KEY_CMD, &HD2, 1 '發送鍵盤寫入命令
KBCWait4IBE
SetPortVal KBC_KEY_DATA, &HE0, 1 '寫入擴展鍵標志信息
KBCWait4IBE '等待鍵盤緩沖區為空
SetPortVal KBC_KEY_CMD, &HD2, 1 '發送鍵盤寫入命令
KBCWait4IBE
SetPortVal KBC_KEY_DATA, (btScancode Or &H80), 1 '寫入按鍵信息,釋放鍵
End Sub
還應該注意的是,如果要從擴展鍵轉換到普通鍵,那么普通鍵的KeyDown事件應該發送兩次。也就是說,如果我想模擬先按下一個擴展鍵,再按下一個普通鍵,那么就應該向端口發送兩次該普通鍵被按下的信息。比如,我想模擬先按下左方向鍵,再按下空格鍵這個事件,由於左方向鍵是擴展鍵,空格鍵是普通鍵,那么流程就應該是這樣的:
[quote]MyKeyDownEx VK_LEFT '按下左方向鍵
Sleep 200 '延時200毫秒
MyKeyUpEx VK_LEFT '釋放左方向鍵
Sleep 500
MyKeyDown VK_SPACE '按下空格鍵,注意要發送兩次
MyKeyDown VK_SPACE
Sleep 200
MyKeyUp VK_SPACE '釋放空格鍵