本文目錄如下
一、基于windows 消息機制的滑鼠鍵盤模拟
(一)、應用程式級模拟
(二)、系統級模拟
1、 用API函數keybd_event 模拟鍵盤事件
2、 SendInput函數模拟全局鍵盤滑鼠事件
3、用全局鈎子模拟鍵盤消息
二、驅動級模拟
*******************************************************************************************
我們怎樣才能用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);
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;
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); //擡起滑鼠左鍵
(二)、系統級模拟(針對所有程式的視窗,我稱之為全局模拟)
模拟全局鍵盤消息常見的可以有以下一些方法:
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);
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的傳回值
TKeybdInput = tagKEYBDINPUT;
其中wVk是将要操作的按鍵的虛鍵碼。wScan是安全碼,一般不用。dwFlags指定鍵盤所進行的操作,為0時表示按下某鍵,KEYEVENTF_KEYUP表示放開某鍵。time是時間戳,可以使用API函數GetTickCount的傳回值。dwExtraInfo是擴充資訊,可以使用API函數GetMessageExtraInfo的傳回值。例如擊鍵“A”的程式如下:
procedure KeyPressA;
Inputs : array [0..1] of TInput;
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
dwFlags:=KEYEVENTF_KEYUP;
SendInput(2,Inputs[0],SizeOf(TInput));
注意:在Windows.pas單元中并沒有字母和數字的虛鍵碼的聲明,在我寫的SIMouseKeyboard.pas單元檔案中對所有的虛鍵碼進行了重新聲明,包含了字母、數字和标點符号。
(2)、滑鼠模拟
TMouseInput記錄結構的聲明如下:
tagMOUSEINPUT = packed record
dx: Longint;
dy: Longint;
mouseData: DWORD;
dwFlags: DWORD;
dwExtraInfo: DWORD;
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;
Inputs[0].Itype:=INPUT_MOUSE;
with Inputs[0].mi do
dx:=0;
dy:=0;
mouseData:=0;
dwFlags:=MOUSEEVENTF_LEFTDOWN;
Inputs[1].Itype:=INPUT_MOUSE;
with Inputs[1].mi do
dx:=0;
dwFlags:=MOUSEEVENTF_LEFTUP;
滑鼠的移動總是很麻煩,上面的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;
Input : TInput;
Input.Itype:=INPUT_MOUSE;
with Input.mi do
dx:=($FFFF div (GetSystemMetrics(SM_CXSCREEN)-1)) * 150;
dy:=($FFFF div (GetSystemMetrics(SM_CYSCREEN)-1)) * 120;
dwFlags:=MOUSEEVENTF_MOVE or MOUSEEVENTF_ABSOLUTE;
SendInput(1,Input,SizeOf(TInput));
(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 }
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);
function HookProc(iCode:Integer;wParam:wParam;lParam:lParam):LRESULT;stdcall;
recOK:=1;
if iCode < 0 then
Result := CallNextHookEx(hHook,iCode,wParam,lParam)
recOK:=0
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);
procedure TForm1.FormCreate(Sender: TObject);
Button1.Caption:= '紀錄';
Button2.Caption:= '停止';
Button3.Caption:= '回放';
Button4.Caption:= '範例';
Button2.Enabled:=False;
Button3.Enabled:=False;
procedure TForm1.Button1Click(Sender: TObject);
EventLog:=0; // 建立鍵盤滑鼠操作消息紀錄鍊
hHook:=SetwindowsHookEx(WH_JOURNALRECORD,HookProc,HInstance,0);
Button2.Enabled:=True;
Button1.Enabled:=False;
procedure TForm1.Button2Click(Sender: TObject);
UnHookWindowsHookEx(hHook);
hHook:=0;
Button1.Enabled:=True;
Button3.Enabled:=True;
procedure TForm1.Button3Click(Sender: TObject);
PlayLog:=0; //建立鍵盤滑鼠操作消息紀錄回放鍊
hPlay:=SetwindowsHookEx(WH_JOURNALPLAYBACK,PlayProc,
HInstance,0);
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 &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;
const
KBC_KEY_CMD = $64; //鍵盤指令端口
KBC_KEY_DATA = $60; //鍵盤資料端口
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;
}
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); //通常在程式關閉時調用
ShutdownWinIo;
}
{**********以上為WINIO.dll中API函數的調用***************}
procedure KBCWait4IBE; //等待鍵盤緩沖區為空
dwVal: DWord;
repeat
GetPortVal($64, @dwVal, 1);
{這句表示從&H64端口讀取一個位元組并把讀出的資料放到變量dwVal中. GetPortVal函數的用法是 GetPortVal (端口号,存放讀出資料的變量位址,讀入的長度}
until (dwVal and $2) = 0;
{上面的是一個根據KBC規範寫的過程,它的作用是在向鍵盤端口寫入資料前等待一段時間,後面将會用到。}
procedure MyKeyDown(vKeyCoad: Integer); // 這個用來模拟按下鍵,參數vKeyCoad傳入按鍵的虛拟碼
btScancode: DWord;
btScancode := MapVirtualKey(vKeyCoad, 0);
KBCWait4IBE; // 發送資料前應該先等待鍵盤緩沖區為空
SetPortVal($64, $D2, 1); // 發送鍵盤寫入指令
{SetPortVal函數用于向端口寫入資料,它的用法是:SetPortVal(端口号,欲寫入的資料,寫入資料的長度)}
KBCWait4IBE;
SetPortVal($60, btScancode, 1); //寫入按鍵資訊,按下鍵
end;
procedure MyKeyUp(vKeyCoad: Integer); //這個用來模拟釋放鍵,參數vKeyCoad傳入按鍵的虛拟碼
SetPortVal($64, $D2, 1);
SetPortVal($64, (btScancode or $80), 1);
end;
{
使用方法:
如模拟按鍵 1
MyKeyDown($31);