Przejdź do treści stopki
KORZYSTANIE Z IRONBARCODE

Skaner kodów kreskowych USB w C#: Zbuduj kompletną aplikację skanującą

Skanery BarCode'ów USB łączą się z aplikacjami C# jako standardowe urządzenia wejściowe klawiatury, wysyłając zeskanowane dane jako wpisane znaki, po których następuje naciśnięcie klawisza Enter. Takie działanie klucza klawiatury HID sprawia, że integracja jest prosta — aplikacja odbiera dane tekstowe bez konieczności stosowania specjalnego sterownika lub zestawu SDK. IronBarcode przetwarza te surowe dane wejściowe w celu sprawdzenia poprawności formatów, wyodrębnienia danych strukturalnych i wygenerowania kodów kreskowych odpowiedzi, zamieniając proste skanowanie w kompletny strumień danych dla systemów zarządzania zapasami, punktów sprzedaży detalicznej i systemów śledzenia logistycznego.

Operacje w handlu detalicznym, magazynowaniu i produkcji zależą od dokładnego i szybkiego skanowania BARCODE-ów. Gdy programista podłącza skaner USB do aplikacji Windows Forms lub WPF, skaner zachowuje się identycznie jak klawiatura — dane trafiają do pola TextBox, a naciśnięcie klawisza Enter sygnalizuje, że otrzymano pełny BARCODE. Wyzwaniem nie jest pozyskanie danych; przetwarza to poprawnie. Weryfikacja kodów kreskowych IronBarcode sprawdza integralność formatu, wyodrębnia pola, takie jak numery partii lub identyfikatory aplikacji GS1, i może natychmiast wygenerować nowy kod kreskowy w odpowiedzi.

Ten przewodnik krok po kroku opisuje tworzenie gotowej do użycia aplikacji C# do skanowania kodów kreskowych za pomocą skanera USB. Zainstalujesz bibliotekę, przechwycisz dane ze skanera, zweryfikujesz formaty kodów kreskowych, wygenerujesz etykiety odpowiedzi i zbudujesz procesor oparty na kolejce do obsługi dużych ilości danych. Każda sekcja zawiera kompletny, działający kod przeznaczony dla platformy .NET 10, w odpowiednich miejscach w stylu instrukcji najwyższego poziomu.

Jak działają skanery BarCode USB w języku C#?

Dłączego tryb Wedge klawiatury HID ułatwia integrację?

Większość skanerów BARCODE USB jest domyślnie skonfigurowana w trybie HID keyboard wedge. Po podłączeniu urządzenia do komputera z systemem Windows system operacyjny rejestruje je zarówno jako urządzenie pamięci USB (do konfiguracji), jak i klawiaturę (do wprowadzania danych). Po zeskanowaniu kodu kreskowego urządzenie przekształca zdekodowaną wartość kodu kreskowego na sekwencję znaków i wysyła ją do okna aplikacji, które ma aktualnie fokus, dodając na końcu znak powrotu karetki.

Z punktu widzenia programisty C# oznacza to, że nie potrzebujesz SDK dostawców, bibliotek COM ani specjalnych interfejsów API USB. Do przechwytywania danych wejściowych wystarczy standardowe pole tekstowe z procedurą obsługi zdarzenia KeyDown. Głównym wyzwaniem związanym z integracją jest odróżnienie danych wprowadzonych za pomocą skanera od prawdziwego pisania na klawiaturze. Skanery zazwyczaj przekazują wszystkie znaki w bardzo krótkim czasie — często poniżej 50 milisekund — podczas gdy podczas pisania na klawiaturze przez człowieka naciśnięcia klawiszy rozkładają się na setki milisekund. Synchronizacja impulsu to niezawodny sposób na odfiltrowanie przypadkowych naciśnięć klawiszy.

Profesjonalne skanery obsługują również tryby szeregowe (RS-232 lub wirtualny port COM) oraz bezpośrednie tryby USB HID, co zapewnia większą kontrolę nad znakami przedrostków/przyrostków oraz wyzwalaczami skanowania. Poniższy wzorzec interfejsu obsługuje oba przypadki:

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

Kluczowym szczegółem jest licznik czasu w implementacji klawiatury typu wedge. Resetuje się przy każdym naciśnięciu klawisza i uruchamia się tylko wtedy, gdy przestają napływać znaki — co oznacza, że w przypadku prawdziwych użytkowników klawiatury, którzy piszą powoli, ich niekompletne dane wejściowe zostaną odrzucone, a nie potraktowane jako skan BARCODE.

Jak radzisz sobie z wieloma markami skanerów?

