Search code examples
winui-3winui

Cursor (.cur) in WinUI 3


I want to display a custom cursor (32x32) in WinUI3. It seems that there is no simple way to load a .cur file from the embedded resources or is there?

So far I tried to create a win32 .res file with the cursors embedded and then tried to use them using InputDesktopResourceCursor.CreateFromModule (and variations of that).

However, when I put Win32Resource and WindowsAppSDKSelfContained in the same csproj file, an error occurs "conflicting options specified: Win32 resource file, Win32 manifest". When I remove WindowsAppSDKSelfContained, the project can be compiled, but this is not what I want.

Is there another way to use a .cur file without Win32Resource?


Solution

  • Here is a way to load a custom cursor from a .cur file or from any native HCURSOR. It's based on WinApp SDK's IInputCursorStaticsInterop interface that allows to define an InputCursor from a native HCURSOR.

    So for this XAML:

    <Window
        x:Class="WinUI3App.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    
        <StackPanel>
            <Border
                x:Name="pencilZone"
                Width="50"
                Height="50"
                Background="LightGreen" />
        </StackPanel>
    </Window>
    

    And this window:

    public sealed partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
    
            var cursor = CursorUtilities.LoadCursor(Path.Combine(
                Environment.GetFolderPath(Environment.SpecialFolder.Windows),
                @"cursors\aero_pen.cur"));
    
            pencilZone.ChangeCursor(cursor);
        }
    }
    

    This is what you should see:

    enter image description here

    #nullable enable
    using System;
    using System.ComponentModel;
    using System.Reflection;
    using System.Runtime.InteropServices;
    using Microsoft.UI.Input;
    using Microsoft.UI.Xaml;
    
    namespace WinUI3Tools;
    
    public static class CursorUtilities
    {
        public static void ChangeCursor(this UIElement element, InputCursor? cursor)
        {
            ArgumentNullException.ThrowIfNull(element);
            // the stupid hack...
            typeof(UIElement).InvokeMember("ProtectedCursor", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.SetProperty, null, element, new[] { cursor });
        }
    
        public static InputCursor? LoadCursor(string filePath)
        {
            ArgumentNullException.ThrowIfNull(filePath);
            var hcursor = LoadCursorFromFileW(filePath);
            if (hcursor == 0)
                throw new Win32Exception(Marshal.GetLastWin32Error());
    
            return CreateCursorFromHCURSOR(hcursor);
        }
    
        public static InputCursor? CreateCursorFromHCURSOR(nint hcursor)
        {
            if (hcursor == 0)
                return null;
    
            const string classId = "Microsoft.UI.Input.InputCursor";
            _ = WindowsCreateString(classId, classId.Length, out var hs);
            _ = RoGetActivationFactory(hs, typeof(IActivationFactory).GUID, out var fac);
            _ = WindowsDeleteString(hs);
            if (fac is not IInputCursorStaticsInterop interop)
                return null;
    
            interop.CreateFromHCursor(hcursor, out var cursorAbi);
            if (cursorAbi == 0)
                return null;
    
            return WinRT.MarshalInspectable<InputCursor>.FromAbi(cursorAbi);
        }
    
        [ComImport, Guid("ac6f5065-90c4-46ce-beb7-05e138e54117"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        private interface IInputCursorStaticsInterop
        {
            // IInspectable unused methods
            void GetIids();
            void GetRuntimeClassName();
            void GetTrustLevel();
    
            [PreserveSig]
            int CreateFromHCursor(nint hcursor, out nint inputCursor);
        }
    
        [ComImport, Guid("00000035-0000-0000-c000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        private interface IActivationFactory
        {
            // IInspectable unused methods
            void GetIids();
            void GetRuntimeClassName();
            void GetTrustLevel();
    
            [PreserveSig]
            int ActivateInstance(out nint instance);
        }
    
        [DllImport("api-ms-win-core-winrt-l1-1-0.dll")]
        private static extern int RoGetActivationFactory(nint runtimeClassId, [MarshalAs(UnmanagedType.LPStruct)] Guid iid, out IActivationFactory factory);
    
        [DllImport("user32", CharSet = CharSet.Unicode, SetLastError = true)]
        private static extern nint LoadCursorFromFileW(string name);
    
        [DllImport("user32", CharSet = CharSet.Unicode)]
        private static extern bool DestroyCursor(nint hcursor);
    
        [DllImport("api-ms-win-core-winrt-string-l1-1-0", CharSet = CharSet.Unicode)]
        private static extern int WindowsCreateString(string? sourceString, int length, out nint @string);
    
        [DllImport("api-ms-win-core-winrt-string-l1-1-0", CharSet = CharSet.Unicode)]
        private static extern int WindowsDeleteString(nint @string);
    }