Skip to content

Instantly share code, notes, and snippets.

@xv
Created October 27, 2025 03:52
Show Gist options
  • Select an option

  • Save xv/43d3132a2e4b70a98f5a7815de2479c7 to your computer and use it in GitHub Desktop.

Select an option

Save xv/43d3132a2e4b70a98f5a7815de2479c7 to your computer and use it in GitHub Desktop.
using System.ComponentModel;
using System.Windows.Interop;
using System.Windows.Input;
using System.Runtime.InteropServices;
using System.IO;
using Windows.Win32.Foundation;
using Windows.Win32.UI.WindowsAndMessaging;
using Windows.Win32;
using Microsoft.Win32.SafeHandles;
internal class SafeCursorHandle : SafeHandleZeroOrMinusOneIsInvalid
{
public SafeCursorHandle(nint handle) : base(true) => SetHandle(handle);
public static SafeCursorHandle FromHCursor(nint hcursor) => new(hcursor);
protected override bool ReleaseHandle()
{
if (!IsInvalid)
{
if (!PInvoke.DestroyCursor((HCURSOR)handle))
throw new Win32Exception(Marshal.GetLastWin32Error());
handle = nint.Zero;
}
return true;
}
}
internal class CursorLoader : IDisposable
{
private readonly SafeCursorHandle _handle;
private bool _disposed;
private CursorLoader(SafeCursorHandle handle) =>
_handle = handle ?? throw new ArgumentNullException(nameof(handle));
public void Dispose()
{
if (_disposed)
return;
_handle.Dispose();
_disposed = true;
}
private unsafe static PCWSTR StringToPCWSTR(string str)
{
fixed (char* p = str)
return new PCWSTR(p);
}
public static CursorLoader FromFile(string path)
{
if (string.IsNullOrWhiteSpace(path))
throw new ArgumentNullException(nameof(path));
HANDLE handle = PInvoke.LoadImage(
HINSTANCE.Null,
StringToPCWSTR(path),
GDI_IMAGE_TYPE.IMAGE_CURSOR,
0,
0,
// LR_DEFAULTSIZE allows scaling with DPI
IMAGE_FLAGS.LR_DEFAULTSIZE | IMAGE_FLAGS.LR_LOADFROMFILE);
if (handle == HCURSOR.Null)
throw new Win32Exception(Marshal.GetLastWin32Error());
return new CursorLoader(SafeCursorHandle.FromHCursor(handle));
}
public static CursorLoader FromStream(Stream stream)
{
string? path = null;
try
{
path = Path.GetTempFileName();
using (var fs = File.OpenWrite(path))
stream.CopyTo(fs);
return FromFile(path);
}
finally
{
if (!string.IsNullOrEmpty(path) && File.Exists(path))
File.Delete(path);
}
}
public Cursor ToWpfCursor()
{
if (_handle == null || _handle.IsInvalid)
throw new InvalidOperationException("Cursor is not loaded or handle is invalid.");
return CursorInteropHelper.Create(_handle);
}
}
LoadImage
DestroyCursor
private readonly Cursor _cursor;
public SomeWindowConstructor()
{
// WPF's built-in Cursor instance constructor essentially does the same thing under the hood:
// _cursor = new Cursor(cursorFile, scaleWithDpi)
_cursor = CursorLoader.FromFile(@"C:\Windows\Cursors\aero_arrow.cur").ToWpfCursor();
Cursor = _cursor;
Closing += (_, _) =>
{
if (Cursor == _cursor)
{
Cursor = null; // Necessary to release the reference to _cursor
_cursor?.Dispose();
}
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment