how-to-use-custom-cursors

来源:互联网 发布:js 模拟表单上传文件 编辑:程序博客网 时间:2024/06/12 19:39

WPF Tutorial - How To Use Custom Cursors

Posted in:

So a while back, I did a tutorial on how to do custom cursors in WinForms. 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:

Example WPF Custom Cursor

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! As always, questions and comments are welcome.

 

 

http://www.switchonthecode.com/tutorials/wpf-tutorial-how-to-use-custom-cursors