Skip to footer content
USING IRONBARCODE

C# USB Barcode Scanner: Build a Complete Scanning Application

USB barcode scanners connect to C# applications as standard keyboard input devices, sending scanned data as typed characters followed by an Enter keystroke. This HID keyboard wedge behavior makes integration straightforward -- your application receives text input without any special driver or SDK required. IronBarcode processes that raw input to validate formats, extract structured data, and generate response barcodes, turning a simple scan event into a complete data pipeline for inventory management, retail point-of-sale, and logistics tracking systems.

Retail, warehousing, and manufacturing operations all depend on accurate, fast barcode scanning. When a developer connects a USB scanner to a Windows Forms or WPF application, the scanner behaves identically to a keyboard -- the data arrives in a TextBox, and pressing Enter signals that a full barcode has been received. The challenge is not capturing the data; it is processing it correctly. IronBarcode's barcode validation checks format integrity, extracts fields like batch numbers or GS1 application identifiers, and can immediately generate a new barcode in response.

This guide walks through building a production-ready C# USB barcode scanner application step by step. You will install the library, capture scanner input, validate barcode formats, generate response labels, and assemble a high-volume queue-based processor. Each section includes complete, runnable code targeting .NET 10 with top-level statement style where appropriate.

How Do USB Barcode Scanners Work with C#?

Why Does HID Keyboard Wedge Mode Simplify Integration?

Most USB barcode scanners ship configured in HID keyboard wedge mode by default. When you plug one into a Windows machine, the operating system registers it as both a USB storage device (for configuration) and a keyboard (for data input). When a barcode is scanned, the device translates the decoded barcode value into keystrokes and sends them to whatever application window has focus, appending a carriage return at the end.

From a C# developer's perspective, this means you do not need vendor SDKs, COM libraries, or special USB APIs. A standard TextBox with a KeyDown handler is all that is needed to capture input. The main integration challenge is distinguishing scanner input from genuine keyboard typing. Scanners typically deliver all characters within a very short burst -- often under 50 milliseconds -- while human typing spreads keystrokes over hundreds of milliseconds. Timing the burst is a reliable way to filter out accidental keystrokes.

Professional-grade scanners also support serial (RS-232 or virtual COM port) and direct USB HID modes, which give you more control over prefix/suffix characters and scan triggers. The interface pattern below handles both cases:

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
$vbLabelText   $csharpLabel

The burst timer on the keyboard wedge implementation is the key detail. It resets on every keystroke and only fires if characters stop arriving -- meaning genuine keyboard users who type slowly will have their incomplete input discarded rather than treated as a barcode scan.

How Do You Handle Multiple Scanner Brands?

Enterprise environments frequently run a mix of Honeywell, Zebra (formerly Symbol/Motorola), and Datalogic scanners on the same floor. Each vendor has its own default terminator characters, baud rates, and prefix/suffix conventions. A configuration model keeps your application flexible:

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
$vbLabelText   $csharpLabel

Storing these configurations in a settings file or database means warehouse staff can swap scanner models without requiring a redeployment. The ScannerType field drives which IScannerInput implementation gets instantiated at startup.

How Do You Install IronBarcode in a C# Project?

What Is the Fastest Way to Add IronBarcode via NuGet?

Open the Package Manager Console in Visual Studio and run:

Install-Package IronBarCode
Install-Package IronBarCode
SHELL

Alternatively, use the .NET CLI:

dotnet add package IronBarCode
dotnet add package IronBarCode
SHELL

Both commands pull the current release from NuGet.org and add the assembly reference to your project file. The library targets .NET Standard 2.0, so it works on .NET Framework 4.6.2 through .NET 10 without any additional compatibility shims.

After installing, set your license key before calling any IronBarcode method. For development and evaluation, a free trial key is available from the IronBarcode licensing page:

IronBarCode.License.LicenseKey = "YOUR-LICENSE-KEY";
IronBarCode.License.LicenseKey = "YOUR-LICENSE-KEY";
Imports IronBarCode

IronBarCode.License.LicenseKey = "YOUR-LICENSE-KEY"
$vbLabelText   $csharpLabel

For containerized deployments, IronBarcode works with Docker on Linux, and for cloud functions, it supports AWS Lambda and Azure Functions.

How Do You Validate Scanned Barcodes with IronBarcode?

What Is the Right Approach for Format Verification?

