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 0x100public const int WM_KEYUP = 257;//KEYUP 0x101public const int WM_SYSKEYDOWN = 260;//SYSKEYDOWN 0x104public const int WM_SYSKEYUP = 261;//SYSKEYUP 0x105public const int WH_KEYBOARD = 2; public const int WH_KEYBOARD_LL = 13;//0x00Dpublic delegate long KeyboardProc (int code, Int16 wParam, IntPtr lParam);[StructLayout(LayoutKind.Sequential)]public class KeyboardHookStruct{public int vkCode; //定一個虛擬鍵碼。該代碼必須有一個價值的范圍1至254public 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 msbool 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 進制的字符串 -> 生成 二維碼
這樣是不是就不需要處理 中文 拉。
解碼路徑
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 ,只要編號和解碼使用一個規范就行
- 代碼并不全面,關鍵代碼已經在文章中,其它代碼不影響本文要討論的問題
結束
非常感謝您的耐心閱讀,不足之處,歡迎批評指出。