Building .NET MAUI Barcode Scanner with Visual Studio Code on macOS

Recently, Dynamsoft released a new .NET MAUI Barcode SDK for building barcode scanning applications on Android and iOS. In this tutorial, we will use Visual Studio Code to create a .NET MAUI Barcode Scanner from scratch. Our application will decode barcodes and QR codes from both image files and camera video stream.

This article is Part 1 in a 1-Part Series.

Prerequisites

To get started, you’ll need to install the following tools:

For detailed installation instructions, refer to the Microsoft tutorial.

Why Not Visual Studio for Mac?

Microsoft has announced the retirement of Visual Studio for Mac, with support ending on August 31, 2024. The new .NET MAUI extension for Visual Studio Code offers a superior development experience for cross-platform applications.

Step 1: Scaffold a .NET MAUI Project

Create a new .NET MAUI project in Visual Studio Code:

  1. Open the command palette by pressing Cmd + Shift + P or F1.
  2. Type > .NET: New Project and press Enter.
  3. Select .NET MAUI App and press Enter.
  4. Enter the project name and choose the location to save the project.

To run the project on an Android device or iOS device:

  1. Open the command palette and type > .NET MAUI: Pick Android Device or > .NET MAUI: Pick iOS Device to select a device.

    Pick Android Device

  2. Press F5 to build and run the project.

Step 2: Install Dependencies for Barcode Detection and Android Lifecycle Notifications

To enable barcode detection and handle Android lifecycle notifications, install the following NuGet packages:

Run the following commands to add these packages to your project:

dotnet add package Dynamsoft.BarcodeReaderBundle.Maui
dotnet add package CommunityToolkit.Mvvm

Next, configure the dependencies in the MauiProgram.cs file:

using Microsoft.Extensions.Logging;
using Dynamsoft.CameraEnhancer.Maui;
using Dynamsoft.CameraEnhancer.Maui.Handlers;
using CommunityToolkit.Maui;
using Microsoft.Maui.LifecycleEvents;
using CommunityToolkit.Mvvm.Messaging;

namespace BarcodeQrScanner;

public static class MauiProgram
{
	public static MauiApp CreateMauiApp()
	{
		var builder = MauiApp.CreateBuilder();
		builder
			.UseMauiApp<App>().UseMauiCommunityToolkit()
			.ConfigureFonts(fonts =>
			{
				fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
				fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
			})
			.ConfigureLifecycleEvents(events =>
                            {
#if ANDROID
                                events.AddAndroid(android => android
                                    .OnResume((activity) =>
                                    {
										NotifyPage("Resume");
									})
                                    .OnStop((activity) =>
                                    {
										NotifyPage("Stop");
                                    }));
#endif
                            })
							.ConfigureMauiHandlers(handlers =>
            {
                handlers.AddHandler(typeof(CameraView), typeof(CameraViewHandler));
            });

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

		return builder.Build();
	}

	private static void NotifyPage(string eventName)
	{
		WeakReferenceMessenger.Default.Send(new LifecycleEventMessage(eventName));
	}
}

Step 3: Add Permission Descriptions for Android and iOS

To enable the application to pick images from the gallery and access the camera, you need to add permission descriptions in the AndroidManifest.xml and Info.plist files.

Android

Add the following permissions to your AndroidManifest.xml file:

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" />

iOS

Add the following keys to your Info.plist file to describe why your app needs access to the photo library, camera, and microphone:

<key>NSPhotoLibraryUsageDescription</key>
<string>App needs access to the photo library to pick images.</string>
<key>NSCameraUsageDescription</key>
<string>This app is using the camera</string>
<key>NSMicrophoneUsageDescription</key>
<string>This app needs access to microphone for taking videos.</string>

By adding these permission descriptions, you ensure that your application has the necessary access to the device’s camera and photo library, complying with Android and iOS security requirements.

Step 4: Activate Dynamsoft Barcode Reader SDK

