
简介这是一份面向 VB.NET 初学者的条形码电子扫描枪客户端源程序主要实现扫码枪接入后的条码读取与信息显示可扫描单号、所属客户、机器型号、条码号、扫描时间、备注等字段适合需要快速掌握扫码设备数据接收与 WinForm 界面开发的读者参考。资源共包含 24 个文件以 11 个 VB 源文件为主辅以 4 个 RESX 界面资源文件、1 个 VB 项目文件、1 个解决方案文件及数据库、DLL 等整体压缩包约 195KB结构紧凑便于直接查看代码逻辑与窗体设计。目前已有 418 人学习浏览适用于课设或小型仓库管理场景二次开发。通过阅读源码可了解扫描事件触发、信息显示与多窗体组织方式配合项目中的 Access 数据库.mdb可进一步理解数据存储场景整体对入门者比较友好。1. 项目由来与整体定位第一次接到“VB.NET 条形码电子扫描枪源程序”这个需求时我心里其实有点嘀咕扫码枪说白了不就是一个“会发声的键盘”吗往文本框里输入一串数字而已能有多难等真正把硬件拿到手、接进仓储系统以后才发现从串口协议、USB 模式切换到条码校验位计算再到数据进数据库每一步都有细节能卡住你半天。这篇文章就把我自己的完整实战方案拆开讲清楚包含串口与 USB 两种接入方式的完整代码、条码格式校验、防重扫设计和常见故障排查适合刚接手扫码枪项目的 VB.NET 开发者也适合做 WMS、MES、进销存系统时需要在客户端集成扫描功能的朋友参考。这个源程序不是一个只能扫完往文本框塞字符的 Demo而是可以继续扩展成真实业务模块的底子。整个项目围绕一个核心原则扫描枪只管“把条码变成字符串”业务逻辑和界面更新都不允许阻塞扫码动作。这样一来哪怕仓库工人连续快速扫几十个包裹程序也不会卡、不会丢数据、不会弹窗体假死。后文的内容基本按“方案选型 - 核心代码 - 业务对接 - 问题排查”的顺序展开读者可以按需跳读但建议至少把第 3 节的源码看一遍因为很多坑不在“怎么写”而在“为什么这么写”。1.1 为什么拿 VB.NET 写扫码枪程序选择 VB.NET 不是因为它最新潮而是因为它在 Windows 生态下的桌面客户端场景里足够稳。扫码枪程序往往要跑在仓库、车间、门店的 Windows 工控机上这类机器配置一般也不会装什么新运行时。VB.NET 配合 .NET Framework 4.6.2 或 4.8一台装了 Win7 的老工控机都能直接运行部署成本极低。另一个重要因素是开发效率尤其在做 WinForms 界面时拖控件、绑定事件、调试数据源VB.NET 的体验依然很顺手。很多人担心 VB.NET 已经过气但事实上大量企业内部系统仍然跑着 VB.NET 写的客户端特别是针对扫描枪这种外设的接入.NET 的 SerialPort 类、ProcessCmdKey 拦截、WinForms 消息循环机制都已经非常成熟网上资料也多踩坑时基本都能找到答案。选用 VB.NET 写这个系统就是把复杂留给了框架把稳定留给现场。1.2 项目适合谁能解决什么问题如果你属于以下三类人这个源程序可以直接拿走改接私活或做外包的 VB 程序员需要在进销存、固定资产盘点、图书管理项目中加入扫码功能。企业内部 IT 人员需要用简单可靠的客户端把扫码枪数据写入 SQL Server 或 MySQL。刚入门 .NET 桌面开发的学生想搞明白“硬件输入当成文本输入”到底是怎么实现的。它能解决的核心问题有三个一是扫码枪数据如何被程序稳定完整地接收二是接收到的条码如何清洗校验避免脏数据入库三是多个窗体、多台设备同时扫码时如何不冲突。把这三个问题处理完你会发现后续加打印机、加语音播报、加自动填充表单都是顺水推舟的事。1.3 项目整体功能清单我把最终交付的源程序功能列在下面这样可以一上来就知道自己需要哪些模块功能模块说明串口扫码接收通过 SerialPort 读取 RS232 扫码枪数据USB 扫码接收通过键盘钩子或 ProcessCmdKey 捕获 HID 键盘模式数据条码清洗去掉回车换行、空字符、FNC1 分隔符统一大小写校验位验证支持 EAN-13、UPC-A 等常见校验规则数据入库参数化写入 SQL Server支持批量插入防重扫相同条码在设定时间内重复扫描时只处理一次标签打印生成条码图片并输出到标签打印机2. 方案选型与架构拆解2.1 先分清USB-HID、串口、蓝牙三种接入方式扫码枪和电脑的通信方式看起来五花八门实际归纳起来就三种串口RS232、USB 模拟键盘USB-HID、USB 虚拟串口。很多程序员一开始就走错方向是因为没有搞明白手里的枪到底是哪种模式。接入方式系统识别成什么数据获取方式适用场景USB-HID键盘程序接收键盘输入事件大多数即插即用桌面场景USB 虚拟串口COM 口SerialPort 读取串口数据工业环境、需要双向控制指令的设备串口 RS232COM 口SerialPort 读取串口数据老旧设备、固定工位蓝牙虚拟串口或键盘配对后按上述方式处理手持终端、移动盘点最常见也最容易搞混的是 USB 模式。有些扫码枪默认出厂是 USB-HID 键盘模式你把它插上电脑它就是一个键盘扫码时向系统发送按键消息。这种方式不用装驱动但缺点是无法区分数据是真人敲键盘还是扫码枪扫进来的。另一种是装完驱动后变成虚拟串口程序可以通过 COM 端口直接读取数据稳定性更高也不受焦点窗口影响。建议在项目启动第一天就做一件事把扫码枪插入电脑打开记事本扫码一次看光标处出现的内容。如果扫码后直接出现条码并换行就是 HID 键盘模式。如果完全没反应那就是串口或者虚拟串口模式需要去设备管理器确认 COM 口号。这个简单的动作能帮你避开后面大半的麻烦。2.2 整个源程序的架构分层我把这个源程序按层次拆成四层每个层只干自己的事避免把代码全堆在 Form1.vb 里设备接入层负责 SerialPort 初始化、键盘输入拦截对外只暴露一个事件比如 BarcodeScanned(barcode As String)。业务处理层负责条码清洗、校验位验证、防重判断决定一条数据到底要不要交给数据库。数据访问层负责 SQL Server 或 MySQL 的写入统一封装成方法。界面层只负责把结果显示在 DataGridView、TextBox、Label 上不做逻辑处理。这种分层在小型程序里看起来有点“重”但实际写起来并不复杂而且能让你在换扫码枪、换数据库、换界面时互不影响。我之前接过一个项目原本把所有代码都写在窗体里后来客户从 USB 扫码枪换成串口扫码枪我硬是花了三小时改代码。重构成分层结构以后同类需求 15 分钟就能搞定。2.3 为什么把扫码事件放进独立队列扫描枪的数据接收速度和界面操作速度不在一个量级。USB-HID 模式下一个条码可能瞬间就被拆成十几个键盘按键消息如果每收到一个字符就立刻去更新界面文本框程序会非常忙碌尤其当扫码枪扫描连续批量条码时界面线程会被大量短消息塞满甚至出现假死。更严重的是如果直接在 DataReceived 事件里执行数据库写入会导致串口接收缓冲区积压下一批数据被延迟处理。我的做法是引入一个线程安全的队列设备接入层收到完整条码后只负责入队后台有一个处理线程不断取出条码并交给业务层。这样即使扫码枪连续扫 50 个条码也只是队列长度增加界面依然流畅数据库也能一条条稳定写入。如果你不想自己写队列也可以使用 ConcurrentQueue(Of String)用起来非常简单Imports System.Collections.Concurrent Private _queue As New ConcurrentQueue(Of String) Private _cancelled As Boolean False Private Sub BarcodeScanned(barcode As String) Handles Scanner.BarcodeScanned _queue.Enqueue(barcode) End Sub Private Async Function ProcessQueueAsync() As Task Await Task.Run(Sub() While Not _cancelled Dim bc As String Nothing If _queue.TryDequeue(bc) Then HandleBarcode(bc) Else Threading.Thread.Sleep(20) End If End While End Sub) End Function这里有个经验之谈一开始我以为用时间间隔来判断“是否收到完整条码”这个方法很聪明结果在实际测试中发现操作员扫码速度快时两个条码之间的间隔可能不到 100 毫秒时间超时法经常把两个条码拼到一起。后来我全部改成基于结束符判断也就是检测回车键只有遇到回车才认为一条数据结束。绝大多数扫码枪都可以通过配置码把“后缀”设置为回车这也是最可靠的截断方式。3. 核心代码解析与实操要点3.1 串口扫描枪SerialPort 读取与结束符判断串口扫码枪通常会配置成“扫描条码后自动发送数据 回车换行后缀”。程序要做的是打开对应的 COM 口不断读取数据并且按回车作为分隔符把完整条码切出来。Imports System.IO.Ports Imports System.Text Public Class SerialScanner Private WithEvents _port As SerialPort Private _buffer As New StringBuilder Public Event BarcodeScanned(barcode As String) Public Sub Open(portName As String, baudRate As Integer) _port New SerialPort(portName, baudRate, Parity.None, 8, StopBits.One) _port.Handshake Handshake.None _port.ReadTimeout 500 _port.Open() End Sub Private Sub _port_DataReceived(sender As Object, e As SerialDataReceivedEventArgs) Handles _port.DataReceived Dim data As String _port.ReadExisting() _buffer.Append(data) Dim current As String _buffer.ToString() If current.IndexOfAny({ControlChars.Cr, ControlChars.Lf}) 0 Then Dim lines As String() current.Split(New Char() {ControlChars.Cr, ControlChars.Lf}, StringSplitOptions.RemoveEmptyEntries) For Each line As String In lines If line.Trim().Length 0 Then RaiseEvent BarcodeScanned(line.Trim()) End If Next _buffer.Clear() End If End Sub End Class需要注意几个关键点。设置 ReadTimeout 是防止端口长时间无数据时线程卡死Handshake 一般用 None除非你的扫码枪明确要求硬件流控DataReceived 事件运行在后台线程绝对不要在事件里直接操作 TextBox否则会抛跨线程异常。如果一定要显示请使用 Invoke 或者把数据交给队列。串口参数里最容易忽略的是数据位、停止位和校验位。大多数扫码枪出厂是 9600 波特率、8 数据位、无校验、1 停止位但同一品牌不同型号也可能不一样最好在扫描枪说明书里找“串口参数配置码”现场确认或者在设备管理器里查看端口属性。3.2 USB 扫描枪HID 键盘模式ProcessCmdKey 拦截USB-HID 模式的扫码枪本质是键盘程序想要在任意窗体中接收扫码内容最稳妥的方式是重写窗体的 ProcessCmdKey 方法把所有按键消息先在窗体层面拦截一遍。这样做的好处是即使焦点在 DataGridView 上也能捕获到扫码数据。Protected Overrides Function ProcessCmdKey(ByRef msg As Message, keyData As Keys) As Boolean If keyData Keys.Enter Then If _inputBuffer.Length 0 Then Dim barcode As String _inputBuffer.ToString() _inputBuffer.Clear() RaiseEvent BarcodeScanned(barcode) Return True End If Else Dim c As Char KeyCodeToChar(keyData) If c ControlChars.NullChar Then _inputBuffer.Append(c) Return True End If End If Return MyBase.ProcessCmdKey(msg, keyData) End FunctionKeyCodeToChar 的辅助函数需要把 Keys.A 到 Keys.Z、数字键、小键盘数字、Shift 组合等情况转换成字符。这里有个非常容易被坑的点如果用户输入法停留在中文状态扫码枪发出的字母按键消息可能被输入法吃掉导致条码缺字符所以程序在扫描窗口打开时要主动切换到英文输入法或者设置窗体的 ImeMode 为 Off。另一个问题是回车键冲突。扫码枪发送完整条码后通常会补一个回车这个回车如果交给窗体处理可能会触发默认按钮AcceptButton的 Click 事件导致扫码的同时弹出查询窗口。因此在 ProcessCmdKey 里把带数据的回车事件直接吃掉不让它继续往下传递是保证程序稳定的关键一步。3.3 条码数据清洗与校验位计算不同厂家、不同格式的条码扫出来的原始字符串差别很大。有的带前缀 0有的带 FNC1 分组符有的在条码前包含 ASCII 码 29GS 分隔符这些脏数据如果不处理后面查询库存、匹配订单都会出问题。我的清洗逻辑一般分三步去掉首尾不可见字符vbCr、vbLf、Chr(0)、空格。去掉 GS、RS、FS 等特殊控制字符如果条码里需要保留分组信息可以用指定的可读分隔符替换。按业务规则补全或截断比如 EAN-13 条码固定 13 位不足 13 位时可能在扫码枪配置里已经做了处理程序里再统一校验一次。EAN-13 的校验位计算是仓库类项目里最常见的需求这个算法并不复杂从第 1 位开始奇数位乘以 1偶数位乘以 3累加后取 10 的补数得到校验位。Private Function CalculateEan13CheckDigit(digits As String) As Integer Dim sum As Integer 0 For i As Integer 0 To digits.Length - 1 Dim d As Integer AscW(digits(i)) - 48 If i Mod 2 0 Then sum d Else sum d * 3 End If Next Return (10 - (sum Mod 10)) Mod 10 End Function使用上面这段代码时要注意传入的 digits 是前 12 位不包括校验位。计算完成后再把第 13 位和扫码内容第 13 位做比较不一致就提示“条码校验失败”。大多数专业扫码枪扫出来的 EAN-13 都是完整 13 位校验位算法主要是防止扫码枪配置错误或者条码本身印刷破损。3.4 条码标签旋转里用到的 Math.Sin有些场景下比如打印多联标签或者把条码贴到圆形容器上程序需要在 WinForms 或 GDI 里按一定角度旋转条码图片。很多人在网上搜 VB.NET 条形码旋转最后会碰到 Math.Sin 和 Math.Cos 的用法。这其实不是条码本身需要正弦而是我们在计算旋转后图元坐标时必须用到三角函数。Private Function RotatePoint(p As PointF, center As PointF, angle As Double) As PointF Dim radian As Double angle * Math.PI / 180.0 Dim cosA As Double Math.Cos(radian) Dim sinA As Double Math.Sin(radian) Dim dx As Double p.X - center.X Dim dy As Double p.Y - center.Y Return New PointF( CSng(center.X dx * cosA - dy * sinA), CSng(center.Y dx * sinA dy * cosA) ) End Function这段代码在打印模块里非常实用。如果你并不需要自定义旋转绘制直接使用 BarCodeLib 或 ZXing 库生成条码图片再用 Graphics.RotateTransform 旋转也能达到同样效果。要把 Math.Sin 放在这里一起讲是因为我见过太多人把这个计算和条码生成混在一起最后把角度单位搞混淆。VB.NET 里的 Math.Sin、Math.Cos 接收的是弧度不是角度所以必须先做 angle * Math.PI / 180.0 的转换这是最容易出错的一步。4. 对接业务系统数据库、打印与防重4.1 SQL Server 写入参数化与批量入库扫码数据最终要落库SQL Server 是最常见的去处。很多人喜欢直接拼接 SQL 字符串这在扫码枪项目里非常危险因为条码内容是不可控的可能包含单引号、反斜杠等特殊字符。一旦条码里出现单引号SQL 语句就会报错甚至产生注入风险。所以必须使用参数化命令。Using conn As New SqlConnection(_connStr) conn.Open() Using cmd As New SqlCommand( INSERT INTO ScanLog(Barcode, ScanTime, Operator, Source) VALUES(bc, st, op, src), conn) cmd.Parameters.Add(bc, SqlDbType.NVarChar, 64).Value barcode cmd.Parameters.Add(st, SqlDbType.DateTime).Value DateTime.Now cmd.Parameters.Add(op, SqlDbType.NVarChar, 32).Value operatorName cmd.Parameters.Add(src, SqlDbType.NVarChar, 16).Value USB cmd.ExecuteNonQuery() End Using End Using如果仓库高峰期扫描量很大建议把单条插入改为批量插入比如每攒满 100 条就提交一次事务能显著提升效率。还有一个小细节AddWithValue 虽然写着方便但在 SQL Server 里可能导致参数类型推断不准确尤其是 NVarChar 字段存中文时容易出现隐式转换进而丢失索引所以我更推荐显式写 Add 方法并指定 SqlDbType。4.2 标签打印与条码图片生成在源程序里加入打印功能也是常事。最简单的方法是调用系统安装的条码字体或者使用条码控件但这类方式依赖字体环境换一台电脑字体没装就会打印成普通数字。稳妥方案是引入 ZXing.NET 或者 BarCodeLib 这类库来生成条码图片再交给 PrintDocument 输出。ZXing.NET 在 VB.NET 里用法很直接Dim writer As New BarcodeWriter() writer.Format BarcodeFormat.CODE_128 writer.Options New EncodingOptions With { .Width 300, .Height 80, .Margin 2 } Using bmp As Bitmap writer.Write(ABC123456) bmp.Save(barcode.png, Imaging.ImageFormat.Png) End Using打印时要注意分辨率和页面边距。标签打印机通常用 203 DPI 或者 300 DPI如果图片设置 300x80 像素直接打印到 70mm 宽的标签纸上可能偏小需要按 DPI 换算实际尺寸。PrintDocument 的 MarginBounds 以及 Graphics.DrawImage 的宽高建议用毫米换算成像素也就是 mm / 25.4 * dpi这样打印出来的条码尺寸才准确。这个细节我一开始没注意打印的标签硬生生小了三分之一后来重新看设备参数才醒悟过来。4.3 短间隔重扫的防重设计仓库操作员经常会有“扫完发现没弹窗顺手又扫一次”的操作还要动态地快速连续扫描不同条码。如果程序把重复条码当成新数据入库会造成重复记录。我实现的防重逻辑很简单用一个 Dictionary(Of String, Date) 记录最近扫描的条码和时间如果在设定的防重窗口内比如 3 秒收到相同条码直接丢弃。Private _recent As New Dictionary(Of String, Date) Private Function IsDuplicate(barcode As String) As Boolean Dim last As Date If _recent.TryGetValue(barcode, last) Then If (DateTime.Now - last).TotalSeconds 3 Then Return True End If End If _recent(barcode) DateTime.Now Return False End Function这里有一个容易被忽略的问题字典会无限增长长时间运行后内存会缓慢上升。解决方法是定时清理比如每分钟把超过 10 秒没有重复的条目移出字典。我在实机环境里跑过连续 8 小时扫码测试加上清理逻辑以后内存占用稳定在 80MB 以内效果可以接受。5. 常见问题与排查技巧实录5.1 扫描没反应先查设备模式再查代码这种问题排在第一位因为 70% 的情况不是代码写错是设备模式不对。遇到扫码没反应先做三个动作打开记事本光标定位在记事本里扫一下码。如果记事本正常出现条码说明设备是 HID 键盘模式且硬件没问题问题出在程序焦点或消息拦截。如果记事本完全没动静检查设备管理器里是否多出 COM 口设备是否被识别为 USB 输入设备给扫码枪重新上电。大多数扫码枪在说明书里都有一页“配置码”扫一下“恢复出厂设置”就能回到最稳妥的默认模式。5.2 串口乱码与数据截断串口扫码枪出现乱码基本逃不开三个原因波特率不对、数据位/停止位不对、字符编码不对。波特率不对时数据基本是乱码字符编码不对时中文内容会变成问号或乱码但数字条码通常不受影响。需要注意的是某些扫码枪默认发送的是 GBK 编码而 SerialPort 默认使用 UTF-8 解码两者不一致就会导致乱码。解决办法是把 SerialPort 的 Encoding 属性改成 Encoding.Default或者改成 Encoding.GetEncoding(GBK)。数据截断则多半是结束符判断太激进遇到换行就把前一段清空建议按回车和换行一起处理并用 StringBuilder 缓存残段。5.3 中文输入法和小键盘的干扰USB-HID 模式扫描枪带来的一个经典问题是输入法冲突。中文输入法开启时扫码枪发送的英文字母和数字会被输入法截获程序收不到完整内容或者条码进入输入法组词窗口。解决办法是在扫描窗体的 Load 事件里强制切换英文输入法并设置 Form.InputLanguage 为英文。另一个相关问题是如果用户按下了大写锁定或小键盘状态不对扫描内容也会受影响所以要在 ProcessCmdKey 里对 Keys 值做统一处理优先使用 KeyCode 而不是直接依赖 KeyData 里的字符。5.4 多台扫描枪同时工作怎么办如果一台电脑接了多台扫码枪情况会变复杂。HID 模式无法直接区分是哪把枪扫的除非扫码枪支持设置不同的前缀后缀比如 A 枪扫码后输出“A1_条码”B 枪输出“B2_条码”程序再按前缀区分。串口模式区分起来就简单每个 COM 口对应一台设备各用一个 SerialScanner 实例分别处理即可。我的源程序里预留了 Source 字段在设备初始化时指定来源名称数据库写入时一并存储这样后面统计哪台设备工作量或者排查哪台枪数据异常都很方便。5.5 常见故障速查表现象可能原因解决方式记事本能扫程序没反应焦点窗口未拦截键盘消息使用 ProcessCmdKey 或全局键盘钩子程序收到条码后自动触发按钮回车键未拦截在 ProcessCmdKey 里吃掉 Enter串口乱码波特率或编码不正确检查波特率设置串口 Encoding条码缺了最后一位结束符判断过早检查防重逻辑和回车判断中文乱码输入法冲突或数据库编码不正确ImeMode 设 Off数据库使用 NVarChar多个相同条码重复入库缺少防重机制加入短时间 Dict 防重打印条码模糊图片分辨率过低按 DPI 重新计算图片尺寸6. 写在最后的实战体会断断续续调试这个 VB.NET 条形码电子扫描枪源程序我踩得最深的一个坑还不是代码而是“先入为主”。一开始我总以为扫码枪是串口设备结果手里的型号默认是 USB-HID浪费了整整一个晚上去查波特率和驱动。后来我养成了一个习惯任何扫码枪接入电脑第一步都在记事本里测试根据实际现象决定代码走哪条路。这个习惯至少帮我避开了后面两三个项目里的同类问题。另外要提醒一点条码数据格式的问题尽量在扫码枪配置层面解决而不是全靠程序兜底。比如结尾回车、前缀数据、EAN-13 是否补全这些扫码枪手册里都有配置码扫一下码就能改变输出程序里兜底做太多反而容易在异常数据面前显得措手不及。我的建议是程序做一层最基本的清洗和校验再通过配置码把设备输出统一成“条码内容 回车”这个组合最稳定、最好排查、也最容易向同事解释。如果你准备把这个源程序用到生产环境建议再加上两样东西一键打开调试日志把每次扫码的原始内容记录到本地文件另一个是扫码成功后的声音提示未必需要语音播报简单的“嘀”一声就能让操作员确认扫码成功。这两样都不复杂但能显著降低现场使用时的误操作率。本文还有配套的精品资源点击获取