How to Build a Windows Desktop App for Barcode, Document, and MRZ Detection with C# and .NET WinForms

The Dynamsoft Capture Vision Bundle is a single NuGet package that bundles three powerful vision capabilities: Dynamsoft Barcode Reader (DBR), Dynamsoft Document Normalizer (DDN), and Dynamsoft Label Recognizer (DLR) with MRZ parsing support. All capabilities are exposed through a single unified CaptureVisionRouter API. In this tutorial, we will build a .NET Windows Forms desktop application that lets users scan barcodes and QR codes, detect and normalize document pages, and extract MRZ data from passports, visas, and ID cards — all in real time from a webcam or from local image files.

Development Environment

  • Windows 10 / 11
  • Visual Studio 2022 or the .NET SDK with any editor

NuGet Packages

Add the following packages to your .csproj file:

OpenCV (camera access and image rendering)

Dynamsoft Capture Vision Bundle

A free 30-day trial license is available at the Dynamsoft Customer Portal. A single license key covers all three capabilities.

Create a Windows Forms Project

In Visual Studio 2022 create a new Windows Forms App project targeting .NET (not .NET Framework). Then update the .csproj to add the required packages:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net8.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <UseWindowsForms>true</UseWindowsForms>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Dynamsoft.DotNet.CaptureVision.Bundle" Version="3.4.1000" />
    <PackageReference Include="OpenCvSharp4" Version="4.6.0.20220608" />
  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="OpenCvSharp4.Extensions" Version="4.5.5.20211231" />
    <PackageReference Include="OpenCvSharp4.runtime.win" Version="4.6.0.20220608" />
  </ItemGroup>
</Project>

Windows Forms UI Design

The UI contains the following key controls:

Windows capture vision UI design

  • PictureBox — occupies the left side of the window; displays the live camera feed or the loaded image with detection overlays drawn on top.
  • GroupBox with radio buttons — selects the active detection mode: DBR (barcode), MRZ, or DDN (document).
  • Button controlsLoad File opens an image, Camera Scan toggles the webcam, and Save Image exports the annotated frame.
  • ListBox — stores recently loaded files; supports drag-and-drop for batch loading.
  • RichTextBox — displays decoded barcode text, parsed MRZ fields, or document detection output.
  • ToolStripStatusLabel — shows license activation status at the bottom of the window.
  • ToolStripMenuItem — opens an input dialog for entering a new license key at runtime.

Initialize the Dynamsoft Capture Vision SDK

Add the required using directives at the top of Form1.cs:

using Dynamsoft.CVR;
using Dynamsoft.Core;
using Dynamsoft.DBR;
using Dynamsoft.DDN;
using Dynamsoft.DCP;
using Dynamsoft.DLR;
using Dynamsoft.License;
using Dynamsoft.Utility;

In the Form1 constructor, activate the license and create a CaptureVisionRouter instance. LicenseManager.InitLicense unlocks all capabilities at once:

public Form1()
{
    InitializeComponent();
    FormClosing += new FormClosingEventHandler(Form1_Closing);

    // Activate license — one key covers DBR, DDN, and DLR/MRZ
    string license = "YOUR-LICENSE-KEY";
    int errorCode = LicenseManager.InitLicense(license, out string errorMsg);
    if (errorCode != (int)EnumErrorCode.EC_OK && errorCode != (int)EnumErrorCode.EC_LICENSE_WARNING)
    {
        toolStripStatusLabel1.Text = $"License error: {errorMsg}";
    }
    else
    {
        toolStripStatusLabel1.Text = "License activated successfully.";
    }

    // Single router handles barcode, document, and MRZ processing
    cvRouter = new CaptureVisionRouter();

    // Open the default webcam (index 0)
    capture = new VideoCapture(0);
    isCapturing = false;
}

protected override void OnFormClosing(FormClosingEventArgs e)
{
    base.OnFormClosing(e);
    cvRouter?.Dispose();
}

Detection Mode Selection

A simple enum tracks which detection mode is active. Radio-button CheckedChanged events update it:

private enum DetectionMode { DBR, MRZ, DDN }
private DetectionMode currentMode = DetectionMode.DBR;

private void radioButton_CheckedChanged(object sender, EventArgs e)
{
    RadioButton? rb = sender as RadioButton;
    if (rb != null && rb.Checked)
    {
        if (rb == radioButtonDbr)      currentMode = DetectionMode.DBR;
        else if (rb == radioButtonMrz) currentMode = DetectionMode.MRZ;
        else if (rb == radioButtonDdn) currentMode = DetectionMode.DDN;

        // Re-run detection on the current file when mode changes
        if (!isCapturing && !string.IsNullOrEmpty(_currentFilename))
            DetectFile(_currentFilename);
    }
}