To use the Dynamsoft Barcode Reader SDK, you need to activate it with a valid license key in the MainPage.xaml.cs file. You can obtain a 30-day free trial license from Dynamsoft.

public partial class MainPage : ContentPage, ILicenseVerificationListener
{
    public MainPage()
    {
        InitializeComponent();
        LicenseManager.InitLicense("LICENSE-KEY", this);
    }

    public void OnLicenseVerified(bool isSuccess, string message)
    {
        if (!isSuccess)
        {
            Debug.WriteLine(message);
        }
    }
}

Step 5: Add Two Buttons to the Main Page

First, create a PicturePage for decoding barcodes from image files and a CameraPage for scanning barcodes from the camera video stream. Then, add two buttons to the main page: one for picking an image from the gallery and navigating to PicturePage, and another for requesting camera permissions and navigating to CameraPage.

MainPage.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.MainPage"
             >

    <ScrollView>
        <StackLayout>
            <Button x:Name="takePhotoButton" Text="Image File" HorizontalOptions="Center" VerticalOptions="CenterAndExpand" Clicked="OnTakePhotoButtonClicked" />
            <Button x:Name="takeVideoButton" Text="Video Stream" HorizontalOptions="Center" VerticalOptions="CenterAndExpand" Clicked="OnTakeVideoButtonClicked" />
        </StackLayout>
    </ScrollView>

</ContentPage>

MainPage.xaml.cs

async void OnTakePhotoButtonClicked(object sender, EventArgs e)
{
    try
    {
        var result = await FilePicker.Default.PickAsync(new PickOptions
        {
            FileTypes = FilePickerFileType.Images,
            PickerTitle = "Please select an image"
        });

        if (result != null)
        {
            await Navigation.PushAsync(new PicturePage(result));
        }
    }
    catch (Exception ex)
    {
        // Handle exceptions if any
        Console.WriteLine($"An error occurred: {ex.Message}");
    }
}

async void OnTakeVideoButtonClicked(object sender, EventArgs e)
{

    var status = await Permissions.CheckStatusAsync<Permissions.Camera>();
    if (status == PermissionStatus.Granted)
    {
        await Navigation.PushAsync(new CameraPage());
    }
    else
    {
        status = await Permissions.RequestAsync<Permissions.Camera>();
        if (status == PermissionStatus.Granted)
        {
            await Navigation.PushAsync(new CameraPage());
        }
        else
        {
            await DisplayAlert("Permission needed", "I will need Camera permission for this action", "Ok");
        }
    }
}

