.Net中使用钩子
最近觉得以前一些知识淡忘太厉害,便重新复习了一下钩子部分,顺便练习几个API。
参考http://www.moon-soft.com/doc/36092.htm
什么是钩子?http://www.microsoft.com/china/community/program/originalarticles/techdoc/hook.mspx
本文中提到的API. 或者Hook.均为封装了的类,附件中有包含。
建一个Winform工程,拖一个ListBox,两个Button
ListBox初始化一些列表项目,想实现这么一个功能,就是鼠标在Listbox上Move的时候,显示Tooltipe,方法比较多,作为练习,先使用API完成。
给LstBox添加Move事件,在处理中写以下代码
{
Point pClr = listBox1.PointToClient(Cursor.Position);
int nPos = API.SendMessage(listBox1.Handle,WindowsMessages.LB_ITEMFROMPOINT,0,API.MakeLParam(pClr.X,pClr.Y));
if (nPos >= 0 && nPos <listBox1.Items.Count)
{
toolTip1.SetToolTip(listBox1,listBox1.Items[nPos].ToString());
}
}
当然,EventArgs中已经包含了鼠标当前位置,此处纯属着重说明Cursor.Position为当前屏幕坐标,sendmessage用的是控件坐标。
另外一种实现方法是挂个钩子,监听鼠标信息。钩子的回调处理为以下
{
if (hookCode >= 0)
{
MouseMSG msg = (MouseMSG)Marshal.PtrToStructure(lParam,typeof(MouseMSG));
Point pClr = listBox1.PointToClient(msg.position);
int nPos = API.SendMessage(listBox1.Handle,WindowsMessages.LB_ITEMFROMPOINT,0,API.MakeLParam(pClr.X,pClr.Y));
if (nPos >= 0 && nPos <listBox1.Items.Count)
{
toolTip1.SetToolTip(listBox1,listBox1.Items[nPos].ToString());
}
//return 1;
}
return Hook.CallNextHookEx(nHookID,hookCode,wParam,lParam);
}
在钩子回调函数中,如果返回1,则消息到此为止,不再继续传递,如果为0或者直接调用CallNextHookEx则消息继续传递,上边代码中如果返回1,则会发现鼠标什么处理也做不了了。
钩子处理中主要用到的几个函数
private static extern int SetWindowsHookEx(HookType hookType, HookCallback lpfn, IntPtr hInstance, int threadId);
[DllImport("user32.dll",CharSet=CharSet.Auto, CallingConvention=CallingConvention.StdCall)]
private static extern bool UnhookWindowsHookEx(int idHook);
[DllImport("user32.dll",CharSet=CharSet.Auto, CallingConvention=CallingConvention.StdCall)]
public static extern int CallNextHookEx(int idHook, int nCode, Int32 wParam, IntPtr lParam);
lpfn 钩子子程的地址指针。如果dwThreadId参数为0 或是一个由别的进程创建的线程的标识,lpfn必须指向DLL中的钩子子程。 除此以外,lpfn可以指向当前进程的一段钩子子程代码。钩子函数的入口地址,当钩子钩到任何消息后便调用这个函数。
hInstance 应用程序实例的句柄。标识包含lpfn所指的子程的DLL。如果threadId 标识当前进程创建的一个线程,而且子程代码位于当前进程,hInstance必须为NULL。可以很简单的设定其为本应用程序的实例句柄。
threaded 与安装的钩子子程相关联的线程的标识符。如果为0,钩子子程与所有的线程关联,即为全局钩子。
使用实例
new HookCallback(ProcessDBClick),
IntPtr.Zero,
AppDomain.GetCurrentThreadId());
安装全局钩子
上边使用的是线程钩子,如果要使用全局钩子在钩子的安装上略有不同。如下
Marshal.GetHINSTANCE(Assembly.GetExecutingAssembly().GetModules()[0]),0)
在《HOW TO:在 Visual C# .NET 中设置窗口挂钩》一文中有如下描述: 在 .NET 框架中不支持全局挂钩
您无法在 Microsoft .NET 框架中实现全局挂钩。若要安装全局挂钩,挂钩必须有一个本机动态链接库 (DLL) 导出以便将其本身插入到另一个需要调入一个有效而且一致的函数的进程中。这需要一个 DLL 导出,而 .NET 框架不支持这一点。托管代码没有让函数指针具有统一的值这一概念,因为这些函数是动态构建的代理。网上查找了很多代码,大都另外包含了一个C++的DLL,用于标识包含lpfn所指的子程的DLL,似乎也验证了这一说法。
但实际上并非如此,使用如下代码即可实现全局钩子:
IntPtr pInstance = Marshal.GetHINSTANCE( Assembly.GetExecutingAssembly().ManifestModule );
Win32API.SetWindowsHookEx( WH_MOUSE_LL,m_MouseHookProcedure, pInstance, 0 );注:ManifestModule属性是.Net Framework 2.0中新增加的,所以当你依然使用.Net Framework 1.x的时候,可以使用GetModules方法获取当前程序集的所有模块,然后用其中的一个作为GetHINSTAN方法的参数,来获得合适的句柄指针。
在钩子回调函数中有两个参数wParam和lParam,在键盘钩子和鼠标钩子中可以对应下边的结构体
对于鼠标消息,我们可以定义下面这个结构:
public struct MouseMSG
{
public Point p; //鼠标坐标
public IntPtr HWnd; //鼠标点击的控件的句柄
public uint wHitTestCode;
public int dwExtraInfo;
}
对于键盘消息,我们可以定义下面这个结构:
public struct KeyboardMSG
{
public int vkCode; //按键代码
public int scanCode;
public int flags;
public int time;
public int dwExtraInfo;
}
然后使用
Marshal.PtrToStructure(lParam, type);
可以进行转换。
如果是监听键盘消息的线程钩子,我们可以根据lParam值的正负确定按键是按下还是抬起,根据wParam值确定是按下哪个键。
// 按下的键
Keys keyData = (Keys)wParam;
if(lParam.ToInt32() > 0)
{
// 键盘按下
}
if(lParam.ToInt32() < 0)
{
// 键盘抬起
}
如果是监听键盘消息的全局钩子,按键是按下还是抬起要根据wParam值确定。
wParam = = 0x100 // 键盘按下
wParam = = 0x101 // 键盘抬起
上班了,有空再继续。
source