IronBarcode supports over 30 barcode symbologies including Code 128, EAN-13, Code 39, QR codes, and Data Matrix. For USB scanner applications, the validation pattern re-encodes the scanned string as a barcode image and immediately reads it back through the decoder. This round-trip confirms that the string is a valid value for the declared format:

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
$vbLabelText   $csharpLabel

For GS1-128 barcodes used in supply chain applications, the scanned string includes application identifier prefixes in parentheses such as (01) for GTIN and (17) for expiry date. IronBarcode parses these application identifiers automatically when you specify BarcodeEncoding.GS1_128.

Which EAN-13 Checksum Logic Should Developers Implement?

Retail point-of-sale applications often need to verify EAN-13 check digits independently before passing the value to a pricing lookup. The Luhn-style checksum for EAN-13 alternates weights of 1 and 3 across the first 12 digits:

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
$vbLabelText   $csharpLabel

This pure-logic check runs before encoding to avoid the overhead of round-trip image generation for every scan in a high-volume retail environment. According to the GS1 specification, the check digit algorithm is identical for UPC-A (12 digits) when you drop the leading zero.

How Do You Generate Response Barcodes from Scanned Input?

When Should an Application Create New Barcodes After a Scan?

A common pattern in warehouse receiving is the "scan and relabel" workflow: an incoming item carries a supplier barcode (often EAN-13 or ITF-14), and the warehouse management system needs to print an internal Code 128 label with its own location and lot codes. IronBarcode's generation capabilities handle this in a few lines:

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
$vbLabelText   $csharpLabel

Saving as PDF is especially useful for label printers that accept PDF input over a network share. You can also export as SVG for vector-quality thermal label output, or export as a byte stream to send directly to a label printer API.

IronBarcode supports extensive style customization including custom colors, margin adjustments, human-readable text overlays, and for QR codes, logo embedding for brand-marked mobile labels.

Windows Forms application interface demonstrating IronBarcode's dual barcode generation capabilities. The interface shows successful generation of both a Code 128 linear barcode and QR code for inventory number 'INV-20250917-helloworld'. The input field at the top allows users to enter custom inventory codes, with a 'Generate' button to create the barcodes. The success message 'Item processed successfully - Generated Labels' confirms the operation completed. The Code 128 barcode is labeled as the primary inventory tracking format, while the QR code below is marked as a mobile-friendly alternative. The application uses a professional gray background with clear visual hierarchy, demonstrating how IronBarcode enables developers to create multi-format barcode generation systems for complete inventory management.

How Do You Build a Complete High-Volume Scanning Application?

What Does a Production Queue-Based Implementation Look Like?

For applications that process dozens of scans per minute, a simple synchronous handler on the UI thread becomes a bottleneck. The pattern below decouples scan capture from processing using a ConcurrentQueue<T> and a background processing loop. IronBarcode's async API handles the validation without blocking the UI:

using IronBarCode;
using System.Collections.Concurrent;

public partial class HighVolumeScanner : Form
{
    private readonly ConcurrentQueue<(string Data, DateTime Timestamp)> _scanQueue = new();
    private readonly SemaphoreSlim _semaphore;
    private readonly CancellationTokenSource _cts = new();
    private IScannerInput _scanner;

    public HighVolumeScanner()
    {
        InitializeComponent();
        IronBarCode.License.LicenseKey = "YOUR-LICENSE-KEY";
        _semaphore = new SemaphoreSlim(Environment.ProcessorCount);
        InitializeScanner();
        _ = RunProcessingLoopAsync();
    }

    private void InitializeScanner()
    {
        _scanner = System.IO.Ports.SerialPort.GetPortNames().Any()
            ? new SerialPortScanner("COM3", 115200)
            : new KeyboardWedgeScanner(txtScannerInput);

        _scanner.BarcodeScanned += (_, barcode) =>
            _scanQueue.Enqueue((barcode, DateTime.UtcNow));

        _scanner.StartListening();
    }

    private async Task RunProcessingLoopAsync()
    {
        while (!_cts.Token.IsCancellationRequested)
        {
            if (_scanQueue.TryDequeue(out var scan))
            {
                await _semaphore.WaitAsync(_cts.Token);
                _ = Task.Run(async () =>
                {
                    try { await ProcessScanAsync(scan.Data, scan.Timestamp); }
                    finally { _semaphore.Release(); }
                }, _cts.Token);
            }
            else
            {
                await Task.Delay(10, _cts.Token);
            }
        }
    }