GetTemplate() maps each mode to a built-in Capture Vision preset template:

private string GetTemplate() => currentMode switch
{
    DetectionMode.DBR => PresetTemplate.PT_READ_BARCODES,
    DetectionMode.MRZ => "ReadPassportAndId",
    DetectionMode.DDN => PresetTemplate.PT_DETECT_AND_NORMALIZE_DOCUMENT,
    _                 => PresetTemplate.PT_READ_BARCODES
};

Load Image Files from Disk

Use OpenFileDialog to let the user pick a file; a ListBox holds file history. Drag-and-drop is also supported:

private void buttonFile_Click(object sender, EventArgs e)
{
    StopScan();
    using OpenFileDialog dlg = new OpenFileDialog
    {
        Title = "Open Image",
        Filter = "Image files (*.bmp, *.jpg, *.png) | *.bmp; *.jpg; *.png"
    };
    if (dlg.ShowDialog() == DialogResult.OK)
    {
        listBox1.Items.Add(dlg.FileName);
        _currentFilename = dlg.FileName;
        DetectFile(dlg.FileName);
    }
}

private void listBox1_DragEnter(object sender, DragEventArgs e)
{
    if (e.Data != null && e.Data.GetDataPresent(DataFormats.FileDrop))
        e.Effect = DragDropEffects.Copy;
}

private void listBox1_DragDrop(object sender, DragEventArgs e)
{
    if (e.Data != null && e.Data.GetDataPresent(DataFormats.FileDrop))
    {
        string[]? files = (string[]?)e.Data.GetData(DataFormats.FileDrop);
        if (files != null)
            foreach (string file in files)
                listBox1.Items.Add(file);
    }
}

Process Image Files

DetectFile loads the image with OpenCV and delegates to ProcessFile, which calls CaptureVisionRouter.CaptureMultiPages. CaptureMultiPages handles multi-page input (e.g., PDFs) and returns one CapturedResult per page — for single images we use the first result:

private void DetectFile(string filename)
{
    richTextBoxInfo.Text = "";
    try
    {
        _mat = Cv2.ImRead(filename, ImreadModes.Color);
        ProcessFile(filename, _mat);
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message);
    }
}

private void ProcessFile(string filename, Mat mat)
{
    Mat canvas = new Mat();
    mat.CopyTo(canvas);

    CapturedResult[] results = cvRouter.CaptureMultiPages(filename, GetTemplate());
    if (results != null && results.Length > 0)
        ProcessResult(results[0], mat, canvas, true);

    pictureBoxSrc.Image = BitmapConverter.ToBitmap(canvas);
}

Live Camera Detection

A dedicated worker thread reads frames from the webcam with OpenCV and feeds them to CaptureVisionRouter.Capture as JPEG bytes:

private void StartScan()
{
    buttonCamera.Text = "Stop";
    isCapturing = true;
    thread = new Thread(new ThreadStart(FrameCallback));
    thread.Start();
}

private void StopScan()
{
    buttonCamera.Text = "Camera Scan";
    isCapturing = false;
    if (thread != null) thread.Join();
}

private void FrameCallback()
{
    while (isCapturing)
    {
        capture.Read(_mat);
        if (_mat.Empty()) continue;

        Mat canvas = new Mat();
        _mat.CopyTo(canvas);

        try
        {
            byte[] jpegBytes = GetJpegBytes(_mat);
            CapturedResult result = cvRouter.Capture(jpegBytes, GetTemplate());
            ProcessResult(result, _mat, canvas, false);
        }
        catch (Exception ex)
        {
            Debug.WriteLine($"Error: {ex.Message}");
        }

        if (!canvas.Empty())
        {
            Bitmap bmp = BitmapConverter.ToBitmap(canvas);
            pictureBoxSrc.Image = bmp;
        }
    }
}

private byte[] GetJpegBytes(Mat mat)
{
    Cv2.ImEncode(".jpg", mat, out byte[] bytes);
    return bytes;
}

Route Results to the Correct Handler

A central ProcessResult method dispatches to mode-specific handlers:

private void ProcessResult(CapturedResult result, Mat originalMat, Mat canvas, bool isFileMode)
{
    if (result.GetErrorCode() != (int)EnumErrorCode.EC_OK &&
        result.GetErrorCode() != (int)EnumErrorCode.EC_UNSUPPORTED_JSON_KEY_WARNING)
    {
        this.BeginInvoke((MethodInvoker)delegate
        {
            richTextBoxInfo.Text = $"Error: {result.GetErrorCode()}, {result.GetErrorString()}";
        });
        return;
    }

    switch (currentMode)
    {
        case DetectionMode.DBR: ProcessBarcodeResult(result, canvas, isFileMode);          break;
        case DetectionMode.MRZ: ProcessMrzResult(result, canvas, isFileMode);              break;
        case DetectionMode.DDN: ProcessDocumentResult(result, originalMat, canvas, isFileMode); break;
    }
}

