Developing a Camera-Based Barcode Scanner in .NET MAUI for Windows Desktop

Xiao Ling
8 min read1 day ago

--

Dynamsoft provides two NuGet packages for .NET MAUI development: Dynamsoft.BarcodeReaderBundle.Maui and Dynamsoft.DotNet.BarcodeReader.Bundle. The former targets Android and iOS, while the latter is built for . In this article, we will demonstrate how to use Windows Media API to capture video frames from a USB camera and integrate Dynamsoft Barcode Reader for barcode scanning in a .NET MAUI Windows application.

Windows Barcode Scanner in .NET MAUI

Prerequisites

Steps to Create a .NET MAUI Windows Application for Reading 1D/2D Barcodes

This project showcases barcode detection functionality across two MAUI pages: one for image files and another for a live camera stream. Barcode results will be displayed above the image or video frame.

Step 1: Set Up a .NET MAUI Project

  1. Create a new .NET MAUI project in Visual Studio or Visual Studio Code.
  2. Add the Dynamsoft Barcode Reader and SkiaSharp NuGet packages to your project. The SkiaSharp package enhances rendering of barcode text and contours, outperforming the built-in MAUI graphics API in terms of performance and flexibility.
<PackageReference Include="Dynamsoft.DotNet.BarcodeReader.Bundle" Version="10.4.2000" />
<PackageReference Include="SkiaSharp.Views.Maui.Controls" Version="3.116.1" />

3. Activate the barcode SDK with a valid license key in Platforms/Windows/App.xaml.cs. Replace LICENSE-KEY with your own license key.

public partial class App : MauiWinUIApplication
{

public App()
{
this.InitializeComponent();

string license = "LICENSE-KEY";

string errorMsg;
int errorCode = LicenseManager.InitLicense(license, out errorMsg);
if (errorCode != (int) Dynamsoft.Core.EnumErrorCode.EC_OK) Debug.WriteLine("License initialization error: " + errorMsg);
}

protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}

4. In MauiProgram.cs, initialize and integrate SkiaSharp:

using SkiaSharp.Views.Maui.Controls.Hosting;

public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.UseSkiaSharp()
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
});

#if DEBUG
builder.Logging.AddDebug();
#endif

return builder.Build();
}
}

5. Create two MAUI pages: PicturePage.xaml and CameraPage.xaml.

6. In MainPage.xaml, add two buttons for page navigation:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="BarcodeQrScanner.MainPage">

<ContentPage.Content>
<StackLayout>
<Button x:Name="takePhotoButton"
Text="Image File"
HorizontalOptions="Center"
VerticalOptions="CenterAndExpand"
Clicked="OnFileButtonClicked"/>
<Button x:Name="takeVideoButton"
Text="Camera Stream"
HorizontalOptions="Center"
VerticalOptions="CenterAndExpand"
Clicked="OnCameraButtonClicked"/>
</StackLayout>
</ContentPage.Content>

</ContentPage>

7. In MainPage.xaml.cs, add event handlers for the two buttons:

public partial class MainPage : ContentPage
{

public MainPage()
{
InitializeComponent();
}

private async void OnFileButtonClicked(object sender, EventArgs e)
{
try
{

FileResult? photo = await FilePicker.PickAsync();

await LoadPhotoAsync(photo);
}
catch (Exception ex)
{
Debug.WriteLine($"CapturePhotoAsync THREW: {ex.Message}");
}
}

private async void OnCameraButtonClicked(object sender, EventArgs e)
{
await Navigation.PushAsync(new CameraPage());
}

async Task LoadPhotoAsync(FileResult? photo)
{
if (photo == null)
{
return;
}

await Navigation.PushAsync(new PicturePage(photo.FullPath));
}
}

Clicking the “Image File” button opens a file picker dialog. After selecting an image, the app navigates to the PicturePage with the chosen file path.

Step 2: Read Barcodes from Image Files

The UI of the PicturePage contains an SKCanvasView, which renders the image and its corresponding barcode results when the OnCanvasViewPaintSurface event handler is triggered.

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:skia="clr-namespace:SkiaSharp.Views.Maui.Controls;assembly=SkiaSharp.Views.Maui.Controls"
x:Class="BarcodeQrScanner.PicturePage">