    private async Task ProcessScanAsync(string rawData, DateTime scanTime)
    {
        var options = new BarcodeReaderOptions
        {
            Speed = ReadingSpeed.Balanced,
            ExpectMultipleBarcodes = false,
            ExpectBarcodeTypes = BarcodeEncoding.Code128 | BarcodeEncoding.QRCode,
            MaxParallelThreads = 1
        };

        var testBarcode = BarcodeWriter.CreateBarcode(rawData, BarcodeEncoding.Code128);
        var results = await BarcodeReader.ReadAsync(testBarcode.ToBitmap(), options);

        if (results.Any())
        {
            var item = results.First();
            BeginInvoke(() => UpdateInventoryDisplay(item.Value, scanTime));
        }
        else
        {
            BeginInvoke(() => LogRejectedScan(rawData, scanTime));
        }
    }

    protected override void OnFormClosing(FormClosingEventArgs e)
    {
        _cts.Cancel();
        _scanner.StopListening();
        base.OnFormClosing(e);
    }
}
using IronBarCode;
using System.Collections.Concurrent;

public partial class HighVolumeScanner : Form
{
    private readonly ConcurrentQueue<(string Data, DateTime Timestamp)> _scanQueue = new();
    private readonly SemaphoreSlim _semaphore;
    private readonly CancellationTokenSource _cts = new();
    private IScannerInput _scanner;

    public HighVolumeScanner()
    {
        InitializeComponent();
        IronBarCode.License.LicenseKey = "YOUR-LICENSE-KEY";
        _semaphore = new SemaphoreSlim(Environment.ProcessorCount);
        InitializeScanner();
        _ = RunProcessingLoopAsync();
    }

    private void InitializeScanner()
    {
        _scanner = System.IO.Ports.SerialPort.GetPortNames().Any()
            ? new SerialPortScanner("COM3", 115200)
            : new KeyboardWedgeScanner(txtScannerInput);

        _scanner.BarcodeScanned += (_, barcode) =>
            _scanQueue.Enqueue((barcode, DateTime.UtcNow));

        _scanner.StartListening();
    }

    private async Task RunProcessingLoopAsync()
    {
        while (!_cts.Token.IsCancellationRequested)
        {
            if (_scanQueue.TryDequeue(out var scan))
            {
                await _semaphore.WaitAsync(_cts.Token);
                _ = Task.Run(async () =>
                {
                    try { await ProcessScanAsync(scan.Data, scan.Timestamp); }
                    finally { _semaphore.Release(); }
                }, _cts.Token);
            }
            else
            {
                await Task.Delay(10, _cts.Token);
            }
        }
    }

    private async Task ProcessScanAsync(string rawData, DateTime scanTime)
    {
        var options = new BarcodeReaderOptions
        {
            Speed = ReadingSpeed.Balanced,
            ExpectMultipleBarcodes = false,
            ExpectBarcodeTypes = BarcodeEncoding.Code128 | BarcodeEncoding.QRCode,
            MaxParallelThreads = 1
        };

        var testBarcode = BarcodeWriter.CreateBarcode(rawData, BarcodeEncoding.Code128);
        var results = await BarcodeReader.ReadAsync(testBarcode.ToBitmap(), options);

        if (results.Any())
        {
            var item = results.First();
            BeginInvoke(() => UpdateInventoryDisplay(item.Value, scanTime));
        }
        else
        {
            BeginInvoke(() => LogRejectedScan(rawData, scanTime));
        }
    }

    protected override void OnFormClosing(FormClosingEventArgs e)
    {
        _cts.Cancel();
        _scanner.StopListening();
        base.OnFormClosing(e);
    }
}
Imports IronBarCode
Imports System.Collections.Concurrent
Imports System.Threading
Imports System.Threading.Tasks

