Cursors are one of the most effective elements in our UI design toolbox. Done well, they provide a gentle nudge at the very spot where the user’s attention is currently focused. Done poorly, they can be a circus of distraction. Regardless of your design philosophy or skills, custom cursors should be on the table when working through your next UI design.
This post is about the mechanics of using custom cursors in a Windows App SDK/WinUI 3 desktop application. While it might be a bit more complex than you’d expect, once you understand the workflow, adding a custom cursor should only take a few minutes.
As always seems to be the case, there is quite a bit of wisdom on custom cursors scattered around the web. For the most part, this post is a compilation of that wisdom along with helpful suggestions from a couple of developers who have worked through the same problems. Thanks KevinTXY and Marv51!
In addition to the recipe described here, you can find a working sample on GitHub (see below).
The Short Version
- Create a traditional Win32 resource (.res) file containing one or more 32-bit (i.e., with transparency), 32 × 32-pixel cursors. Make sure the resource ids for your cursors are outside the range [101, 118]. Those are used for system cursors.
- Add the resource file to a Windows App SDK desktop application and then set it as the Win32 resource file in the application properties.
- Subclass one of the UIElement-derived controls like Grid and create a Cursor property that gives you access to the protected ProtectedCursor property of the Grid base class.
- Create an InputCursor using InputDesktopResourceCursor.CreateFromModule, using the name of your project as the module name and the resource Id of the cursor from your .res file.
- Set the Cursor on your derived Grid to the new InputCursor you just created.
The Details
Creating and using a custom cursor with the Windows App SDK requires a couple of Win32 steps to generate a resource file. Once that’s complete, everything else is pure Windows App SDK.
We are going to create two Visual Studio 2022 solutions/projects. We’ll use the first to generate a resource file (.res) containing our actual cursor resources (.cur). The second is a desktop application where we’ll try out our custom cursors.
Part 1 – Creating the Cursors
To take best advantage of the Windows App SDK, we want to create 32 × 32-pixel cursors with transparency (32-bits/pixel). Each cursor should be packaged in its own .cur file. You can read more about .cur files on Wikipedia.
Visual Studio does have a built-in cursor editor. Unfortunately, it doesn’t support 32-bit cursors with transparency. As a result, the cursors in this post were generated using CursorWorkshop from Axialis.
So, the first step is to create one or more cursors in your favorite cursor editor. The gentle nudge or distracting circus is up to you…
Part 2 – Creating the Resource File
While this step involves working with a C++ project, knowledge of C++ is not required. The code for this project can be found on GitHub.
- Create a new C++ | Windows | Library project using the Dynamic-Link Library (DLL) template.
- Right-click on the project in the Solution Explorer and select Add | New Item… from the drop down menu. Select Visual C++ | Resource | Resource File (rc.) in the Resource Library dialog. Name the file something like Win32Resources.rc. This will create both the Win32Resources.rc file as well as a resource.h header.
- Double-click on the resource.h file in the Solution Explorer. This will open the file in the editor. Change the _APS_NEXT_RESOURCE_VALUE to something above 118. Here, we’ll use 201. This is necessary because in the Windows App SDK, values in the range between 101 and 118 are reserved for system cursors. Save and close resource.h.
- Double-click on the Win32Resources.rc file in the Solution Explorer. This opens the file in the Resources View. Right-click on Win32Resources.rc in the Resources View and select Add Resource… from the drop down menu.
- In the Add Resource dialog click the Import button and navigate to and add the .cur file you generated in Step 1. Repeat for any remaining cursors.
- Double-click on resource.h in the Solution Explorer and note the resource Ids for your imported cursors. You’ll need those later.
- Set the Solution Configuration to Release and build the solution. This will generate a Win32Resources.res file in the build output folder, usually something like:
solutionFolder/projectFolder/x64/Release.
We now have a traditional Win32 resource (.res) file that we can use in a modern Windows App SDK application. We also have everything we need from our C++ DLL project, so you are free to close it.
Part 3 – Using Custom Cursors in a Windows App SDK Desktop Application
The code for this project can be found on GitHub.
- Start by creating a new C# | Windows | WinUI project in Visual Studio 2022 using the Blank App, Packaged (WinUI 3 in Desktop) template. If you don’t see that template, you probably don’t have the Windows App SDK installed. It’s a Visual Studio install option and just a mog (minute of google) will get you headed in the right direction.
- Add the resource (.res) file created in Part 2 by right-clicking on the project in the Solution Explorer and selecting Add | Existing Item… in the drop-down menu. You will need to change the file-type filter to All Files (*) to see the .res file you are looking for. Navigate to the file and add it. There is no need change default values of its Build Action or Copy to Output Directory properties. Leave them at None and Do not copy respectively.
- Right-click on the project in the Solution Explorer and select Properties from the drop-down menu. This opens the Application Properties pane.
- Scroll down to the Win32 Resources section. In the Resources combo box, replace the default selection, Icon and manifest, with Resource file. This will enable a Resource file text box and Browse… button.
- Browse to and select the Win32Resources.res file you just added to the project.
- A side-effect of the switch from Icon and manifest to Resource file is that you have now lost your application manifest. You can add it back by double-clicking on the Win32Resources.res file in the Solution Explorer. This opens the file for editing. Right-click on Win32Resources.res in the editor and select Add Resources… In the Add Resource dialog, click the Import button and navigate to and add the auto-generated app.manifest file located in your project folder. In the Custom Resource Type dialog, set the resource type to RT_MANIFEST.
Note: In the Windows App SDK, UIElements have a ProtectedCursor property, which as its name implies is protected. To get access to ProtectedCursor, we are going to have to subclass the UIElement whose cursor we’d like to change. Unfortunately, some UIElement derived classes like Border are sealed, so we’re not going to be able to change their cursors.
- Add a new class derived from Microsoft.UI.Xaml.Controls.Grid. To keep things simple, we’ll call it Grid2.
- Add a new property of type InputCursor to Grid2.
internal class Grid2 : Grid
{
public InputCursor Cursor
{
get => ProtectedCursor;
set => ProtectedCursor = value;
}
}
- Replace the boilerplate StackPanel in MainWindow.xaml with a Grid2 and add Loaded and PointerPressed event handlers. Set its background to something other than the default Transparent, otherwise you won’t get any PointerPressed events.
<Window x:Class="UsingCustomCursors.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:UsingCustomCursors">
<local:Grid2 x:Name="_grid2"
Background="Black"
Loaded="OnGridLoaded"
PointerPressed="OnGridPointerPressed" />
</Window>
- In MainWindow.xaml.cs, remove the myButton_Click event handler.
- Add an InputCursor field for each cursor you plan to create. Note that InputCursor implements IDisposable...
private InputCursor _redCursor;
private InputCursor _greenCursor;
private InputCursor _blueCursor;
- In the Loaded event handler create a new InputCurosr for each of the fields you just declared.
private void OnGridLoaded(object sender, RoutedEventArgs e)
{
_redCursor = InputDesktopResourceCursor.CreateFromModule("UsingCustomCursors", 201);
_greenCursor = InputDesktopResourceCursor.CreateFromModule("UsingCustomCursors", 202);
_blueCursor = InputDesktopResourceCursor.CreateFromModule("UsingCustomCursors", 203);
}
where the first parameter is the name of your desktop application project, and the second is the resource ID of a cursor in the resources (.res) file.
- Finally, in this example, we’ll use the PointerPressed event handler, to change the value of the Cursor property of our Grid2 instance, to a different one of our custom cursors, each time an event is received.
private enum CustomCursor { None, Red, Green, Blue };
private CustomCursor CurrentCursor { get; set; } = CustomCursor.None;
private void OnGridPointerPressed(object sender, PointerRoutedEventArgs e)
{
switch (CurrentCursor)
{
case CustomCursor.None:
_grid2.Cursor = _redCursor;
CurrentCursor = CustomCursor.Red;
break;
case CustomCursor.Red:
_grid2.Cursor = _greenCursor;
CurrentCursor = CustomCursor.Green;
break;
case CustomCursor.Green:
_grid2.Cursor = _blueCursor;
CurrentCursor = CustomCursor.Blue;
break;
case CustomCursor.Blue:
_grid2.Cursor = null;
CurrentCursor = CustomCursor.None;
break;
}
}
For Extra Credit
If your project is a bit more complex, and you want to use a custom cursor with controls that live in a class library, it’s basically the same workflow. Just add your resource (.res) file to the class library instead of the application, and set it as the Win32 resource in the project (not the application) properties (part 3: steps 2-4 above). In this case, there is no need to include an app.manifest when you change the Win32 Resources property from Icon and manifest to Resource file. Finally, when creating your cursors, use the name of the class library project as the first parameter to CreateFromModule.
Wrapping it up…
So, that’s it. You now have a custom cursor in a Windows App SDK desktop application. You can see all this in action in the UsingCustomCursors project on GitHub. Next up: maybe we’ll get back to our Win2D drawing application…
That’s all for now — welcome to the circus.