问题背景

某检测场有一批扫码器,购于微光互联,型号 TX800-U,用于在不同办理窗口间扫描纸质材料上的二维码,简化录入过程。扫码器通过 USB 接入 PC 系统 (windows),自动安装驱动,接电即可使用,扫描的信息会直接输出在光标所在位置,扫码器大概长这样:

问题现象

在一次 IT 系统升级后,发现它们扫描不了车辆外观检验单上的车牌二维码了,扫车架号二维码是没问题的,两者的区别就是是否带汉字,车牌第一个字符为地区标识,例如“京”,而车架号全部由数字和大写字母组成。

拿到设备后,第一时间验证了上面的问题,扫码后都有滴的一声,但是车牌号没有任何信息上屏。为了验证这个问题确实和汉字有关,找到一个制造二维码的网站 (草料二维码),造了几个不同的二维码供扫码器扫描,发现带汉字的果然扫不出来,而只要去掉汉字,就能正确上屏。

同样的二维码,通过微信扫一扫是可以得到汉字结果的,另外升级 IT 系统前扫码器也是正常的,所以初步判断这个问题和汉字编码相关,可能是升级系统后修改了默认字符编码集导致扫码器出问题了。

问题的解决

联系了厂家的售后,拉了个微信群,开发人员说这是已知问题,要想解决需要二次开发。

二次开发不就是调用 sdk 接口吗,这个我熟啊。从官网找到对应产品和型号:

下载了 windows 上的 C/C++ 二次开发 sdk:

另外发现一个配置工具,感觉蛮有用,一起搞下来:

话说这公司够实诚,设计图纸都开源了

开发者模式

撸了一遍文档,大概明白了,这个扫码器默认工作在普通模式,这种模式下会将扫到字符直接输出到系统光标位置;如果想要二次开发,需要先将扫码器设置为开发者模式,在这种模式下,扫到的信息不会输出到光标,而是借由 sdk 接口返回给调用者,在这里就可以对数据进行任意加工了。来看看如何配置开发者模式,共有两种方式

  • 通过配置工具 VguangConfig
  • 直接扫描文档中的二维码

其实第一步最终也是生成一个二维码,殊途同归,不过可以选择的设置项更丰富一些,先来看看这种方式吧

VguangConfig

打开后的界面是这样,当扫码器处于普通模式时会自动识别并连接设备:

如果已经处于开发者模式,则无法自动连接,这里直接点“下一页”

工作方式选择“开发”后点“下一页”:

这里有一些高级设置,在当前场景下主要关注扫码设置这页,里面有诸如码制、前后缀、添加回车换行符、扫码间隔时间、扫码后动作、背光灯开关等,一般选择默认即可。配置好后点右侧的“生成配置码”,得到一张二维码配置图:

扫码器扫这个码后再重新加电就可以按新模式工作了。同理可以设置扫码器按普通模式工作:

文档中的配置二维码

上面那个配置工具的优点是灵活,缺点是只支持 windows 平台,如果没有 windows 机器,可以直接使用开发文档中几个预定义的配置二维码:

这个文档位于 C/CPP 开发包解压后的如下路径:“USB接口C-CPP语言SDK20220411\USB接口C&CPP语言SDK20220411\扫码器C&CPP简易开发指南v2.1.pdf”,其它开发包是否有这个文件没有验证过。

对比两组图,生成的二维码和文档中的几乎一样,看起来后者也像通过工具生成的。

Demohidprotocal

进入开发模式后再扫码就只是滴滴叫不上屏了,此时需要使用 sdk 写一个程序来获取扫码器的输出,在 C/CPP 开发包有中一个现成的 demo:USB接口C-CPP语言SDK20220411\USB接口C&CPP语言SDK20220411\Demohidprotocal\Release\Demohidprotocal.exe,这是 release 版本,选择 debug 版本也行,启动后界面如下:

表示连接扫码器成功,分别扫描车架号和车牌号:

vbar_open success!
开始解码:
二维码长度:18
LFV3A23C083027701
二维码长度:10
浜琈D0926

车架号是正常的,而车牌号果然是乱码。