Step 6: Read Barcodes from Image Files

  1. Add an Image control to display the selected image and a GraphicsView control to overlay the barcode results.

     <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
                  xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                  x:Class="BarcodeQrScanner.PicturePage"
                  Title="Barcode Reader">
        
         <Grid>
             <Image x:Name="PickedImage" Aspect="AspectFit" VerticalOptions="CenterAndExpand" HorizontalOptions="CenterAndExpand" SizeChanged="OnImageSizeChanged"/>
             <GraphicsView x:Name="OverlayGraphicsView" />
         </Grid>
     </ContentPage>
    

    Ensure the size of the GraphicsView matches the size of the Image control, and update the GraphicsView size when the Image control size changes.

     private void OnImageSizeChanged(object sender, EventArgs e)
     {
         // Adjust the GraphicsView size to match the Image size
         OverlayGraphicsView.WidthRequest = PickedImage.Width;
         OverlayGraphicsView.HeightRequest = PickedImage.Height;
     }
    
  2. Get the image width and height for calculating the overlay position:

     public PicturePage(FileResult result)
     {
         InitializeComponent();
         LoadImageWithOverlay(result);
     }
    
     async private void LoadImageWithOverlay(FileResult result)
     {
         // Get the file path
         var filePath = result.FullPath;
         var stream = await result.OpenReadAsync();
    
         float originalWidth = 0;
         float originalHeight = 0;
    
         try
         {
             var image = PlatformImage.FromStream(stream);
             originalWidth = image.Width;
             originalHeight = image.Height;
    
             ...
         }
         catch (Exception ex)
         {
             Console.WriteLine($"An error occurred: {ex.Message}");
         }
     }
    
  3. Reset the file stream position for displaying the image:

     stream.Position = 0;
     ImageSource imageSource = ImageSource.FromStream(() => stream);
     PickedImage.Source = imageSource;
    
  4. Decode barcodes from the image file:

     private CaptureVisionRouter router = new CaptureVisionRouter();
     CapturedResult capturedResult = router.Capture(filePath, EnumPresetTemplate.PT_READ_BARCODES);
     DecodedBarcodesResult? barcodeResults = null;
    
     if (capturedResult != null) {
         // Get the barcode results
         barcodeResults = capturedResult.DecodedBarcodesResult;
     }
    
  5. Draw the barcode results over the image:

     public class ImageWithOverlayDrawable : IDrawable
     {
         private readonly DecodedBarcodesResult? _barcodeResults;
         private readonly float _originalWidth;
         private readonly float _originalHeight;
        
         private bool _isFile;
        
         public ImageWithOverlayDrawable(DecodedBarcodesResult? barcodeResults, float originalWidth, float originalHeight, bool isFile = false)
         {
             _barcodeResults = barcodeResults;
             _originalWidth = originalWidth;
             _originalHeight = originalHeight;
             _isFile = isFile;
         }
        
         public void Draw(ICanvas canvas, RectF dirtyRect)
         {
             // Calculate scaling factors
             float scaleX = (int)dirtyRect.Width / _originalWidth;
             float scaleY = (int)dirtyRect.Height / _originalHeight;
        
             // Set scaling to maintain aspect ratio
             float scale = Math.Min(scaleX, scaleY);
        
             canvas.StrokeColor = Colors.Red;
             canvas.StrokeSize = 2;
             canvas.FontColor = Colors.Red;
        
             if (_barcodeResults != null) {
                 var items = _barcodeResults.Items;
                 foreach (var item in items) {
                     Microsoft.Maui.Graphics.Point[] points = item.Location.Points;
    
                     if (_isFile){
                         canvas.DrawLine((float)points[0].X * scale, (float)points[0].Y * scale, (float)points[1].X * scale, (float)points[1].Y * scale);
                         canvas.DrawLine((float)points[1].X * scale, (float)points[1].Y * scale, (float)points[2].X * scale, (float)points[2].Y * scale);
                         canvas.DrawLine((float)points[2].X * scale, (float)points[2].Y * scale, (float)points[3].X * scale, (float)points[3].Y * scale);
                         canvas.DrawLine((float)points[3].X * scale, (float)points[3].Y * scale, (float)points[0].X * scale, (float)points[0].Y * scale);
                     }
        
                     canvas.DrawString(item.Text, (float)points[0].X * scale, (float)points[0].Y * scale - 10, HorizontalAlignment.Left);
                 }
             }
         }
     }
    
     var drawable = new ImageWithOverlayDrawable(barcodeResults, originalWidth, originalHeight, true);
    
     // Set drawable to GraphicsView
     OverlayGraphicsView.Drawable = drawable;
     OverlayGraphicsView.Invalidate();
    

    Barcode Reader

