(整理自網絡)
本文目錄如下
一、基於windows 消息機制的鼠標鍵盤模擬
(一)、應用程序級模擬
(二)、系統級模擬
1、 用API函數keybd_event 模擬鍵盤事件
2、 SendInput函數模擬全局鍵盤鼠標事件
3、用全局鈎子模擬鍵盤消息
二、驅動級模擬
*******************************************************************************************
一、基於windows 消息機制的鼠標鍵盤模擬
我們怎樣才能用Delphi來寫一個程序,用來代替人們按鍵的方法呢?那就讓我們來先了解一下windows中響應鍵盤事件的機制。
當用戶按下鍵盤上的一個鍵時,鍵盤內的芯片會檢測到這個動作,並把這個信號傳送到計算機。如何區別是哪一個鍵被按下了呢?鍵盤上的所有按鍵都有一個編碼,稱作鍵盤掃描碼。當你按下一個鍵時,這個鍵的掃描碼就被傳給系統。掃描碼是跟具體的硬件相關的,同一個鍵,在不同鍵盤上的掃描碼有可能不同。鍵盤控制器就是將這個掃描碼傳給計算機,然后交給鍵盤驅動程序。鍵盤驅動程序會完成相關的工作,並把這個掃描碼轉換為鍵盤虛擬碼。什么是虛擬碼呢?因為掃描碼與硬件相關,不具有通用性,為了統一鍵盤上所有鍵的編碼,於是就提出了虛擬碼概念。無論什么鍵盤,同一個按鍵的虛擬碼總是相同的,這樣程序就可以識別了。簡單點說,虛擬碼就是我們經常可以看到的像VK_A,VK_B這樣的常數,比如鍵A的虛擬碼是65,寫成16進制就是&H41,注意,人們經常用16進制來表示虛擬碼。當鍵盤驅動程序把掃描碼轉換為虛擬碼后,會把這個鍵盤操作的掃描碼和虛擬碼還有其它信息一起傳遞給操作系統。然后操作系統則會把這些信息封裝在一個消息中,並把這個鍵盤消息插入到消息列隊。最后,要是不出意外的話,這個鍵盤消息最終會被送到當前的活動窗口那里,活動窗口所在的應用程序接收到這個消息后,就知道鍵盤上哪個鍵被按下,也就可以決定該作出什么響應給用戶了。
這個過程可以簡單的如下表示:
用戶按下鍵盤上的一個鍵 >>>>> 鍵盤控制器就把這個鍵的掃描碼傳給計算機,然后交給鍵盤驅動程序 >>>>> 鍵盤驅動程序會把這個掃描碼轉換為鍵盤虛擬碼(VK_A,VK_B這樣的常數,比如鍵A的虛擬碼是65,寫成16進制就是&H41)傳給操作系統 >>>>> 操操作系統則會把這些信息封裝在一個消息中,並把這個鍵盤消息插入到消息列隊 >>>>> 鍵盤消息被發送到當前活動窗口
明白了這個過程,我們就可以編程實現在其中的某個環節來模擬鍵盤操作了。在Delphi中,有多種方法可以實現鍵盤模擬,我們就介紹幾種比較典型的。
(一)、應用程序級模擬(只針對某個程序,我稱之為局部模擬)
windows提供了幾個這樣的API函數可以實現直接向目標程序發送消息的功能,常用的有SendMessage和PostMessage,它們的區別是PostMessage函數直接把消息仍給目標程序就不管了,而SendMessage把消息發出去后,還要等待目標程序返回些什么東西才好。這里要注意的是,模擬鍵盤消息一定要用PostMessage函數才好,用SendMessage是不正確的(因為模擬鍵盤消息是不需要返回值的,不然目標程序會沒反應),切記切記!
PostMessage函數的delphi聲明如下:
PostMessage(
hWnd: HWND; {目標程序上某個控件的句柄}
Msg: UINT; {消息的類型}
wParam: WPARAM; {32位指定附加的消息特定的信息}
lParam: LPARAM {32位指定附加的消息特定的信息}
): BOOL;
參數hwnd 是你要發送消息的目標程序上某個控件的句柄,參數Msg 是消息的類型,表示你要發送什么樣的消息,最后wParam 和lParam這兩個參數是隨消息附加的數據,具體內容要由消息決定。
再來看看Msg 這個參數,要模擬按鍵就靠這個了。
鍵盤消息常用的有如下幾個:
WM_KEYDOWN 表示一個普通鍵被按下
WM_KEYUP 表示一個普通鍵被釋放
WM_SYSKEYDOWN 表示一個系統鍵被按下,比如Alt鍵
WM_SYSKEYUP 表示一個系統鍵被釋放,比如Alt鍵
如果你確定要發送以上幾個鍵盤消息,那么再來看看如何確定鍵盤消息中的wParam 和lParam 這兩個參數。在一個鍵盤消息中,wParam 參數的含義較簡單,它表示你要發送的鍵盤事件的按鍵虛擬碼,比如你要對目標程序模擬按下A鍵,那么wParam 參數的值就設為VK_A ,至於lParam 這個參數就比較復雜了,因為它包含了多個信息,一般可以把它設為0。即
PostMessage (MyHwnd, WM_KEYDOWN, key, 0);
但是如果你想要你的模擬更真實一些,那么建議你還是設置一下這個參數。那么我們就詳細了解一下lParam 吧。
lParam 是一個32 bit的參數,它在內存中占4個字節,寫成二進制就是
00000000 00000000 00000000 00000000
一共是32位,我們從右向左數,假設最右邊那位為第0位(注意是從0而不是從1開始計數),最左邊的就是第31位。那么該參數的
0-15位表示鍵的發送次數等擴展信息,
16-23位為按鍵的掃描碼,
24-31位表示是按下鍵還是釋放鍵。
大家一般習慣寫成16進制的,那么就應該是
&H00 00 00 00 ,
第0-15位一般為&H0001,如果是按下鍵,那么24-31位為&H00,釋放鍵則為&HC0,
那么16-23位的掃描碼怎么會得呢?這需要用到一個API函數MapVirtualKey,這個函數可以將虛擬碼轉換為掃描碼,或將掃描碼轉換為虛擬碼,還可以把虛擬碼轉換為對應字符的ASCII碼。它的delphi 聲明如下:
MapVirtualKey(
uCode: UINT; {key code, scan code or virtual key}
uMapType: UINT {flags for translation mode}
): UINT; {returns translated key code}
參數uCode 表示待轉換的碼,參數uMapType 表示從什么轉換為什么,如果是虛擬碼轉掃描碼,則uMapType 設置為0,如果是虛擬掃描碼轉虛擬碼,則wMapType 設置為1,如果是虛擬碼轉ASCII碼,則uMapType 設置為2.相信有了這些,我們就可以構造鍵盤事件的lParam參數了。
下面給出一個構造lParam參數的函數:
function VKB_param(VirtualKey:Integer;flag:Integer):Integer; //函數名
var
s,Firstbyte,Secondbyte:String;
S_code:Integer;
Begin
if flag=1 then //按下鍵
begin
Firstbyte :='00'
end
else //彈起鍵
begin
Firstbyte :='C0'
end;
S_code:= MapVirtualKey(VirtualKey, 0);
Secondbyte:='00'+inttostr(s_code);
Secondbyte:=copy(Secondbyte,Length(Secondbyte)-1,2);
s:='$'+Firstbyte + Secondbyte + '0001';
Result:=strtoint(s);
End;
使用按鍵的方法:
說明:key為鍵值,如1鍵[不是數字鍵的1]的值是$31,flag傳遞的是按鍵狀態,1是按下,0是彈起。
lparam := VKB_param(key, 1); {按下鍵}
PostMessage (MyHwnd, WM_KEYDOWN, key, lParam);
lParam := VKB_param(key, 0); {松開鍵}
PostMessage (MyHwnd, WM_KEYUP, key, lParam);
var
hwnd, lparam:Cardinal;
begin
hwnd:=FindWindowEx(FindWindow(nil,'無標題 - 記事本'),0,'edit',nil);
lparam := VKB_param(97, 1); {按下鍵}
PostMessage (hwnd,WM_KEYDOWN,vk_F3,lparam) ; //按下F3鍵
//PostMessage (hwnd,WM_CHAR,97,lparam); //輸入字符A (edit控件接收字符)
lParam := VKB_param(key, 0); {松開鍵}
PostMessage (hwnd,WM_KEYUP,vk_F3,lparam); //釋放F3鍵
end;
模擬鼠標點擊
Var
P1:Tpoint;
Lparam:integer;
begin
GetCursorPos(P1); // 獲取屏幕坐標
P1.X:= P1.X+100;
P1.Y:=P1.Y+100;
lparam:=p1.X+ p1.Y shl 16;
sendmessage(h,messages.WM_LBUTTONDOWN ,0,lparam);// 按下鼠標左鍵
sendmessage(h,messages.WM_LBUTTONUP ,0, lparam); //抬起鼠標左鍵
End;
(二)、系統級模擬(針對所有程序的窗口,我稱之為全局模擬)
模擬全局鍵盤消息常見的可以有以下一些方法:
1、 用API函數keybd_event,這個函數可以用來模擬一個鍵盤事件
keybd_event(
bVk: Byte; {virtual-key code}
bScan: Byte; {scan-code}
dwFlags: DWORD; {option flags}
dwExtraInfo: DWORD {additional information about the key}
); {this procedure does not return a value}
keybd_event(VK_A, 0, 0, 0) '按下A鍵
keybd_event (VK_A, 0, KEYEVENTF_KEYUP, 0) '釋放A鍵
注意有時候按鍵的速度不要太快,否則會出問題,可以用API函數Sleep來進行延時,delphi聲明如下:
Sleep(
dwMilliseconds: DWORD {specifies the number of milliseconds to pause}
);
參數dwMilliseconds表示延時的時間,以毫秒為單位。
那么如果要模擬按下功能鍵怎么做呢?比如要按下Ctrl+C實現拷貝這個功能,可以這樣:
keybd_event (VK_Ctrl, 0, 0, 0 ); //按下Ctrl鍵
keybd_event (VK_C, 0, 0, 0); //按下C鍵
Sleep(500 ); //延時500毫秒
keybd_event (VK_C, 0, KEYEVENTF_KEYUP, 0 ); //釋放C鍵
keybd_event (VK_Ctrl, 0, KEYEVENTF_KEYUP, 0 ); //釋放Ctrl鍵
好了,現在你可以試試是不是可以騙過目標程序了,這個函數對大部分的窗口程序都有效,可是仍然有一部分游戲對它產生的鍵盤事件熟視無睹,這時候,你就要用上bScan這個參數了。一般的,bScan都傳0,但是如果目標程序是一些DirectX游戲,那么你就需要正確使用這個參數傳入掃描碼,用了它可以產生正確的硬件事件消息,以被游戲識別。這樣的話,就可以寫成這樣:
keybd_event (VK_A, MapVirtualKey(VK_A, 0), 0, 0); //按下A鍵
keybd_event (VK_A, MapVirtualKey(VK_A, 0), KEYEVENTF_KEYUP, 0 ); //釋放A鍵
以上就是用keybd_event函數來模擬鍵盤事件。
模擬按下鼠標左鍵
mouse_event(MOUSEEVENTF_LEFTDOWN,0,0,0,0);//鼠標左鍵按下mouse_event(MOUSEEVENTF_LEFTUP,0,0,0,0);//鼠標左鍵彈起
2、 SendInput函數也可以模擬全局鍵盤鼠標事件。
SendInput可以直接把一條消息插入到消息隊列中,算是比較底層的了。
SendInput的參數其實很簡單,在Windows.pas就有函數的聲明如下:
function SendInput (
cInputs: UINT; pInputs中記錄數組的元素數目
var pInputs: TInput; TInput類型記錄數組的第1個元素
cbSize: Integer 定義TInput的大小,一般為SizeOf(TInput)
): UINT; stdcall;
cInputs:定義pInputs中記錄數組的元素數目。pInputs:TInput類型記錄數組的第1個元素。每個元素代表插人到系統消息隊列的鍵盤或鼠標事件。cbSize:定義TInput的大小,一般為SizeOf(TInput)。函數返回成功插入系統消息隊列中事件的數目,失敗返回0。調用SendInput關鍵的就是要搞清楚它的幾個記錄結構的意思,在Windows.pas中對TInput的聲明如下:
tagINPUT = packed record
Itype: DWORD;
case Integer of
0: (mi: TMouseInput);
1: (ki: TKeybdInput);
2: (hi: THardwareInput);
end;
TInput = tagINPUT;
其中mi、ki、hi是3個共用型的記錄結構,Itype指出記錄結構中所使用的類型,它有3個值。INPUT_MOUSE:表示使用mi記錄結構,忽略ki和hi;INPUT_KEYBOARD:表示使用ki記錄結構,忽略mi和hi。
(1)、鍵盤模擬
TKeybdInput記錄結構的聲明如下:
tagKEYBDINPUT = packed record
wVk: WORD; 是將要操作的按鍵的虛鍵碼
wScan: WORD; 是安全碼,一般不用
dwFlags: DWORD; 指定鍵盤所進行的操作,為0時表示按下某鍵,KEYEVENTF_KEYUP表示放開某鍵
time: DWORD;
dwExtraInfo: DWORD; 是擴展信息,可以使用API函數GetMessageExtraInfo的返回值
end;
TKeybdInput = tagKEYBDINPUT;
其中wVk是將要操作的按鍵的虛鍵碼。wScan是安全碼,一般不用。dwFlags指定鍵盤所進行的操作,為0時表示按下某鍵,KEYEVENTF_KEYUP表示放開某鍵。time是時間戳,可以使用API函數GetTickCount的返回值。dwExtraInfo是擴展信息,可以使用API函數GetMessageExtraInfo的返回值。例如擊鍵“A”的程序如下:
procedure KeyPressA;
var
Inputs : array [0..1] of TInput;
begin
Inputs[0].Itype:=INPUT_KEYBOARD;
with Inputs[0].ki do
begin
wVk:=VK_A;
wScan:=0;
dwFlags:=0;
time:=GetTickCount;
dwExtraInfo:=GetMessageExtraInfo;
end;
Inputs[1].Itype:=INPUT_KEYBOARD;
with Inputs[1].ki do
begin
wVk:=VK_A;
wScan:=0;
dwFlags:=KEYEVENTF_KEYUP;
time:=GetTickCount;
dwExtraInfo:=GetMessageExtraInfo;
end;
SendInput(2,Inputs[0],SizeOf(TInput));
end;
注意:在Windows.pas單元中並沒有字母和數字的虛鍵碼的聲明,在我寫的SIMouseKeyboard.pas單元文件中對所有的虛鍵碼進行了重新聲明,包含了字母、數字和標點符號。
(2)、鼠標模擬
TMouseInput記錄結構的聲明如下:
tagMOUSEINPUT = packed record
dx: Longint;
dy: Longint;
mouseData: DWORD;
dwFlags: DWORD;
time: DWORD;
dwExtraInfo: DWORD;
end;
TMouseInput = tagMOUSEINPUT;
其中dx、dy是鼠標移動時的坐標差(不是象素單位),在鼠標移動時有效。mouseData是鼠標滾輪滾動值,在滾動鼠標滾輪時有效。當mouseData小於0時向下滾動,當mouseData大於0時向上滾動,mouseData的絕對值一般設為120。dwFlags指定鼠標所進行的操作,例如,MOUSEEVENTF_MOVE表示移動鼠標,MOUSEEVENTF_LEFTDOWN表示按下鼠標左鍵,MOUSEEVENTF_LEFTUP表示放開鼠標左鍵。time是時間戳,可以使用API函數GetTickCount的返回值。dwExtraInfo是擴展信息,可以使用API函數GetMessageExtraInfo的返回值。例如單擊鼠標左鍵的程序如下:
procedure MouseClick;
var
Inputs : array [0..1] of TInput;
begin
Inputs[0].Itype:=INPUT_MOUSE;
with Inputs[0].mi do
begin
dx:=0;
dy:=0;
mouseData:=0;
dwFlags:=MOUSEEVENTF_LEFTDOWN;
time:=GetTickCount;
dwExtraInfo:=GetMessageExtraInfo;
end;
Inputs[1].Itype:=INPUT_MOUSE;
with Inputs[1].mi do
begin
dx:=0;
dy:=0;
mouseData:=0;
dwFlags:=MOUSEEVENTF_LEFTUP;
time:=GetTickCount;
dwExtraInfo:=GetMessageExtraInfo;
end;
SendInput(2,Inputs[0],SizeOf(TInput));
end;
鼠標的移動總是很麻煩,上面的dx、dy不是以象素為單位的,而是以鼠標設備移動量為單位的,它們之間的比值受鼠標移動速度設置的影響。具體的解決方法我已經在《Delphi下利用WinIo模擬鼠標鍵盤詳解》一文中進行了討論,這里不再重復。dwFlags可以設置一個MOUSEEVENTF_ABSOLUTE標志,這使得可以用另外一種方法移動鼠標。當dwFlags設置了MOUSEEVENTF_ABSOLUTE標志,dx、dy為屏幕坐標值,表示將鼠標移動到dx,dy的位置。但是這個坐標值也不是以象素為單位的。這個值的范圍是0到65535($FFFF),當dx等於0、dy等於0時表示屏幕的最左上角,當dx等於65535、dy等於65535時表示屏幕的最右下角,相當於將屏幕的寬和高分別65536等分。API函數GetSystemMetrics(SM_CXSCREEN)可以返回屏幕的寬度,函數GetSystemMetrics(SM_CYSCREEN)可以返回屏幕的高度,利用屏幕的寬度和高度就可以將象素坐標換算成相應的dx、dy。注意:這種換算最多會出現1象素的誤差。例如:將鼠標指針移動到屏幕150,120坐標處的程序如下:
procedure MouseMove;
var
Input : TInput;
begin
Input.Itype:=INPUT_MOUSE;
with Input.mi do
begin
dx:=($FFFF div (GetSystemMetrics(SM_CXSCREEN)-1)) * 150;
dy:=($FFFF div (GetSystemMetrics(SM_CYSCREEN)-1)) * 120;
mouseData:=0;
dwFlags:=MOUSEEVENTF_MOVE or MOUSEEVENTF_ABSOLUTE;
time:=GetTickCount;
dwExtraInfo:=GetMessageExtraInfo;
end;
SendInput(1,Input,SizeOf(TInput));
end;
(3)、SendInput與WInIo的對比
在《Delphi下利用WinIo模擬鼠標鍵盤詳解》一文中已經說了WinIo的很多缺點,SendInput幾乎沒有這些缺點。SendInput的模擬要比WinIo簡單的多。事件是被直接插入到系統消息隊列的,所以它的速度比WinIo要快。系統也會保證數據的完整性,不會出現數據包混亂的情況。利用“絕對移動”可以將鼠標指針移動到准確的位置,同鼠標的配置隔離不會出現兼容性的問題。SendInput的缺點也是最要命的,它會被一些程序屏蔽。所以說在SendInput與WInIo都可以使用的情況下優先考慮SendInput。另外SendInput與WInIo可以接合使用,一些程序對鼠標左鍵單擊敏感,可以使用WinIo模擬鼠標左鍵單擊,其它操作由SendInput模擬。
3、用全局鈎子也可以模擬鍵盤消息。如果你對windows中消息鈎子的用法已經有所了解,那么你可以通過設置一個全局HOOK來模擬鍵盤消息,比如,你可以用WH_JOURNALPLAYBACK這個鈎子來模擬按鍵。WH_JOURNALPLAYBACK是一個系統級的全局鈎子,它和WH_JOURNALRECORD的功能是相對的,常用它們來記錄並回放鍵盤鼠標操作。WH_JOURNALRECORD鈎子用來將鍵盤鼠標的操作忠實地記錄下來,記錄下來的信息可以保存到文件中,而WH_JOURNALPLAYBACK則可以重現這些操作。當然亦可以單獨使用WH_JOURNALPLAYBACK來模擬鍵盤操作。
SetWindowsHookEx函數,它可以用來安裝消息鈎子:
SetWindowsHookEx(
idHook: Integer; {hook type flag}
lpfn: TFNHookProc; {a pointer to the hook function}
hmod: HINST; {a handle to the module containing the hook function}
dwThreadId: DWORD {the identifier of the associated thread}
): HHOOK;
先安裝WH_JOURNALPLAYBACK這個鈎子,然后你需要自己寫一個鈎子函數,在系統調用它時,把你要模擬的事件傳遞給鈎子參數lParam所指向的EVENTMSG區域,就可以達到模擬按鍵的效果。不過用這個鈎子模擬鍵盤事件有一個副作用,就是它會鎖定真實的鼠標鍵盤,不過如果你就是想在模擬的時候不會受真實鍵盤操作的干擾,那么用用它倒是個不錯的主意。
在不需要監視系統消息時需要調用提供UnHookWindowsHookEx來解除對消息的監視。 下面來建立程序,在Delphi中建立一個工程,在Form1上添加3個按鈕用於程序操作。另外再添加一個按鈕控件和一個Edit控件用於驗證操作。
下面是Form1的全部代碼
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
StdCtrls;
type
TForm1 = class(TForm)
Button1: TButton;
Button2: TButton;
Button3: TButton;
Edit1: TEdit;
Button4: TButton;
procedure FormCreate(Sender: TObject);
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
procedure Button3Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
EventArr:array[0..1000]of EVENTMSG;
EventLog:Integer;
PlayLog:Integer;
hHook,hPlay:Integer;
recOK:Integer;
canPlay:Integer;
bDelay:Bool;
implementation
{$R *.DFM}
Function PlayProc(iCode:Integer;wParam:wParam;lParam:lParam):LRESULT;stdcall;
begin
canPlay:=1;
Result:=0;
if iCode < 0 then // 必須將消息傳遞到消息鏈的下一個接受單元
Result := CallNextHookEx(hPlay,iCode,wParam,lParam)
else if iCode = HC_SYSMODALON then
canPlay:=0
else if iCode = HC_SYSMODALOFF then
canPlay:=1
else if ((canPlay =1 )and(iCode=HC_GETNEXT)) then begin
if bDelay then begin
bDelay:=False;
Result:=50;
end;
pEventMSG(lParam)^:=EventArr[PlayLog];
end
else if ((canPlay = 1)and(iCode = HC_SKIP))then begin
bDelay := True;
PlayLog:=PlayLog+1;
end;
if PlayLog>=EventLog then begin
UNHookWindowsHookEx(hPlay);
end;
end;
function HookProc(iCode:Integer;wParam:wParam;lParam:lParam):LRESULT;stdcall;
begin
recOK:=1;
Result:=0;
if iCode < 0 then
Result := CallNextHookEx(hHook,iCode,wParam,lParam)
else if iCode = HC_SYSMODALON then
recOK:=0
else if iCode = HC_SYSMODALOFF then
recOK:=1
else if ((recOK>0) and (iCode = HC_ACTION)) then begin
EventArr[EventLog]:=pEventMSG(lParam)^;
EventLog:=EventLog+1;
if EventLog>=1000 then begin
UnHookWindowsHookEx(hHook);
end;
end;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
Button1.Caption:= '紀錄';
Button2.Caption:= '停止';
Button3.Caption:= '回放';
Button4.Caption:= '范例';
Button2.Enabled:=False;
Button3.Enabled:=False;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
EventLog:=0; // 建立鍵盤鼠標操作消息紀錄鏈
hHook:=SetwindowsHookEx(WH_JOURNALRECORD,HookProc,HInstance,0);
Button2.Enabled:=True;
Button1.Enabled:=False;
end;
procedure TForm1.Button2Click(Sender: TObject);
begin
UnHookWindowsHookEx(hHook);
hHook:=0;
Button1.Enabled:=True;
Button2.Enabled:=False;
Button3.Enabled:=True;
end;
procedure TForm1.Button3Click(Sender: TObject);
begin
PlayLog:=0; //建立鍵盤鼠標操作消息紀錄回放鏈
hPlay:=SetwindowsHookEx(WH_JOURNALPLAYBACK,PlayProc,
HInstance,0);
Button3.Enabled:=False;
end;
end.
二、驅動級模擬 (繞過了windows消息)
有一些使用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,得到斷碼,表示釋放這個鍵
好了,現在的問題就是在delphi中如何向端口寫入數據了。因為在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個文件拷貝到你的程序的文件夾下。
下面給出使用WINIO模擬按鍵的單元和使用方法:
{****************************************************************************}
unit MNwinio;
interface
const
KBC_KEY_CMD = $64; //鍵盤命令端口
KBC_KEY_DATA = $60; //鍵盤數據端口
implementation
function InitializeWinIo: Boolean; stdcall; external 'WinIo.dll' name 'InitializeWinIo';
{
本函數初始化WioIO函數庫。
必須在調用所有其它功能函數之前調用本函數。
如果函數調用成功,返回值為非零值。
如果調用失敗,則返回值為0。
procedure TForm1.FormActivate(Sender: TObject); //通常在程序啟動時調用
begin
if InitializeWinIo = False then
begin
Messagebox(handle, '初始化失敗!', '提示', MB_OK + MB_IconError)
end;
end;
}
function InstallWinIoDriver(pszWinIoDriverPath: PString; IsDemandLoaded: boolean
= false): Boolean; stdcall; external 'WinIo.dll' name 'InstallWinIoDriver';
function RemoveWinIoDriver: Boolean; stdcall; external 'WinIo.dll' name
'RemoveWinIoDriver';
function GetPortVal(PortAddr: Word; PortVal: PDWord; bSize: Byte): Boolean;
stdcall; external 'WinIo.dll' name 'GetPortVal';
function SetPortVal(PortAddr: Word; PortVal: DWord; bSize: Byte): Boolean;
stdcall; external 'WinIo.dll' name 'SetPortVal';
function GetPhysLong(PhysAddr: PByte; PhysVal: PDWord): Boolean; stdcall;
external 'WinIo.dll' name 'GetPhysLong';
function SetPhysLong(PhysAddr: PByte; PhysVal: DWord): Boolean; stdcall; external
'WinIo.dll' name 'SetPhysLong';
function MapPhysToLin(PhysAddr: PByte; PhysSize: DWord; PhysMemHandle: PHandle):
PByte; stdcall; external 'WinIo.dll' name 'MapPhysToLin';
function UnMapPhysicalMemory(PhysMemHandle: THandle; LinAddr: PByte): Boolean;
stdcall; external 'WinIo.dll' name 'UnmapPhysicalMemory';
procedure ShutdownWinIo; stdcall; external 'WinIo.dll' name 'ShutdownWinIo';
{ 本函數在內存中清除WinIO庫
本函數必須在中止應用函數之前或者不再需要WinIO庫時調用
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction); //通常在程序關閉時調用
begin
ShutdownWinIo;
end;
}
{**********以上為WINIO.dll中API函數的調用***************}
procedure KBCWait4IBE; //等待鍵盤緩沖區為空
var
dwVal: DWord;
begin
repeat
GetPortVal($64, @dwVal, 1);
{這句表示從&H64端口讀取一個字節並把讀出的數據放到變量dwVal中. GetPortVal函數的用法是 GetPortVal (端口號,存放讀出數據的變量地址,讀入的長度}
until (dwVal and $2) = 0;
{上面的是一個根據KBC規范寫的過程,它的作用是在向鍵盤端口寫入數據前等待一段時間,后面將會用到。}
end;
procedure MyKeyDown(vKeyCoad: Integer); // 這個用來模擬按下鍵,參數vKeyCoad傳入按鍵的虛擬碼
var
btScancode: DWord;
begin
btScancode := MapVirtualKey(vKeyCoad, 0);
KBCWait4IBE; // 發送數據前應該先等待鍵盤緩沖區為空
SetPortVal($64, $D2, 1); // 發送鍵盤寫入命令
{SetPortVal函數用於向端口寫入數據,它的用法是:SetPortVal(端口號,欲寫入的數據,寫入數據的長度)}
KBCWait4IBE;
SetPortVal($60, btScancode, 1); //寫入按鍵信息,按下鍵
end;
procedure MyKeyUp(vKeyCoad: Integer); //這個用來模擬釋放鍵,參數vKeyCoad傳入按鍵的虛擬碼
var
btScancode: DWord;
begin
btScancode := MapVirtualKey(vKeyCoad, 0);
KBCWait4IBE;
SetPortVal($64, $D2, 1);
KBCWait4IBE;
SetPortVal($64, (btScancode or $80), 1);
end;
end.
{
使用方法:
如模擬按鍵 1
MyKeyDown($31);
Sleep(50);
MyKeyUp($31);
}