找到 Demohidprotocol 源码 (USB接口C-CPP语言SDK20220411\USB接口C&CPP语言SDK20220411\Demohidprotocal\Demohidprotocal\main.cpp):

#include "channel.h"
#include <stdio.h>
#include <windows.h>
#include <string.h> struct vbar_channel *dev; /*背光灯开关控制 state为1时打开补光灯,为0时关闭补光灯*/
void lightswitch(int state)
{ unsigned char buf[1025] = { 0 };
if (state == 1)
{
buf[0] = 0x55;
buf[1] = 0xAA;
buf[2] = 0x24;
buf[3] = 0x01;
buf[4] = 0x00;
buf[5] = 0x01;
buf[6] = 0xDB;
vbar_channel_send(dev, buf, 1024);
}
else
{
buf[0] = 0x55;
buf[1] = 0xAA;
buf[2] = 0x24;
buf[3] = 0x01;
buf[4] = 0x00;
buf[5] = 0x00;
buf[6] = 0xDA;
vbar_channel_send(dev, buf, 1024);
}
}
/*扫码开关控制 state为1时打开扫码,为0时关闭扫码*/
void scanswitch(int state)
{ unsigned char buf[1025] = {0};
if (state == 1)
{
buf[0] = 0x55;
buf[1] = 0xAA;
buf[2] = 0x05;
buf[3] = 0x01;
buf[4] = 0x00;
buf[5] = 0x00;
buf[6] = 0xfb;
vbar_channel_send(dev, buf, 1024);
}
else
{
buf[0] = 0x55;
buf[1] = 0xAA;
buf[2] = 0x05;
buf[3] = 0x01;
buf[4] = 0x00;
buf[5] = 0x01;
buf[6] = 0xfa;
vbar_channel_send(dev, buf, 1024);
}
}
int main() {
dev = vbar_channel_open(1, 1);
if (!dev) {
printf("open dev fail!\n");
return -1;
}
else
{
printf("open dev success!\n");
}
printf("开始解码:\r\n"); scanswitch(1);
//接收扫码
unsigned char bufresult[1024] = {0};
unsigned char bufferrecv_1[1024] = {0};
unsigned char readBuffers[2048] = {0};
while (1)
{
if (vbar_channel_recv(dev, bufresult, 1024, 200) > 0)
{
if (bufresult[0] == 0x55 && bufresult[1] == 0xAA && bufresult[2] == 0x30)
{
int datalen = bufresult[4] + (bufresult[5] << 8); if (datalen <= 1017)
{
for (int s1 = 0; s1 < datalen; s1++)
{
readBuffers[s1] = bufresult[6 + s1];
} }
if (1017 < datalen && datalen <= 2041)
{
for (int s1 = 0; s1 < 1018; s1++)
{
readBuffers[s1] = bufresult[6 + s1];
}
vbar_channel_recv(dev, bufferrecv_1, 1024, 200);
for (int s2 = 0; s2 < datalen + 7 - 1025; s2++)
{
readBuffers[s2 + 1018] = bufferrecv_1[s2];
}
} printf("二维码长度:%d\n", datalen);
readBuffers[datalen + 7] = '\0';
printf("%.*s\n", datalen, readBuffers); }
}
}
}

谜之编码风格,另外这接口设计的也有点凌乱,程序中出现了好多魔数:1017/1018/2041/200/7,看着头大。所幸读取的数据位于 readBuffers 缓冲中,只要对它做个编码转换就 OK 啦。

编码转换

windows 中文版编码一般是 gb2312,汉字源编码则可能是 utf-8,为了验证这一点,搬出来了 iconv:

$ echo "浜琈D0926" | iconv -f 'utf-8' -t 'cp936'
京MD0926

看来确实如此,注意这里使用 cp936 而不是  gb2312 作为 iconv 的第二个参数。如果没有 iconv,也有许多线上的编码转换工具可用:

确定了字符集转换方向,直接从网上搜罗来一些现成的实现:

