C# WPF 无焦点自动获取USB 二维码扫码枪内容,包含中文
- 前言
- 项目背景
- 需要预知的知识
- 实现方案
- 第一步 安装键盘钩子
- 第二步 获取输入的值
- 第3 步 解决中文乱码
- 问题分析
- 解决思路
- 工具函数
- 结束
前言
USB接口的扫码枪基本就相当于一个电脑外设,等同于一个快速输入键盘。现如令二维码的使用相当流行。那我们开发程序必须要跟上节奏,使用上二维码,扫码器有很多通信接口,其中USB 接口更通用。
项目背景
本人自从业以来,一直从业计量行业相关的系统开发,主要开发无人值守智能称重系统。在称重系统里主要用到的关键主流是车牌识别抽像机,红外光栅,道闸,语音等设备。称重系统通常是以前端客户端运行,有些大客户有自己的综合平台,要求称重系统能接入平台,内部的业务控制权由客户的平台系统管理。称重系统作为一个子系统,只要按平台系统的要求工作。客户要求,称重系统能够离线运行。这导致不得使用物理设备来传输转载一些必须信息,以前主要的解决方案是 IC 卡,现在有了二维码,并且二维码的信息量比 IC 卡大很多,读写方便,Ic 的读写都需要硬件投入和软件对接,使用起来不方便,不经济。因此 二维码 是最优的选择,使用手机App 或者小程序动态生成二维码,USB接口的扫码设备,即插即用,不需要依赖具体那个品牌,随时可以更换,非常方便。所有我们面对客户的新需求采用二维码的解决方案。
需要预知的知识
- 托管代码与非托管代码的交互
- Win32 APi 安装钩子来监听系统的行为(键盘输入)
- 编码的转换 (最重要,二维码中有中文内容)
实现方案
USB扫码枪为即插即用,通过类似键盘的方式和系统进行交互,扫描出来的数据获取方式有两种实现方式。
(1)文本框输入获取焦点,扫描后自动显示在文本框内。
(2)使用键盘钩子,勾取扫描枪虚拟按键,进行键盘虚拟码和ASCII码的转换后获取数据。
在无人值守的场景下,第一种方试 Pass掉,在程序进行开发时,一般使用第二种方式,有以下两个优势,当我们的程序最小化,被隐藏,失去焦点的情况下有新的扫码事件时也能够快速响应,并进行自动工作中,下面在接收USB扫码枪扫描数据方面的问题进行探讨分享。
第一步 安装键盘钩子
安装键盘钩子,需要用到 Win32 API ,代码如下 :
public const int WM_KEYDOWN = 256;//KEYDOWN 0x100
public const int WM_KEYUP = 257;//KEYUP 0x101
public const int WM_SYSKEYDOWN = 260;//SYSKEYDOWN 0x104
public const int WM_SYSKEYUP = 261;//SYSKEYUP 0x105
public const int WH_KEYBOARD = 2;
public const int WH_KEYBOARD_LL = 13;//0x00D
public delegate long KeyboardProc (int code, Int16 wParam, IntPtr lParam);
[StructLayout(LayoutKind.Sequential)]
public class KeyboardHookStruct
{
public int vkCode; //定一个虚拟键码。该代码必须有一个价值的范围1至254
public int scanCode; // 指定的硬件扫描码的关键
public int flags; // 键标志
public int time; // 指定的时间戳记的这个讯息
public int dwExtraInfo; // 指定额外信息相关的信息
}
//idHook 钩子类型,即确定钩子监听何种消息,上面的代码中设为2,即监听键盘消息并且是线程钩子,如果是全局钩子监听键盘消息应设为13,
//线程钩子监听鼠标消息设为7,全局钩子监听鼠标消息设为14。lpfn 钩子子程的地址指针。如果dwThreadId参数为0 或是一个由别的进程创建的
//线程的标识,lpfn必须指向DLL中的钩子子程。 除此以外,lpfn可以指向当前进程的一段钩子子程代码。钩子函数的入口地址,当钩子钩到任何
//消息后便调用这个函数。hInstance应用程序实例的句柄。标识包含lpfn所指的子程的DLL。如果threadId 标识当前进程创建的一个线程,而且子
//程代码位于当前进程,hInstance必须为NULL。可以很简单的设定其为本应用程序的实例句柄。threaded 与安装的钩子子程相关联的线程的标识符
//如果为0,钩子线程与所有的线程关联,即为全局钩子
//使用此功能,安装了一个钩子
[DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
public static extern int SetWindowsHookEx(int idHook, KeyboardProc lpfn, IntPtr hInstance, int threadId);
//调用此函数卸载钩子
[DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
public static extern bool UnhookWindowsHookEx(int idHook);
代码只列出部分钩子类型,详情更多 参见:
SetWindowsHookExA 函数 参数 idHook类型:
在WPF 项目中应用 代码如下
// 在窗体显示后注册 钩子
private void Window_ContentRendered(object sender, EventArgs e)
{
this.mainNotifyIcon.Visibility = Visibility.Visible;
InitTime();
//注册 钩子
RegistHook();
}
#region hook
/// <summary>
/// 安装钩子成功后的 ID
/// </summary>
private int mHookID;
private Win32Helper.KeyboardProc KeyboardHookDelegate;
private void RegistHook()
{
KeyboardHookDelegate = new Win32Helper.KeyboardProc(KeyboardProc);
IntPtr intPtr = Marshal.GetHINSTANCE(System.Reflection.Assembly.GetExecutingAssembly().GetModules()[0]);
mHookID = Win32Helper.SetWindowsHookEx(Win32Helper.WH_KEYBOARD_LL, KeyboardHookDelegate, intPtr, 0);
}
public long KeyboardProc(int nCode, Int16 wParam, IntPtr lParam)
{
// 侦听键盘事件
if (nCode >= 0)
{
if(wParam == Win32Helper.WM_KEYUP)
{
//获取键盘钩子的结构体
var mstruct = (Win32Helper.KeyboardHookStruct)Marshal.PtrToStructure(lParam, typeof(Win32Helper.KeyboardHookStruct));
// 其它处理
//notify main window
OnQrScanerInput(keyData,mstruct.time);
}
}
//如果返回1,则结束消息,这个消息到此为止,不再传递。
//如果返回0或调用CallNextHookEx函数则消息出了这个钩子继续往下传递,也就是传给消息真正的接受者
return Win32Helper.CallNextHookEx(mHookID , nCode, wParam, lParam);
}
#endregion
第二步 获取输入的值
获取键盘的ASCII 码
/// <summary>
/// keyboard input value or qrcode scanner value
/// </summary>
private string qrValue =string.Empty;
private int LastInputTime = 0;
/// <summary>
/// keyboard has input
/// </summary>
private void OnQrScanerInput(Keys key,int timestamp)
{
//time interval 20 ms
bool ishandleing = false;
int maxInputInterval = 20;
if (LastInputTime == 0) LastInputTime = timestamp;
int ms = timestamp - LastInputTime;
Console.WriteLine("ms:" + ms);
LastInputTime = timestamp;
if (ms > maxInputInterval)
{
qrValue = String.Empty;
return;
}
if (key == Keys.Back || key == Keys.Apps
|| key == Keys.Cancel|| key == Keys.Tab || key == Keys.LShiftKey )
{
return;
}
if(key == Keys.Decimal || key == Keys.OemPeriod)
{
qrValue += ".";
}
else if (key == Keys.Oem5)
{
qrValue += "|";
}
else if (key == Keys.OemMinus)
{
qrValue += "_";
}
else if (key == Keys.Enter)
{
// not do nothing
}
else
{
//Console.WriteLine(key + ":" + (int)key);
var temp = Convert.ToChar((int)key).ToString();
qrValue += temp;
}
}
上述代码中 var temp = Convert.ToChar((int)key).ToString(); qrValue += temp; 最终得到输入的内容,同时也处理了一些特殊按键,如下
- key == Keys.Back || key == Keys.Apps || key == Keys.Cancel|| key == Keys.Tab || key == Keys.LShiftKey ,不需要拼接,因为真实使用不会有这些
- Keys.Oem5 “|”
- Keys.OemMinus - 或’’ ,"" 要判断上一个输入的键 是否为 左右 Shift
- key == Keys.Decimal || key == Keys.OemPeriod 小数点
结果
在上面的载图中我们能看到 有乱码,下面我们就来解决它
第3 步 解决中文乱码
问题分析
- 程序够获取 输入的键 是否铵 左右Shift,当无法适应输入法去转化,所以处理中是个很麻烦的事情。
- 二维码内容格式 单号|车牌号|类型|重量
- 二维码内容值例:XS_YL_20230816011232001|云DDD732|2|12.32
要生成二维码的内容 :XS_YL_20230816011232001|云DDD732|2|12.32
云DDD732 为车牌号,是一定有存在中文的。
解决思路
解决思路: 就是把车牌号 这个段的内容进行编号的转换
字符串 -> bytes 数组 -> 转换拼接成HEX 进制的字符串 -> 生成 二维码
![转换后的值](https://img-blog.csdnimg.cn/15bcbf88b95741dc8cd95ae2e8a73e84.png#pic_center)这样是不是就不需要处理 中文 拉。
解码路径
hex 字符串-> bytes 数组 -> 字符串
var data = qrContent.Split('|');
if (data.Length < 4)
{
Growl.Info("无效的二维码: "+data.Length);
return;
}
// 转换 车牌号
currCarNumber = ICReaderHelper.StringToStr(data[1].Replace(" ",""));
if (!string.IsNullOrEmpty(currCarNumber))
{
var msg = "已识别,请稍候。";
CommonFunction.SpeakAsync(msg, true);
}
工具函数
一 、十六进制代表的字符串转换成 普通字符串,程序可识别的
/// <summary>
/// 返回十六进制代表的字符串 空格有和没都 可以
/// </summary>
/// <param name="hexStr"></param>
/// <returns></returns>
public static string StringToStr(string hexStr)
{
if (string.IsNullOrEmpty(hexStr))
{
return "";
}
if (hexStr.Contains(" "))
{
hexStr = hexStr.Replace(" ", "");
}
if (hexStr.Length <= 0) return "";
byte[] vBytes = new byte[hexStr.Length / 2];
for (int i = 0; i < hexStr.Length; i += 2)
if (!byte.TryParse(hexStr.Substring(i, 2), NumberStyles.HexNumber, null, out vBytes[i / 2]))
{
vBytes[i / 2] = 0;
}
return Encoding.GetEncoding("GB2312").GetString(vBytes);
}
二 、将字符转化成十六进制的字符串,
/// <summary>
/// 将字符转化成十六进制的字符串
/// </summary>
/// <param name="strValue"></param>
/// <param name="hasSpace">是否含有空格</param>
/// <returns></returns>
public static string StringToHex(string strValue, bool hasSpace = true)
{
return BitConverter.ToString(ASCIIEncoding.GetEncoding("GB2312").GetBytes(strValue)).Replace("-", hasSpace == true ? " " : "");
}
- 我采用的编码规范 GB2312 ,只要编号和解码使用一个规范就行
- 代码并不全面,关键代码已经在文章中,其它代码不影响本文要讨论的问题
结束
非常感谢您的耐心阅读,不足之处,欢迎批评指出。