最近公司买了个扫描枪,要给设备二维码做扫描录入工作。原来跟我说的是串口的扫描枪,没想到后来买到的是 usb 的,似乎是考虑到 usb 的比较普遍。果然串口还是没 usb 来得方便啊,在使用上。
以前没搞过 usb 的,一开始还以为不能工作,后来开了个记事本发现能正常录入了。。。
上网确认了一下果然是通过类似键盘事件进行输入的,那这样就上键盘钩子吧。
因为要实现无输入焦点监听任意键盘输入,所以这里使用了 WH_KEYBOARD_LL 类型的钩子。另外 MSDN 里提到可以使用 Raw Input 代替 WH_KEYBOARD_LL 这个钩子,因为 RawInput 是异步监视的,不像 LL 钩子要切换进程空间,可以获得更好的性能。
下面是处理输入字符逻辑的代码
void UsbScanner::UsbScannerImpl2::SaveChar(uint16_t vkey, uint16_t scancode, bool keydown) { auto diff = chrono::steady_clock::now() - mLastReceiveTime; if (!mDataBuffer.empty() && diff > chrono::milliseconds(mTimeoutThreshold)) { Flush(); } char name[255] = { 0 }; if (GetKeyNameTextA(scancode << 16, name, _countof(name)) == 0) return; if (vkey == VK_SPACE) name[0] = ' '; else if (vkey == VK_RETURN) name[0] = '\n'; else if (strlen(name) > 1) return; bool cap2 = !!(GetKeyState(VK_CAPITAL) & 1); bool shift2 = !!(GetKeyState(VK_LSHIFT) & 0x8000) || !!(GetKeyState(VK_RSHIFT) & 0x8000); // GetKeyNameText 返回的是大写字母 name[0] = cap2 ? toupper(name[0]) : tolower(name[0]); if (shift2) { if (islower(name[0])) name[0] -= 32; else if (isupper(name[0])) name[0] += 32; else { string l = "`1234567890-=[];',./\\"; string u = "~!@#$%^&*()_+{}:\"<>?|"; size_t pos = l.find_first_of(name[0]); if (pos != string::npos) name[0] = u[pos]; } } if (keydown) { mLastReceiveTime = chrono::steady_clock::now(); mDataBuffer.push_back(name[0]); } }
钩子的设置和钩子函数参数的意义参考 msdn。主要的处理都在这里面了,对于每个键盘输入取出按键对应的字符,并查看当前的 shift 和 capital 状态将输入进行适当的转换。如果在指定的超时时间内,则认为是同一批输入,将其存放到缓存里。除此之外还有一个线程进行定期的检查,如果超时时间内没有输入则自动将缓存的数据发送给设置的回调函数。
另外因为钩子需要消息循环的支持,所以在初始化过程中创建了一个隐藏的窗口并进行消息队列的处理。
下面是 RawInput 版的初始化代码
bool UsbScanner::UsbScannerImpl2::Start(std::function<void(const string &)> receiver, int timeoutThreshold, unsigned long tid) { if (!mStop) return false; assert(tid == 0); mTimeoutThreshold = timeoutThreshold; mReceiver = receiver; mStop = false; mReady = false; mProcess = std::thread([this, tid]() { WNDCLASSEXW wcex; wcex.cbSize = sizeof(WNDCLASSEX); wcex.style = CS_HREDRAW | CS_VREDRAW; wcex.lpfnWndProc = DefWindowProc; wcex.cbClsExtra = 0; wcex.cbWndExtra = 0; wcex.hInstance = GetModuleHandle(NULL); wcex.hIcon = NULL; wcex.hCursor = LoadCursor(nullptr, IDC_ARROW); wcex.hbrBackground = NULL; wcex.lpszMenuName = NULL; wcex.lpszClassName = Detail::WndClassName; wcex.hIconSm = NULL; RegisterClassExW(&wcex); mWnd = CreateWindowW(Detail::WndClassName, L"", WS_OVERLAPPEDWINDOW, 1, 0, 1, 1, nullptr, nullptr, GetModuleHandle(NULL), nullptr); if (mWnd != NULL) { UpdateWindow(mWnd); ShowWindow(mWnd, SW_HIDE); RAWINPUTDEVICE Rid[1]; Rid[0].usUsagePage = 0x01; Rid[0].usUsage = 0x06; Rid[0].dwFlags = RIDEV_INPUTSINK; Rid[0].hwndTarget = mWnd; if (RegisterRawInputDevices(Rid, 1, sizeof(Rid[0])) == FALSE) mStop = true; else mReady = true; } else { mStop = true; } { std::unique_lock<std::mutex> lock(mConditionLock); mCondition.notify_one(); } MSG msg; while (!mStop) { if (PeekMessage(&msg, mWnd, NULL, NULL, PM_NOREMOVE) == 0) { if (!mDataBuffer.empty() && chrono::steady_clock::now() - mLastReceiveTime > chrono::milliseconds(mTimeoutThreshold)) { Flush(); } else { this_thread::sleep_for(chrono::milliseconds(1)); } } else { GetMessage(&msg, mWnd, NULL, NULL); if (msg.message != WM_INPUT) { TranslateMessage(&msg); DispatchMessage(&msg); } else { UINT dwSize; GetRawInputData((HRAWINPUT)msg.lParam, RID_INPUT, NULL, &dwSize, sizeof(RAWINPUTHEADER)); vector<BYTE> lpb; lpb.resize(dwSize); if (GetRawInputData((HRAWINPUT)msg.lParam, RID_INPUT, lpb.data(), &dwSize, sizeof(RAWINPUTHEADER)) != dwSize) throw exception("GetRawInputData does not return correct size !\n"); RAWINPUT* raw = (RAWINPUT*)lpb.data(); if (raw->header.dwType == RIM_TYPEKEYBOARD) { SaveChar( raw->data.keyboard.VKey, raw->data.keyboard.MakeCode, raw->data.keyboard.Flags == RI_KEY_MAKE ); } } } } }); std::unique_lock<std::mutex> lock(mConditionLock); mCondition.wait(lock, [this] { return mReady || mStop; }); if (mStop) { mProcess.detach(); return false; } return true; }
除去一些线程同步的代码外,基本的流程就是创建窗口,然后调用 RegisterRawInputDevices 进行注册,随后等待 WM_INPUT 事件即可。按键的处理逻辑都是一样的。
完整代码见此,里面也包含有使用钩子版的,选择一个使用即可。