More research: as it turns out, we can't safely use the Shell Extensions solution. Shell and CLR still don't mix, even after all this time. See here.
So I've decided to draw an image that looks like the Shell Icons and use a custom cursor to display them. Tedious, yes, but it must be done.
Here's the code, made into extension methods and tuned up a bit for memory leaks.
Public Module Gdi
<Extension()>
Public Function ToCursor(Bitmap As Bitmap, HotSpot As Point) As Cursor
Dim oInfo As Interop.Structures.IconInfo
Dim hIcon As IntPtr
oInfo = New Interop.Structures.IconInfo
hIcon = Bitmap.GetHicon
Interop.Functions.GetIconInfo(hIcon, oInfo)
oInfo.HotSpotX = HotSpot.X
oInfo.HotSpotY = HotSpot.Y
oInfo.IsIcon = False
ToCursor = New Cursor(Interop.Functions.CreateIconIndirect(oInfo))
If oInfo.Color <> IntPtr.Zero Then Interop.Functions.DeleteObject(oInfo.Color)
If oInfo.Mask <> IntPtr.Zero Then Interop.Functions.DeleteObject(oInfo.Mask)
If hIcon <> IntPtr.Zero Then Interop.Functions.DestroyIcon(hIcon)
End Function
<Extension()>
Public Function ToBitmap(Cursor As Cursor) As Bitmap
Dim oInfo As Interop.Structures.IconInfo
Dim oData As BitmapData
Dim hIcon As IntPtr
oInfo = New Interop.Structures.IconInfo
hIcon = Cursor.Handle
Interop.Functions.GetIconInfo(hIcon, oInfo)
Using oBitmap As Bitmap = Bitmap.FromHbitmap(oInfo.Color)
Interop.Functions.DeleteObject(oInfo.Color)
Interop.Functions.DeleteObject(oInfo.Mask)
oData = oBitmap.LockBits(New Rectangle(0, 0, oBitmap.Width, oBitmap.Height), ImageLockMode.ReadOnly, oBitmap.PixelFormat)
ToBitmap = New Bitmap(oData.Width, oData.Height, oData.Stride, PixelFormat.Format32bppArgb, oData.Scan0)
oBitmap.UnlockBits(oData)
End Using
End Function
End Module
Public Class Functions
<DllImport("User32")> _
Public Shared Function CreateIconIndirect(ByRef Icon As Structures.IconInfo) As IntPtr
End Function
<DllImport("User32")> _
Public Shared Function GetIconInfo(Icon As IntPtr, ByRef Info As Structures.IconInfo) As <MarshalAs(UnmanagedType.Bool)> Boolean
End Function
<DllImport("Gdi32")> _
Public Shared Function DeleteObject(Handle As IntPtr) As Boolean
End Function
<DllImport("User32", CharSet:=CharSet.Auto)> _
Public Shared Function DestroyIcon(Handle As IntPtr) As Boolean
End Function
End Class
Namespace Structures
Public Structure IconInfo
Public IsIcon As Boolean
Public HotSpotX As Integer
Public HotSpotY As Integer
Public Mask As IntPtr
Public Color As IntPtr
End Structure
End Namespace
Unfortunately ToBitMap() doesn't work for low-res cursors, such as Default. With a little more effort, though, it can be made to turn out a decent image. Perhaps oBitmap = Icon.FromHandle(Cursor).ToBitmap
would suffice, but the resulting image quality leaves it an individual choice.