<ContentPage.Content>
<Grid>
<skia:SKCanvasView x:Name="canvasView"
HorizontalOptions="Fill"
VerticalOptions="Fill"
PaintSurface="OnCanvasViewPaintSurface"/>

</Grid>
</ContentPage.Content>

</ContentPage>

The C# code implementation is as follows:

  1. Load and decode the image file into an SKBitmap object. Since image files may contain EXIF orientation data, the orientation must be corrected before rendering.
bitmap = LoadAndCorrectOrientation(imagepath);

SKBitmap LoadAndCorrectOrientation(string imagePath)
{
using var stream = new SKFileStream(imagePath);
using var codec = SKCodec.Create(stream);

SKBitmap bitmap = SKBitmap.Decode(codec);

var origin = codec.EncodedOrigin;
if (origin == SKEncodedOrigin.TopLeft)
{
return bitmap;
}

SKMatrix matrix = SKMatrix.CreateIdentity();
int rotatedWidth = bitmap.Width;
int rotatedHeight = bitmap.Height;

switch (origin)
{
case SKEncodedOrigin.RightTop:
matrix = SKMatrix.CreateRotationDegrees(90, 0, 0);
rotatedWidth = bitmap.Height;
rotatedHeight = bitmap.Width;
break;
case SKEncodedOrigin.BottomRight:
matrix = SKMatrix.CreateRotationDegrees(180, 0, 0);
break;
case SKEncodedOrigin.LeftBottom:
matrix = SKMatrix.CreateRotationDegrees(270, 0, 0);
rotatedWidth = bitmap.Height;
rotatedHeight = bitmap.Width;
break;
default:
break;
}

SKBitmap rotatedBitmap = new SKBitmap(rotatedWidth, rotatedHeight);
using (var surface = new SKCanvas(rotatedBitmap))
{
switch (origin)
{
case SKEncodedOrigin.RightTop:
surface.Translate(rotatedWidth, 0);
break;
case SKEncodedOrigin.BottomRight:
surface.Translate(rotatedWidth, rotatedHeight);
break;
case SKEncodedOrigin.LeftBottom:
surface.Translate(0, rotatedHeight);
break;
}
surface.Concat(matrix);
surface.DrawBitmap(bitmap, 0, 0);
}

return rotatedBitmap;
}

2. Initialize the CaptureVisionRouter object.

private CaptureVisionRouter cvr = new CaptureVisionRouter();

3. Read barcodes with CaptureVisionRouter. The decoding runs asynchronously to avoid blocking the UI thread.

async void DecodeFile(string imagepath)
{
await Task.Run(() =>
{
result = cvr.Capture(imagepath, PresetTemplate.PT_READ_BARCODES);
isDataReady = true;
return Task.CompletedTask;
});
canvasView.InvalidateSurface();
}

4. Render the image and barcode results:

void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
if (!isDataReady)
{
return;
}
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();

if (bitmap != null)
{
var imageCanvas = new SKCanvas(bitmap);

float textSize = 18;
float StrokeWidth = 2;

if (DeviceInfo.Current.Platform == DevicePlatform.Android || DeviceInfo.Current.Platform == DevicePlatform.iOS)
{
textSize = (float)(18 * DeviceDisplay.MainDisplayInfo.Density);
StrokeWidth = 4;
}

SKPaint skPaint = new SKPaint
{
Style = SKPaintStyle.Stroke,
Color = SKColors.Blue,
StrokeWidth = StrokeWidth,
};

SKPaint textPaint = new SKPaint
{
Style = SKPaintStyle.Stroke,
Color = SKColors.Red,
StrokeWidth = StrokeWidth,
};

SKFont font = new SKFont() { Size = textSize };
if (isDataReady)
{
if (result != null)
{
ResultLabel.Text = "";
DecodedBarcodesResult? barcodesResult = result.GetDecodedBarcodesResult();
if (barcodesResult != null)
{
BarcodeResultItem[] items = barcodesResult.GetItems();
foreach (BarcodeResultItem barcodeItem in items)
{
Dynamsoft.Core.Point[] points = barcodeItem.GetLocation().points;
imageCanvas.DrawText(barcodeItem.GetText(), points[0][0], points[0][1], SKTextAlign.Left, font, textPaint);
imageCanvas.DrawLine(points[0][0], points[0][1], points[1][0], points[1][1], skPaint);
imageCanvas.DrawLine(points[1][0], points[1][1], points[2][0], points[2][1], skPaint);
imageCanvas.DrawLine(points[2][0], points[2][1], points[3][0], points[3][1], skPaint);
imageCanvas.DrawLine(points[3][0], points[3][1], points[0][0], points[0][1], skPaint);
}
}
}
}


float scale = Math.Min((float)info.Width / bitmap.Width,
(float)info.Height / bitmap.Height);
float x = (info.Width - scale * bitmap.Width) / 2;
float y = (info.Height - scale * bitmap.Height) / 2;
SKRect destRect = new SKRect(x, y, x + scale * bitmap.Width,
y + scale * bitmap.Height);

canvas.DrawBitmap(bitmap, destRect);
}

}

