由於C#屏蔽了很多操作系統內核級的操作,將保護機制進行了加強,通過普通方法是無法完成如后台鍵鼠模擬、進程內存讀寫、網絡封包攔截等操作的。
而C#又提供了調用非托管代碼的DllImport,使得我們可以調用操作系統較為底層的API來完善程序功能。
本文就C#調用Win32API函數PostMessage完成指定窗體后台鍵鼠模擬作為示例,粗略講解一下C#對非托管代碼的調用及Window的消息處理機制。
(如果您對DllImport和Window消息機制有較為深入的理解,閱讀本篇文章只是為了了解如何發送鍵鼠模擬指定和PostMessage中wParam與lParam的具體含義請略過前面的章節)
首先是C#調用非托管函數。
我們使用DllImport特性從指定動態鏈接庫連接函數到我們的代碼中,如下
1 [DllImport("user32", SetLastError = true)] 2 private static extern bool PostMessage( 3 int hWnd, 4 uint Msg, 5 int wParam, 6 int lParam 7 );
上面的代碼用於在運行時將我們定義的PostMessage方法替換成user32.dll中的同名方法。
(注意:1.dll的名稱可以不包括后綴。
2.dll的路徑需要在程序路徑下或Path路徑下。
3.com組件可以使用Tlbimp轉換為元數據后在項目中Using而不需要如此操作。
4.參數類型最好為32位整型來傳遞值數據或者封裝為IntPtr類型進行傳遞(大多數情況下使用),使用對象引用或委托也是可以的;值得一提的是雖然系統會自動整編,但是在結構數據的存儲上您則需要一下特定處理。
5.SetLastError屬性讓您可以在函數調用出錯后可以調用GetLastError函數(也是外部函數)得到錯誤代碼,通過查閱msdn手冊或者在C#代碼中構造System.ComponentModel.Win32Exception類實例來具體化錯誤信息。
6.DllImport特性的EntryPoint屬性讓您可以設置鏈接的外部方法名,默認為當前方法名,如果您希望更改方法名則可以使用指定方法名的方式實現。)
總的說來,DllImport有點像Java的JNI,都是引用外部的函數,而extern關鍵字更像是來自於C++。
然后是Windows的消息機制。
首先我們知道,程序運行起來首先會有一個主線程,這時候線程沒有屬於自己的消息隊列,他是非消息線程或工作者線程;系統假定線程不會用於任何與用戶相關的任務,這樣可以減少線程對系統資源的要求。
而后當該線程調用一個與圖形用戶界面有關的函數 ( 如檢查它的消息隊列或建立一個窗口 ),系統就會為該線程分配一些另外的資源,以便它能夠執行與用戶界面有關的任務,這個時候線程與消息隊列相關,它就成了用戶界面線程;當用戶操作,系統就會產生消息並送入某隊列。(如我們使用C和C++開發WindowGUI程序,在WinMain中總會調用GetMessage函數循環獲取消息隊列中的消息;不熟悉C/C++的朋友也沒關系,這句話的意思大概是說在進程啟動后會使用循環主動從消息隊列中獲取消息)
所以如果要模擬鍵鼠操作,我們所要做的很簡單,就是使用某個函數(當然獲取窗體句柄等其他函數)把特定消息(我們自己構造)放到對應線程的消息隊列就OK了。
函數我們可以使用PostMessage,至於能不能成功,很大一定程度上取決於我們消息是否構造得逼真。
放着PostMessage先不管,我們看看“消息”在WindowsAPI中的定義:
1 typedef struct tagMSG { // msg 2 HWND hwnd; 3 UINT message; 4 WPARAM wParam; 5 LPARAM lParam; 6 DWORD time; 7 POINT pt; 8 } MSG;
hwnd參數表示該哪個Window來處理這個消息。
message表示消息是什么。鍵盤按鍵、鼠標點擊還是其他。
wParam和lParam是參數,他們在message不同時擁有不同的含義。
time表示時間。
pt表示坐標。
個人表示很糾結,最討厭C和C++把類型名字定義太多,明明都是整型卻偏要搞這么多花樣(筆者只是隨口一提,讀者要明白上文中各參數類型並不完全相同)。
我們需要構造的消息就是上面這個,time和pt不用我們構造,系統會生成。我們需要將前面四個參數構造出來。
請讀者原諒筆者先賣個關子,筆者希望先把PostMessage函數做一個講解。
PostMessage函數是將一個消息的組成部分合成一個消息(如我們調用時則只需依次傳入上文消息的前四個參數即可)並放入對應線程消息隊列的方法,它的返回值表示操作組裝和放入隊列是否成功,而非執行是否成功。它是異步的操作,如下圖:
我們在調用PostMessage之后,消息就會進入消息隊列。輪到該消息被獲取和響應時我們就能看到效果了。
如果您希望立即或很快看到效果則可以使用SendMessage方法,該方法和PostMessage使用方法完全相同(不過它是同步的,且不善於處理非執行線程的消息插入,在某些情況下可能會造成死鎖或進程停止,配合鈎子使用時則需尤為注意),您可以通過返回值判斷操作是否成功。只是它的返回是在消息被處理之后,調用線程可能出現畫面停頓或變成白色,不是很推薦大家使用。
最后是PostMessage方法的調用傳參。
第一個參數是要交由處理的控件句柄。這個參數的獲取很重要,一般來說我們需要獲取到真正的控件句柄而非其父窗體或控件的句柄,因為它們不能正確處理這個消息。(比如按鈕點擊,我們最好拿到按鈕的句柄而非其父控件的句柄,可以通過鼠標位置拿到控件句柄,使用GetCursorPos和WindowFromPoint方法即可,他們都來自外部函數)
第二個參數為消息的整型表示,它被定義在頭文件中以WM_開頭的宏里,從0x1到0x400。查閱msdn或c語言Windows的平台sdk頭文件即可看到,值得一提的是在多個頭文件中都有消息定義。
第三個和第四個參數是擴展信息,在消息類型(第二個參數)不同時它們的值和含義會有很大差別。
下面是msdn上對鼠標、鍵盤相關消息中擴展信息的描述(個人翻譯,可能有出入)
當鼠標左鍵按下:
參數1:略
參數2:WM_LBUTTONDOWN=0x201
參數3:指示此時Ctrl、Shift、鼠標左鍵、鼠標中鍵、鼠標右鍵的按下情況(這個數值為32位,是使用MK_LBUTTON=1、MK_RBUTTON=2、MK_SHIFT=4、MK_CTROL=8、MK_MBUTTON=16、MK_XBUTTON1=32(需要nt5.x)、MK_XBUTTON2=64(需要nt5.x)按位或得到的。它們被定義在winuser.h頭文件中。也就說如果此時只是單純的鼠標左鍵按下,則此參數應該為1,如果此時Ctrl鍵已經被按下了,則應該設置為9)。
參數4:鼠標點擊的坐標,相對於窗體而言(這個數值是32位的,高位存y坐標,地位存x坐標,傳遞方法可以為x+y*65536或者x+(y<<16))。
當鼠標左鍵抬起時參數3則需要變動,此時最低位應置零。
鼠標右鍵、中鍵按下和抬起、雙擊實現一樣(在右鍵抬起時第二位應該置零哦)。
鼠標滾輪滾動時實現略有不同:
參數1:略
參數2:WM_MOUSEWHEEL=0x20A
參數3:指示此時Ctrl、Shift、鼠標左鍵、鼠標中鍵、鼠標右鍵的按下情況及鼠標滾輪的滾動情況(該數值為32位,低位表示按鍵情況,依然可以通過按位或組裝;高位表示滾動情況,一般來說,對於向下滾動永遠為n*-120(n一般為1),對於向上滾動永遠為n*120(n一般為1);分別對應-1*120與1*120。即如果向下滾動且未按下任何鍵,此數值應設置為-120*65536即0xFF880000,向上滾動則設置為120*65536即0x00780000;如果此時Ctrl鍵是按下狀態,則數值應加上8)。
參數4:鼠標位置,高位y,低位x。
鍵盤按下時:
參數1:略
參數2:WM_KEYDOWN=0x0100
參數3:指示按下的鍵的虛擬碼在winuser.h頭文件中查看VK_開頭的宏定義即可
參數4:指示擴展信息(此數值為32位,0-15位表示按鍵被按下的次數,應當置為1;16-23表示按鍵對應的硬件掃描碼,這個值和硬件有關,不過可以使用MapVirtualKey函數來得到;24位表示這個鍵是否是擴展鍵,如右Ctrl和Alt,在101和102鍵盤中,此值為1,否則為0;25-28位為保留位;29位指示此時Alt鍵是否處於按下狀態;30位只是此鍵之前的狀態,如之前為按下狀態,此值為1,反之為0,此處則應置零;第31位為特殊標識,在WM_KEYDOWN消息中此值始終為0)。
鍵盤抬起時:
參數1:略
參數2:WM_KEYDOWN=0x0101
參數3:同上
參數4:擴展信息,同上(第30位和31位都為1)。
普通組合按鍵:
Ctrl和Shift按鍵還好,先按下Ctrl或Shift再按下其他鍵,松開其他鍵再松開Ctrl或Shift即可。
Alt組合按鍵:
對於Alt組合按鍵則復雜一點,Alt是系統關鍵按鍵,它是默認的快捷鍵組合鍵。我們發送Alt組合按鍵時應該先發一個Alt鍵,然后發送其他按鍵。有兩種方式:
方式一:省略其他信息,直接發送一條消息表示組合按鍵,如下
1 PostMessage(hwnd, WM_SYSKEYDOWN, 'A', 1 << 29); 2 //Thread.Sleep(50); 3 //PostMessage(hwnd, WM_SYSKEYUP, 'A', 1 << 29);
如果看懂了上文對lParam的解釋,這個代碼應該容易理解。
方式二:發送完整消息,先發送Alt按下,然后發送組合按鍵(值得一提的是此時應該使用WM_SYSKEYDOWN,lParam第29位也應該置為1),然后發送組合按鍵松開(同前),最后發送Alt松開的消息。
如果大家還不是很明白,請大家下載我寫的范例來看。斷斷續續搞了兩三天,代碼寫得漏洞百出,不過先發出來大家稍微看看,有錯誤的地方請大家見諒(無情提示:不要盲目地認為我的代碼是正確的,事實證明代碼里的功能很大程度上不完善)。我把使用到的資料也一並打包了。
(最后編輯時間2013-05-14 15:14:41)