简介:条形码电子扫描枪源程序是一套面向扫码数据采集场景的VB.NET项目,适合需要快速搭建桌面端扫码客户端的开发人员,也适合作为课程设计参考。程序用于录入单号、所属客户、机器型号、条码号、扫描时间及备注等业务字段,并把扫描结果实时展示在主窗口,运行过程需连接条码扫描枪,整体属于客户端采集模块。压缩包共24个文件,以VB源码和resx资源文件为主,另含项目配置、模块与数据库文件,大小仅195KB,轻量而完整,便于直接研究。源码中设计了多个窗体与公共模块,覆盖从数据读取、界面显示到数据库存储的完整链路;vbproj、sln等配置可帮助还原开发环境,mdb数据库则用于查看扫描记录和客户信息的具体表结构。资源已有419人学习,适合有一定VB.NET基础、希望深入了解串口类设备交互和桌面应用分层设计的开发者。
1. 接入前的关键选择:这一步没搞对,VB.NET 条形码扫描程序就是白做
很多第一次接条形码电子扫描枪的 VB.NET 开发者,会把「源程序」理解成一段直接读串口的代码,装上就能扫。实际上,拿到一把新扫描枪,第一件事不是打开 Visual Studio,而是先确认它到底走哪种数据通道:串口、USB-HID 模拟键盘、还是 USB 虚拟串口。这个选择直接决定你的程序是用 SerialPort 控件读数据,还是用窗体焦点接收键盘输入,代码架构完全两样。这篇文章就围绕「VB.NET条形码电子扫描枪源程序」这套完整落地路径,讲清从设备选型、串口读取、条码清洗、入库防抖,到自动重连和提示音反馈的每一步。适合正在做仓库盘点、门店收银、生产线扫码工位,或者想把手动录入改成扫码录入的人。
2. 先定接入方式:三种模式对比与选型依据
扫码枪从外观上看都是手柄加扳机,但数据到底怎么进电脑,决定了你后续 90% 的代码怎么写。很多开发者在网上找一个 SerialPort 示例,复制过去发现端口打不开,或者扫了没反应,多半就是接入模式没对齐。下面把三种模式摊开讲,并给出确认设备类型的命令,帮你在动手写 VB.NET 源程序之前先站对位置。
2.1 三种接入模式:串口、USB-HID、虚拟串口
| 接入模式 | 系统识别结果 | 数据形态 | 适合场景 |
|---|---|---|---|
| 纯串口 RS232 | 直接生成 COM 口 | 字节流,按回车分隔 | 老设备、工控机、远距离布线 |
| USB-HID 模拟键盘 | 识别为键盘设备 | 模拟键盘击键,输入到当前焦点 | 即插即用,不需要写串口代码 |
| USB 转虚拟串口 | 安装驱动后生成 COM 口 | 和串口一样,字节流按回车分隔 | 最常见的现役扫码枪,写法和串口相同 |
注意表格里的第二行:很多廉价扫码枪出厂默认是 USB-HID 模式,插上就能在记事本里打出条码和回车,但你在设备管理器里根本看不到 COM 口。这时候拿 SerialPort 去枚举端口,列表里自然找不到它。所以选型时的第一准则不是「哪种更好」,而是「你的枪当前是哪种」。
我一般会优先建议用虚拟串口模式写 VB.NET 程序,因为它既保留串口编程的稳定性,又不需要处理全局键盘钩子或焦点抢占问题。如果你手上是 HID 模式的枪,先看说明书里有没有切换模式的配置码,大多数支持切换到虚拟串口或串口模式。
2.2 确认设备模式:两条命令和一个枚举函数
判断当前枪处于哪种模式,不靠猜,直接在系统里查。先打开命令行,执行两条命令之一:
# 列出当前系统占用的 COM 端口 mode# 列出状态正常的串口设备及硬件 ID Get-PnpDevice -Class Ports -Status OK | Select-Object FriendlyName, InstanceId如果mode能看到 COM3、COM4 之类的端口,说明系统已经把它当串口设备识别了,直接用 SerialPort 打开即可。如果Get-PnpDevice里只有蓝牙虚拟 COM 口,没有扫码枪的端口,去设备管理器里找「人体学输入设备」或「键盘设备」,那它就是 HID 模式。
在 VB.NET 里确认可用端口,用这个枚举函数就够了:
Imports System.IO.Ports Public Function GetAllSerialPorts() As List(Of String) Dim ports As New List(Of String) For Each portName As String In SerialPort.GetPortNames() ports.Add(portName) Next Return ports End Function这段代码的作用是把系统当前可用的 COM 口全部列出来。运行后如果列表是空的,而你的扫描枪已经通过 USB 线连到电脑上,基本可以断定它处于 HID 模式,或者虚拟串口驱动没有正确安装。此时先去装驱动或切换模式,不要继续在代码层面死磕。
2.3 串口参数为什么默认 9600,8,N,1
串口通信双方必须用同一套参数才能解析出正确数据。条形码扫描枪出厂默认几乎都是 9600 波特率、8 数据位、无校验、1 停止位,简写为 9600,8,N,1。
| 参数 | 默认值 | 说明 |
|---|---|---|
| 波特率 | 9600 | 部分高端枪支持 115200,需要扫码切换 |
| 数据位 | 8 | 标准 ASCII 数据宽度 |
| 校验位 | None | 大多数扫码枪不启用校验 |
| 停止位 | 1 | 常见配置 |
| 流控制 | None | 少数老设备需要硬件流控 RTS/CTS |
不要小看这组参数。如果扫码枪被别人用配置码改过波特率,你的程序打开端口没问题,但收到的全是乱码。遇到这种情况,先看说明书里的恢复出厂配置码,扫一下把枪重置到 9600,8,N,1,再回来调试程序。记住这一条,能省下大量排查时间。
3. 串口接收核心代码:回车拆包与半包处理的完整写法
进入代码之前,要先理解一个常被忽略的事实:扫描枪发数据不是一次性把整串条码倒进缓冲区,而是像打字机一样逐字节进入串口。接收端必须在「数据还没来完」和「数据已经多来了一条」之间做判断。这一章先给一个最小可运行的接收窗体,再逐步解释参数边界和半包处理。
3.1 最小可运行的串口接收窗体
下面的代码是一个可直接运行的窗体:打开 COM3,等待扫码枪发送数据,每收到一条完整条码就显示到文本框,并触发保存按钮点击事件。
Imports System.IO.Ports Imports System.Text Public Class ScanForm Private WithEvents comPort As SerialPort Private Sub ScanForm_Load(sender As Object, e As EventArgs) Handles MyBase.Load comPort = New SerialPort("COM3", 9600, Parity.None, 8, StopBits.One) comPort.NewLine = vbCr ' 扫描枪默认以回车符结束一条条码 comPort.ReceivedBytesThreshold = 1 comPort.Open() End Sub Private Sub comPort_DataReceived(sender As Object, e As SerialDataReceivedEventArgs) Handles comPort.DataReceived Dim line As String = "" Try line = comPort.ReadLine() Catch ex As Exception Return End Try If String.IsNullOrEmpty(line) Then Return BeginInvoke(New Action(Sub() HandleBarcode(line))) End Sub Private Sub HandleBarcode(barcode As String) txtBarcode.Text = barcode.Trim() btnSave.PerformClick() End Sub End Class这段代码的关键点是NewLine = vbCr。扫码枪向串口发送条码内容后,一般会追加一个回车符(ASCII 13),ReadLine 读到回车就认为一条条码结束了。如果你的枪配置的是回车加换行(CRLF),就把 NewLine 改成vbCrLf,或者在处理条码时统一 Trim 掉控制字符。ReceivedBytesThreshold = 1表示串口收到第一个字节就触发 DataReceived 事件,不代表整条数据已到齐,所以不能在这个事件里直接假设 line 是完整条码。ReadLine 的原理是等到 NewLine 出现才返回,如果枪没有发回车,它会一直等下去,直到 ReceiveTimeout 抛异常。因此 catch 块里不能什么都不做就退出,否则可能丢掉半截数据。
3.2 ReceivedBytesThreshold 与 ReadLine 的边界
很多初学者会把 DataReceived 事件理解成「一条条码到达了」,这是不对的。这个事件只在缓冲区有数据时触发,而且你读走一部分数据后它会再次触发。ReceivedBytesThreshold设成 1 是最灵敏的状态,也最容易出现数据还没读完就触发下一次事件的情况。
ReadLine 的阻塞行为也要理解透:它内部会累积字节,直到遇到你在 NewLine 里指定的字符串才返回。好处是代码简单,坏处是如果扫描枪只发了条码内容但没有回车后缀,ReadLine 会一直阻塞,直到超时。这在实际扫码中偶尔会发生,尤其是枪的后缀被误配置成「无」的时候。
更稳妥的做法是不要把 ReadLine 当作唯一手段。你可以把 ReceiveTimeout 设为 300 毫秒,配合 catch 块读取当前缓冲区里的残留数据,但这样处理超时和半包很别扭。建议直接走下一小节的拆分方案,它不依赖超时,逻辑也更干净。
3.3 半包与粘包:无阻塞拆分方案
所谓半包,就是一条条码的字节还没全部到达,程序就开始读取了;所谓粘包,是两条条码在缓冲区里连在一起,一次读出来。解决这两个问题最可靠的方式是 StringBuilder 缓冲加手动寻找回车符。
Imports System.Text Private sb As New StringBuilder() Private Sub comPort_DataReceived(sender As Object, e As SerialDataReceivedEventArgs) Handles comPort.DataReceived sb.Append(comPort.ReadExisting()) Dim text As String = sb.ToString() Dim pos As Integer Do pos = text.IndexOf(vbCr) If pos < 0 Then Exit Do Dim barcode As String = text.Substring(0, pos).Trim() text = text.Substring(pos + 1) If Not String.IsNullOrEmpty(barcode) Then BeginInvoke(New Action(Sub() HandleBarcode(barcode))) End If Loop sb.Clear() sb.Append(text) ' 剩下的是不完整的半包,留到下一次事件拼接 End Sub这段代码的工作方式是这样的:每次串口有数据进来,先把新读取的字节追加到 sb 里,然后从字符串里查找回车符。找到一个回车符,就把它前面的内容当作一条完整条码取出,并清掉这部分;如果一串数据里有多个回车,就循环拆出多条;如果找不到回车,说明数据还没来完整,保留在 sb 里等待下次事件。这种方式天然解决了半包和粘包,同时不依赖 ReadLine 的超时等待,后台线程不会因为一条异常数据被卡死。
4. 把扫码数据变成业务记录:解析、防抖与入库实现
串口把条码字符串交给 UI 层之后,真正的「源程序」才刚刚开始。你拿到的 barcode 变量里很可能不只是一个干净的条码,还可能带着前缀、后缀、空格、换行,甚至因为编码问题混入肉眼看不见的控制字符。这一章处理数据从字符串到数据库记录之间的全部环节。
4.1 条码数据里的脏字符:前缀、后缀、转义、换行
扫码枪设备上常设置有前缀和后缀参数,比如前缀是 ABC,后缀是回车加换行,程序收到的原始字符串就变成了ABC6901234567890\r\n。另外不同品牌枪对条码中的特殊字符处理也不一样,有些会把 GS、RS 等分隔符直接输出成不可见字符。
处理这类脏数据的第一步是统一清洗:
Private Function CleanBarcode(raw As String) As String Return raw.Replace(vbLf, "").Replace(vbCr, "").Trim() End Function这个函数把所有换行和回车都移除,再做一次 Trim 去掉首尾空格。注意它不处理前缀,因为前缀有时候是业务需要的标识(比如表示「这是托盘条码」),是否保留要看上游系统约定。如果确认不需要前缀,就在扫码枪设置里把前缀清空,而不是在代码里写死 Replace 操作。
4.2 入库前防抖:同一条码被扫两次的过滤逻辑
实际工位上有一个高频场景:同一张条码连续被扫两次,或者操作员扫了一下没听到提示音又补扫一次,系统就会记下两条重复记录。防抖的原理不复杂,记录上一条条码和到达时间,如果短时间内再次收到相同条码,直接丢弃。
Private lastBarcode As String = "" Private lastTime As DateTime = DateTime.MinValue Private Sub SaveBarcode(barcode As String) Dim key As String = CleanBarcode(barcode) If key = "" Then Return If key = lastBarcode AndAlso (Now - lastTime).TotalMilliseconds < 500 Then Return End If lastBarcode = key lastTime = Now ' 这里继续写数据库插入逻辑 End Sub这里的 500 毫秒窗口是个经验值,覆盖「扫一次没反应又立刻补扫」的人为重复,同时不影响正常连续扫两张相同条码的工位。如果你的业务要求同一张条码每次扫码都记录,就把窗口缩小到 100 毫秒,或者干脆去掉这个判断,只保留空码过滤。
4.3 入库与界面联动:SqlClient 示例
数据入库我用 SqlClient 连接 SQL Server 的情况最多,写法如下:
Imports System.Data.SqlClient Private connStr As String = "Data Source=localhost;Initial Catalog=ScanDB;User ID=sa;Password=your_password;" Private Sub InsertScanLog(barcode As String) Using conn As New SqlConnection(connStr) Using cmd As New SqlCommand( "INSERT INTO scan_log(barcode, scan_time) VALUES(@code, GETDATE())", conn) cmd.Parameters.AddWithValue("@code", barcode) conn.Open() cmd.ExecuteNonQuery() End Using End Using End Sub注意两点:一是 SQL 语句必须用参数化写法,条码里如果混入单引号,直接拼接字符串会引发语法错误,甚至注入风险;二是Using语句保证数据库连接和命令对象在调用结束后自动释放,避免连接泄漏。如果扫码频率高、每秒可能入库几十条,建议把 INSERT 放进事务或改用批量插入,否则每次都重连数据库会拖慢扫码节奏。
界面联动方面,通常我会在窗体上放一个 DataGridView,数据源绑定到一个 DataTable。每次收到条码先插入数据库,再往 DataTable 加一行,DataGridView 用BindingSource刷新显示。扫码工位上的操作员能实时看到「当前刚扫的条码 + 时间 + 总数」,比只闪一下文本框要直观得多。
5. 避坑与排查:5 个实测入坑点,条码丢失和 COM 口漂移都在里面
这一章不是理论推演,是扫码枪接入 VB.NET 项目时实实在在踩过的坑。很多问题看起来像玄学,实际上是串口时序和设备模式没对齐。下面五条按「现象 → 原因 → 解决」的顺序整理,每一条都能对应到具体的代码调整或配置操作。
5.1 COM 口漂移:重启之后串口打不开
现象:程序在开发机上运行正常,换到现场工控机或重启电脑后,报端口不存在或打开失败。原因:USB 转虚拟串口设备在不同 USB 口、不同插拔顺序下,Windows 分配的 COM 号会变,今天 COM3,明天 COM4,程序里写死 COM3 就会失联。
解决:不要把端口号写死在代码里,启动时枚举可用端口,让用户在界面上选择,或者按硬件 ID 自动匹配。最省事的做法是在窗体上加一个 ComboBox,加载时用SerialPort.GetPortNames()填充,默认选中第一个可用端口。现场运维人员不需要懂代码,只要在下拉框里看到自己的枪对应的 COM 口,选一下就行。
5.2 跨线程 UI 崩:DataReceived 里不能直接写控件
现象:扫码第一下正常,第二下程序直接卡死或弹出「线程间操作无效」异常。原因:DataReceived 事件在后台线程执行,后台线程直接修改 TextBox 或 Button 的 Text 属性,触发了跨线程访问保护。
解决:所有 UI 更新都通过BeginInvoke或Invoke调度到 UI 线程执行。前面 3.1 和 3.3 里的写法已经用BeginInvoke(New Action(Sub() ...))包了一层,这是标准做法。还要注意一点:在 HandleBarcode 里不要再调用串口对象的读取方法,UI 线程与后台线程交错读写同一个缓冲区,会引发新的数据错乱。
5.3 快速扫码丢数据:ReadLine 超时而不是数据丢失
现象:单张扫没问题,两张连着快速扫时,第二张偶尔没反应。原因:常见于使用 ReadLine 的方案。当第二条数据到达时,后台线程可能还阻塞在第一次 ReadLine 的超时等待中,数据到了没人读,缓冲区被覆盖或事件被吞掉。
解决:改用 3.3 的 StringBuilder 方案,它不依赖 ReadLine 的超时行为,只要 DataReceived 事件触发就去ReadExisting读走全部当前数据,逻辑上不会因为线程阻塞丢字节。另外一个常见辅助办法是把ReceivedBytesThreshold调大一点,比如设成条码长度的 70%,减少碎片事件,但这个数字需要实测调整,不是万能。
5.4 条码莫名其妙的脏字符:前缀后缀与编码
现象:在记事本里扫码显示正常,进 VB.NET 程序后条码前面多了几个字符,或者尾随一个不认识的方块符号。原因:记事本的显示会忽略不可见控制字符,而程序里拿到的是原始字节流。最常见的是扫码枪设置了前缀(比如 STX 字符)或后缀被配成了 CRLF,还有一个可能是串口接收时使用了错误的编码,把高位 ASCII 字节解析成了乱码。
解决:先看原始字节,不要直接看字符串。把收到的内容用Encoding.ASCII或Encoding.UTF8显式转换,再按 4.1 的 CleanBarcode 统一过滤。前缀方面,进扫码枪设置菜单把前缀清零;如果前缀是业务必需的,就在解析代码里加规则:以某个固定字符开头才接受,否则丢弃并记日志,方便定位是哪个环节引入的脏数据。
5.5 端口被占:第二个实例打开同一 COM 口
现象:程序异常退出后重新运行,提示端口被占用或 Access Denied。原因:进程异常终止时,SerialPort 对象没有走正常的 Close 流程,端口句柄被系统保留,新实例自然打开失败。还有一种情况是开发时启动了两个调试实例,两个进程同时抢同一个 COM 口。
解决:代码里给 SerialPort 的关闭加 Try/Finally 保护,确保异常也执行 Dispose。调试时如果出现端口被占,打开任务管理器结束残留进程,而不是反复重启程序。想彻底避免多开,用 Mutex 做单实例控制,第二个实例启动时直接提示已有程序在运行并退出。
6. 进阶技巧:自动重连、全局焦点与提示音
主流程跑通之后,现场使用体验才是决定一个源程序能不能真正落地的东西。仓库和车间的环境多变,USB 线被碰松、电脑休眠唤醒后串口失效、嘈杂环境下操作员听不到扫码成功的声音,这些问题都需要在程序层面补上最后一层。
6.1 自动重连:端口断开后程序不退出
用一个 Timer 定时检查串口状态,如果断开就尝试重新打开:
Private Sub TimerReconnect_Tick(sender As Object, e As EventArgs) Handles TimerReconnect.Tick If comPort IsNot Nothing AndAlso Not comPort.IsOpen Then Try comPort.Open() statusLabel.Text = "串口已重新连接" Catch ex As Exception statusLabel.Text = "等待重连..." End Try End If End Sub检查周期设 3 秒比较合适,太频繁会频繁尝试打开端口导致 CPU 占用,太长则扫码枪恢复后要等很久才能用。现场碰掉 USB 线再插上,程序会在 3 秒内自动恢复。
6.2 扫码完成的三重反馈:声音、颜色、状态栏
代码里在 HandleBarcode 的末尾加三行反馈逻辑:
System.Media.SystemSounds.Asterisk.Play() txtBarcode.BackColor = Color.LightGreen statusLabel.Text = $"已扫码:{barcode},总数:{totalCount}"声音解决操作员不盯屏幕的问题,颜色变化解决「这条码到底有没有进系统」的即时确认,状态栏记录总数方便管理人员扫一眼就知道进度。这三样配合起来,比单纯弹 MessageBox 更不容易打断连续扫码节奏。
6.3 键盘模拟模式下的焦点自动填入
如果你最终不得不使用 USB-HID 模式的枪,不需要串口代码,但要处理焦点问题。把窗体 KeyPreview 设为 True,然后在 KeyPress 事件里判断回车:
Private Sub ScanForm_KeyPress(sender As Object, e As KeyPressEventArgs) Handles Me.KeyPress If e.KeyChar = Chr(13) Then btnSave.PerformClick() e.Handled = True End If End Sub注意 HID 模式下数据直接进当前焦点控件,如果你的焦点在按钮上,回车会触发按钮点击而不是触发这个事件,所以要么让焦点始终停在一个文本框,要么在事件里先判断焦点控件类型。最省心的还是让枪切到虚拟串口模式,回到串口代码这套稳定方案。
把扫描枪接到 VB.NET 工程里这件事,说穿了就是管好那根看不见的数据线:接入模式定生死,回车符是天然的条码分隔线,半包处理是稳定性的核心,UI 和入库只是把数据送到该去的地方。我从最早被换行符和 COM 口漂移折腾到半夜,到现在的固定套路只有一句话:串口只管收,后台只管拆,界面只管显示,业务只管入库,层与层之间的坑各自在自己的边界内解决。希望帮到你。
本文还有配套的精品资源,点击获取