std::wstring utf8_to_unicode(std::string const& utf8)
{
int need = MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, NULL, 0);
if (need > 0)
{
std::wstring unicode;
unicode.resize(need);
MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, &unicode[0], need);
return unicode;
} return std::wstring();
} std::string unicode_to_gb2312(std::wstring const& unicode)
{
int need = WideCharToMultiByte(936, 0, unicode.c_str(), -1, NULL, 0, NULL, NULL);
if (need > 0)
{
std::string gb2312;
gb2312.resize(need);
WideCharToMultiByte(936, 0, unicode.c_str(), -1, &gb2312[0], need, 0, 0);
return gb2312;
} return std::string();
} std::string utf8_to_gb2312(std::string const& utf8)
{
std::wstring unicode = utf8_to_unicode(utf8);
return unicode_to_gb2312(unicode);
}

windows 上编码转换都是先转到 unicode,再转其它编码,比较好理解。那么 demo 中的输出就可以改为:

std::string gb2312 = utf8_to_gb2312(std::string((char *)readBuffers, datalen));
printf("%.*s\n", gb2312.lenght(), gb2312.c_str());

再次运行:

二维码长度:10
京MD0926

恢复正常!

输出到剪贴板

上面的过程虽然能正确解析 utf-8 数据了,但还需要用户复制 console 输出的结果,很不方便,如果能将结果直接输出到剪贴板上岂不是很爽?说干就干:

void copy_to_system_clipboard(std::string const& data)
{
printf("ready to copy data: %s\n", data.c_str());
BOOL ret = OpenClipboard(NULL);
if (!ret)
{
printf("open clipboard failed\n");
return;
} do
{
ret = EmptyClipboard();
if (!ret)
{
printf("empty clipboard failed\n");
break;
} HGLOBAL hdata = GlobalAlloc(GMEM_MOVEABLE, data.length() + 1);
if (hdata == NULL)
{
printf("alloc data for clipboard failed");
break;
} char* str = (char *) GlobalLock(hdata);
memcpy(str, data.c_str(), data.length());
str[data.length()] = 0;
GlobalUnlock(hdata); // HANDLE h = SetClipboardData(CF_UNICODETEXT, hdata);
HANDLE h = SetClipboardData(CF_TEXT, hdata);
if (!h)
{
printf("set clipboard data failed");
break;
} printf("copy to clipboard ok\n");
} while (0);
CloseClipboard();
}

基本上是抄了网上一个例子实现的,只是增加了一些错误提示。调用点稍微改造就大功告成:

printf("%.*s\n", datalen, readBuffers);
std::string gb2312 = utf8_to_gb2312(std::string((char *)readBuffers, datalen));
copy_to_system_clipboard(gb2312);

再次运行:

二维码长度:10
浜琈D0926
ready to copy data: 京MD0926
copy to clipboard ok

此时在任一文本框中按 Ctrl+V,均能得到号牌数据。

这里请注意 copy_to_system_clipboard 中的 SetClipboardData 调用,使用 CF_TEXT 而不是 CF_UNICODETEXT,否则会得到下面的乱码输出:

ꦾ䑍㤰㘲

另外测试中发现可以同时启动多个 demo,相互之间不冲突,均能从接口拿到扫描后的数据,神奇。

输出到当前光标

上面的解决方案已经很好了,但是如果能像之前一样输出到光标就更棒了,用户可以无疑切换。作为资深 MFCer,立刻想到了一种解决方案:查找当前桌面前台 (Foreground) 窗口,找到它的活动子窗口并投递 WM_SETTEXT 消息。下面是参考网上一个例子的实现:

void set_text_to_active_windows(std::string const& data)
{
int ret = 0;
std::wstring unicode;
HWND wnd = GetForegroundWindow();
//HWND wnd = GetActiveWindow();
//HWND wnd = GetDesktopWindow();
if (wnd == NULL)
{
printf("no active windows\n");
return;
} printf("get active window\n");
DWORD SelfThreadId = GetCurrentThreadId();
DWORD ForeThreadId = GetWindowThreadProcessId(wnd, NULL);
if (!AttachThreadInput(ForeThreadId, SelfThreadId, true))
{
printf("attach thread input failed\n");
return;
} printf("attach thread input\n");
//wnd = GetFocus();
wnd = GetActiveWindow();
if (wnd == NULL)
{
printf("no focus windows\n");
return;
} printf("get focus window\n");
AttachThreadInput(ForeThreadId, SelfThreadId, false);
unicode = gb2312_to_unicode(data);
ret = SendMessage(wnd, WM_SETTEXT, 0, (LPARAM)unicode.c_str());
printf("send text to active window return %d: %s\n", ret, data.c_str());
}