W środowiskach Enterprise często na tym samym piętrze używa się skanerów firm Honeywell, Zebra (dawniej Symbol/Motorola) i Datalogic. Każdy dostawca ma swoje własne domyślne znaki końcowe, prędkości transmisji oraz konwencje dotyczące przedrostków i przyrostków. Model konfiguracyjny zapewnia elastyczność aplikacji:

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

Przechowywanie tych konfiguracji w pliku ustawień lub bazie danych oznacza, że pracownicy magazynu mogą wymieniać modele skanerów bez konieczności ponownego wdrażania. Pole ScannerType określa, która implementacja IScannerInput zostanie zainicjowana podczas uruchamiania.

Jak zainstalować IronBarcode w projekcie C#?

Jaki jest najszybszy sposób dodania IronBarcode za pośrednictwem NuGet?

Otwórz konsolę menedżera pakietów w Visual Studio i uruchom:

Install-Package IronBarCode
Install-Package IronBarCode
SHELL

Alternatywnie można użyć interfejsu CLI platformy .NET:

dotnet add package IronBarCode
dotnet add package IronBarCode
SHELL

Oba polecenia pobierają aktualną wersję z NuGet.org i dodają odwołanie do zestawu do pliku projektu. Biblioteka jest przeznaczona dla .NET Standard 2.0, więc działa na platformach od .NET Framework 4.6.2 do .NET 10 bez żadnych dodatkowych nakładek kompatybilnościowych.

Po instalacji należy ustawić klucz licencyjny przed wywołaniem jakiejkolwiek metody IronBarcode. W celu opracowania i oceny dostępny jest bezpłatny klucz próbny na stronie licencyjnej IronBarcode:

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

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

W przypadku wdrożeń kontenerowych IronBarcode współpracuje z Dockerem w systemie Linux, a w przypadku funkcji chmurowych obsługuje AWS Lambda i Azure Functions.

Jak weryfikować zeskanowane BarCodes za pomocą IronBarcode?

Jakie jest właściwe podejście do weryfikacji formatu?

IronBarcode obsługuje ponad 30 symboli kodów kreskowych, w tym Code 128, EAN-13, Code 39, kody QR i Data Matrix. W przypadku aplikacji skanerów USB wzorzec walidacji ponownie koduje zeskanowany ciąg znaków jako obraz BARCODE i natychmiast odczytuje go za pomocą dekodera. Ta operacja round-trip potwierdza, że ciąg znaków jest prawidłową wartością dla zadeklarowanego formatu:

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

W przypadku BARCODE'ów GS1-128 stosowanych w aplikacjach łańcucha dostaw zeskanowany ciąg znaków zawiera prefiksy identyfikatorów aplikacji w nawiasach, takie jak (01) dla GTIN i (17) dla daty ważności. IronBarcode automatycznie analizuje te identyfikatory aplikacji po wpisaniu BarcodeEncoding.GS1_128.

Którą logikę sum kontrolnych EAN-13 powinni wdrożyć programiści?

Aplikacje kasowe w handlu detalicznym często muszą samodzielnie weryfikować cyfry kontrolne EAN-13 przed przekazaniem wartości do wyszukiwania cen. Suma kontrolna typu Luhn dla EAN-13 stosuje naprzemiennie wagi 1 i 3 w pierwszych 12 cyfrach:

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

Ta czysto logiczna kontrola jest przeprowadzana przed kodowaniem, aby uniknąć obciążenia związanego z generowaniem obrazu w obie strony dla każdego skanowania w środowisku detalicznym o dużym natężeniu ruchu. Zgodnie ze specyfikacją GS1 algorytm cyfry kontrolnej jest identyczny dla kodu UPC-A (12 cyfr) po usunięciu początkowego zera.

Jak generować BarCodes odpowiedzi na podstawie zeskanowanych danych wejściowych?

Kiedy aplikacja powinna tworzyć nowe BarCodes po skanowaniu?

Typowym schematem w procesie przyjmowania towarów w magazynie jest procedura "skanowania i ponownego etykietowania": przychodząca pozycja posiada BarCode dostawcy (często EAN-13 lub ITF-14), a system zarządzania magazynem musi wydrukować wewnętrzną etykietę Code 128 z własnymi kodami lokalizacji i partii. Funkcje generowania IronBarcode pozwalają to zrobić w kilku wierszach:

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

Zapisywanie w formacie PDF jest szczególnie przydatne w przypadku drukarek etykiet, które akceptują pliki PDF przesyłane przez udział sieciowy. Można również wyeksportować plik jako SVG w celu uzyskania etykiety termicznej o jakości wektorowej lub wyeksportować jako strumień bajtów w celu wysłania bezpośrednio do interfejsu API drukarki etykiet.

