C#通过键盘钩子获取数据

C#通过键盘钩子获取数据

前言

在做关于使用USB读卡器、扫码枪等项目的开发时,有可能需要通过键盘钩子去获取刷卡器、扫码枪扫描到的数据,当然,如果设备是串口、或者非系统独占设备的USB,可以采用串口通信、HID设备读写去获取数据。
本文是记录了C#如何通过键盘钩子去获取刷卡器读取上来的数据。

一、项目背景

需要获取刷卡器读到的卡号,然后根据卡号从客户的WebService中获取数据。所以第一步工作是如何获取卡号。

二、弯路

一开始拿到读卡器时老板告诉我,这个刷卡器时USB通信的,所以一开始我花了两三天时间去了解C#如何打开USB设备。已经通过CreateFile方法获取到了设备路径,但始终没有访问权限,所以后面的ReadFile一直无法使用。最后在一篇文章中得知,我拿到的刷卡器是系统独占设备,是无法访问的,所以只能换其他方式,最终各种搜索,决定用键盘钩子去抓取数卡器读到的数据。

三、键盘钩子工作原理

在刷卡器读到卡号时,实际上是键盘输入事件,比如我读到的卡号时123456,实际上就相当于我的键盘输入了7次,字符是“1 2 3 4 5 6 \r”,所以我只要监控键盘的输入,将“1 2 3 4 5 6 \r”字符串抓到就可以了。
但是如何区别是刷卡器读到的数据还是键盘打出来的字符串呢?这个就需要通过字符增加的时间间隔去区分,刷卡器刷卡得到数据的时间间隔是ms级别的,用键盘打出这些字符是不会这么快的,所以通过字符输入的间隔时间能够成功过滤键盘事件。

四、代码

1. 安装钩子

开始监控键盘事件。需要使用Windows API函数GetModuleHandle获取当前运行的应用程序句柄,Process.GetCurrentProcess().MainModule.ModuleName就是获取当前运行模块的名称。获取句柄后通过SetWindowsHookEx函数安装钩子,或者说开始监控键盘输入。
SetWindowsHookEx

  1. 第一个参数是钩子使用范围是全局的还是局部的、是否包括鼠标移动等等,具体可参考注释中的MSDN,我用的13是只监控键盘输入事件,不包括鼠标移动;
  2. 第二个参数是一个事件HookProc,用来获取键盘事件输入的字符,下一步会介绍具体实现方法;
  3. 第三个参数是当前运行模块的句柄;
  4. 第四个参数是:挂钩过程将与之关联的线程的标识符。为0是全局钩子,只要钩子程序开启,就开始监控键盘输入事件,如果不为0,表示监视指定的线程。
 delegate int HookProc(int nCode, Int32 wParam, IntPtr lParam);
 HookProc hookproc;//钩子过程的委托在设置钩子之后由GC收集,垃圾收集委托后,系统在尝试调用回调时崩溃,所以必须放在外面保证其必须处于活动状态,直到保证永远不会被调用
        public bool Start()
        {
            if (hKeyboardHook == 0)
            {
                hookproc = new HookProc(KeyboardHookProc);
                IntPtr modulePtr = GetModuleHandle(Process.GetCurrentProcess().MainModule.ModuleName);
                hKeyboardHook = SetWindowsHookEx(13, hookproc, modulePtr, 0);   
            }
            return (hKeyboardHook != 0);
        }
        /// <summary>
        /// 获取一个应用程序或动态链接库的模块句柄
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        [DllImport("kernel32.dll")]
        public static extern IntPtr GetModuleHandle(string name);
 /// <summary>
        /// 安装钩子,参数设置详见https://docs.microsoft.com/zh-cn/windows/win32/api/winuser/nf-winuser-setwindowshookexa/
        /// </summary>
        /// <param name="idHook"></param>
        /// <param name="lpfn"></param>
        /// <param name="hInstance"></param>
        /// <param name="threadId"></param>
        /// <returns></returns>
        [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
        private static extern int SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hInstance, int threadId);