调用点仅需稍加改造就可以了:

printf("%.*s\n", datalen, readBuffers);
std::string gb2312 = utf8_to_gb2312(std::string((char *)readBuffers, datalen));
copy_to_system_clipboard(gb2312);
set_text_to_active_windows(gb2312);

编译运行,先启动一个 notepad 应用,将光标置于其中,便于稍后看输出结果,然而扫码后没有任何输出。将上面的 GetForegroundWindow 替换为 GetActiveWindow 或 GetDesktopWindows 都没有效果,更神奇的是加的许多 printf 调试日志也没有输出,这真是见了鬼了:

open dev success!
开始解码:
二维码长度:10
浜琈D0926
ready to copy data: 京MD0926
copy to clipboard ok
send text to active window return 0: 京MD0926
二维码长度:18
LFV3A23C083027701
ready to copy data: LFV3A23C083027701
copy to clipboard ok
send text to active window return 0: LFV3A23C083027701

只输出最终的一个调用结果。一开始怀疑是 console 程序和 win32 界面程序的不同,决定新建一个新的 win32 应用试试,由于 Win32 应用的主线程要做消息循环,这里启动一个单独的线程跑扫码的逻辑:

int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine); // TODO: Place code here. // Initialize global strings
LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
LoadStringW(hInstance, IDC_DEMOHIDPROTOCAL, szWindowClass, MAX_LOADSTRING);
MyRegisterClass(hInstance); // Perform application initialization:
if (!InitInstance (hInstance, nCmdShow))
{
return FALSE;
} hThread = CreateThread(NULL, 0, qrscanner_loop, NULL, 0, NULL);
HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_DEMOHIDPROTOCAL)); MSG msg; // Main message loop:
while (GetMessage(&msg, nullptr, 0, 0))
{
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
} return (int) msg.wParam;
}

qrscanner_loop 就是之前 console main 那一堆东西了,为了展示信息,在默认的视图中间填充一个 edit 控件:

BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
hInst = hInstance; // Store instance handle in our global variable HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr); if (!hWnd)
{
return FALSE;
} RECT rect = { 0 };
GetClientRect(hWnd, &rect);
hEdit = CreateWindowW(TEXT("Edit"), TEXT(""),
WS_CHILD | WS_VISIBLE | ES_LEFT | ES_MULTILINE | ES_WANTRETURN | WS_VSCROLL | ES_AUTOVSCROLL,
rect.left, rect.top, rect.right-rect.left, rect.bottom-rect.top, hWnd, (HMENU)10002, hInstance, NULL);
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd); return TRUE;
}

至于编辑框随视图大小变化而变化这种基本功,就不赘述了,后面会放出完整源码。注意这里的 hEdit,它存储着编辑框的句柄,后面会用到。

console 改 win32 最大的变化是 printf 日志输出没有了,为了解决这个问题,改写 printf 为 my_printf,在里面做些文章:

extern HWND hEdit;
void my_printf(char const* format, ...)
{
char line[4096] = { 0 };
va_list vp;
va_start(vp, format);
vsprintf(line, format, vp);
va_end(vp); // replace '\n' to '\r\n'
if (strlen(line) > 0)
line[strlen(line) - 1] = '\r';
strcat(line, "\n"); //std::wstring data = gb2312_to_unicode(line);
// SendMessage(hEdit, WM_SETTEXT, 0, (WPARAM)data.c_str());
SendMessage(hEdit, EM_SETSEL, -2, -1);
SendMessageA(hEdit, EM_REPLACESEL, true, (long)line);
SendMessage(hEdit, WM_VSCROLL, SB_BOTTOM, 0);
OutputDebugStringA(line);
}

基本就是将日志发往刚才的 hEdit,注意这里不使用 WM_SETTEXT 以免冲掉历史消息,最后上张效果图:

再看下新版 set_text_to_active_windows 的实现:

