VB.NET条形码扫描枪开发:键盘模拟与串口模式全解析
发布时间:2026/10/15 16:00:35 锦皓数字建站

简介面向VB.NET初学者的条形码电子扫描枪客户端源码可用于单号、所属客户、机器型号、条码号、扫描时间及备注等信息的实时采集与展示适合需要对接条码扫描仪、开发桌面端数据录入界面的学习场景。压缩包共24个文件以.vb窗体及业务逻辑代码为主辅以resx界面资源、项目配置与解决方案文件并含一个mdb数据库文件整体仅195KB可在Visual Studio中直接打开。已有419人学习。源码包含Form1、Form2、Form3等多个窗体及Module模块完整演示了扫描触发后的数据接收、界面刷新与信息入库流程对理解VB.NET的窗体结构、资源引用及简单数据库操作均有实际参考价值适合期望掌握扫码枪客户端基础实现的开发者研读。1. 先分清扫码枪是键盘模式还是串口模式VB.NET 开发的第一步拿到“VB.NET条形码电子扫描枪源程序”这类标题时大多数人的第一反应是赶紧找串口代码实际上真正卡住进度的不是串口操作而是没先分清手里的扫码枪到底工作在哪种模式。绝大多数 USB 扫码枪插上电脑后会被系统识别成键盘扫描条码等于一段极快的键盘输入末尾自动带一个回车只有带 RS232 接口或专门扫过配置码切模式的枪才走串口。模式判断错了后面的代码框架全是错的键盘模拟要管理焦点和输入法串口模式要管理波特率和断帧。这篇文章就是按这两种模式分别讲 VB.NET 怎么收条码、怎么把收码模块做成可复用组件适合要做进销存、扫码入库、固定资产盘点、设备巡检这类系统又第一次接扫码枪的 VB.NET 从业者。2. 键盘模拟模式VB.NET 文本框接收扫码枪数据的标准做法2.1 怎么识别你的扫码枪是键盘模拟三个判断特征有些说明书上写“USB HID”有些写“键盘口”术语不同本质都是键盘模拟。判断方法很简单把扫码枪插上电脑打开记事本扫一张条码。如果记事本里出现了条形码数字最后多了一个回车那它就是键盘模拟。第二个特征是打开设备管理器“键盘”或“HID 键盘设备”分类下能找到一个新设备而“端口 (COM 和 LPT)”下没有新增 COM 口。第三个特征是扫完后焦点不在扫码窗口也不报错字符只是跑到了当前光标所在处。这三个特征全中就不需要研究串口参数直接按键盘流处理。这个判断为什么重要因为 VB.NET 源程序中两种模式的代码量差很多接错口完全是两套排错方向。键盘模拟模式依靠 Windows 消息循环丢码多发生在输入法、焦点抢占、回车被别的控件截获串口模式依靠串口驱动丢码多发生在缓冲区拼帧和波特率。如果一上来就写串口代码接一把 HID 枪打开串口时大概率报“拒绝访问”或干脆没这个 COM 口。我一般接到新枪第一件事永远是打开记事本扫一下再决定要不要看设备管理器这个习惯能省掉后续八成排错时间。2.2 用 TextBox 接收条码的最小程序KeyDown 判回车如果确认是键盘模拟最稳的做法是窗体放一个 TextBox把焦点固定在里面。扫码枪会在几十毫秒内连续提交一串字符最后提交一个 Enter。程序只需要在 KeyDown 里判断回车从文本框取出完整字符串然后清空等下一帧。Private Sub TxtScan_KeyDown(sender As Object, e As KeyEventArgs) Handles TxtScan.KeyDown If e.KeyCode Keys.Enter Then 屏蔽回车本身的默认行为避免触发窗体确认按钮或提示音 e.SuppressKeyPress True Dim barcode As String TxtScan.Text.Trim() If barcode.Length 0 Then HandleBarcode(barcode) TxtScan.Clear() End If End If End Sub Private Sub HandleBarcode(barcode As String) 业务入口这里可以做库存查询、入库登记等 StatusLabel.Text 最近扫码 barcode End Sub逻辑说明KeyDown 比 KeyPress 早触发而且能拿到 KeyEventArgs拦截回车最干净。e.SuppressKeyPress True 可以把这次按键从系统输入队列里消掉避免界面同时响一声或者窗体 AcceptButton 被回车误触发。HandleBarcode 是业务入口建议在里面只做收集和派发不要直接写数据库。TxtScan.Clear() 放在取码之后防止下一把扫太快时旧数据混进来。这里有两个小参数值得注意。TxtScan.MaxLength 建议设 32 或 64防止扫码枪连续按出超长垃圾TxtScan.ImeMode 建议设 Disable避免中文输入法截走字母数字这点后面专门说。TextBox 本身保持可编辑状态不要因为“只是显示”就设 ReadOnlyReadOnly 状态下 KeyDown 虽然会触发但个别输入法环境下取到的字符会不完整没必要冒这个险。2.3 焦点管理与输入法锁定避免扫码枪丢码的三步键盘模拟模式最容易翻车的地方就是焦点。用户扫一把之前可能刚点过表格、按钮、回车搜索框扫码枪的字符会落进那个控件里而不是你的 TxtScan。常见做法是从三个方向堵第一步窗体 Load 时让 TxtScan 先拿焦点第二步在 TxtScan 的 Leave 事件里做一个可暂停的焦点夺回用户要正常点鼠标时先解除锁定点完再锁回。直接无条件夺回会让人无法操作其他控件必须加开关。Private focusLocked As Boolean True Private Sub TxtScan_Leave(sender As Object, e As EventArgs) Handles TxtScan.Leave If focusLocked Then Me.TxtScan.Focus() End If End Sub Private Sub btnSearch_Click(sender As Object, e As EventArgs) Handles btnSearch.Click focusLocked False 业务查询逻辑 focusLocked True Me.TxtScan.Focus() End Sub第三步把窗体 KeyPreview 打开所有键盘事件先经过窗体级处理。如果不想锁焦点可以直接在窗体级 KeyDown 里接收键盘流把 2.2 的代码搬到窗体事件里并用一个 StringBuilder 攒字符Me.KeyPreview True Private scanBuffer As New System.Text.StringBuilder() Private Sub Form1_KeyDown(sender As Object, e As KeyEventArgs) Handles Me.KeyDown If e.KeyCode Keys.Enter Then e.SuppressKeyPress True Dim barcode As String scanBuffer.ToString() If barcode.Length 0 Then HandleBarcode(barcode) scanBuffer.Clear() End If ElseIf e.KeyCode Keys.Back Then If scanBuffer.Length 0 Then scanBuffer.Remove(scanBuffer.Length - 1, 1) End If ElseIf Not e.Shift AndAlso Not e.Control AndAlso Not e.Alt Then 只收集可打印字符减少人工键盘干扰 scanBuffer.Append(ChrW(e.KeyValue)) End If End Sub这里 KeyPreviewTrue 让窗体先拿到所有子控件的键盘消息不再依赖焦点scanBuffer 是模块级变量过滤 Shift/Ctrl/Alt 是防止用户在界面上正常操作时把快捷键字符拼进条码。这个方案适合扫码为主、输入很少的工位机缺点是正常打字也会进 buffer所以不能在办公电脑上无条件开。3. 串口模式VB.NET 的 SerialPort 读条码与参数配置3.1 什么时候必须转串口先看三种场景键盘模拟虽然简单但有边界扫码枪必须有稳定焦点连续快速扫码时字符消息可能有挤压和丢失界面不能锁死。遇到三种场景就得转串口一是用户要一边扫码一边敲键盘焦点不能只盯文本框二是系统需要扫码后控制灯光蜂鸣、断线重连、电压异常检测三是条码长度长或含特殊字符键盘模拟有可能吞掉部分换行和符号。那怎么把枪切成串口多数支持串口的 USB 扫码枪出厂默认可能是 HID 键盘也可能默认是串口取决于厂商固件。需要扫说明书里的切换配置码通常叫“串口模式配置码”或“COM 口模式码”。切换后重新插拔设备管理器里出现 COM 口才算成功。我一般会连续扫三个配置码串口模式、波特率 9600、回车后缀然后重新枚举端口。不要在没确认 COM 口出现之前写代码那样只会一直撞“端口不存在”的异常。注意一个常见误区串口号可能变化。同一把枪换一个 USB 口就可能从 COM3 变 COM5。源程序最好做成下拉框枚举现有串口而不是写死 COM3否则交付到客户机器上必翻车。3.2 SerialPort 读条码的最小代码DataReceived 事件与条码拼接VB.NET 中串口操作集中在 System.IO.Ports.SerialPort使用前先确认工程引用了 System.IO.Ports 命名空间。核心心法只有一条DataReceived 事件在后台线程触发不能在里面直接操作 Windows 窗体控件而且它不代表收到完整一帧条码。下面是可用的最小读取代码Imports System.IO.Ports Public Class Form1 Private WithEvents scannerPort As New SerialPort() Private bufferedText As New System.Text.StringBuilder() Private Sub Form1_Shown(sender As Object, e As EventArgs) Handles MyBase.Shown With scannerPort .PortName COM3 .BaudRate 9600 .DataBits 8 .Parity Parity.None .StopBits StopBits.One .ReadTimeout 500 End With Try scannerPort.Open() StatusLabel.Text 串口已打开 scannerPort.PortName Catch ex As Exception StatusLabel.Text 打开失败 ex.Message End Try End Sub Private Sub scannerPort_DataReceived(sender As Object, e As SerialDataReceivedEventArgs) Handles scannerPort.DataReceived Dim newData As String scannerPort.ReadExisting() bufferedText.Append(newData) Dim wholeText As String bufferedText.ToString() 按回车或换行切出完整条码 Dim splitPos As Integer wholeText.IndexOfAny({ControlChars.Cr, ControlChars.Lf}) While splitPos 0 Dim barcode As String wholeText.Substring(0, splitPos).Trim() If barcode.Length 0 Then 回界面线程处理 Me.BeginInvoke(New Action(Sub() HandleBarcode(barcode))) End If wholeText wholeText.Substring(splitPos 1) splitPos wholeText.IndexOfAny({ControlChars.Cr, ControlChars.Lf}) End While bufferedText.Clear() bufferedText.Append(wholeText) End Sub End Class逻辑说明DataReceived 只是“有数据来了”不是“条码完整了”。USB 转串口驱动会把一次扫描的字节拆成好几段到达直接用 ReadLine 很容易只拿到半截。这里先 ReadExisting 把当前缓冲全部取走拼入 StringBuilder再用 CR 或 LF 切分成帧。IndexOfAny 同时认两种换行是为了兼容扫码枪默认输出 CR、也兼容用户把枪配置改成输出 LF 的情况。切完一帧后调用 BeginInvoke 回到界面线程避免跨线程操作控件抛异常。参数说明BaudRate 必须与扫码枪内部配置一致出厂多数是 9600也有新枪默认 115200DataBits 一般 8Parity 和 StopBits 一般 None 和 One。ReadTimeout 只影响同步读取本代码用的是 ReadExisting 异步读取这里设 500 毫秒是保险兜底。bufferedText 在切完所有帧后清空再保存剩余部分所以下一帧数据到了不会与旧数据重叠。3.3 串口参数速查表波特率、数据位、校验位怎么定参数常见值选择依据BaudRate9600 / 19200 / 115200与扫码枪当前配置一致出厂多数 9600DataBits8条码字符是 ASCII 或 GB23128 位足够ParityNone新枪默认无校验老款少数用 Even需看配置码StopBitsOne默认值极少改动HandshakeNone码枪不需要流控开了反而可能卡住很多时候乱码不是代码问题是两边参数不一致。调试助手显示正常、VB.NET 里乱码先对照这张表把两端四项全部调一致。扫码枪没有专属字库它就是把条码转成二进制流波特率错一位读出来的字符就会整体错乱。若参数完全一致仍然乱码再检查扫码枪配置有没有打开“UK 键盘布局”或“附加前缀后缀”之类的选项那些选项会在条码前后插入额外控制字符看着也像乱码。4. 扫码枪数据丢、乱、重复的五个高频问题排查4.1 键盘模式下最容易遇到的两个丢码坑坑 1中文输入法把条码字符吃掉了。现象是扫带字母的条码时文本框里只出现一半字符数字有时还变成全角。原因是中文输入法在中文状态下会先截获键盘事件把字母暂存在组词窗口里扫码枪最后的回车反而让拼音上屏程序取到的是“组词结果”而不是条码。解决办法是把接收文本框的 ImeMode 设为 Disable不是 OffDisable 是彻底绕开输入法处理程序启动时再主动切一次英文输入法更保险。Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load TxtScan.ImeMode ImeMode.Disable InputLanguage.CurrentInputLanguage InputLanguage.FromCulture(New Globalization.CultureInfo(en-US)) End Sub这里 InputLanguage 切换属于强制手段适合工位机如果客户电脑只装中文输入法切换失败会被忽略不影响程序。真正要防的是用户在扫码瞬间点输入法切换键ImeModeDisable 能拦住大部分情况。坑 2回车被窗体 AcceptButton 抢走。现象是扫完码条码取到了但窗体上某个确认按钮也被按下弹了多余对话框。原因是窗体设置了 AcceptButton扫描枪自带的回车会被 Windows 当作“按默认按钮”。解决办法是去掉 AcceptButton或者只让 TxtScan 处理回车并设 e.SuppressKeyPress True。还有一点容易被忽略Form.KeyPreview 也设为 False否则窗体会在子控件之前收到回车把它当成默认按钮按键。4.2 串口模式下最容易翻车的三个参数坑坑 3长条码总是丢后半段。现象是 Code128、GS1 这类长码读出来总是只有前半截后面字母偶尔消失。原因是用 ReadLine 想一次读完但串口数据分片到达ReadLine 等到设好的超时时间就返回提前截断了。解决方法是按第 3.2 节的缓冲拼帧处理不要依赖单次 ReadLine。条码没有固定长度靠超时判断一帧结束本身就是错的。坑 4一打开串口就报“Access to the port is denied”。现象是 port.Open() 抛异常程序无法继续。原因是扫码枪被其他软件占用常见的有串口调试助手、扫码枪厂商工具、还可能开了两个程序实例。解决是先关掉所有可疑工具再用 SerialPort.GetPortNames() 枚举一下当前可用端口和设备管理器里的 COM 号对应For Each name As String In SerialPort.GetPortNames() Debug.WriteLine(name) Next如果 GetPortNames 里能看到 COM 口但 Open 仍然报占用基本可以确定是别的进程占着。找到占用进程并结束它再重新打开。程序自己也要做好释放关闭窗体时调用 scannerPort.Close() 和 Dispose()不然调试模式下程序崩溃后端口不会立即释放要等十几秒甚至重启 IDE。坑 5读出来带一堆控制字符和乱码。现象是条码字符串里出现 Chr(0)、CR 混排、首尾空格乱跳甚至夹杂不可见字符。原因多半是扫码枪开启了前后缀、代码页不匹配或者波特率错位导致字节错乱。先用串口调试助手抓一次原始数据把每个字节的十六进制列出来确认结尾是 0DCR还是 0ALF再改代码里的切帧字符。清洗时不要用正则在事件里逐条跑字符串简单替换足够Private Function CleanBarcode(raw As String) As String Dim code As String raw 清掉可能的空字符、回车、换行和首尾空格 code code.Replace(Chr(0), ).Replace(vbCr, ).Replace(vbLf, ) code code.Trim() Return code End Function三个 Replace 分别对应串口调试助手里最容易见到的三种垃圾字节。Chr(0) 常见于部分国产扫码枪的默认前缀CR/LF 是行尾符本应作为切帧标记但在某些配置下会混进数据体Trim 用来处理首尾空格。清洗必须在切帧之后做切帧之前做会把换行符删掉导致无法判断条码边界。还需要补一个几乎所有模式都会踩的坑不要在扫码事件里写数据库或 Sleep。事件里查库存可能耗时几百毫秒第二把扫的字符已经堆在缓冲区表现出来就是“越扫越卡最后丢码”。正确做法是扫码事件只收条码把业务操作投递到工作线程或任务队列界面保持随时可扫。5. 把源程序按三层拆开扫码枪接收层如何做成可复用组件5.1 源程序到底该有什么接收层、业务层、界面层一个能用的扫码枪 VB.NET 源程序按模块划分至少有四块接收模块文本框事件或串口事件、清洗模块去前后缀、去控制字符、路由模块按条码前缀走不同业务、界面模块配置串口、显示最近扫码结果。如果没有这层拆分代码通常是一大坨换一把扫码枪就要重构一遍。我一般按三层组织接收层负责拿“原始帧”业务层只认“干净条码字符串”界面层只显示和做配置入口。这样做有一个最直接的好处键盘模拟和串口模式只影响接收层。换模式时只需要替换接收层的实现NormalizeBarcode 和 HandleBarcode 一行都不用动。对“VB.NET条形码电子扫描枪源程序”这类标题来说这个骨架才是源程序里真正值钱的部分串口读码本身反而是几十行固定写法。5.2 用事件回调把扫码数据交给业务层实现一个 ScannerService 类把第 3 章串口代码封装成一个单独的 ScannerService 类窗体只订阅事件这是最方便的复用方式。该类负责串口生命周期、缓冲区拼帧、线程切换所有业务逻辑从外部通过事件获得条码。Imports System.IO.Ports Imports System.Threading Public Class ScannerService Public Event BarcodeScanned(barcode As String) Public Event ErrorOccurred(message As String) Private port As SerialPort Private bufferedText As New System.Text.StringBuilder() Private uiContext As SynchronizationContext Public Sub New() 必须在窗体线程创建实例捕获同步上下文 uiContext SynchronizationContext.Current End Sub Public Sub Start(portName As String, baudRate As Integer) StopService() Try port New SerialPort With { .PortName portName, .BaudRate baudRate, .DataBits 8, .Parity Parity.None, .StopBits StopBits.One, .ReadTimeout 500 } AddHandler port.DataReceived, AddressOf OnDataReceived port.Open() Catch ex As Exception RaiseEvent ErrorOccurred(ex.Message) End Try End Sub Public Sub StopService() If port IsNot Nothing Then RemoveHandler port.DataReceived, AddressOf OnDataReceived If port.IsOpen Then port.Close() End If port.Dispose() port Nothing End If End Sub Private Sub OnDataReceived(sender As Object, e As SerialDataReceivedEventArgs) bufferedText.Append(port.ReadExisting()) Dim wholeText As String bufferedText.ToString() Dim splitPos As Integer wholeText.IndexOfAny({ControlChars.Cr, ControlChars.Lf}) While splitPos 0 Dim barcode As String wholeText.Substring(0, splitPos).Trim() If barcode.Length 0 Then RaiseBarcodeScanned(barcode) End If wholeText wholeText.Substring(splitPos 1) splitPos wholeText.IndexOfAny({ControlChars.Cr, ControlChars.Lf}) End While bufferedText.Clear() bufferedText.Append(wholeText) End Sub Private Sub RaiseBarcodeScanned(barcode As String) If uiContext Is Nothing Then RaiseEvent BarcodeScanned(barcode) Else uiContext.Post(Sub() RaiseEvent BarcodeScanned(barcode), Nothing) End If End Sub End Class逻辑说明构造时在 UI 线程捕获 SynchronizationContext之后 DataReceived 在后台线程完成切帧RaiseBarcodeScanned 把条码事件送回界面线程窗体里直接更新控件不需要再自己 BeginInvoke。Start 方法开头先调用 StopService是为了防止重复启动时事件被挂两遍一份串口数据被重复处理。StopService 里 RemoveHandler 和 Dispose 成对出现这是 SerialPort 组件最容易造成端口不被释放的地方。参数说明Start(portName, baudRate) 中 baudRate 建议由窗体的 ComboBox 传入可选项设 9600、19200、115200。portName 不要写死用 SerialPort.GetPortNames() 枚举填充下拉框。如果使用这个类的项目不是 WinForms 而是控制台uiContext 为 Nothing事件会在后台线程触发调用方只要自己不操作 UI 就没有问题。5.3 条码校验与前缀路由源程序里最容易忽略的清洗层扫码枪出来的字符串不能直接进库存表。至少要过两层去附加字符按规则确认条码类型。实际项目中我见过太多次“扫出来能显示但是入库报错”原因就是条码里藏着不可见前缀。下面两个方法负责清洗和路由Public Shared Function NormalizeBarcode(raw As String) As String Dim code As String raw 去掉扫码枪附加的控制字符 code code.Replace(vbCr, ).Replace(vbLf, ).Replace(Chr(0), ) code code.Trim() 部分内网简化码是 8 位入库要求 13 位标准码 If code.Length 8 Then code code.PadLeft(13, 0c) End If Return code End Function Public Shared Function RouteBarcode(code As String) As String If code.StartsWith(69) Then Return 商品 ElseIf code.StartsWith(25) OrElse code.StartsWith(30) Then Return 内部物料 Else Return 未知类型 End If End Function说明NormalizeBarcode 只做字符替换不用正则扫码枪是高速字符流这里用正则是拿大炮打蚊子反而慢。PadLeft 只在内网业务明确要求“8 位简化码转 13 位标准码”时才启用不能对所有短码补零否则会把丢码问题掩盖成错误条码。RouteBarcode 的前缀规则每个系统不一样正确做法是做成配置文件或数据库字典不要在源码里写死“69”“25”这样的分支。这层跟接收层完全解耦键盘模式和串口模式清洗同一套逻辑不会出现两套代码行为不一致。6. 把扫码枪接收层封装成 UserControl一个可复用的条码输入控件两套接收代码都跑通后我会把键盘模拟方案再封装成一个 UserControl放到底层类库里。控件内部维护一个隐藏文本框外部只暴露一个 BarcodeScanned 事件使用方一行代码就能订阅Public Class BarcodeInputControl Inherits UserControl Private inputBox As New TextBox() Public Event BarcodeScanned(barcode As String) Public Sub New() 隐藏文本框负责吃键盘流控件本身只做事件转发 inputBox.Location New Point(-320, 0) inputBox.Size New Size(100, 20) inputBox.ImeMode ImeMode.Disable AddHandler inputBox.KeyDown, AddressOf inputBox_KeyDown Controls.Add(inputBox) End Sub Private Sub inputBox_KeyDown(sender As Object, e As KeyEventArgs) If e.KeyCode Keys.Enter Then e.SuppressKeyPress True Dim code As String inputBox.Text.Trim() If code.Length 0 Then RaiseEvent BarcodeScanned(code) inputBox.Clear() End If End If End Sub Public Sub FocusInput() inputBox.Focus() End Sub End Class这个控件把键盘模式的字符接收、回车判定、输入法限制全部收进去窗体上拖一个控件再写一行AddHandler barcodeInput1.BarcodeScanned, AddressOf OnBarcode就完成接入。串口模式下也可以把 ScannerService 实例塞进同类控件对外暴露 PortName 和 BaudRate 两个属性切换输入源时业务层不用感知。我第一次做这类源程序时把串口读取、条码清洗、库存查询写成同一段事件后来换一把扫码枪改了整整一下午那之后就坚持接收层独立这个习惯让后续所有扫码项目都很快落地。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。