2. KeyboardHookProc事件

这个函数是用来解析并收集键盘输入的字符,在1中安装钩子后,第一次键盘输入事件会执行一次KeyboardHookProc,收集一次字符后继续下一个钩子CallNextHookEx,第二次键盘输入会继续执行该函数,直到所有字符都收集后组成一个字符串供后续使用。

 public struct KeyBoards
{
	public int VirtKey;      //虚拟码
	public int ScanCode;     //扫描码
	public string KeyName;   //键名
	public uint AscII;       //AscII
	public char Chr;         //字符
	public bool IsValid;     //事件发生是否来自设备
	public string KeyBoard;   //条码信息
	public DateTime Time;    //扫描时间
}
private struct EventMsg
{
	public int message;
	public int paramL;
	public int paramH;
	public int Time;
	public int hwnd;
}
 public int ts { get; set; }//扫码间隔时间
public delegate void KeyBoardDelegate(KeyBoards KeyBoard);
public event KeyBoardDelegate KeyBoardEvent;
delegate int HookProc(int nCode, Int32 wParam, IntPtr lParam);
event HookProc hookproc;//钩子过程的委托在设置钩子之后由GC收集,垃圾收集委托后,系统在尝试调用回调时崩溃,所以必须放在外面保证其必须处于活动状态,直到保证永远不会被调用
KeyBoards KeyBoard = new KeyBoards();
int hKeyboardHook = 0;
public string strKeyBoard = "";
public string strbarcoding { get; set; }
public int length;
public int KeyBoardNum = 0;//刷卡读到的第一个字符时,DateTime.Now.Subtract(KeyBoard.Time).TotalMilliseconds一定会大于300ms,如果不处理会过滤第一个字符,所以第一个要被保留
        