void set_text_to_active_windows(std::string const& data)
{
int ret = 0;
wchar_t const* str;
HWND wnd = GetForegroundWindow();
//HWND wnd = GetActiveWindow();
//HWND wnd = GetDesktopWindow();
if (wnd == NULL)
{
my_printf("no active windows\n");
return;
} my_printf("get active window\n");
DWORD SelfThreadId = GetCurrentThreadId();
DWORD ForeThreadId = GetWindowThreadProcessId(wnd, NULL);
if (!AttachThreadInput(ForeThreadId, SelfThreadId, true))
{
my_printf("attach thread input failed\n");
return;
} my_printf("attach thread input\n");
//wnd = GetFocus();
wnd = GetActiveWindow();
if (wnd == NULL)
{
my_printf("no focus windows\n");
return;
} my_printf("get focus window\n");
AttachThreadInput(ForeThreadId, SelfThreadId, false);
//std::wstring unicode = gb2312_to_unicode(data);
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> converter;
std::wstring tst = converter.from_bytes(data);
str = tst.data();
//ret = SendMessageA(wnd, WM_SETTEXT, 0, (LPARAM)data.c_str());
for (int i=0; str[i] != '\n'; ++i)
{
ret = SendMessage(hEdit, WM_CHAR, str[i], 0);
} my_printf("send text to active window return %d: %s\n", ret, data.c_str());
}

与之前版本相比,除了 printf 变为 my_printf,最大的变化是在结尾部分:使用 WM_CHAR 消息代替 WM_SETTEXT。这样做是为了更好的模拟光标行为,毕竟不能假设用户光标一定位于 windows edit 控件上,有可能位于绘制界面框架 (Qt) 或描述界面框架 (Electron) 生成的 App 的控件上,这个消息可以实现字符被一个个输入编辑框的效果,兼容上述所有控件。

满怀期待的启动应用后,出现和 console 程序一样的行为——光标下没有任何输出,且不打印任何调试日志,遇到中文字符还会崩溃:

看崩溃点没什么头绪,表现还不如 console 呢,这下把我整不会了,最终这个方案宣告失败。不过留着还是有意义的,万一有人基于它实现了光标输出呢…

结语

本文尝试解决扫码器在遇到中文时不输出字符的问题,总体上解决了这个问题,优雅的解决方案因技术问题没有实现,不优雅的解决方案针对检测场的需求来说也够用了。

最早想的技术方案其实是不想动 demo 程序的,当时想通过在外面包一层 shell 脚本来解决,熟悉的读者知道我喜欢用这种方式解决一些问题,当时主要有两个原因导致想这样干:

  • 家里的 windows 笔记本没装 VS,安装 VS2015 一来比较慢,二来拖累机器运行速度,不想装
  • demo 程序已经比较完整,只缺一个编码转换的工作,而用脚本调用 iconv 一行就能搞定,何必费力写 c++ 呢?

后面亲自试过后,发现有两个问题 shell 脚本无法绕开:

  • demo 的输出在经过 msys2 处理后,无法正确断行,导致无法从输出信息中提取扫码器读取的数据,对于这个问题

    • 开始怀疑是管道重定向后 stdout 不再是行缓冲的,而在 shell 层面无法改变一个程序的 stdout 缓冲类型
    • 后来修改 demo 源码,增加 setvbuf 调用 (参考《[apue] 标准 I/O 库那些事儿》中缓冲一节),重新编译但不起作用
    • 最终定性是 msys2 与 demo 之间的兼容性问题,不好搞,放弃
  • 想要将数据复制到系统剪贴板,可以直接在 msys2 中使用 windows 的 clip 命令接收要放置的数据 (echo "${data}" | clip),但是如果想将数据输出到光标,对不起办不到。这个必需用 c++ 进行系统开发 (后来也没走通,不过这是后话)

最终是将公司的 windows 本带回来专门搞这个事情,那个开发环境配置的比较全面,不用浪费时间再配了。说到这里,突然想到为何没有人搞在线的 VS 开发环境?linux 上的 gcc 这种环境一搜一大把,提交个文件或直接在 web 界面里写 c++ 代码,就能编译出可执行文件,而免费的 VS 线上开发环境却几乎没有!如果有人搞个 VS 的在线编译环境,肯定能火,哪怕编译一次收个十元二十元的,我估计也有人用。

