服务咨询热线:

022-88711099

当前位置:

键盘钩子在C#中的设计

发布时间:2012-03-25 01:53:13 作者:Charles Chen 访问量:1259

键盘钩子在C#中的设计

趁空闲的时间,对键盘钩子进行了学习,通过C#这门语言来设计和实现:下面是我设计的类图:

键盘钩子包括两类:全局钩子和私有钩子,这里我分成两个类来设计:


public delegate int HOOKPROC(int nCode, int wParam, int lParam);
public enum HookType
{
WH_KEYBOARD = 2,//私有钩子
WH_KEYBOARD_LL = 13//全局钩子
}






Hook基类实现
public abstract class hook
{
//设置钩子
[DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
public static extern int SetWindowsHookEx(HookType idHook, HOOKPROC lpfn, IntPtr hInstance, int threadId);
//抽调钩子
[DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
public static extern bool UnhookWindowsHookEx(int idHook);

[DllImport("kernel32")]
public static extern int GetCurrentThreadId();

///


/// 钩子处理委托
///

public HOOKPROC proc;
///
/// 钩子类型
///

public HookType type;
///
/// 钩子的句柄
///

public int hHook = 0;

public hook(HOOKPROC proc, HookType type)
{
this.proc = proc;
this.type = type;
}

public abstract int SetWindowsHookEx();
public virtual void UnhookWindowsHookEx()
{
bool retKeyboard = true;
if (hHook != 0)
{
retKeyboard = UnhookWindowsHookEx(hHook);
hHook = 0;
}
if (!retKeyboard)
{
throw new Exception("UnhookWindowsHookEx failed.");
}
}
}






全局钩子和私有钩子实现
public class PublicHook : hook
{
public PublicHook(HOOKPROC proc)
: base(proc, HookType.WH_KEYBOARD_LL)
{ }

public override int SetWindowsHookEx()
{
if (hHook == 0)
hHook = SetWindowsHookEx(this.type, this.proc, Marshal.GetHINSTANCE(Assembly.GetExecutingAssembly().GetModules()[0]), 0);
return hHook;
}
}

public class PrivateHook : hook
{
public PrivateHook(HOOKPROC proc)
: base(proc, HookType.WH_KEYBOARD)
{ }

public override int SetWindowsHookEx()
{
if (hHook == 0)
hHook = SetWindowsHookEx(this.type, this.proc, IntPtr.Zero, GetCurrentThreadId());
return hHook;
}
}




主界面调用实现
private hook hook = null;
private void btnOpen_Click(object sender, EventArgs e)
{
if (this.comboBox1.SelectedIndex == 0)
hook = new PublicHook(MyKeyboardProc);
else
hook = new PrivateHook(MyKeyboardProc);
int hHook = hook.SetWindowsHookEx();
if (hHook == 0)
{
MessageBox.Show("设置钩子失败!");
}
}
private void btnClose_Click(object sender, EventArgs e)
{
try
{
if (hook != null)
hook.UnhookWindowsHookEx();
}
catch
{
MessageBox.Show("关闭钩子失败!");
}
}

public int MyKeyboardProc(int nCode, int wParm, int lParam)
{
MessageBox.Show("你已经按下了按钮!");
return 0;
}





运行后主界面效果如下图所示:





下面备注一下在使用过程中一些类库说明及注意事项:

1、AppDomain.GetCurrentThreadId()在.net 2.0中过时了,VS2005和VS2008警告这个方法已经过时,建议使用 System.Threading.Thread.CurrentThread.ManagedThreadId,但实际上这两个值是不一样的。 AppDomain.GetCurrentThreadId()的实际上调用Win32 API,其返回的是该线程在Windows中的ThreadId,即同这个等价:
[DllImport("kernel32")]
public static extern int GetCurrentThreadId();

而System.Threading.Thread.CurrentThread.ManagedThreadId返回的是作为一个 ManagedThread在.Net CLR中的ThreadId,所以这和Windows的ThreadId是完全不同的。

2、使用API函数SetWindowsHookEx()把一个应用程序定义的钩子子程安装到钩子链表中。SetWindowsHookEx函数总是在Hook链的开头安装Hook子程。当指定类型的Hook监视的事件发生时,系统就调用与这个Hook关联的Hook链的开头的Hook子程。每一个 Hook链中的Hook子程都决定是否把这个事件传递到下一个Hook子程。Hook子程传递事件到下一个Hook子程需要调用 CallNextHookEx函数.函数签名如下:
[DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
public static extern int CallNextHookEx(int idHook, int nCode, IntPtr wParam, IntPtr lParam);




代码
HHOOK SetWindowsHookEx(
int idHook, // 钩子的类型,即它处理的消息类型
HOOKPROC lpfn, // 钩子子程的地址指针。如果dwThreadId参数为0
// 或是一个由别的进程创建的线程的标识,
// lpfn必须指向 DLL中的钩子子程。
// 除此以外,lpfn可以指向当前进程的一段钩子子程代码。
// 钩子函数的入口地址,当钩子钩到任何消息后便调用这个函数。
HINSTANCE hMod, // 应用程序实例的句柄。标识包含lpfn所指的子程的
DLL。
// 如果dwThreadId 标识当前进程创建的一个线程,
// 而且子程代码位于当前进程,hMod必须为NULL。
// 可以很简单的设定其为本应用程序的实例句柄。
DWORD dwThreadId // 与安装的钩子子程相关联的线程的标识符。
// 如果为0,钩子子程与所有的线程关联,即为全局钩子。
);

函数成功则返回钩子子程的句柄,失败返回NULL。

以上所说的钩子子程与线程相关联是指在一钩子链表中发给该线程的消息同时发送给钩子子程,且被钩子子程先处理。

3、系统钩子和线程钩子:
SetWindowsHookEx()函数的最后一个参数决定了此钩子是系统钩子还是线程钩子。
线程勾子用于监视指定线程的事件消息。线程勾子一般在当前线程或者当前线程派生的线程内。
系统勾子监视系统中的所有线程的事件消息。因为系统勾子会影响系统中所有的应用程序,所以勾子函数必须放在独立的动态链接库(DLL) 中。系统自动将包含"钩子回调函数"的DLL映射到受钩子函数影响的所有进程的地址空间中,即将这个DLL注入了那些进程。
几点说明:
(1)如果对于同一事件(如鼠标消息)既安装了线程勾子又安装了系统勾子,那么系统会自动先调用线程勾子,然后调用系统勾子。
(2)对同一事件消息可安装多个勾子处理过程,这些勾子处理过程形成了勾子链。当前勾子处理结束后应把勾子信息传递给下一个勾子函数。
(3)勾子特别是系统勾子会消耗消息处理时间,降低系统性能。只有在必要的时候才安装勾子,在使用完毕后要及时卸载。

4、钩子处理函数:public int MyKeyboardProc(int nCode, int wParm, int lParam)

可以添加自己想要的信息处理(如发邮件,增加文件等)
这里可以添加自己想要的信息处理
public int MyKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0)
{
KeyBoardHookStruct kbh = (KeyBoardHookStruct)Marshal.PtrToStructure(lParam, typeof(KeyBoardHookStruct));
if (kbh.vkCode == (int)Keys.S && (int)Control.ModifierKeys == (int)Keys.Control) // 截获F8
{
MessageBox.Show("快捷键已拦截!不能保存!");
return 1;
}
if ((int)Control.ModifierKeys == (int)Keys.Delete && (int)Control.ModifierKeys == (int)Keys.Alt && (int)Control.ModifierKeys == (int)Keys.Control)
{
MessageBox.Show("捕捉到Ctrl+Alt+Delete");
return 1;
}
if (kbh.vkCode == (int)Keys.Y
&& (int)Control.ModifierKeys == (int)Keys.Control + (int)Keys.Alt) //截获Ctrl+Alt+Y
{
About msg = new About();
msg.Show();
MessageBox.Show("不能全部保存!");
return 1;
}
if (kbh.vkCode == (int)Keys.A)
{
MessageBox.Show("A");
this.label1.Text += "A";
}
if (kbh.vkCode == (int)Keys.B)
{
MessageBox.Show("B");
this.label1.Text += "B";
}
if (kbh.vkCode == (int)Keys.Enter)
{
this.label1.Text = "执行成功!";
}
if (kbh.vkCode == (int)Keys.Back)
{
this.label1.Text = this.label1.Text.Remove(this.label1.Text.Length - 1);
}
if (kbh.vkCode == (int)Keys.D1)
{
this.label1.Text += "1";
}
}
return CallNextHookEx(hHook, nCode, wParam, lParam);
}



注意:如果返回1,则结束消息,这个消息到此为止,不再传递。如果返回0 或调用CallNextHookEx函数则消息出了这个钩子继续往下传递,也就是传给消息真正的接受者.

//键盘Hook结构函数(详情查看msdn的Platform SDK中的KBDLLHOOKSTRUCT结构)

[StructLayout(LayoutKind.Sequential)]
public class KeyBoardHookStruct
{
public int vkCode;//表达一个在1到254间的虚拟键盘码
public int scanCode;//表示硬件扫描码
public int flags;
public int time;
public int dwExtraInfo;
}

5、钩子函数执行两次的解决办法:http://www.boluor.com/solution-to-the-keyboard-hook-function-is-executed-twice.html

最后,欢迎朋友们进行指正,谢谢!

本文由客户端添加