Read Barcodes and QR Codes

GetDecodedBarcodesResult() returns all detected barcodes. For each BarcodeResultItem, the text value, format string, and quadrilateral location are retrieved. DrawQuadrilateral draws the overlay on the canvas using OpenCV:

private void ProcessBarcodeResult(CapturedResult result, Mat canvas, bool isFileMode)
{
    DecodedBarcodesResult? barcodesResult = result.GetDecodedBarcodesResult();
    BarcodeResultItem[]? items = barcodesResult?.GetItems();

    if (items == null || items.Length == 0)
    {
        this.BeginInvoke((MethodInvoker)delegate { richTextBoxInfo.Text = "No barcode detected."; });
        return;
    }

    StringBuilder sb = new StringBuilder();
    sb.AppendLine($"Detected {items.Length} barcode(s):\n");

    foreach (BarcodeResultItem item in items)
    {
        string text   = item.GetText();
        string format = item.GetFormatString();
        sb.AppendLine($"Format: {format}");
        sb.AppendLine($"Text: {text}\n");
        DrawQuadrilateral(canvas, item.GetLocation(), new Scalar(0, 255, 0), text);
    }

    this.BeginInvoke((MethodInvoker)delegate { richTextBoxInfo.Text = sb.ToString(); });
}

The DrawQuadrilateral helper converts the SDK’s Quadrilateral to an OpenCV Point[] and draws it with Cv2.DrawContours. The quad points are accessed as pts[i][0] (x) and pts[i][1] (y) because the SDK returns them as float arrays:

private void DrawQuadrilateral(Mat canvas, Quadrilateral? quad, Scalar color, string label = "")
{
    if (quad == null) return;
    var pts = quad.points;
    if (pts != null && pts.Length >= 4)
    {
        Point[] contourPoints = new Point[4];
        for (int i = 0; i < 4; i++)
            contourPoints[i] = new Point((int)pts[i][0], (int)pts[i][1]);

        Cv2.DrawContours(canvas, new Point[][] { contourPoints }, 0, color, 2);

        if (!string.IsNullOrEmpty(label))
        {
            int minX = contourPoints.Min(p => p.X);
            int minY = contourPoints.Min(p => p.Y);
            Cv2.PutText(canvas, label, new Point(minX, minY - 5),
                HersheyFonts.HersheySimplex, 0.7, color, 2);
        }
    }
}

Detect and Parse MRZ Data

MRZ detection uses GetRecognizedTextLinesResult() to draw text-line quadrilaterals, then GetParsedResult() to access structured fields parsed from the ICAO 9303 standard. This covers TD1 (ID card), TD2 (visa), and TD3 (passport) document types:

private void ProcessMrzResult(CapturedResult result, Mat canvas, bool isFileMode)
{
    // Overlay text-line bounding boxes
    RecognizedTextLinesResult? textLinesResult = result.GetRecognizedTextLinesResult();
    if (textLinesResult != null)
    {
        TextLineResultItem[]? lines = textLinesResult.GetItems();
        if (lines != null)
            foreach (var line in lines)
                DrawQuadrilateral(canvas, line.GetLocation(), new Scalar(0, 255, 0));
    }

    ParsedResult? parsedResult = result.GetParsedResult();
    ParsedResultItem[]? items = parsedResult?.GetItems();

    if (items == null || items.Length == 0)
    {
        this.BeginInvoke((MethodInvoker)delegate { richTextBoxInfo.Text = "No MRZ detected."; });
        return;
    }

    StringBuilder sb = new StringBuilder();
    foreach (ParsedResultItem item in items)
    {
        string docType = item.GetCodeType();
        sb.AppendLine($"Document Type: {docType}");

        // Raw MRZ lines
        string? line1 = item.GetFieldValue("line1");
        string? line2 = item.GetFieldValue("line2");
        string? line3 = item.GetFieldValue("line3");
        sb.AppendLine("\nRaw MRZ Text:");
        if (line1 != null) sb.AppendLine($"  Line 1: {line1}");
        if (line2 != null) sb.AppendLine($"  Line 2: {line2}");
        if (line3 != null) sb.AppendLine($"  Line 3: {line3}");

        // Structured fields
        string? docId      = docType == "MRTD_TD3_PASSPORT"
                                ? item.GetFieldValue("passportNumber")
                                : item.GetFieldValue("documentNumber");
        string? surname    = item.GetFieldValue("primaryIdentifier");
        string? givenname  = item.GetFieldValue("secondaryIdentifier");
        string? nationality= item.GetFieldValue("nationality");
        string? issuer     = item.GetFieldValue("issuingState");
        string? dob        = item.GetFieldValue("dateOfBirth");
        string? expiry     = item.GetFieldValue("dateOfExpiry");
        string? gender     = item.GetFieldValue("sex");

        sb.AppendLine("\nParsed Information:");
        if (docId      != null) sb.AppendLine($"  Document ID:      {docId}");
        if (surname    != null) sb.AppendLine($"  Surname:          {surname}");
        if (givenname  != null) sb.AppendLine($"  Given Name:       {givenname}");
        if (nationality!= null) sb.AppendLine($"  Nationality:      {nationality}");
        if (issuer     != null) sb.AppendLine($"  Issuing Country:  {issuer}");
        if (gender     != null) sb.AppendLine($"  Gender:           {gender}");
        if (dob        != null) sb.AppendLine($"  Date of Birth:    {dob}");
        if (expiry     != null) sb.AppendLine($"  Expiration Date:  {expiry}");
    }

    this.BeginInvoke((MethodInvoker)delegate { richTextBoxInfo.Text = sb.ToString(); });
}