上面说了一些解决过程中的探索,下面谈谈这个扫码器的问题,如果它能将编码转换功能集成在硬件里,通过配置来决定如何进行编码转换,那么这个场景就不需要二次开发 sdk 了!只要运行下 VguangConfig 并做一些勾选工作就可以了,如果再将常用的几种编码转换做成二维码配置放在文档中,直接扫对应的码就搞定了!后续给厂家反馈时,厂家表示可以考虑,其实就是增加一个 iconv.dll 的事儿,不难!

最后说一下系统升级导致扫码器不能用的问题,这就是典型的没做系统集成测试案例啊!新系统没有兼容老系统的一些隐性规则,导致下游出问题,其实完全可以让升级系统的软件厂商改进一下它这个二维码的生成方式,是用 utf8 还是 gb2312,搞成可配置的,操作人员通过配置来保持以前的编码方式不变,这个问题也能得到解决。

下载

扫码器 sdk 官网就可以下载,两个应用的源码及可执行文件链接如下:

Demohidprotocol console 版

Demohidprotocol win32 版

console 版可以直接用,win32 版还是个半成品,感兴趣的读者可以尝试探索一下。

console 版的可执行文件为 debug 版本,release 版本不知为何编译报错:

1>------ Build started: Project: Demohidprotocal, Configuration: Release Win32 ------
1>main.obj : error LNK2001: unresolved external symbol __imp__SetClipboardData@8
1>main.obj : error LNK2001: unresolved external symbol __imp__EmptyClipboard@0
1>main.obj : error LNK2001: unresolved external symbol __imp__CloseClipboard@0
1>main.obj : error LNK2001: unresolved external symbol __imp__OpenClipboard@4
1>D:\BaiduNetdiskDownload\USB接口C&CPP语言SDK20220411\Demohidprotocal\Release\Demohidprotocal.exe : fatal error LNK1120: 4 unresolved externals
========== Build: 0 succeeded, 1 failed, 0 up-to-date, 0 skipped ==========

看起来和操作系统剪贴板相关,因时间关系没有进一步去研究。

参考

[1]. 草料二维码

[2]. 微光互联

[3]. Windows下的字符集转换(ASCII、UICODE、UTF8、GB2312和BIG5互转)

[4]. 编码转换

[5]. 剪贴板操作

[6]. Windows/Mac/Linux/ssh将shell内容输出到剪贴板