Public Partial Class HighVolumeScanner
    Inherits Form

    Private ReadOnly _scanQueue As New ConcurrentQueue(Of (Data As String, Timestamp As DateTime))()
    Private ReadOnly _semaphore As SemaphoreSlim
    Private ReadOnly _cts As New CancellationTokenSource()
    Private _scanner As IScannerInput

    Public Sub New()
        InitializeComponent()
        IronBarCode.License.LicenseKey = "YOUR-LICENSE-KEY"
        _semaphore = New SemaphoreSlim(Environment.ProcessorCount)
        InitializeScanner()
        _ = RunProcessingLoopAsync()
    End Sub

    Private Sub InitializeScanner()
        _scanner = If(System.IO.Ports.SerialPort.GetPortNames().Any(),
                      New SerialPortScanner("COM3", 115200),
                      New KeyboardWedgeScanner(txtScannerInput))

        AddHandler _scanner.BarcodeScanned, Sub(_, barcode)
                                                _scanQueue.Enqueue((barcode, DateTime.UtcNow))
                                            End Sub

        _scanner.StartListening()
    End Sub

    Private Async Function RunProcessingLoopAsync() As Task
        While Not _cts.Token.IsCancellationRequested
            Dim scan As (Data As String, Timestamp As DateTime)
            If _scanQueue.TryDequeue(scan) Then
                Await _semaphore.WaitAsync(_cts.Token)
                _ = Task.Run(Async Function()
                                 Try
                                     Await ProcessScanAsync(scan.Data, scan.Timestamp)
                                 Finally
                                     _semaphore.Release()
                                 End Try
                             End Function, _cts.Token)
            Else
                Await Task.Delay(10, _cts.Token)
            End If
        End While
    End Function

    Private Async Function ProcessScanAsync(rawData As String, scanTime As DateTime) As Task
        Dim options As New BarcodeReaderOptions With {
            .Speed = ReadingSpeed.Balanced,
            .ExpectMultipleBarcodes = False,
            .ExpectBarcodeTypes = BarcodeEncoding.Code128 Or BarcodeEncoding.QRCode,
            .MaxParallelThreads = 1
        }

        Dim testBarcode = BarcodeWriter.CreateBarcode(rawData, BarcodeEncoding.Code128)
        Dim results = Await BarcodeReader.ReadAsync(testBarcode.ToBitmap(), options)

        If results.Any() Then
            Dim item = results.First()
            BeginInvoke(Sub() UpdateInventoryDisplay(item.Value, scanTime))
        Else
            BeginInvoke(Sub() LogRejectedScan(rawData, scanTime))
        End If
    End Function

    Protected Overrides Sub OnFormClosing(e As FormClosingEventArgs)
        _cts.Cancel()
        _scanner.StopListening()
        MyBase.OnFormClosing(e)
    End Sub
End Class
$vbLabelText   $csharpLabel

The SemaphoreSlim limits concurrent validation tasks to the number of logical processors, preventing runaway thread creation during burst scanning events. The BeginInvoke calls marshal UI updates back to the main thread safely.

How Do You Tune Performance for Different Scan Volumes?

The BarcodeReaderOptions.Speed property accepts ReadingSpeed.Faster, ReadingSpeed.Balanced, and ReadingSpeed.Detailed. For USB scanner input where the string value is already known, Balanced is appropriate -- the decoder only needs to confirm the format, not locate a barcode in an image. According to IronBarcode's reading speed documentation, Faster mode skips some distortion correction algorithms, which is safe for clean scanner output but may miss damaged barcodes in image-based scenarios.

The following table summarizes when to use each speed mode:

IronBarcode reading speed modes and their appropriate use cases
Speed Mode Best For Trade-off
Faster Clean USB scanner input, high-volume throughput May miss badly damaged or skewed barcodes
Balanced Mixed input -- USB scanner plus image imports Moderate CPU usage, good accuracy
Detailed Damaged labels, low-contrast printing, PDF imports Higher CPU usage, slowest throughput

For applications that also process images or PDFs alongside USB scanner input, IronBarcode can read barcodes from PDF documents and multi-page TIFF files using the same API surface.

Professional Windows Forms barcode scanner application showcasing IronBarcode's real-time inventory tracking capabilities. The interface features a clean two-panel design with a sophisticated dark blue header. The left panel displays a scanning history list showing four successfully scanned inventory items (INV-001 through INV-004) with precise timestamps and scan status indicators. Each item includes detailed metadata like barcode type and confidence level. The right panel shows a dynamically generated summary barcode displaying 'Items: 4' with professional styling and proper margins. Action buttons at the bottom include 'Clear List', 'Export Data', and 'Print Labels' for complete inventory management. The status bar indicates 'Scanner: Connected | Mode: Continuous | Last Scan: 2 seconds ago', demonstrating the application's real-time monitoring capabilities and professional enterprise-ready design that IronBarcode enables for production inventory systems.

How Do You Handle Edge Cases and Errors in Scanner Applications?

What Failure Modes Should Developers Anticipate?

USB scanner applications fail in predictable ways. The most common issues and their mitigations are:

Scanner disconnection -- When a USB scanner is unplugged, the keyboard wedge TextBox loses its virtual keyboard. The simplest mitigation is a periodic timer that checks _inputBox.Focused and refocuses it if the scanner is still listed in connected HID devices. For serial scanners, SerialPort.GetPortNames() detects reconnection.

Ambiguous barcode formats -- Some products carry barcodes that are valid in multiple symbologies. For example, a 12-digit string is a valid UPC-A and also valid Code 128. Specifying ExpectBarcodeTypes in BarcodeReaderOptions constrains the decoder to your expected formats and eliminates ambiguity. The IronBarcode troubleshooting guide covers format-specific recognition tips.

Invalid format exceptions -- If BarcodeWriter.CreateBarcode receives a string that violates the selected encoding's rules (for instance, alphabetic characters in a numeric-only EAN-13 field), it throws an IronBarcode.Exceptions.InvalidBarcodeException. Wrapping the call in a try-catch and falling back to a string-only validation path keeps the application running.

Keystroke timing collisions -- In environments where operators also type manually into the same TextBox, the burst timer approach described earlier is the primary defense. A secondary guard is minimum length: most real barcodes are at least 8 characters, so strings shorter than that can be treated as keyboard input.

The Microsoft .NET documentation on System.IO.Ports.SerialPort is useful when troubleshooting serial scanner connectivity, particularly around ReadTimeout and WriteTimeout settings. For regulatory compliance in retail, the GS1 General Specifications define valid value ranges for each application identifier.

How Do You Extend the Application to Mobile and Web Platforms?

The scanner interface pattern shown above -- IScannerInput with BarcodeScanned event -- abstracts hardware from processing logic. Swapping implementations allows the same validation and generation code to run on different platforms:

  • .NET MAUI provides a camera-based scanner implementation for Android and iOS tablets used as mobile receiving stations
  • Blazor Server supports browser-based scanning with JavaScript camera access feeding into the same BarcodeScanned event
  • Android and iOS native implementations give mobile developers camera scan capability with the same IronBarcode decoder on the backend

For cloud-native architectures, the validation and label generation steps can run as Azure Functions triggered by queue messages, with the desktop application acting only as the scanner input gateway. This separation is particularly useful when label printing logic must be centralized for compliance auditing.

What Are Your Next Steps?

Building a USB barcode scanner application with IronBarcode involves four concrete stages: capturing the keyboard wedge input with burst-timing detection, validating the scanned value through IronBarcode's decoder, generating response labels in the required format, and processing high scan volumes with a concurrent queue. Each stage is independent and testable in isolation.

From here, consider extending the application with multi-barcode reading for batch processing scenarios, crop region optimization for image-based inputs, or MSI barcode support for older warehouse equipment. The IronBarcode documentation covers all supported formats and advanced reader configuration options.

Start a free trial to get a development license key and begin integrating IronBarcode into your scanning application today.

Frequently Asked Questions

What is IronBarcode and how does it relate to USB barcode scanners?

IronBarcode is a library that enables developers to build robust C# applications for USB barcode scanning. It offers features like barcode validation, data extraction, and barcode generation.

Can IronBarcode validate barcode data from a USB scanner?

Yes, IronBarcode can validate barcode data captured from a USB scanner, ensuring data integrity and accuracy in your C# applications.

How does IronBarcode handle barcode generation?

IronBarcode can generate new barcodes on the fly, allowing developers to create and print barcodes easily within their C# applications.

Is there error handling support in IronBarcode for USB barcode scanning?

Yes, IronBarcode includes comprehensive error handling to manage common issues that may arise during USB barcode scanning and processing.

What types of barcodes can be scanned using IronBarcode?

IronBarcode supports scanning a wide range of barcode symbologies, including QR codes, UPC, Code 39, and more, making it versatile for various applications.

Can IronBarcode extract structured information from scanned barcodes?

Yes, IronBarcode can extract structured information from scanned barcodes, aiding in efficient data processing and management.

How can I get started with building a USB barcode scanner application in C#?

To start building a USB barcode scanner application in C#, you can utilize IronBarcode along with the provided code examples and documentation to guide your development process.

Jordi Bardia
Software Engineer
Jordi is most proficient in Python, C# and C++, when he isn’t leveraging his skills at Iron Software; he’s game programming. Sharing responsibilities for product testing, product development and research, Jordi adds immense value to continual product improvement. The varied experience keeps him challenged and engaged, and he ...
Read More

Iron Support Team

We're online 24 hours, 5 days a week.
Chat
Email
Call Me