由於C#屏蔽了很多作業系統核心級的操作,將保護機制進行了加強,通過普通方法是無法完成如後台鍵鼠類比、進程記憶體讀寫、網路封包攔截等操作的。
而C#又提供了調用Unmanaged 程式碼的DllImport,使得我們可以叫用作業系統較為底層的API來完善程式功能。
本文就C#調用Win32API函數PostMessage完成指定表單後台鍵鼠類比作為樣本,粗略講解一下C#對Unmanaged 程式碼的調用及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 lParam7 );
上面的代碼用於在運行時將我們定義的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鬆開的訊息。
如果大家還不是很明白,請大家下載我寫的範例來看。斷斷續續搞了兩三天,代碼寫得漏洞百出,不過先發出來大家稍微看看,有錯誤的地方請大家見諒(無情提示:不要盲目地認為My Code是正確的,事實證明代碼裡的功能很大程度上不完善)。我把使用到的資料也一併打包了。
歡迎您移步我們的交流群,無聊的時候大家一起打發時間:
或者通過QQ與我聯絡:
(最後編輯時間2013-05-14 15:14:41)