Step 3: Capture Video Frames from a USB Camera and Read Barcodes in Real-Time

To access a camera stream in a .NET MAUI Windows application, we need to create a shared camera view and a platform-specific view handler. The handler maps the shared view to the native Windows camera view ( MediaPlayerElement).

Define the Shared Camera View

Create a file named CameraView.cs and add the following code:

using Dynamsoft.CVR;

namespace BarcodeQrScanner.Controls
{
public class ResultReadyEventArgs : EventArgs
{
public ResultReadyEventArgs(CapturedResult? result, int previewWidth, int previewHeight)
{
Result = result;
PreviewWidth = previewWidth;
PreviewHeight = previewHeight;
}

public CapturedResult? Result { get; private set; }
public int PreviewWidth { get; private set; }
public int PreviewHeight { get; private set; }
}

public class CameraView : View
{
public event EventHandler<ResultReadyEventArgs>? ResultReady;

public void NotifyResultReady(CapturedResult? result, int previewWidth, int previewHeight)
{
if (ResultReady != null)
{
ResultReady(this, new ResultReadyEventArgs(result, previewWidth, previewHeight));
}
}

public void UpdateResolution(int width, int height)
{
WidthRequest = width;
HeightRequest = height;
}

public void StartPreview() => Handler?.Invoke(nameof(ICameraHandler.StartPreview));
public void StopPreview() => Handler?.Invoke(nameof(ICameraHandler.StopPreview));
}

public interface ICameraHandler : IViewHandler
{
void StartPreview();
void StopPreview();
}
}

Explanation of the Code:

  • ResultReadyEventArgs is a custom event argument class that encapsulates the barcode result and preview resolution. When an EventHandler is instantiated in a MAUI page, these arguments are passed to the page for processing.
  • CameraView extends the View class and is used in the MAUI page to display the camera preview.
  • NotifyResultReady relays barcode results and preview dimensions from the platform-specific handler.
  • UpdateResolution sets the width and height of the camera view.
  • StartPreview and StopPreview trigger the respective actions in the handler.
  • ICameraHandler is an interface extending IViewHandler, defining methods to control the camera preview.

Implement the Platform-Specific View Handler

Create a file named CameraPreviewHandler.cs in the Platforms/Windows folder and add the following code:

  1. Map the ICameraHandler interface to the CameraPreviewHandler class. This enables the StartPreview and StopPreview functions:
public static CommandMapper<CameraView, CameraPreviewHandler> CommandMapper = new(ViewCommandMapper)
{
[nameof(ICameraHandler.StartPreview)] = MapStartPreview,
[nameof(ICameraHandler.StopPreview)] = MapStopPreview
};

private static void MapStartPreview(CameraPreviewHandler handler, CameraView view, object? arg)
{
handler.StartPreview();
}

private static void MapStopPreview(CameraPreviewHandler handler, CameraView cameraView, object? arg)
{
handler.StopPreview();
}

public void StartPreview()
{
if (!_isPreviewing)
_ = InitializeCameraAsync();
}

public void StopPreview()
{
if (_isPreviewing)
CleanupCamera();
}

2. Initialize and start camera preview:

private async Task InitializeCameraAsync()
{
try
{
_mediaCapture = new MediaCapture();

var allSourceGroups = MediaFrameSourceGroup.FindAllAsync().GetAwaiter().GetResult();

var settings = new MediaCaptureInitializationSettings
{
//SourceGroup = allSourceGroups.FirstOrDefault(),
MemoryPreference = MediaCaptureMemoryPreference.Cpu,
StreamingCaptureMode = StreamingCaptureMode.Video
};
await _mediaCapture.InitializeAsync(settings);

var frameSource = _mediaCapture.FrameSources.FirstOrDefault(source => source.Value.Info.MediaStreamType == MediaStreamType.VideoRecord
&& source.Value.Info.SourceKind == MediaFrameSourceKind.Color).Value;
if (frameSource != null)
{
if (VirtualView != null)
{
VirtualView.UpdateResolution((int)frameSource.CurrentFormat.VideoFormat.Width, (int)frameSource.CurrentFormat.VideoFormat.Height);
}
MediaFrameFormat? frameFormat;
frameFormat = frameSource.SupportedFormats.OrderByDescending(f => f.VideoFormat.Width * f.VideoFormat.Height).FirstOrDefault();

if (frameFormat != null)
{
await frameSource.SetFormatAsync(frameFormat);
platformView.AutoPlay = true;
platformView.Source = MediaSource.CreateFromMediaFrameSource(frameSource);

_frameReader = await _mediaCapture.CreateFrameReaderAsync(frameSource);
_frameReader.AcquisitionMode = MediaFrameReaderAcquisitionMode.Realtime;
if (_frameReader != null)
{
_frameReader.FrameArrived += OnFrameAvailable;
await _frameReader.StartAsync();
}

_isPreviewing = true;

}
}
}
catch (Exception ex)
{
Debug.WriteLine($"Camera init failed: {ex}");
}
}

private void OnFrameAvailable(MediaFrameReader sender, MediaFrameArrivedEventArgs args)
{
// Process the frame
}

The OnFrameAvailable event handler is triggered whenever a new frame is available.

3. Process frames and decode barcodes with CaptureVisionRouter.

private CaptureVisionRouter cvr = new CaptureVisionRouter();

private void OnFrameAvailable(MediaFrameReader sender, MediaFrameArrivedEventArgs args)
{
using (var frame = sender.TryAcquireLatestFrame())
{
if (frame?.VideoMediaFrame?.SoftwareBitmap == null) return;

var bitmap = SoftwareBitmap.Convert(
frame.VideoMediaFrame.SoftwareBitmap,
BitmapPixelFormat.Bgra8,
BitmapAlphaMode.Premultiplied
);

ProcessFrame(bitmap);
}
}

private void ProcessFrame(SoftwareBitmap bitmap)
{
using (bitmap)
{
using (SoftwareBitmap grayscale = SoftwareBitmap.Convert(bitmap, BitmapPixelFormat.Gray8, BitmapAlphaMode.Ignore))
{
byte[] buffer = new byte[grayscale.PixelWidth * grayscale.PixelHeight];
grayscale.CopyToBuffer(buffer.AsBuffer());
ImageData data = new ImageData(buffer, grayscale.PixelWidth, grayscale.PixelHeight, grayscale.PixelWidth, EnumImagePixelFormat.IPF_GRAYSCALED);
CapturedResult? result = cvr.Capture(data, PresetTemplate.PT_READ_BARCODES);

try
{
VirtualView.NotifyResultReady(result, bitmap.PixelWidth, bitmap.PixelHeight);
}
catch (Exception e)
{

}
}
}
}

4. Stop the camera preview:

private void CleanupCamera()
{
if (_frameReader != null)
{
_frameReader.FrameArrived -= OnFrameAvailable;
_frameReader?.StopAsync().AsTask().Wait();
_frameReader?.Dispose();
_mediaCapture?.Dispose();
_isPreviewing = false;
_frameReader = null;
}
}

Register the Handler

In MauiProgram.cs, register the handler for the CameraView:

using Microsoft.Extensions.Logging;
using SkiaSharp.Views.Maui.Controls.Hosting;
#if WINDOWS
using BarcodeQrScanner.Platforms.Windows;
#endif
using BarcodeQrScanner.Controls;

namespace BarcodeQrScanner;

public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.UseSkiaSharp()
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
}).ConfigureMauiHandlers(handlers =>
{
#if WINDOWS
handlers.AddHandler(typeof(CameraView), typeof(CameraPreviewHandler));
#endif
}); ;

#if DEBUG
builder.Logging.AddDebug();
#endif

return builder.Build();
}
}