IronBarcode obsługuje szerokie możliwości dostosowywania stylu, w tym niestandardowe kolory, regulację marginesów, nakładki tekstowe czytelne dla człowieka, a w przypadku kodów QR — osadzanie logo na etykietach mobilnych oznaczonych marką.

Interfejs aplikacji Windows Forms demonstrujący możliwości generowania podwójnych BarCodes przez IronBarcode. Interfejs pokazuje pomyślne wygenerowanie zarówno liniowego BarCODE-u Code 128, jak i kodu QR dla numeru inwentarzowego INV-20250917-helloworld. Pole wprowadzania danych u góry pozwala użytkownikom na wpisanie własnych kodów magazynowych, a przycisk Generuj służy do tworzenia BARCODE'ów. Komunikat o pomyślnym zakończeniu operacji Element przetworzony pomyślnie — wygenerowano etykiety potwierdza zakończenie operacji. BarCode Code 128 jest oznaczony jako podstawowy format śledzenia zapasów, natomiast QR poniżej jest oznaczony jako alternatywa dostosowana do urządzeń mobilnych. Aplikacja wykorzystuje profesjonalne szare tło z wyraźną hierarchią wizualną, pokazując, w jaki sposób IronBarcode umożliwia programistom tworzenie systemów generowania kodów kreskowych w wielu formatach do kompleksowego zarządzania zapasami.

Jak zbudować kompletną aplikację do skanowania dużych ilości dokumentów?

Jak wygląda implementacja oparta na kolejce produkcyjnej?

W przypadku aplikacji przetwarzających dziesiątki skanów na minutę prosty synchroniczny handler w wątku interfejsu użytkownika staje się wąskim gardłem. Poniższy wzorzec oddziela przechwytywanie skanów od przetwarzania za pomocą ConcurrentQueue<t> i pętli przetwarzania w tle. Asynchroniczny interfejs API IronBarcode obsługuje walidację bez blokowania interfejsu użytkownika:

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.Zrównoważony,
            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.Zrównoważony,
            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

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.Zrównoważony,
            .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

SemaphoreSlim ogranicza liczbę równoczesnych zadań walidacji do liczby procesorów logicznych, zapobiegając niekontrolowanemu tworzeniu wątków podczas intensywnych operacji skanowania. BeginInvoke bezpiecznie przekazuje aktualizacje interfejsu użytkownika z powrotem do głównego wątku.

Jak dostosować wydajność do różnych wolumenów skanowania?

Właściwość BarcodeReaderOptions.Speed akceptuje wartości ReadingSpeed.Szybciej, ReadingSpeed.Zrównoważony oraz ReadingSpeed.Szczegółowe. W przypadku danych wejściowych ze skanera USB, gdzie wartość ciągu znaków jest już znana, odpowiednie jest użycie Zrównoważony — dekoder musi jedynie potwierdzić format, a nie lokalizować BARCODE na obrazie. Zgodnie z dokumentacją IronBarcode dotyczącą szybkości odczytu, tryb Szybciej pomija niektóre algorytmy korekcji zniekształceń, co jest bezpieczne w przypadku czystego wyniku skanera, ale może spowodować pominięcie uszkodzonych kodów kreskowych w scenariuszach opartych na obrazach.

Poniższa tabela podsumowuje, kiedy należy używać poszczególnych trybów prędkości:

Tryby prędkości odczytu IronBarcode i odpowiednie przypadki ich zastosowania
Tryb szybki Najlepsze dla Kompromis
Szybciej Czyste dane wejściowe ze skanera USB, duża przepustowość Może pomijać poważnie uszkodzone lub przekrzywione BARCODES
Zrównoważony Mieszane dane wejściowe — skaner USB Plus oraz import obrazów Umiarkowane obciążenie procesora, dobra dokładność
Szczegółowe Uszkodzone etykiety, druk o niskim kontraście, import plików PDF Wyższe zużycie procesora, najniższa przepustowość

W przypadku aplikacji, które oprócz danych z skanera USB przetwarzają również obrazy lub pliki PDF, IronBarcode może odczytywać kody kreskowe z dokumentów PDF i wielostronicowych plików TIFF przy użyciu tego samego interfejsu API.