private int KeyboardHookProc(int nCode, Int32 wParam, IntPtr lParam)
{
	KeyBoardNum++;
	bool notChar = false;
	KeyBoard.IsValid = true;
	if (nCode == 0)
	{
		 EventMsg msg = (EventMsg)Marshal.PtrToStructure(lParam, typeof(EventMsg));
                if (wParam == 0x100)
                {
                    KeyBoard.VirtKey = msg.message & 0xff;  //虚拟码
                    KeyBoard.ScanCode = msg.paramL & 0xff;  //扫描码,有可能是鼠标键盘按键,有可能是HID设备发送过来的字符
                    StringBuilder strKeyName = new StringBuilder(255);
                    if (GetKeyNameText(KeyBoard.ScanCode * 65536, strKeyName, 255) > 0)//将接收到的扫描码转化成实际字符
                      {
                        KeyBoard.KeyName = strKeyName.ToString().Trim(new char[] { ' ', '\0' });
                    }
                    else
                    {
                        KeyBoard.KeyName = "";
                    }
                    byte[] kbArray = new byte[256];
                    uint uKey = 0;
                    GetKeyboardState(kbArray);
                       if (ToAscii(KeyBoard.VirtKey, KeyBoard.ScanCode, kbArray, ref uKey, 0))
                    {
                        KeyBoard.AscII = uKey;
                        KeyBoard.Chr = Convert.ToChar(uKey);//真正获取到的字符,要保存
                    }
                    else
                    {
                        notChar = true;   //转到ascii字符失败,这不是一个正常字符,要去掉
                    }
                     if (DateTime.Now.Subtract(KeyBoard.Time).TotalMilliseconds > ts)  //40ms可以过键盘输入
                    {
                        KeyBoard.IsValid = false;
                        if ((KeyBoardNum - 1) % 11 == 0)
                        {
                            KeyBoardNum = 1;
                            KeyBoard.IsValid = true;
                        }
                        if (notChar == false)
                            strKeyBoard = KeyBoard.Chr.ToString();
                        else
                            strKeyBoard = "";
                    }
                     else
                    {
                        KeyBoard.IsValid = true;
                        if (notChar == false)
                        {
                            // strKeyBoard += KeyBoard.Chr.ToString();
                        }
                        KeyBoard.KeyBoard = strKeyBoard;
                        strbarcoding = strKeyBoard;
                    }
                     KeyBoard.Time = DateTime.Now;
                    if (KeyBoardEvent != null && KeyBoard.IsValid) KeyBoardEvent(KeyBoard);    //触发KeyBoardEvent事件,将数据收集
		}
	}
	 return CallNextHookEx(hKeyboardHook, nCode, wParam, lParam);
}
        /// <summary>
        /// 继续下一个钩子
        /// </summary>
        /// <param name="idHook"></param>
        /// <param name="nCode"></param>
        /// <param name="wParam"></param>
        /// <param name="lParam"></param>
        /// <returns></returns>
        [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
        private static extern int CallNextHookEx(int idHook, int nCode, Int32 wParam, IntPtr lParam);
                /// <summary>
        /// 检取表示键名的字符串,(键盘输入后,将键盘翻译成真正字符)
        /// </summary>
        /// <param name="lParam">指定被处理的键盘消息</param>
        /// <param name="lpBuffer">指向接受键名的缓冲区的指针</param>
        /// <param name="nSize">指定键名的最大字符长度,包括空结束符</param>
        /// <returns></returns>
        [DllImport("user32", EntryPoint = "GetKeyNameText")]
        private static extern int GetKeyNameText(int lParam, StringBuilder lpBuffer, int nSize);
           /// <summary>
        /// 获取虚拟键状态,将虚拟键的状态拷贝到缓冲区 
        /// </summary>
        /// <param name="pbKeyState"></param>
        /// <returns></returns>
        [DllImport("user32", EntryPoint = "GetKeyboardState")]
        private static extern int GetKeyboardState(byte[] pbKeyState);
                /// <summary>
        /// 将虚拟码和键盘状态翻译为相应的字符和字符串
        /// </summary>
        /// <param name="VirtualKey"></param>
        /// <param name="ScanCode"></param>
        /// <param name="lpKeyState"></param>
        /// <param name="lpChar"></param>
        /// <param name="uFlags"></param>
        /// <returns></returns>
        [DllImport("user32", EntryPoint = "ToAscii")]
        private static extern bool ToAscii(int VirtualKey, int ScanCode, byte[] lpKeyState, ref uint lpChar, int uFlags);
        /// <summary>
        /// 获取一个应用程序或动态链接库的模块句柄
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        [DllImport("kernel32.dll")]
        public static extern IntPtr GetModuleHandle(string name);

KeyBoardEvent收集按键事件获取的字符,所以它的处理是需要将这些字符加起来成为一个字符串,就是我们刷卡后最终得到的卡号。

       List<string> codes = new List<string>();
        /// <summary>
        /// 刷卡后,触发该事件,codes为刷卡机数据收集器
        /// </summary>
        /// <param name="barcode"></param>
        void KeyBoardCollect(KeyBoardHook.KeyBoards barcode)
        {
            codes.Add(barcode.Chr.ToString());
        }

注册KeyBoardEvent

KeyBoardEvent += KeyBoardCollect;

3. 卸载钩子

当不再使用钩子时即可卸载钩子。

public bool Stop()
{
	if (hKeyboardHook != 0)
	{
		bool result = UnhookWindowsHookEx(hKeyboardHook);
		hKeyboardHook = 0;         //将hKeyboardHook 置为0
		return result;
	}
	else
	{
		return false;
	}
}
        /// <summary>
        /// 卸载钩子
        /// </summary>
        /// <param name="idHook"></param>
        /// <returns></returns>
        [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
        private static extern bool UnhookWindowsHookEx(int idHook);

总结

钩子是应用程序获取Windows消息的一个常用技术,在实际使用时还是要根据项目设置需要的钩子,全局、局部、监视特殊按键与否、鼠标事件等等。在本项目中的使用中,除了需要注意API函数的使用外,还需要注意如何正确获取字符并组成字符串。

  • 2
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值