Detect and Normalize Documents

GetProcessedDocumentResult() provides both edge-detection quads (GetDetectedQuadResultItems) and normalized images (GetEnhancedImageResultItems). Normalized images are deskewed, perspective-corrected, and optionally converted to grayscale or binary:

private void ProcessDocumentResult(CapturedResult result, Mat originalMat, Mat canvas, bool isFileMode)
{
    ProcessedDocumentResult? docResult = result.GetProcessedDocumentResult();

    DetectedQuadResultItem[]? detectedItems = docResult?.GetDetectedQuadResultItems();
    if (detectedItems != null)
        foreach (var quadItem in detectedItems)
            DrawQuadrilateral(canvas, quadItem.GetLocation(), new Scalar(0, 0, 255), "Document");

    EnhancedImageResultItem[]? enhancedItems = docResult?.GetEnhancedImageResultItems();

    if ((detectedItems == null || detectedItems.Length == 0) &&
        (enhancedItems  == null || enhancedItems.Length  == 0))
    {
        this.BeginInvoke((MethodInvoker)delegate { richTextBoxInfo.Text = "No document detected."; });
        return;
    }

    StringBuilder sb = new StringBuilder();
    if (detectedItems != null && detectedItems.Length > 0)
        sb.AppendLine($"Detected {detectedItems.Length} document edge(s).");

    if (enhancedItems != null && enhancedItems.Length > 0)
    {
        sb.AppendLine($"Normalized {enhancedItems.Length} document(s):");
        for (int i = 0; i < enhancedItems.Length; i++)
        {
            ImageData? imageData = enhancedItems[i].GetImageData();
            if (imageData != null)
                sb.AppendLine($"  Document {i + 1}: {imageData.GetWidth()}x{imageData.GetHeight()}");
        }
    }

    this.BeginInvoke((MethodInvoker)delegate { richTextBoxInfo.Text = sb.ToString(); });
}

Runtime License Entry

An InputBox helper dialog lets users enter a new license key at runtime without restarting the application:

public static string InputBox(string title, string promptText, string value)
{
    Form form = new Form();
    RichTextBox textBox = new RichTextBox();
    Button buttonOk     = new Button();
    Button buttonCancel = new Button();

    form.Text = title;
    textBox.Text = value;
    buttonOk.Text = "OK";
    buttonCancel.Text = "Cancel";
    buttonOk.DialogResult     = DialogResult.OK;
    buttonCancel.DialogResult = DialogResult.Cancel;

    textBox.SetBounds(12, 36, 372, 20);
    buttonOk.SetBounds(60, 72, 80, 30);
    buttonCancel.SetBounds(260, 72, 80, 30);

    form.ClientSize = new System.Drawing.Size(400, 120);
    form.Controls.AddRange(new Control[] { textBox, buttonOk, buttonCancel });
    form.FormBorderStyle = FormBorderStyle.FixedDialog;
    form.StartPosition   = FormStartPosition.CenterScreen;
    form.MinimizeBox     = false;
    form.MaximizeBox     = false;
    form.AcceptButton    = buttonOk;
    form.CancelButton    = buttonCancel;

    form.ShowDialog();
    return textBox.Text;
}

private void enterLicenseKeyToolStripMenuItem_Click(object sender, EventArgs e)
{
    string license = InputBox("Enter License Key", "", "");
    if (!string.IsNullOrEmpty(license))
    {
        int errorCode = LicenseManager.InitLicense(license, out string errorMsg);
        toolStripStatusLabel1.Text = errorCode == (int)EnumErrorCode.EC_OK
            ? "License activated successfully."
            : $"License error: {errorMsg}";
    }
}

Source Code

https://github.com/yushulx/dotnet-barcode-qr-code-sdk/tree/main/example/official/barcode_document_mrz