Step 7: Scan Barcodes from Camera Video Stream

  1. In the CameraPage layout, add a CameraView control to display the camera video stream and a GraphicsView control to overlay the barcode results on the video stream.

     <?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:controls="clr-namespace:Dynamsoft.CameraEnhancer.Maui;assembly=Dynamsoft.CaptureVisionRouter.Maui"
                  x:Class="BarcodeQrScanner.CameraPage"
                  Title="Barcode Scanner">
         <Grid Margin="0">
             <controls:CameraView x:Name="CameraPreview"
                                   SizeChanged="OnImageSizeChanged"/>
             <GraphicsView x:Name="OverlayGraphicsView"/>
         </Grid>
     </ContentPage>
    
     private void OnImageSizeChanged(object sender, EventArgs e)
     {
         // Adjust the GraphicsView size to match the Image size
         OverlayGraphicsView.WidthRequest = PickedImage.Width;
         OverlayGraphicsView.HeightRequest = PickedImage.Height;
     }
    
  2. In the CameraPage.xaml.cs file, instantiate CameraEnhancer and start the camera preview. Use WeakReferenceMessenger to handle Android lifecycle events.

     using Dynamsoft.Core.Maui;
     using Dynamsoft.CaptureVisionRouter.Maui;
     using Dynamsoft.BarcodeReader.Maui;
     using Dynamsoft.CameraEnhancer.Maui;
     using System.Diagnostics;
     using CommunityToolkit.Mvvm.Messaging;
    
     public partial class CameraPage : ContentPage, ICapturedResultReceiver, ICompletionListener
     {
         private CameraEnhancer? enhancer = null;
         private CaptureVisionRouter router;
         private float previewWidth = 0;
         private float previewHeight = 0;
        
         public CameraPage()
         {
             InitializeComponent();
             enhancer = new CameraEnhancer();
             router = new CaptureVisionRouter();
             router.SetInput(enhancer);
             router.AddResultReceiver(this);
        
             WeakReferenceMessenger.Default.Register<LifecycleEventMessage>(this, (r, message) =>
             {
                 if (message.EventName == "Resume")
                 {
                     if (this.Handler != null && enhancer != null)
                     {
                         enhancer.Open();
                     }
                 }
                 else if (message.EventName == "Stop")
                 {
                     enhancer?.Close();
                 }
             });
         }
        
         protected override void OnHandlerChanged()
         {
             base.OnHandlerChanged();
             if (this.Handler != null && enhancer != null)
             {
                 enhancer.SetCameraView(CameraPreview);
                 enhancer.Open();
             }
         }
        
         protected override async void OnAppearing()
         {
             base.OnAppearing();
             await Permissions.RequestAsync<Permissions.Camera>();
             router?.StartCapturing(EnumPresetTemplate.PT_READ_BARCODES, this);
         }
        
         protected override void OnDisappearing()
         {
             base.OnDisappearing();
             enhancer?.Close();
             router?.StopCapturing();
         }
     }
    
  3. Receive the barcode results via callback functions and draw them on the video stream using the GraphicsView.

     public void OnCapturedResultReceived(CapturedResult result)
     {
         MainThread.BeginInvokeOnMainThread(() =>
         {
             var drawable = new ImageWithOverlayDrawable(null, previewWidth, previewHeight, false);
    
             // Set drawable to GraphicsView
             OverlayGraphicsView.Drawable = drawable;
             OverlayGraphicsView.Invalidate();
         });
     }
    
     public void OnDecodedBarcodesReceived(DecodedBarcodesResult result)
     {
         if (previewWidth == 0 && previewHeight == 0)
         {
             IntermediateResultManager manager = router.GetIntermediateResultManager();
             ImageData data = manager.GetOriginalImage(result.OriginalImageHashId);
                
             // Create a drawable with the barcode results
             previewWidth = (float)data.Width;
             previewHeight = (float)data.Height;
         }
    
         MainThread.BeginInvokeOnMainThread(() =>
         {
             var drawable = new ImageWithOverlayDrawable(result, previewWidth, previewHeight, false);
    
             // Set drawable to GraphicsView
             OverlayGraphicsView.Drawable = drawable;
             OverlayGraphicsView.Invalidate();
         });
     }
    

    Barcode Scanner

Known Issues

iOS Text Rendering Issue The canvas.DrawString method does not work properly on iOS, resulting in no text being rendered on the GraphicsView.

Source Code

https://github.com/yushulx/maui-barcode-qrcode-scanner