微光互联 TX800-U 扫码器无法输出中文到光标的问题的更多相关文章

  1. Android 连接蓝牙扫码器 无输入框

    Android 的APP 需要集成一个蓝牙扫码器, 特别的是,需要扫码的地方是没有输入框的(EditText),不能通过直觉上理解的通过对EditText输入事件进行监听处理,取得扫码结果.并且设备也 ...

  2. 安卓扫码:简单的ZXing使用记录

    ZXing是Google提供的条形码.二维码等的生成.解析的库.最近工作需求去研究了一下,主要是研究怎么扫描二维码(QRCode).网上教程也不少,但大多看了不明所以,甚至看了半天都不知道解码到底从哪 ...

  3. FNScanner二维码接口openView自定义扫码Demo

    本文出自APICloud官方论坛 FNScanner 模块是一个二维码/条形码扫描器,是 scanner 模块的优化升级版.在 iOS 平台上本模块底层集成了 Zbar 和系统自带的条形码/二维码分析 ...

  4. UNIAPP实现PDA扫码

    目前我接触到了两种方法,以扫码pda安卓采集器(可以直接理解为手机上有个激光扫码)的设置划分. 1.扫描设置 --> 键盘方式输出(键盘类型:物理键盘),注意设置要看具体的型号: 2.扫码设置 ...

  5. 快开宝PDA开单器出入库扫码:让批发零售变得更简单

    快开宝PDA开单器出现前 批发商户是这样开单和管理的 ★员工痛苦:需要记客户.价格.库存等等,应对报错价.错漏单.盘错货等各种状况. ★老板麻烦:每天要守店.对单.核账,经常因错漏单.库存乱.积压货. ...

  6. 分享一下,PHP实现第四方QQ微信扫码登陆,不接入qq互联以及微信开发者平台就可以实现用户对接鹅厂,phpQQ微信扫码登陆

    自己抓的QQ包以及整合了网上一些已经封装好了的代码具体如下:QQ: <?php class QQ extends Curl_Api { //获取登录验证码 public function QRc ...

  7. Android扫码二维码、美女瀑布流、知乎网易音乐、动画源码等

    Android精选源码 QRCode 扫描二维码.扫描条形码.相册获取图片后识别.生...   一个简洁好看的loading弹窗   Android用瀑布流展示美女图片源码   Android知乎阅读 ...

  8. Channels实现扫码登录

    参考了下别人说的原理,根据自己的逻辑实现,没有完全按照别人的原理来,所以不一定适用于大家,仅供参考吧!!! 流程如下: 1. web发起websocket链接,后端接受链接后立马发送第一次消息,为全局 ...

  9. ThinkPHP5微信扫码支付

    1.把微信官网下载的demo放在根目录/vendor/目录下,这里我的是/vendor/wxpay_pc目录 2.把cert里面的文件替换成自己项目的证书(登陆微信商户平台,账户中心,API安全下载) ...

随机推荐

  1. C#中引用类型的变量做为参数在方法调用时加不加 ref 关键字的不同之处

    ​ 一直以为对于引用类型做为参数在方法调用时加不加 ref 关键字是没有区别的.但是今天一调试踪了一下变量内存情况才发现大有不同. 直接上代码,结论是:以下代码是使用了 ref 关键字的版本.它输出1 ...

  2. DBPack SQL Tracing 功能及数据加密功能详解

    上周,我们正式发布了 DBPack SQL Tracing 功能和数据加密功能,现对这两个功能做如下说明. SQL Tracing 通过 DBPack 代理开启的全局事务,会自动在 http head ...

  3. Windows Embedded CE 6.0开发环境的搭建(2)

    最近开始在学习嵌入式,在这里首先得安装Windows Embedded CE 6.0,其中遇到了很多问题,电脑的系统以及相关配置都会在安装过程中受到影响,因此笔者就安装中的问题以及环境搭建来介绍一下. ...

  4. BMP位图之1位位图(一)

    起始结构 typedef struct tagBITMAPFILEHEADER { WORD bfType; //类型名,字符串"BM", DWORD bfSize; //文件大小 ...

  5. Apache DolphinScheduler 1.2.0 task 任务存储结构说明

    本文章经授权转载 Table of Contents 任务总体存储 Shell节点 SQL节点 存储过程节点 SPARK节点 MapReduce(MR)节点 Python节点 Flink节点 HTTP ...

  6. HMS Core Discovery第17期直播预告|音随我动,秒变音色造型师

    [导读] 随着音视频内容品类的不断丰富及音乐创作门槛不断降低,大量用户正热切的参与到全民创作的大潮中.我们应该怎么去拥抱移动端影音潜力市场?音频编辑又可以有什么新玩法? 本期直播<音随我动,秒变 ...

  7. 【IDEA】IDEA打开欢迎页面

    概述 IDEA在默认情况下,会进入最后一个项目.如果项目比较大的话会加载的比较久,这个就比较烦人了,目前我觉得最好的办法就是在设置中直接进入欢迎页面. 解决方案 ① 进入设置 ② Appearance ...

  8. Html飞机大战(九): 使徒来袭 (设计敌机)

    好家伙,本篇介绍敌机 好了,按照惯例我们来理一下思路:   我们有一个敌机类,第一步当然是实例一个敌机对象, 然后我们把这个敌机放入我们的敌机群(敌机数组) 然后是熟悉的移动和绘制   那我们回顾一下 ...

  9. lombok Builder注解

    使用了@Builder 将会失去无参构造函数 可以通过 @Tolerate 来添加构造函数

  10. 当web项目没有配置<welcome-file>index_1.jsp</welcome-file>默认标签启动tomcat后默认访问的页面是什么呢?

    当web项目没有配置index_1.jsp默认标签启动tomcat后默认访问的页面是什么呢? 结果我启动后居然默认打开了index.jsp页面 为什么呢?为什么会访问我的.indexjsp页面呢?不是 ...