Windows UI Library 3 (WinUI 3) is a modern, native UI platform available as part of the Windows App SDK. It represents the latest evolution in the Windows user interface development, offering a comprehensive set of tools and APIs for building Windows apps.
WinUI 3 incorporates Microsoft’s Fluent Design System, which emphasizes intuitive, visually appealing interfaces. It provides a wide range of controls, styles, and input features that enable developers to create both elegant and functional user experiences.
It supports the development of both Desktop and UWP (Universal Windows Platform) applications, enabling developers to create versatile apps that can run across a wide range of Windows devices, from IoT devices to PCs. Built with performance and reliability in mind, WinUI 3 apps are optimized for smooth and efficient operation, ensuring a responsive user experience.
WinUI 3 is an open-source project, offering backwards compatibility with existing UWP and Windows Forms applications and allowing developers to gradually migrate and modernize their applications. Additionally, WinUI 3 is extensible, supporting custom controls and third-party libraries.
In this learning path you will implement a Win UI 3 application, which will perform square matrix multiplication. The idea is to reproduce the same functionality used in Windows Forms learning path . You will also be able to measure performance improvements on Arm64 architecture.
You can find the complete code used in this learning path here .
Before you begin the implementation, install Visual Studio 2022 with the following workloads:
Then, click the ‘Individual components’ tab and check ‘.NET 6.0 Runtime (Long Term Support)’.
Open Visual Studio and click ‘Create a new project’.
In the next window, search for the ‘Blank App, Packaged (WinUI 3 in Desktop)’ template. Select this template and click the ‘Next’ button.
This action opens the ‘Configure your new project’ window. Here, you should:
Your project should now be ready. Next, you will design the view using XAML declarations and implement the logic using the C# code.
First, you will create four anonymous styles. These styles will control the margins, font size, and font weights of texts displayed in the following controls: TextBlock, NumberBox, Button, and ListBox.
In a WinUI 3 application, you define the styles in the App.xaml
file. Open this file and modify it as shown in the following code snippet:
<?xml version="1.0" encoding="utf-8"?>
<Application
x:Class="Arm64.WinUIApp.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Arm64.WinUIApp">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
<!-- Other merged dictionaries here -->
</ResourceDictionary.MergedDictionaries>
<!-- Other app resources here -->
<Style TargetType="TextBlock">
<Setter Property="Margin"
Value="10" />
<Setter Property="FontSize"
Value="16" />
<Setter Property="FontWeight"
Value="SemiBold" />
</Style>
<Style TargetType="NumberBox">
<Setter Property="Margin"
Value="10" />
<Setter Property="FontSize"
Value="16" />
<Setter Property="FontWeight"
Value="SemiBold" />
</Style>
<Style TargetType="Button">
<Setter Property="Margin"
Value="10" />
<Setter Property="FontSize"
Value="16" />
<Setter Property="FontWeight"
Value="SemiBold" />
</Style>
<Style TargetType="ListBox">
<Setter Property="FontSize"
Value="14" />
<Setter Property="FontWeight"
Value="SemiBold" />
</Style>
</ResourceDictionary>
</Application.Resources>
</Application>
There are four style declarations, which differ by the ‘TargetType’ attribute. This attribute indicates the controls to which the style will be applied. For the TextBlock controls, which represents labels, modify three properties:
Next, you will declare the following user interface:
This view uses a tabular layout, comprising five rows and two columns. To create such a layout using XAML, you can use the Grid control. Open the MainWindow.xaml
file and modify it as follows:
<?xml version="1.0" encoding="utf-8"?>
<Window x:Class="Arm64.WinUIApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid>
<!--Grid configuration-->
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!--Row 1: Processor architecture-->
<TextBlock Text="Processor architecture" />
<TextBlock x:Name="TextBlockProcessorArchitecture"
Grid.Column="1" />
<!--Row 2: Matrix size-->
<TextBlock Grid.Row="1"
Text="Matrix size" />
<NumberBox x:Name="NumberBoxMatrixSize"
Grid.Row="1"
Grid.Column="1"
Minimum="100"
Maximum="500"
Value="100"
SmallChange="100"
LargeChange="200"
SpinButtonPlacementMode="Inline" />
<!--Row 3: Execution count-->
<TextBlock Grid.Row="2"
Text="Execution count" />
<NumberBox x:Name="NumberBoxExecutionCount"
Grid.Row="2"
Grid.Column="1"
Minimum="10"
Maximum="100"
Value="10"
SmallChange="10"
LargeChange="20"
SpinButtonPlacementMode="Inline" />
<!--Row 4: Buttons-->
<Button x:Name="ButtonStart"
Grid.Row="3"
Content="Start"
Click="ButtonStart_Click" />
<Button x:Name="ButtonClear"
Grid.Row="3"
Grid.Column="1"
Content="Clear"
Click="ButtonClear_Click" />
<!--Row 5: ListBox-->
<ListBox x:Name="ListBoxResults"
Grid.Row="4"
Grid.ColumnSpan="2" />
</Grid>
</Window>
The declaration above is quite lengthy, so let’s break it down. First, you configure the Grid’s appearance:
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
These declarations will create a Grid composed of two columns, each spanning half of the available window’s width. Then, you create five rows. The height of the first four rows will be adjusted automatically based on the height of the controls placed in those rows. The last row will occupy the remaining space in the window.
In the first row of the Grid, put two TextBlock controls:
<TextBlock Text="Processor architecture" />
<TextBlock x:Name="TextBlockProcessorArchitecture"
Grid.Column="1" />
The first control will display the fixed text ‘Processor architecture’. The second one will be programmatically modified to display the architecture of the processor, for example, AMD64 or ARM64.
The next two controls define a fixed label (‘Matrix size’) and a NumberBox. The NumberBox is configured to allow the user to select a matrix size between 100-500 in steps of 100, with a default value of 100.:
<TextBlock Grid.Row="1"
Text="Matrix size" />
<NumberBox x:Name="NumberBoxMatrixSize"
Grid.Row="1"
Grid.Column="1"
Minimum="100"
Maximum="500"
Value="100"
SmallChange="100"
LargeChange="200"
SpinButtonPlacementMode="Inline" />
Next, you have the following declarations:
<TextBlock Grid.Row="2"
Text="Execution count" />
<NumberBox x:Name="NumberBoxExecutionCount"
Grid.Row="2"
Grid.Column="1"
Minimum="10"
Maximum="100"
Value="10"
SmallChange="10"
LargeChange="20"
SpinButtonPlacementMode="Inline" />
The first control is used to display the fixed string ‘Execution count’. The second one allows the user to specify the number of executions for the matrix multiplications. Here, you enable the user to select an execution count between 10-100 in steps of 10.
Finally, the view declares two buttons and one ListBox:
<Button x:Name="ButtonStart"
Grid.Row="3"
Content="Start"
Click="ButtonStart_Click" />
<Button x:Name="ButtonClear"
Grid.Row="3"
Grid.Column="1"
Content="Clear"
Click="ButtonClear_Click" />
<ListBox x:Name="ListBoxResults"
Grid.Row="4"
Grid.ColumnSpan="2" />
In this section, you will implement the application’s logic. First, create two helper classes:
They will serve the same purpose as in the Windows Forms learning path .
Next, you will implement event handlers for the two buttons and additional code to programmatically control the window’s appearance.
To implement the helper classes, proceed as follows:
using System;
namespace Arm64.WinUIApp.Helpers
{
public static class MatrixHelper
{
private static readonly Random random = new();
private static double[,] GenerateRandomMatrix(int matrixSize)
{
var matrix = new double[matrixSize, matrixSize];
for (int i = 0; i < matrixSize; i++)
{
for (int j = 0; j < matrixSize; j++)
{
matrix[i, j] = random.NextDouble();
}
}
return matrix;
}
private static double[,] MatrixMultiplication(double[,] matrix1, double[,] matrix2)
{
if (matrix1.Length != matrix2.Length)
{
throw new ArgumentException("The matrices must be of equal size");
}
if (matrix1.GetLength(0) != matrix1.GetLength(1) || matrix2.GetLength(0) != matrix2.GetLength(1))
{
throw new ArgumentException("The matrices must be square");
}
int matrixSize = matrix2.GetLength(0);
var result = new double[matrixSize, matrixSize];
for (int i = 0; i < matrixSize; i++)
{
for (int j = 0; j < matrixSize; j++)
{
result[i, j] = 0;
for (int k = 0; k < matrixSize; k++)
{
result[i, j] += matrix1[i, k] * matrix2[k, j];
}
}
}
return result;
}
public static void SquareMatrixMultiplication(int matrixSize)
{
var matrix1 = GenerateRandomMatrix(matrixSize);
var matrix2 = GenerateRandomMatrix(matrixSize);
MatrixMultiplication(matrix1, matrix2);
}
}
}
using System;
using System.Diagnostics;
namespace Arm64.WinUIApp.Helpers
{
public static class PerformanceHelper
{
private static readonly Stopwatch stopwatch = new();
public static double MeasurePerformance(Action method, int executionCount)
{
stopwatch.Restart();
for (int i = 0; i < executionCount; i++)
{
method();
}
stopwatch.Stop();
return stopwatch.ElapsedMilliseconds;
}
}
}
You will now use the previously implemented classes to create event handlers for the buttons. Additionally, you will modify the constructor of the MainWindow class to dynamically read the processor architecture, which will then be displayed in the associated TextBlock. You will also resize the application window.
Proceed as follows:
public MainWindow()
{
InitializeComponent();
// Set the window size
AppWindow.Resize(new Windows.Graphics.SizeInt32(800, 1200));
// Update ProcessorArchitecture Label
TextBlockProcessorArchitecture.Text = $"{Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE")}";
// Update Window Title
Title = "ARM";
}
private void ButtonStart_Click(object sender, RoutedEventArgs e)
{
int matrixSize = Convert.ToInt32(NumberBoxMatrixSize.Value);
int executionCount = Convert.ToInt32(NumberBoxExecutionCount.Value);
var executionTime = PerformanceHelper.MeasurePerformance(
() => MatrixHelper.SquareMatrixMultiplication(matrixSize),
executionCount);
ListBoxResults.Items.Add($"Size: {matrixSize}, Count: {executionCount}, " +
$"Time: {executionTime} ms");
}
private void ButtonClear_Click(object sender, RoutedEventArgs e)
{
ListBoxResults.Items.Clear();
}
When the application is running and the user clicks the Start button, the application will read the values from the NumberBox controls (NumberBoxMatrixSize and NumberBoxExecutionCount). These values will be used to set the matrix size and the execution count. Using these values, the application will invoke the MeasurePerformance static method of the PerformanceHelper class to measure the time required to perform matrix multiplication for the given matrix size and execution count. The computation time will then be added to the ListBox. At any point, the user can clear the ListBox by clicking the Clear button.