C#Win32API编程之PostMessage
由于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松开的消息。
如果大家还不是很明白,请大家下载我写的范例来看。断断续续搞了两三天,代码写得漏洞百出,不过先发出来大家稍微看看,有错误的地方请大家见谅(无情提示:不要盲目地认为我的代码是正确的,事实证明代码里的功能很大程度上不完善)。我把使用到的资料也一并打包了。
欢迎您移步我们的交流群,无聊的时候大家一起打发时间:
或者通过QQ与我联系:
(最后编辑时间2013-05-14 15:14:41)