Step 4: Integrate the Camera View in the Camera Page

Add CameraView and SKCanvasView to CameraPage.xaml:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="BarcodeQrScanner.CameraPage"
xmlns:controls="clr-namespace:BarcodeQrScanner.Controls"
xmlns:skia="clr-namespace:SkiaSharp.Views.Maui.Controls;assembly=SkiaSharp.Views.Maui.Controls"
Title="CameraPage">

<Grid>
<controls:CameraView
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand"
x:Name="CameraView"
ResultReady="OnResultReady"/>

<skia:SKCanvasView
Margin="0"
HorizontalOptions="Fill" VerticalOptions="Fill"
PaintSurface="OnOverlayPaint"
x:Name="Overlay"/>

<HorizontalStackLayout HorizontalOptions="Center" VerticalOptions="End" Spacing="10">
<HorizontalStackLayout>
<Button x:Name="startButton"
Text="StartStream"
HorizontalOptions="Center"
VerticalOptions="Center"
Clicked="OnStartButtonClicked"/>
</HorizontalStackLayout>

<HorizontalStackLayout>
<Button x:Name="stopButton"
Text="StopStream"
HorizontalOptions="Center"
VerticalOptions="Center"
Clicked="OnStopButtonClicked"/>
</HorizontalStackLayout>

</HorizontalStackLayout>

</Grid>

</ContentPage>

The Grid layout ensures that the SKCanvasView overlays the CameraView with identical dimensions. The HorizontalStackLayout contains two buttons to start and stop the camera stream.

The corresponding C# code is as follows:

using Dynamsoft.DBR;
using SkiaSharp;
using SkiaSharp.Views.Maui;
using BarcodeQrScanner.Controls;
using Dynamsoft.CVR;
namespace BarcodeQrScanner;

public partial class CameraPage : ContentPage
{
private static object _lockObject = new object();
private int imageWidth;
private int imageHeight;
private CapturedResult? result;

public CameraPage()
{
InitializeComponent();
}

private void OnStopButtonClicked(object sender, EventArgs e)
{
CameraView.StopPreview();
}

private void OnStartButtonClicked(object sender, EventArgs e)
{
CameraView.StartPreview();
}

void OnOverlayPaint(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();

float textSize = 18;
float StrokeWidth = 2;
SKPaint skPaint = new SKPaint
{
Style = SKPaintStyle.Stroke,
Color = SKColors.Blue,
StrokeWidth = StrokeWidth,
};

SKPaint textPaint = new SKPaint
{
Style = SKPaintStyle.Stroke,
Color = SKColors.Red,
StrokeWidth = StrokeWidth,
};

SKFont font = new SKFont() { Size = textSize };

canvas.DrawRect(0, 0, info.Width, info.Height, skPaint);

lock (_lockObject)
{
if (result == null) { return; }

DecodedBarcodesResult? barcodesResult = result.GetDecodedBarcodesResult();
if (barcodesResult != null)
{
BarcodeResultItem[] items = barcodesResult.GetItems();
foreach (BarcodeResultItem barcodeItem in items)
{
Dynamsoft.Core.Point[] points = barcodeItem.GetLocation().points;
canvas.DrawText(barcodeItem.GetText(), points[0][0], points[0][1], SKTextAlign.Left, font, textPaint);
canvas.DrawLine(points[0][0], points[0][1], points[1][0], points[1][1], skPaint);
canvas.DrawLine(points[1][0], points[1][1], points[2][0], points[2][1], skPaint);
canvas.DrawLine(points[2][0], points[2][1], points[3][0], points[3][1], skPaint);
canvas.DrawLine(points[3][0], points[3][1], points[0][0], points[0][1], skPaint);
}
}

}

}

private void OnResultReady(object sender, ResultReadyEventArgs e)
{
lock (_lockObject)
{
imageWidth = e.PreviewWidth;
imageHeight = e.PreviewHeight;
result = e.Result;
Overlay.InvalidateSurface();
}
}
}

Source Code

https://github.com/yushulx/maui-barcode-mrz-document-scanner/tree/main/examples/WindowsDesktop

Originally published at https://www.dynamsoft.com on February 20, 2025.

--

--

Xiao Ling
Xiao Ling

Written by Xiao Ling

Manager of Dynamsoft Open Source Projects | Tech Lover

No responses yet