C# 模擬鍵盤輸入
1. 使用.Net Framework的庫函式
SendKeys.SendWait("123{TAB}abc");
namespace System.Windows.Forms
名稱空間下的SendKeys
是.Net提供的模擬鍵盤輸入的工具類。其中有Send()
和SendWait()
這兩個方法,都可以傳送按鍵訊息。區別在於SendWait()
是會等待按鍵訊息被處理完成才返回的,而Send()
則不用。這就類似於SendMessage
和PostMessage
的關係。
上面程式碼中的{TAB}
代表Tab鍵。鍵盤上一些特殊的按鍵都有對應的程式碼,具體的對照表可以參照微軟MSDN上的介紹:SendKeys Class
當然,還可以使用Windows API,API原型如下:
/// <summary>
/// 合成一次擊鍵事件
/// </summary>
/// <param name="bVk">定義一個虛擬鍵碼。鍵碼值必須在1~254之間</param>
/// <param name="bScan">定義該鍵的硬體掃描碼</param>
/// <param name="dwFlags">定義函式操作的各個方面的一個標誌位集。應用程式可使用如下一些預定義常數的組合設定標誌位</param>
/// <param name="dwExtraInfo">定義與擊鍵相關的附加的32位值</param>
[DllImport("user32")]
public static extern void keybd_event(byte bVk, byte bScan, uint dwFlags,
uint dwExtraInfo);
甚至用SendMessage
和PostMessage
也是可以做到的。
但以上這些實現方法都是在Windows訊息層面的,對於像記事本等常規應用程式是沒問題的。但是對於一些遊戲、QQ登入框、網銀登入框等則無效。這是因為這些程式不是從Windows訊息中獲取按鍵資訊的,而是直接從底層驅動層面獲取按鍵資訊的。
在一篇關於研究遊戲外掛的博文裡看到博主描述了這個問題:
引自:https://blog.csdn.net/zdrl/article/details/12707835
在解釋更詳細的原理之前,我們先來抓出幕後黑手,看看是哪個給遊戲撐腰?讓它有膽子違抗Windows訊息命令。究竟是判斷了真實鍵盤資訊,還是有其他原因。結果在DirectX程式設計中發現了DirectInput這個API。就是它繞過了Windows的訊息機制,它的目的是為了讓遊戲的實時性控制更好、更快。Windows訊息是佇列形式的,在傳遞過程中會有延時,比如格鬥類遊戲對實時性控制要求是非常高的,Window訊息機制不能滿足這個需求。而DirectInput直接和鍵盤驅動程式打交道,效率當然要高出一大截。我認為大部分遊戲不響應訊息的真正的原因在這裡,而不是故意寫了反作弊系統。
2. 使用WinIO.dll
由於上述方法只能模擬Windows訊息層面的按鍵,以至於對一些應用程式無效,所以下面就採用直接在驅動層面模擬按鍵的方法。
這裡需要用到一個元件,那就是使用WinIO.dll,這是是國外大佬開發的一個dll。
引自百度百科:https://baike.baidu.com/item/winio/2877240?fr=aladdin
WinIO程式庫允許在32位的Windows應用程式中直接對I/O埠和實體記憶體進行存取操作。通過使用一種核心模式的裝置驅動器和其它幾種底層程式設計技巧,它繞過了Windows系統的保護機制。
使用此元件的環境要求:
- 系統Win7或Win10均可。
- 需要PS/2鍵盤(老式的針孔插頭的鍵盤),USB鍵盤不行。
- 正規使用的話需要官方授權簽名,否則就得將Windows開啟測試模式。
- 使用此元件的應用程式需要以管理員的身份啟動。
- 此元件還有32位和64位的區分。
- 與dll配套的還有個.sys的檔案,要跟dll放在同一目錄下。
Windows開啟測試模式的方法:
以管理員身份開啟cmd,輸入開啟測試模式的命令並執行。然後重啟電腦,看到桌面右下角出現“測試模式”字樣即可。
開啟測試模式的命令:
bcdedit /set testsigning on
關閉測試模式的命令:
bcdedit /set testsigning off
開啟測試模式成功:
開啟測試模式成功
呼叫WinIO64.dll的示例程式碼:
public class WinIO64
{
private const int KBC_KEY_CMD = 0x64;
private const int KBC_KEY_DATA = 0x60;
#region WinIo64.dll
[DllImport("WinIo64.dll")]
public static extern bool InitializeWinIo();
[DllImport("WinIo64.dll")]
public static extern bool GetPortVal(IntPtr wPortAddr, out int pdwPortVal,
byte bSize);
[DllImport("WinIo64.dll")]
public static extern bool SetPortVal(uint wPortAddr, IntPtr dwPortVal, byte
bSize);
[DllImport("WinIo64.dll")]
public static extern byte MapPhysToLin(byte pbPhysAddr, uint dwPhysSize,
IntPtr PhysicalMemoryHandle);
[DllImport("WinIo64.dll")]
public static extern bool UnmapPhysicalMemory(IntPtr PhysicalMemoryHandle,
byte pbLinAddr);
[DllImport("WinIo64.dll")]
public static extern bool GetPhysLong(IntPtr pbPhysAddr, byte pdwPhysVal);
[DllImport("WinIo64.dll")]
public static extern bool SetPhysLong(IntPtr pbPhysAddr, byte dwPhysVal);
[DllImport("WinIo64.dll")]
public static extern void ShutdownWinIo();
#endregion
[DllImport("user32.dll")]
public static extern int MapVirtualKey(uint Ucode, uint uMapType);
private WinIO64()
{
IsInitialize = true;
}
public static void Initialize()
{
if (InitializeWinIo())
{
KBCWait4IBE();
IsInitialize = true;
}
else
MessageBox.Show("Load WinIO Failed!");
}
public static void Shutdown()
{
if (IsInitialize)
ShutdownWinIo();
IsInitialize = false;
}
private static bool IsInitialize { get; set; }
///等待鍵盤緩衝區為空
private static void KBCWait4IBE