這兩個函式在 MSDN 上已被註明是過時的函式,M$ 也建議以 SendInput 取代。非到必要時,其實不建議用模擬輸入之方式,速度慢、穩定性差、mouse/keyboard 都會被鎖死。
模擬按鍵、滑鼠動作,其實有更好的選擇,如 AutoIt、AotoHotkey、按鍵精靈 等都是不錯的輔助,並不太建議用 C/C++ 、VB 、C# 等軟體達成。
首先還是不厭其煩,先提二個 API
SetCursorPos : 設定滑鼠位置。
SetForegroundWindow : 將視窗提到最上層。
同時下面講的方式其實很不入流,所謂不入流指的是它的架構並不好、難維護,
要做得好,架構預計必須採用 multi-thread、狀態機 方式設計。
滑鼠事件
滑鼠事件可用 mouse_event 達成,
VOID WINAPI mouse_event(
_In_ DWORD dwFlags,
_In_ DWORD dx,
_In_ DWORD dy,
_In_ DWORD dwData,
_In_ ULONG_PTR dwExtraInfo
);
關鍵在於第一個參數,msdn 上有個參數很多人不懂 ( 我也是後來問有玩硬體的人才懂的) - MOUSEEVENTF_XDOWN、MOUSEEVENTF_XUP,其它的在 MSDN 大致都可忘文生義,都算明顯。
MOUSEEVENTF_ABSOLUTE:以絕對位置方式指定。
MOUSEEVENTF_LEFTDOWN:按下滑鼠左鍵。
MOUSEEVENTF_LEFTUP:放開滑鼠左鍵。
MOUSEEVENTF_MIDDLEDOWN:按下滑鼠中鍵。
MOUSEEVENTF_MIDDLEUP:放開滑鼠中鍵。
MOUSEEVENTF_MOVE:移動滑鼠游標位置。當有指定 MOUSEEVENTF_ABSOLUTE 時是以絕對位置移動,否則以相對位置移動。
MOUSEEVENTF_RIGHTDOWN:按下滑鼠右鍵。
MOUSEEVENTF_RIGHTUP:放開滑鼠右鍵。
MOUSEEVENTF_WHEEL:移動滑鼠滾輪。
MOUSEEVENTF_XDOWN:按下滑鼠 XButton。
MOUSEEVENTF_XUP:放開滑鼠 XButton。
XDOWN、XUP 指的是所謂的 XButton。什麼是 XButton ? 一些滑鼠除了左鍵、右鍵、滾輪(或中鍵) 之外,會在滑鼠側邊再額外加上一些按鍵,這些按鍵就稱為 XButton。有興趣的話可以到 羅技 查 G700 (G 系列大概都是遊戲用吧 ),裡面有 13 個 XButton。但這份 API 裡面參數只有 XButton1, XButton2。
其它 keyboard / mouse 名稱可稍參考 codeproject 這篇文章。關於其它一些注意事項,回到 msdn 查看較清楚。
/* mouse event */ #include <windows.h> int main() { // once L-click mouse_event (MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_LEFTUP, 0, 0, 0, 0 ) ; // once R-click mouse_event (MOUSEEVENTF_RIGHTDOWN | MOUSEEVENTF_RIGHTUP, 0, 0, 0, 0 ) ; // once L-double-click mouse_event (MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_LEFTUP, 0, 0, 0, 0 ) ; mouse_event (MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_LEFTUP, 0, 0, 0, 0 ) ; // mouse-move-absolute mouse_event (MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE, 0 * 65536 / 1280 , 0 * 65536 / 1024, 0, 0 ); return 0; }
有得講的大概是移動部份,這裡它的算法是,解析度最小值設 0;解析度最大值設 65536,於是要經過一些轉換才可準確移動,而指定了 MOUSEEVENTF_ABSOLUTE 是以絕對位置方式指定,若不加上的話就變相對位置方式指定 ( 所以可以出現負值 )。
每 200ms 送出一個 'A"
這只是簡單的 keybd_event 範例,假設當按下一個 'A' 時,約如下。
/* send 'A' each 200 ms */ #include <windows.h> int main() { while(1){ keybd_event('A', 0, 0, 0); keybd_event('A', 0, KEYEVENTF_KEYUP, 0); Sleep(200); } return 0; }
上面程式可以拿記事本出來試。
按住 'X' 時,不停送出 'A'
上面的方法無疑沒辦法決定什麼時候停止送出訊息。於是換個方式,當按住 'X' 時,就不停的送出 'A',按鍵鬆開時就停下來。
這行為可能會讓人覺得很白痴,原因是因為一般打字的時候,如果一直按住 'A' ,它會每 20ms 重覆輸入 'A",但在一些軟體或遊戲上,必須先鬆開再按下才有效,故部份有人會有這問題存在。
關鍵在於 GetKeyState 這項 API 之傳回值 (引數是 virtual key code )。它傳回的是 SHORT 資料型別 (一般是給 16bits ),
正在按下時:最高 bit 設 1;
toggle off ( Caps-Lock):最低 bit 設 1;
toggle on ( Caps-Lock):最低 bit 設 0。
當最高 bit 設 1 的時候,便代表傳回的會是一個負數,於此可寫下要求之功能。
/* Press-Combo */ #include <windows.h> int main() { while(1) while(GetKeyState('X') < 0) { keybd_event('A', 0, 0, 0); keybd_event('A', 0, KEYEVENTF_KEYUP, 0); Sleep(10); // important } return 0; }
這種方法關鍵在於 Sleep 一定要有,如果沒有的話,由於處理速度太快,會有 lag 現象 (比如說,已經放開 'X' 鍵了,然後還多送幾次 'A' 鍵 )。同時 Sleep 要設多少.. 就自己去試試吧,筆者是設 10~50 左右。
上面程式可以拿記事本出來試。
ESC 結束程式,按一下 'X' 不停送出 'A',再按一下 'X' 就停下來
這個問題算是讓人覺得比較亂的。會放上一些錯誤想法的 code。首先,我們想確認 GetKeyState 到底有哪幾種狀態,這個小程式自己寫可以知道,只有四種:
0x0000 -> 放掉
0x0001 -> 放掉
0x8000 -> 壓住,從 0x0001 到 0x0000 之過渡期
0x8001 -> 壓住,從 0x0000 到 0x0001 之過渡期
所以,如果我們可以去看 GetKeyState('X'),如果是 0 的話就不停送出 'A',如果非零的話就停止這動作 ??
同時我們想先設定 'X' 的初始狀態,剛好又有兩個 API 叫 SetKeyboardState / GetKeyboardState ,我們先拿這兩個 API 試一下對 'X' 這個鍵狀態的傳回值是怎樣
/* SetKeyboardState GetKeyboardState */ #include <windows.h> #include <stdio.h> int main() { BYTE State[256]; GetKeyboardState(State); State['X'] = 0; SetKeyboardState(State); while(1){ printf("%04hx\n", GetKeyState('X')); Sleep(20); } return 0; }
有興趣可以多跑幾次,一開始它的狀態也真的會是 0 ,且循環長得如下 (括號起來是循環)。
0x0000-> ( 0xff80->0x0000->0xff81->0x0001 )
尷尬的點來了,一開始設的 0x0000 有可能是紅色部份,也可能是藍色部份,
所以直接判斷 GetKeyState('X') 是否為 0/1 這方法是不可行的。
索性就直接再用一個 flag , 紀錄 'X' 鍵是否正在被按,如果 'X' 被按過的話, Flag = 1 - Flag,當 Flag == 1 成立的時候就送出 'A',這想法應該是可行的,但還少了一個邏輯:如果 'X' 一直長期被按住的話,依 os 預設的連 keyin 效果,可能每 20ms 就會將 Flag 切換一次,也就是鍵盤彈跳問題。
解除鍵盤彈跳問題也不難,又多了一個 flag, first_press, 平常都是設成 0,當探測到 GetKeyState('X') < 0 的時候就設成 1。
整體概念有點亂 ( 所以才說用狀態機維護會好點 ),程式碼大致如下。
/* Start - to */ #include <windows.h> int main() { static int Go=0; BYTE State[256]; SHORT ret, first_press=1; /* first_press */ while(GetKeyState(VK_ESCAPE) >=0 ){ ret = GetKeyState('X'); if(ret < 0 && first_press==1) Go = 1 - Go; if(Go==1) { keybd_event('A', 0, 0, 0); keybd_event('A', 0, KEYEVENTF_KEYUP, 0); } if(ret < 0 ) first_press = 0; else first_press=1; Sleep(5); } return 0; }
上述程式可在記事本底下測試。
ESC 結束、X 連擊,加入 F1 禁能功能
再加入一個鍵:F1,F1 並不會使得程式結束,只會使得 X 連擊的功能暫時消失,直到下次再按一次 F1。當然, F1 也要考慮鍵盤彈跳問題。
/* enable */ #include <windows.h> #include <stdio.h> int main() { static int Go=0; SHORT ret_X, ret_F1; SHORT first_X = 1; SHORT first_F1 = 1; SHORT enable = 1; puts("\nCombo : X "); puts("Enable : F1"); puts("Exit : ESC"); while(GetKeyState(VK_ESCAPE) >=0 ){ ret_F1 = GetKeyState(VK_F1); ret_X = GetKeyState('X'); // ------------------------------------------------------ // F1 if(ret_F1 < 0 && first_F1){ /* Press F1 now */ first_F1 = 0; Go = 0; enable = 1-enable; } if(ret_F1 >= 0) { /* Release F1 */ first_F1 = 1; } // ------------------------------------------------------ // X if(enable){ if(ret_X < 0 && first_X) { /* Press X */ first_X = 0; Go = 1 - Go; } if(ret_X >=0 ) {/* Release X */ first_X = 1; } if(Go){ keybd_event('A', 0, 0, 0); keybd_event('A', 0, KEYEVENTF_KEYUP, 0); Sleep(5); } } } return 0; }
上述程式可在記事本裡測試。
這支程式在測試時會發現,按下 F1 時同時也會呼叫 Help,這和使用 RegisterHotkey 有所不同, RegisterHotkey 會遮掉原本 HotKey 所對應之功能,但 keybd_event 並不會。
其他議題
像是 SendMessage、PostMessage 之類的方式,這在 另一篇文章 已有略提,就不再贅述。
SendInput 這支函式使用上其實也不難,重點都在填結構體怎麼填,有興趣可試試。
另外若是要做「截取」,也就是紀錄 keyboard 按鍵,與本文離題已甚遠,以後有空時再做介紹,
有興趣可先查 keyword : WH_KEYBOARD_LL 、SetWindowsHookEx
至於 C/C++ 有沒有別人包好 library 做這些事?這我就不清楚了。
留言列表