![Profesjonalna aplikacja do skanowania kodów kreskowych w środowisku Windows Forms, prezentująca możliwości IronBarcode w zakresie śledzenia zapasów w czasie rzeczywistym. Interfejs charakteryzuje się przejrzystym, dwupanelowym układem z eleganckim, ciemnoniebieskim nagłówkiem. W lewym panelu wyświetlana jest lista historii skanowania zawierająca cztery pomyślnie zeskanowane pozycje magazynowe (od INV-001 do INV-004) wraz z dokładnymi znacznikami czasu i wskaźnikami statusu skanowania. Każda pozycja zawiera szczegółowe metadane, takie jak typ BarCode i poziom pewności. W prawym panelu widoczny jest dynamicznie generowany BARCODE podsumowujący, wyświetlający "Pozycje: 4" w Professionalnym stylu i z odpowiednimi marginesami. Przyciski akcji na dole obejmują "Wyczyść listę", "Eksportuj dane" i "PRINT etykiety" w celu kompleksowego zarządzania zapasami. Pasek stanu wskazuje "Skaner: Podłączony" | Tryb: Ciągły | "Ostatnie skanowanie: 2 sekundy temu" – pokazuje możliwości monitorowania w czasie rzeczywistym oraz Professional, gotowy do wdrożenia w przedsiębiorstwie projekt, który IronBarcode umożliwia w systemach magazynowych. (/static-assets/barcode/blog/csharp-usb-barcode-scanner/csharp-usb-barcode-scanner-3.webp)

Jak radzisz sobie z sytuacjami granicznymi i błędami w aplikacjach skanujących?

Jakich rodzajów awarii powinni się spodziewać programiści?

Aplikacje skanerów USB ulegają awariom w przewidywalny sposób. Najczęstsze problemy i sposoby ich rozwiązania to:

Odłączenie skanera — gdy skaner USB zostanie odłączony, okno tekstowe typu keyboard wedge traci wirtualną klawiaturę. Najprostszym rozwiązaniem jest okresowy timer, który sprawdza _inputBox.Focused i ponownie ustawia ostrość, jeśli skaner nadal znajduje się na liście podłączonych urządzeń HID. W przypadku skanerów szeregowych SerialPort.GetPortNames() wykrywa ponowne połączenie.

Niejednoznaczne formaty kodów kreskowych — niektóre produkty posiadają kody kreskowe, które są ważne w wielu systemach symbolicznych. Na przykład 12-cyfrowy ciąg znaków jest prawidłowym kodem UPC-A, a także prawidłowym kodem 128. Określenie ExpectBarcodeTypes w BarcodeReaderOptions ogranicza dekoder do oczekiwanych formatów i eliminuje niejasności. Przewodnik rozwiązywania problemów IronBarcode zawiera wskazówki dotyczące rozpoznawania konkretnych formatów.

Wyjątki związane z nieprawidłowym formatem — jeśli BarcodeWriter.CreateBarcode otrzyma ciąg znaków, który narusza zasady wybranego kodowania (na przykład znaki alfabetyczne w polu EAN-13 przeznaczonym wyłącznie dla znaków numerycznych), zgłasza wyjątek IronBarCode.Exceptions.InvalidBarcodeException. Otoczenie wywołania blokiem try-catch i przejście na ścieżkę walidacji opartą wyłącznie na ciągach znaków pozwala na dalsze działanie aplikacji.

Kolizje czasowe naciśnięć klawiszy — w środowiskach, w których operatorzy również wpisują dane ręcznie do tego samego pola tekstowego, podstawową metodą ochrony jest opisane wcześniej podejście oparte na liczniku impulsów. Dodatkowym zabezpieczeniem jest minimalna długość: większość prawdziwych BARCODE ma co najmniej 8 znaków, więc ciągi krótsze niż ta długość mogą być traktowane jako dane wprowadzone z klawiatury.

Dokumentacja Microsoft .NET dotycząca System.IO.Ports.SerialPort jest przydatna podczas rozwiązywania problemów z łącznością skanera szeregowego, szczególnie w odniesieniu do ustawień ReadTimeout i WriteTimeout. W celu zapewnienia zgodności z przepisami w handlu detalicznym, specyfikacje ogólne GS1 definiują prawidłowe zakresy wartości dla każdego identyfikatora aplikacji.

Jak rozszerzyć aplikację na platformy mobilne i internetowe?

Przedstawiony powyżej wzorzec interfejsu skanera — IScannerInput z wydarzeniem BarcodeScanned — oddziela sprzęt od logiki przetwarzania. Zamiana implementacji pozwala na uruchamianie tego samego kodu walidacji i generowania na różnych platformach:

  • .NET MAUI zapewnia implementację skanera opartego na kamerze dla tabletów z systemem Android i iOS, wykorzystywanych jako mobilne stacje odbiorcze
  • Blazor Server obsługuje skanowanie w przeglądarce z wykorzystaniem dostępu do kamery za pomocą JavaScript, przekazując dane do tego samego zdarzenia BarcodeScanned
  • Natywne implementacje na Androida i iOS zapewniają programistom aplikacji mobilnych możliwość skanowania za pomocą aparatu, korzystając z tego samego dekodera IronBarcode w tle

W przypadku architektur natywnych dla chmury etapy walidacji i generowania etykiet mogą być wykonywane jako funkcje Azure Functions wyzwalane przez komunikaty z kolejki, przy czym aplikacja desktopowa pełni jedynie rolę bramy wejściowej skanera. Rozdzielenie to jest szczególnie przydatne, gdy logika drukowania etykiet musi być scentralizowana na potrzeby audytu zgodności.

Jakie są Twoje kolejne kroki?

Tworzenie aplikacji do skanowania kodów kreskowych za pomocą skanera USB z wykorzystaniem IronBarcode obejmuje cztery konkretne etapy: przechwytywanie danych z klawiatury z wykrywaniem synchronizacji impulsów, sprawdzanie poprawności zeskanowanej wartości za pomocą dekodera IronBarcode, generowanie etykiet odpowiedzi w wymagańym formacie oraz przetwarzanie dużych ilości skanów przy użyciu kolejki współbieżnej. Każdy etap jest niezależny i można go przetestować osobno.

W tym miejscu warto rozważyć rozszerzenie aplikacji o funkcję odczytu wielu BARCODE-ów do scenariuszy przetwarzania wsadowego, optymalizację obszaru kadrowania dla danych wejściowych opartych na obrazach lub obsługę BARCODE-ów MSI dla starszego sprzętu magazynowego. Dokumentacja IronBarcode obejmuje wszystkie obsługiwane formaty oraz zaawansowane opcje konfiguracji czytnika.

Rozpocznij bezpłatny okres próbny, aby uzyskać klucz licencyjny dla programistów i już dziś zacznij integrować IronBarcode ze swoją aplikacją do skanowania.

Często Zadawane Pytania

Czym jest IronBarcode i jaki ma związek ze skanerami kodów kreskowych USB?

IronBarcode to biblioteka, która umożliwia programistom tworzenie solidnych aplikacji w języku C# do skanowania kodów kreskowych przez USB. Oferuje takie funkcje, jak weryfikacja kodów kreskowych, ekstrakcja danych i generowanie kodów kreskowych.

Czy IronBarcode może weryfikować dane BarCode ze skanera USB?

Tak, IronBarcode może weryfikować dane BARCODE przechwycone ze skanera USB, zapewniając integralność i dokładność danych w aplikacjach C#.

W jaki sposób IronBarcode obsługuje generowanie kodów kreskowych?

IronBarcode może generować nowe kody kreskowe w locie, umożliwiając programistom łatwe tworzenie i drukowanie kodów kreskowych w ramach ich aplikacji C#.

Czy IronBarcode obsługuje obsługę błędów podczas skanowania kodów kreskowych przez USB?

Tak, IronBarcode zawiera kompleksową obsługę błędów, która pozwala zarządzać typowymi problemami, jakie mogą pojawić się podczas skanowania i przetwarzania kodów kreskowych przez USB.

Jakie rodzaje kodów kreskowych można skanować za pomocą IronBarcode?

IronBarcode obsługuje skanowanie szerokiej gamy symboli kodów kreskowych, w tym kodów QR, UPC, Code 39 i innych, dzięki czemu jest wszechstronnym narzędziem do różnych zastosowań.

Czy IronBarcode może wyodrębniać uporządkowane informacje ze skanowanych BarCodes?

Tak, IronBarcode może wyodrębniać uporządkowane informacje ze skanowanych BarCodes, wspomagając efektywne przetwarzanie danych i zarządzanie nimi.

Jak zacząć tworzyć aplikację do skanowania kodów kreskowych USB w języku C#?

Aby rozpocząć tworzenie aplikacji do skanowania kodów kreskowych USB w języku C#, możesz skorzystać z biblioteki IronBarcode wraz z dostarczonymi przykładami kodu i dokumentacją, które pomogą Ci w procesie programowania.

Jordi Bardia
Inżynier oprogramowania
Jordi jest najbardziej biegły w Pythonie, C# i C++. Kiedy nie wykorzystuje swoich umiejętności w Iron Software, programuje gry. Dzieląc odpowiedzialność za testowanie produktów, rozwój produktów i badania, Jordi wnosi ogromną wartość do ciągłej poprawy produktów. Różnorodne doświadczenia ...
Czytaj więcej

Zespol wsparcia Iron

Jestesmy online 24 godziny, 5 dni w tygodniu.
Czat
Email
Zadzwon do mnie