C# USB条码扫描器:构建完整的扫描应用程序
USB 条形码扫描器作为标准键盘输入设备连接到 C# 应用程序,将扫描的数据作为键入的字符发送,然后按 Enter 键。 这种 HID 键盘楔形行为使集成变得简单——您的应用程序无需任何特殊驱动程序或 SDK 即可接收文本输入。 IronBarcode处理原始输入以验证格式、提取结构化数据并生成响应条形码,将简单的扫描事件转化为库存管理、零售销售点和物流跟踪系统的完整数据管道。
零售、仓储和制造运营都依赖于准确、快速的条形码扫描。 当开发人员将 USB 扫描仪连接到 Windows Forms 或 WPF 应用程序时,扫描仪的行为与键盘完全相同——数据会显示在文本框中,按下 Enter 键表示已接收到完整的条形码。 真正的挑战不在于如何获取数据; 它正在正确处理。 IronBarcode 的条形码验证功能可检查格式完整性,提取批号或 GS1 应用标识符等字段,并可立即生成新的条形码作为响应。
本指南将逐步引导您构建一个可用于生产环境的 C# USB 条码扫描器应用程序。您将安装库、捕获扫描器输入、验证条码格式、生成响应标签,并组装一个高吞吐量的基于队列的处理器。 每个部分都包含完整的、可运行的代码,目标框架为.NET 10,并在适当情况下采用顶级语句样式。
如何使用 C# 实现 USB 条形码扫描器?
为什么 HID 键盘楔形模式能够简化集成?
大多数 USB 条码扫描器出厂时默认配置为 HID 键盘楔形模式。 当您将 USB 设备插入 Windows 电脑时,操作系统会将其注册为 USB 存储设备(用于配置)和键盘(用于数据输入)。 当扫描条形码时,设备会将解码后的条形码值转换为按键,并将其发送到当前具有焦点的应用程序窗口,并在末尾添加回车符。
从 C# 开发人员的角度来看,这意味着您不需要供应商 SDK、COM 库或特殊的 USB API。只需一个带有 KeyDown 事件处理程序的标准 TextBox 即可捕获输入。 主要的集成挑战在于区分扫描仪输入和真正的键盘输入。 扫描器通常会在很短的时间内(通常不到 50 毫秒)完成所有字符的扫描,而人类打字则会将击键过程分散到数百毫秒内。 控制连击时间是过滤掉意外按键的可靠方法。
专业级扫描仪还支持串行(RS-232 或虚拟 COM 端口)和直接 USB HID 模式,让您可以更好地控制前缀/后缀字符和扫描触发器。 下面这种接口模式可以处理这两种情况:
public interface IScannerInput
{
event EventHandler<string> BarcodeScanned;
void StartListening();
void StopListening();
}
public class KeyboardWedgeScanner : IScannerInput
{
public event EventHandler<string> BarcodeScanned;
private readonly TextBox _inputBox;
private readonly System.Windows.Forms.Timer _burstTimer;
private readonly System.Text.StringBuilder _buffer = new();
public KeyboardWedgeScanner(TextBox inputBox)
{
_inputBox = inputBox;
_burstTimer = new System.Windows.Forms.Timer { Interval = 80 };
_burstTimer.Tick += OnBurstTimeout;
_inputBox.KeyPress += OnKeyPress;
}
private void OnKeyPress(object sender, KeyPressEventArgs e)
{
if (e.KeyChar == (char)Keys.Enter)
{
_burstTimer.Stop();
string value = _buffer.ToString().Trim();
_buffer.Clear();
if (value.Length > 0)
BarcodeScanned?.Invoke(this, value);
}
else
{
_buffer.Append(e.KeyChar);
_burstTimer.Stop();
_burstTimer.Start();
}
e.Handled = true;
}
private void OnBurstTimeout(object sender, EventArgs e)
{
_burstTimer.Stop();
_buffer.Clear(); // incomplete burst -- discard
}
public void StartListening() => _inputBox.Focus();
public void StopListening() => _inputBox.Enabled = false;
}
public class SerialPortScanner : IScannerInput
{
public event EventHandler<string> BarcodeScanned;
private readonly System.IO.Ports.SerialPort _port;
public SerialPortScanner(string portName, int baudRate = 9600)
{
_port = new System.IO.Ports.SerialPort(portName, baudRate);
_port.DataReceived += OnDataReceived;
}
private void OnDataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e)
{
string data = _port.ReadLine().Trim();
if (data.Length > 0)
BarcodeScanned?.Invoke(this, data);
}
public void StartListening() => _port.Open();
public void StopListening() => _port.Close();
}
public interface IScannerInput
{
event EventHandler<string> BarcodeScanned;
void StartListening();
void StopListening();
}
public class KeyboardWedgeScanner : IScannerInput
{
public event EventHandler<string> BarcodeScanned;
private readonly TextBox _inputBox;
private readonly System.Windows.Forms.Timer _burstTimer;
private readonly System.Text.StringBuilder _buffer = new();
public KeyboardWedgeScanner(TextBox inputBox)
{
_inputBox = inputBox;
_burstTimer = new System.Windows.Forms.Timer { Interval = 80 };
_burstTimer.Tick += OnBurstTimeout;
_inputBox.KeyPress += OnKeyPress;
}
private void OnKeyPress(object sender, KeyPressEventArgs e)
{
if (e.KeyChar == (char)Keys.Enter)
{
_burstTimer.Stop();
string value = _buffer.ToString().Trim();
_buffer.Clear();
if (value.Length > 0)
BarcodeScanned?.Invoke(this, value);
}
else
{
_buffer.Append(e.KeyChar);
_burstTimer.Stop();
_burstTimer.Start();
}
e.Handled = true;
}
private void OnBurstTimeout(object sender, EventArgs e)
{
_burstTimer.Stop();
_buffer.Clear(); // incomplete burst -- discard
}
public void StartListening() => _inputBox.Focus();
public void StopListening() => _inputBox.Enabled = false;
}
public class SerialPortScanner : IScannerInput
{
public event EventHandler<string> BarcodeScanned;
private readonly System.IO.Ports.SerialPort _port;
public SerialPortScanner(string portName, int baudRate = 9600)
{
_port = new System.IO.Ports.SerialPort(portName, baudRate);
_port.DataReceived += OnDataReceived;
}
private void OnDataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e)
{
string data = _port.ReadLine().Trim();
if (data.Length > 0)
BarcodeScanned?.Invoke(this, data);
}
public void StartListening() => _port.Open();
public void StopListening() => _port.Close();
}
Imports System
Imports System.Text
Imports System.Windows.Forms
Imports System.IO.Ports
Public Interface IScannerInput
Event BarcodeScanned As EventHandler(Of String)
Sub StartListening()
Sub StopListening()
End Interface
Public Class KeyboardWedgeScanner
Implements IScannerInput
Public Event BarcodeScanned As EventHandler(Of String) Implements IScannerInput.BarcodeScanned
Private ReadOnly _inputBox As TextBox
Private ReadOnly _burstTimer As Timer
Private ReadOnly _buffer As New StringBuilder()
Public Sub New(inputBox As TextBox)
_inputBox = inputBox
_burstTimer = New Timer With {.Interval = 80}
AddHandler _burstTimer.Tick, AddressOf OnBurstTimeout
AddHandler _inputBox.KeyPress, AddressOf OnKeyPress
End Sub
Private Sub OnKeyPress(sender As Object, e As KeyPressEventArgs)
If e.KeyChar = ChrW(Keys.Enter) Then
_burstTimer.Stop()
Dim value As String = _buffer.ToString().Trim()
_buffer.Clear()
If value.Length > 0 Then
RaiseEvent BarcodeScanned(Me, value)
End If
Else
_buffer.Append(e.KeyChar)
_burstTimer.Stop()
_burstTimer.Start()
End If
e.Handled = True
End Sub
Private Sub OnBurstTimeout(sender As Object, e As EventArgs)
_burstTimer.Stop()
_buffer.Clear() ' incomplete burst -- discard
End Sub
Public Sub StartListening() Implements IScannerInput.StartListening
_inputBox.Focus()
End Sub
Public Sub StopListening() Implements IScannerInput.StopListening
_inputBox.Enabled = False
End Sub
End Class
Public Class SerialPortScanner
Implements IScannerInput
Public Event BarcodeScanned As EventHandler(Of String) Implements IScannerInput.BarcodeScanned
Private ReadOnly _port As SerialPort
Public Sub New(portName As String, Optional baudRate As Integer = 9600)
_port = New SerialPort(portName, baudRate)
AddHandler _port.DataReceived, AddressOf OnDataReceived
End Sub
Private Sub OnDataReceived(sender As Object, e As SerialDataReceivedEventArgs)
Dim data As String = _port.ReadLine().Trim()
If data.Length > 0 Then
RaiseEvent BarcodeScanned(Me, data)
End If
End Sub
Public Sub StartListening() Implements IScannerInput.StartListening
_port.Open()
End Sub
Public Sub StopListening() Implements IScannerInput.StopListening
_port.Close()
End Sub
End Class
键盘楔形实现中的突发计时器是关键细节。 每次按键都会重置,并且只有在字符停止输入时才会触发——这意味着打字速度较慢的真正键盘用户,其未完成的输入将被丢弃,而不是被当作条形码扫描处理。
如何处理多个扫描仪品牌?
Enterprise环境中经常在同一楼层混合运行 Honeywell、Zebra(以前称为 Symbol/Motorola)和 Datalogic 扫描仪。 每个厂商都有自己的默认终止符、波特率和前缀/后缀约定。 配置模型使您的应用程序具有灵活性:
public class ScannerConfiguration
{
public string ScannerType { get; set; } = "KeyboardWedge";
public string PortName { get; set; } = "COM3";
public int BaudRate { get; set; } = 9600;
public string Terminator { get; set; } = "\r\n";
public bool EnableBeep { get; set; } = true;
public Dictionary<string, string> BrandSettings { get; set; } = new();
public static ScannerConfiguration GetHoneywellConfig() => new()
{
ScannerType = "Serial",
BaudRate = 115200,
BrandSettings = new Dictionary<string, string>
{
{ "Prefix", "STX" },
{ "Suffix", "ETX" },
{ "TriggerMode", "Manual" }
}
};
public static ScannerConfiguration GetZebraConfig() => new()
{
ScannerType = "KeyboardWedge",
BrandSettings = new Dictionary<string, string>
{
{ "ScanMode", "Continuous" },
{ "BeepVolume", "High" }
}
};
}
public class ScannerConfiguration
{
public string ScannerType { get; set; } = "KeyboardWedge";
public string PortName { get; set; } = "COM3";
public int BaudRate { get; set; } = 9600;
public string Terminator { get; set; } = "\r\n";
public bool EnableBeep { get; set; } = true;
public Dictionary<string, string> BrandSettings { get; set; } = new();
public static ScannerConfiguration GetHoneywellConfig() => new()
{
ScannerType = "Serial",
BaudRate = 115200,
BrandSettings = new Dictionary<string, string>
{
{ "Prefix", "STX" },
{ "Suffix", "ETX" },
{ "TriggerMode", "Manual" }
}
};
public static ScannerConfiguration GetZebraConfig() => new()
{
ScannerType = "KeyboardWedge",
BrandSettings = new Dictionary<string, string>
{
{ "ScanMode", "Continuous" },
{ "BeepVolume", "High" }
}
};
}
Option Strict On
Public Class ScannerConfiguration
Public Property ScannerType As String = "KeyboardWedge"
Public Property PortName As String = "COM3"
Public Property BaudRate As Integer = 9600
Public Property Terminator As String = vbCrLf
Public Property EnableBeep As Boolean = True
Public Property BrandSettings As Dictionary(Of String, String) = New Dictionary(Of String, String)()
Public Shared Function GetHoneywellConfig() As ScannerConfiguration
Return New ScannerConfiguration() With {
.ScannerType = "Serial",
.BaudRate = 115200,
.BrandSettings = New Dictionary(Of String, String) From {
{"Prefix", "STX"},
{"Suffix", "ETX"},
{"TriggerMode", "Manual"}
}
}
End Function
Public Shared Function GetZebraConfig() As ScannerConfiguration
Return New ScannerConfiguration() With {
.ScannerType = "KeyboardWedge",
.BrandSettings = New Dictionary(Of String, String) From {
{"ScanMode", "Continuous"},
{"BeepVolume", "High"}
}
}
End Function
End Class
将这些配置存储在设置文件或数据库中,意味着仓库工作人员可以更换扫描仪型号而无需重新部署。 ScannerType 字段决定在启动时实例化哪个 IScannerInput 实现。
如何在 C# 项目中安装IronBarcode ?
通过NuGet添加IronBarcode的最快方法是什么?
在 Visual Studio 中打开软件包管理器控制台并运行:
Install-Package IronBarCode
Install-Package IronBarCode
或者,使用.NET CLI:
dotnet add package IronBarCode
dotnet add package IronBarCode
这两条命令都会从NuGet获取最新版本,并将程序集引用添加到您的项目文件中。该库面向.NET Standard 2.0,因此无需任何额外的兼容性补丁即可在.NET Framework 4.6.2 到.NET 10 上运行。
安装完成后,请在调用任何IronBarcode方法之前设置您的许可证密钥。 用于开发和评估的免费试用密钥可从IronBarcode许可页面获取:
IronBarCode.License.LicenseKey = "YOUR-LICENSE-KEY";
IronBarCode.License.LicenseKey = "YOUR-LICENSE-KEY";
Imports IronBarCode
IronBarCode.License.LicenseKey = "YOUR-LICENSE-KEY"
对于容器化部署, IronBarcode可与 Linux 上的 Docker 配合使用;对于云函数,它支持AWS Lambda和Azure Functions 。
如何使用IronBarcode验证扫描的条形码?
格式验证的正确方法是什么?
IronBarcode支持 30 多种条形码符号体系,包括Code 128 、 EAN-13 、 Code 39 、 QR 码和Data Matrix 。 对于 USB 扫描器应用,验证模式会将扫描的字符串重新编码为条形码图像,并立即通过解码器将其读取回来。 这次往返验证确认该字符串是声明格式的有效值:
public class BarcodeValidator
{
public async Task<ValidationResult> ValidateAsync(string scannedText, BarcodeEncoding preferredFormat = BarcodeEncoding.Code128)
{
var result = new ValidationResult { RawInput = scannedText };
try
{
var barcode = BarcodeWriter.CreateBarcode(scannedText, preferredFormat);
var readResults = await BarcodeReader.ReadAsync(barcode.ToBitmap());
if (readResults.Any())
{
var first = readResults.First();
result.IsValid = true;
result.Format = first.BarcodeType;
result.Value = first.Value;
result.Confidence = first.Confidence;
}
else
{
result.IsValid = false;
result.Error = "No barcode could be decoded from the scanned input.";
}
}
catch (Exception ex)
{
result.IsValid = false;
result.Error = ex.Message;
}
return result;
}
}
public record ValidationResult
{
public string RawInput { get; init; } = "";
public bool IsValid { get; set; }
public BarcodeEncoding Format { get; set; }
public string Value { get; set; } = "";
public float Confidence { get; set; }
public string Error { get; set; } = "";
}
public class BarcodeValidator
{
public async Task<ValidationResult> ValidateAsync(string scannedText, BarcodeEncoding preferredFormat = BarcodeEncoding.Code128)
{
var result = new ValidationResult { RawInput = scannedText };
try
{
var barcode = BarcodeWriter.CreateBarcode(scannedText, preferredFormat);
var readResults = await BarcodeReader.ReadAsync(barcode.ToBitmap());
if (readResults.Any())
{
var first = readResults.First();
result.IsValid = true;
result.Format = first.BarcodeType;
result.Value = first.Value;
result.Confidence = first.Confidence;
}
else
{
result.IsValid = false;
result.Error = "No barcode could be decoded from the scanned input.";
}
}
catch (Exception ex)
{
result.IsValid = false;
result.Error = ex.Message;
}
return result;
}
}
public record ValidationResult
{
public string RawInput { get; init; } = "";
public bool IsValid { get; set; }
public BarcodeEncoding Format { get; set; }
public string Value { get; set; } = "";
public float Confidence { get; set; }
public string Error { get; set; } = "";
}
Imports System
Imports System.Linq
Imports System.Threading.Tasks
Public Class BarcodeValidator
Public Async Function ValidateAsync(scannedText As String, Optional preferredFormat As BarcodeEncoding = BarcodeEncoding.Code128) As Task(Of ValidationResult)
Dim result As New ValidationResult With {.RawInput = scannedText}
Try
Dim barcode = BarcodeWriter.CreateBarcode(scannedText, preferredFormat)
Dim readResults = Await BarcodeReader.ReadAsync(barcode.ToBitmap())
If readResults.Any() Then
Dim first = readResults.First()
result.IsValid = True
result.Format = first.BarcodeType
result.Value = first.Value
result.Confidence = first.Confidence
Else
result.IsValid = False
result.Error = "No barcode could be decoded from the scanned input."
End If
Catch ex As Exception
result.IsValid = False
result.Error = ex.Message
End Try
Return result
End Function
End Class
Public Class ValidationResult
Public Property RawInput As String = ""
Public Property IsValid As Boolean
Public Property Format As BarcodeEncoding
Public Property Value As String = ""
Public Property Confidence As Single
Public Property Error As String = ""
End Class
对于供应链应用中使用的GS1-128 条形码,扫描的字符串包含括号中的应用标识符前缀,例如 GTIN 的 (01) 和到期日期的 (17)。 当您指定 BarcodeEncoding.GS1_128 时,IronBarcode 会自动解析这些应用程序标识符。
开发人员应该实现哪种EAN-13校验和逻辑?
零售POS应用通常需要在将EAN-13值传递给定价查询之前,独立验证EAN-13校验位。EAN-13的Luhn式校验和在前12位数字中交替使用权重1和3:
public static bool ValidateEan13Checksum(string value)
{
if (value.Length != 13 || !value.All(char.IsDigit))
return false;
int sum = 0;
for (int i = 0; i < 12; i++)
{
int digit = value[i] - '0';
sum += (i % 2 == 0) ? digit : digit * 3;
}
int expectedCheck = (10 - (sum % 10)) % 10;
return expectedCheck == (value[12] - '0');
}
public static bool ValidateEan13Checksum(string value)
{
if (value.Length != 13 || !value.All(char.IsDigit))
return false;
int sum = 0;
for (int i = 0; i < 12; i++)
{
int digit = value[i] - '0';
sum += (i % 2 == 0) ? digit : digit * 3;
}
int expectedCheck = (10 - (sum % 10)) % 10;
return expectedCheck == (value[12] - '0');
}
Public Shared Function ValidateEan13Checksum(value As String) As Boolean
If value.Length <> 13 OrElse Not value.All(AddressOf Char.IsDigit) Then
Return False
End If
Dim sum As Integer = 0
For i As Integer = 0 To 11
Dim digit As Integer = AscW(value(i)) - AscW("0"c)
sum += If(i Mod 2 = 0, digit, digit * 3)
Next
Dim expectedCheck As Integer = (10 - (sum Mod 10)) Mod 10
Return expectedCheck = (AscW(value(12)) - AscW("0"c))
End Function
这种纯逻辑检查在编码之前运行,以避免在高容量零售环境中每次扫描时产生往返图像生成的开销。 根据GS1 规范,当去掉前导零时,UPC-A(12 位数字)的校验位算法是相同的。
如何根据扫描的输入生成响应条形码?
应用程序何时应该在扫描后创建新条形码?
仓库收货中常见的模式是"扫描和重新贴标签"工作流程:入库物品带有供应商条形码(通常是 EAN-13 或 ITF-14),仓库管理系统需要打印一个带有其自身位置和批号的内部 Code 128 标签。 IronBarcode 的生成功能只需几行代码即可完成:
public class InventoryLabelGenerator
{
private readonly string _outputDirectory;
public InventoryLabelGenerator(string outputDirectory)
{
_outputDirectory = outputDirectory;
Directory.CreateDirectory(_outputDirectory);
}
public async Task<string> GenerateLabelAsync(string internalCode, string locationCode)
{
string fullCode = $"{internalCode}|{locationCode}|{DateTime.UtcNow:yyyyMMdd}";
// Primary Code 128 label for scanners
var linearBarcode = BarcodeWriter.CreateBarcode(fullCode, BarcodeEncoding.Code128);
linearBarcode.ResizeTo(500, 140);
linearBarcode.SetMargins(12);
linearBarcode.AddAnnotationTextAboveBarcode(fullCode);
linearBarcode.ChangeBarCodeColor(IronSoftware.Drawing.Color.Black);
// QR code companion for mobile apps
var qrCode = BarcodeWriter.CreateQrCode(fullCode);
qrCode.ResizeTo(200, 200);
qrCode.SetMargins(8);
string timestamp = DateTime.UtcNow.ToString("yyyyMMddHHmmss");
string pngPath = Path.Combine(_outputDirectory, $"{internalCode}_{timestamp}.png");
string pdfPath = Path.Combine(_outputDirectory, $"{internalCode}_{timestamp}.pdf");
await Task.Run(() =>
{
linearBarcode.SaveAsPng(pngPath);
linearBarcode.SaveAsPdf(pdfPath);
});
return pngPath;
}
}
public class InventoryLabelGenerator
{
private readonly string _outputDirectory;
public InventoryLabelGenerator(string outputDirectory)
{
_outputDirectory = outputDirectory;
Directory.CreateDirectory(_outputDirectory);
}
public async Task<string> GenerateLabelAsync(string internalCode, string locationCode)
{
string fullCode = $"{internalCode}|{locationCode}|{DateTime.UtcNow:yyyyMMdd}";
// Primary Code 128 label for scanners
var linearBarcode = BarcodeWriter.CreateBarcode(fullCode, BarcodeEncoding.Code128);
linearBarcode.ResizeTo(500, 140);
linearBarcode.SetMargins(12);
linearBarcode.AddAnnotationTextAboveBarcode(fullCode);
linearBarcode.ChangeBarCodeColor(IronSoftware.Drawing.Color.Black);
// QR code companion for mobile apps
var qrCode = BarcodeWriter.CreateQrCode(fullCode);
qrCode.ResizeTo(200, 200);
qrCode.SetMargins(8);
string timestamp = DateTime.UtcNow.ToString("yyyyMMddHHmmss");
string pngPath = Path.Combine(_outputDirectory, $"{internalCode}_{timestamp}.png");
string pdfPath = Path.Combine(_outputDirectory, $"{internalCode}_{timestamp}.pdf");
await Task.Run(() =>
{
linearBarcode.SaveAsPng(pngPath);
linearBarcode.SaveAsPdf(pdfPath);
});
return pngPath;
}
}
Imports System.IO
Imports System.Threading.Tasks
Public Class InventoryLabelGenerator
Private ReadOnly _outputDirectory As String
Public Sub New(outputDirectory As String)
_outputDirectory = outputDirectory
Directory.CreateDirectory(_outputDirectory)
End Sub
Public Async Function GenerateLabelAsync(internalCode As String, locationCode As String) As Task(Of String)
Dim fullCode As String = $"{internalCode}|{locationCode}|{DateTime.UtcNow:yyyyMMdd}"
' Primary Code 128 label for scanners
Dim linearBarcode = BarcodeWriter.CreateBarcode(fullCode, BarcodeEncoding.Code128)
linearBarcode.ResizeTo(500, 140)
linearBarcode.SetMargins(12)
linearBarcode.AddAnnotationTextAboveBarcode(fullCode)
linearBarcode.ChangeBarCodeColor(IronSoftware.Drawing.Color.Black)
' QR code companion for mobile apps
Dim qrCode = BarcodeWriter.CreateQrCode(fullCode)
qrCode.ResizeTo(200, 200)
qrCode.SetMargins(8)
Dim timestamp As String = DateTime.UtcNow.ToString("yyyyMMddHHmmss")
Dim pngPath As String = Path.Combine(_outputDirectory, $"{internalCode}_{timestamp}.png")
Dim pdfPath As String = Path.Combine(_outputDirectory, $"{internalCode}_{timestamp}.pdf")
Await Task.Run(Sub()
linearBarcode.SaveAsPng(pngPath)
linearBarcode.SaveAsPdf(pdfPath)
End Sub)
Return pngPath
End Function
End Class
将文件保存为 PDF 格式对于可以通过网络共享接收 PDF 输入的标签打印机来说尤其有用。 您还可以导出为 SVG 格式以获得矢量质量的热敏标签输出,或者导出为字节流以直接发送到标签打印机 API。
IronBarcode支持广泛的样式自定义,包括自定义颜色、边距调整、人可读文本叠加,以及为二维码嵌入徽标,以制作品牌标记的移动标签。

