So a while back, I did a tutorial on how to do custom cursors inWinForms. In that
post I said that at some point in the future, I would write a post on
how to do custom cursors in WPF - and here we are! A lot of the code
used today is based off of the code from that previous tutorial, so if
you haven't read it, I would go do so before you continue.
Sadly, creating and using a custom cursor for WPF is actually more
difficult than for WinForms. This is pretty much all due in the end to a
single problem - WPF does not use GDI, but the cursor still does. Since
the cursor is something at the Windows level (it needs to exist across
all application and work smoothly), it is still GDI, and has all the
benefits and flaws that come with that fact. So we end up needing to
cross that boundary every time you want to do something special with the
cursor in WPF.
But don't worry! Fortunately, WPF wraps all the standard cursors (so you
can still easily change to, say, a wait cursor), but as soon as you want
to create a special image to use as your cursor, you are on your own.
Well, not really on your own - that is what this tutorial is here for.
And so, in we go. First, we are going to look at the rather small change
to the actual "cursor creation part of the code. If you remember, to
create a cursor for WinForms, you can just say something like this:
IntPtr curPtr;
/* CurPtr gets set to
a pointer to an icon */
Cursor myCur = new Cursor(curPtr);
Sadly, you can't quite do that in WPF. This is because it is a different
Cursor object we are creating, and Microsoft did not give us a
constructor that takes an
IntPtr
. For WinForms, we used a
System.Windows.Forms.Cursor
, but for WPF, we will be creating a
System.Windows.Input.Cursor
. But while there is no constructor for it,
there is still a way to get a Cursor out of an IntPtr. It just happens
to be hidden elsewhere:IntPtr curPtr;
/* CurPtr gets set to
a pointer to an icon */
SafeFileHandle handle = new SafeFileHandle(ptr, true);
Cursor myCur = System.Windows.Interop.CursorInteropHelper.Create(handle);
So first we have to convert the IntPtr into a
SafeHandle
(SafeFileHandle
extends SafeHandle
- you can't just create a
SafeHandle
, it is an abstract class). Then we pass that handle into
the nice and easy to find (thats sarcasm in case you can't tell)
Create
method under System.Windows.Interop.CursorInteropHelper
, and
we get a Cursor
back that we can use with WPF.
So if we bring in the rest of the code from the previous tutorial, we
might get a class that looks like this:
using System;
using System.Windows.Interop;
using Microsoft.Win32.SafeHandles;
using System.Runtime.InteropServices;
using System.Windows.Input;
namespace WPFCursorTest
{
public class CursorHelper
{
private struct IconInfo
{
public bool fIcon;
public int xHotspot;
public int yHotspot;
public IntPtr hbmMask;
public IntPtr hbmColor;
}
[DllImport("user32.dll")]
private static extern IntPtr CreateIconIndirect(ref IconInfo icon);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetIconInfo(IntPtr hIcon, ref IconInfo pIconInfo);
public static Cursor CreateCursor(System.Drawing.Bitmap bmp, int xHotSpot,
int yHotSpot)
{
IconInfo tmp = new IconInfo();
GetIconInfo(bmp.GetHicon(), ref tmp);
tmp.xHotspot = xHotSpot;
tmp.yHotspot = yHotSpot;
tmp.fIcon = false;
IntPtr ptr = CreateIconIndirect(ref tmp);
SafeFileHandle handle = new SafeFileHandle(ptr, true);
return CursorInteropHelper.Create(handle);
}
}
}
One thing you will need to remeber if you use this is that we are
dealing with a
System.Drawing.Bitmap
here. This is the old GDI type
bitmap object - a concept foreign to the new WPF classes. So you will
probably need to pull in a reference to System.Drawing
in your
references section in your Visual Studio project, since WPF projects do
not have this reference by default.
I tried, quite hard, to figure out a way to create a cursor in WPF
without having to lean on
System.Drawing.Bitmap
. But in the end, I
need to get an Icon pointer out of the bitmap (thats the function
GetHicon
), and the WPF bitmap objects refuse to ever give you a
pointer.
But wait, not all hope is lost! I may have to deal with a
System.Drawing.Bitmap
, but that does not mean that everyone will need
to. It is possible to wrap this method in another method that can take
in a WPF construct, instead of a GDI one:public static Cursor CreateCursor(UIElement element, int xHotSpot, int yHotSpot)
{
element.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
element.Arrange(new Rect(0, 0, element.DesiredSize.Width,
element.DesiredSize.Height));
RenderTargetBitmap rtb = new RenderTargetBitmap((int)element.DesiredSize.Width,
(int)element.DesiredSize.Height, 96, 96, PixelFormats.Pbgra32);
rtb.Render(element);
PngBitmapEncoder encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(rtb));
MemoryStream ms = new MemoryStream();
encoder.Save(ms);
System.Drawing.Bitmap bmp = new System.Drawing.Bitmap(ms);
ms.Close();
ms.Dispose();
Cursor cur = InternalCreateCursor(bmp, xHotSpot, yHotSpot);
bmp.Dispose();
return cur;
}
Here, I am taking in a UIElement - a core WPF construct. I measure and
arrage it (to make sure that it is internally rendered properly), and
then I "take a picture of it". I do this by creating a
RenderTargetBitmap
. This is a class for WPF thats lets you take a
UIElement and draw it to a bitmap - but remember, this is a WPF bitmap
(a System.Windows.Media.Imaging.RenderTargetBitmap
, to be precise). It
is not the same thing as a System.Drawing.Bitmap
. So I create my
RenderTargetBitmap
to be the correct size, and I give it a dpi of 96
(pretty standard). Then, I render the UIElement
on the bitmap.
Ok, now I have a
RenderTargetBitmap
in my hands, and I want to get to
a System.Drawing.Bitmap
. Microsoft could not have possibly made this
more difficult, and I'm really quite annoyed by it. There are a couple
methods that let you go from a System.Drawing.Bitmap
to a WPF Bitmap
(they create a System.Windows.Interop.InteropBitmap
), but there are no
methods that go in the other direction. Or at least none that I could
find - and I scoured the docs and read through Bitmap code in Reflector
for quite a while. If anyone knows of such a method (or a better way
than the way I am about to describe), please let me know.
I convert from a
RenderTargetBitmap
to a System.Drawing.Bitmap
by
first creating an encoder, and encoding my bitmap as a PNG (you could
pick any encoder you like). I then create a MemoryStream
and save my
encoded bitmap to the stream. And now, since the System.Drawing.Bitmap
has a constructor that can take a stream, I can create my new bitmap.
Once that is done, I clean up the memory stream, and proceed to create
the cursor. And, of course, once the cursor is created, I dispose the
System.Drawing.Bitmap
that I had created.
So in the end, the code for the whole class looks like this:
using System;
using System.Windows.Interop;
using Microsoft.Win32.SafeHandles;
using System.Runtime.InteropServices;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.IO;
using System.Windows;
namespace WPFCursorTest
{
public class CursorHelper
{
private struct IconInfo
{
public bool fIcon;
public int xHotspot;
public int yHotspot;
public IntPtr hbmMask;
public IntPtr hbmColor;
}
[DllImport("user32.dll")]
private static extern IntPtr CreateIconIndirect(ref IconInfo icon);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetIconInfo(IntPtr hIcon, ref IconInfo pIconInfo);
private static Cursor InternalCreateCursor(System.Drawing.Bitmap bmp,
int xHotSpot, int yHotSpot)
{
IconInfo tmp = new IconInfo();
GetIconInfo(bmp.GetHicon(), ref tmp);
tmp.xHotspot = xHotSpot;
tmp.yHotspot = yHotSpot;
tmp.fIcon = false;
IntPtr ptr = CreateIconIndirect(ref tmp);
SafeFileHandle handle = new SafeFileHandle(ptr, true);
return CursorInteropHelper.Create(handle);
}
public static Cursor CreateCursor(UIElement element, int xHotSpot, int yHotSpot)
{
element.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
element.Arrange(new Rect(0, 0, element.DesiredSize.Width,
element.DesiredSize.Height));
RenderTargetBitmap rtb = new RenderTargetBitmap((int)element.DesiredSize.Width,
(int)element.DesiredSize.Height, 96, 96, PixelFormats.Pbgra32);
rtb.Render(element);
PngBitmapEncoder encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(rtb));
MemoryStream ms = new MemoryStream();
encoder.Save(ms);
System.Drawing.Bitmap bmp = new System.Drawing.Bitmap(ms);
ms.Close();
ms.Dispose();
Cursor cur = InternalCreateCursor(bmp, xHotSpot, yHotSpot);
bmp.Dispose();
return cur;
}
}
}
Kind of ugly, don't you think? But at least it is encapsulated.
Now you probably wondering how to use this class. Well, it is really
easy:
public partial class CursorTest : Window
{
public CursorTest()
{
InitializeComponent();
TextBlock tb = new TextBlock();
tb.Text = "{ } Switch On The Code";
tb.FontSize = 10;
tb.Foreground = Brushes.Green;
this.Cursor = CursorHelper.CreateCursor(tb, 5, 5);
}
}
Here, I have a window, and I want my cursor to say "{ } Switch On The
Code". So in the constructor, I make a
TextBlock
with that text, and
just call CreateCursor
. And all I need to do is set the Cursor
property of my window to the result. That code would make something that
looks like this:
I hope this code helps anyone who has been trying to create and use
custom cursors in WPF. If you would like, you can download a Visual
Studio solution
here, which
contains both the
CursorHelper
class and the sample test window shown
above. And if anyone comes up with a way to get rid of the need for
having to bring in System.Drawing
, or figures out how to convert from
a WPF and GDI bitmap easily, let us know!
Source Files:
Unfortunately this solution doesn't work with .Net Framework v. 4.7.2
ReplyDeleteThe App crashes when the cursor is disposed.
This comment has been removed by the author.
Delete