Creating a Cursor from a Font Symbol in a WPF Application

Introduction

I had to create a large cursor for the application I was working on and, since I was already using a symbol font, decided to use a symbol from the font for the cursor.

NOTE

The code has been considerably updated to fix issues with the cursor since the initial version. The image in the cursor was not properly centered so that the actual size of the cursor was not close to the value that is specified in the call. The hourglass also now has a three images of the sand going down and then a 90 degree rotation. 

The Code

I started out trying to use code that I used to generate images from fonts for buttons and such, which is included in the code, and what is used to generate the image in the Sample. That did not work, and I had to search the internet and play around with it a lot. Unfortunately, there seem to be several Microsoft libraries for drawing, and I guess not a lot of thought was put into creating the different ones. The code below is what eventually worked.

The Creation of the GlyphRun

This code is similar to the code used to create the Image for the button shown in the sample, but slightly different because it seemed that the object I needed was slightly different from the one needed for the Image in WPF. What this does is convert the font character(s) into a Bitmap:
public static GlyphRun GetGlyphRun(double size, FontFamily fontFamily, string text)
{
 Typeface typeface = new Typeface(fontFamily, FontStyles.Normal,
  FontWeights.Normal, FontStretches.Normal);
 GlyphTypeface glyphTypeface;
 if (!typeface.TryGetGlyphTypeface(out glyphTypeface))
  throw new InvalidOperationException("No glyphtypeface found");
 ushort[] glyphIndexes = new ushort[text.Length];
 double[] advanceWidths = new double[text.Length];

 for (int n = 0; n < text.Length; n++)
 {
  advanceWidths[n] = glyphTypeface.AdvanceWidths[glyphIndexes[n]
   = GetGlyph(text[n], glyphTypeface)];
 }
 var centerX = (1 - advanceWidths[0]) * size / 2;
 Point origin = new Point(centerX, size * .85);

 GlyphRun glyphRun = new GlyphRun(glyphTypeface, 0, false, size,
   glyphIndexes, origin, advanceWidths, null, null, null, null,
   null, null);

 return glyphRun;
}
This method uses another method to get the character, and deal with the Exception that is thrown when the font does not have a character associated with the location, replacing the character with a space symbol:
private static ushort GetGlyph(char text, GlyphTypeface glyphTypeface)
{
 try { return glyphTypeface.CharacterToGlyphMap[text]; }
 catch { return 42; }
}
In creating the GlyphRun, the GetGlyphRun method uses the defaults for FontSyleFontWeight, and FontStretch. It will actually take a string and create a GlyphRun with all the characters.

Creating the memory stream object

The next method is used to create the Cursor object. The Cursor requires a specific binary format, and so a MemoryStream is used to create the binary object (for structure see https://en.wikipedia.org/wiki/ICO_(file_format)). This can also be done with unsafe code but the MemoryStreammethod works without requiring the unsafe keyword.
private static Cursor CreateCursorObject(int size, double xHotPointRatio, double yHotPointRatio,
    BitmapSource rtb)
{
 using (var ms1 = new MemoryStream())
 {
  var penc = new PngBitmapEncoder();
  penc.Frames.Add(BitmapFrame.Create(rtb));
  penc.Save(ms1);

  var pngBytes = ms1.ToArray();
  var byteCount = pngBytes.GetLength(0);

  //.cur format spec <a href="http://en.wikipedia.org/wiki/ICO_(file_format"><font color="#0066cc">http://en.wikipedia.org/wiki/ICO_(file_format</font></a>)
  using (var stream = new MemoryStream())
  {
   //ICONDIR Structure
   stream.Write(BitConverter.GetBytes((Int16) 0), 0, 2); //Reserved must be zero; 2 bytes
   stream.Write(BitConverter.GetBytes((Int16) 2), 0, 2); //image type 1 = ico 2 = cur; 2 bytes
   stream.Write(BitConverter.GetBytes((Int16) 1), 0, 2); //number of images; 2 bytes
   //ICONDIRENTRY structure
   stream.WriteByte(32); //image width in pixels
   stream.WriteByte(32); //image height in pixels

   stream.WriteByte(0); //Number of Colors. Should be 0 if the image doesn't use a color palette
   stream.WriteByte(0); //reserved must be 0

   stream.Write(BitConverter.GetBytes((Int16) (size*xHotPointRatio)), 0, 2);
   //2 bytes. In CUR format: Specifies the number of pixels from the left.
   stream.Write(BitConverter.GetBytes((Int16) (size*yHotPointRatio)), 0, 2);
   //2 bytes. In CUR format: Specifies the number of pixels from the top.

   //Specifies the size of the image's data in bytes
   stream.Write(BitConverter.GetBytes(byteCount), 0, 4);
   stream.Write(BitConverter.GetBytes((Int32) 22), 0, 4);
   //Specifies the offset of BMP or PNG data from the beginning of the ICO/CUR file
   stream.Write(pngBytes, 0, byteCount); //write the png data.
   stream.Seek(0, SeekOrigin.Begin);
   return new System.Windows.Input.Cursor(stream);
  }
 }
}

The Transform for Flip and Rotate

The TransformImage is called when either there is a horizontal or vertical flip or a rotate.
private static void TransformImage(DrawingGroup drawingGroup, double angle, FlipValues flip)
{
 if (flip == FlipValues.None && Math.Abs(angle) < .1) return;
 if (flip == FlipValues.None)
  drawingGroup.Transform = new RotateTransform(angle);
 if (Math.Abs(angle) < .1)
  drawingGroup.Transform = new ScaleTransform(flip == FlipValues.Vertical ? -1 : 1, flip == FlipValues.Horizontal ? -1 : 1);
 else
 {
  var transformGroup = new TransformGroup();
  transformGroup.Children.Add(new ScaleTransform(flip == FlipValues.Vertical ? -1 : 1, flip == FlipValues.Horizontal ? -1 : 1));
  transformGroup.Children.Add(new RotateTransform(angle));
  drawingGroup.Transform = transformGroup;
 }
}

Base public method call

These two methods are used by the CreateCursor to return the Cursor object that uses the specified symbol in the FontFamily. This is the public method that is called to create the cursor object:
public static System.Windows.Input.Cursor CreateCursor(int size, double xHotPointRatio,
 double yHotPointRatio, FontFamily fontFamily, string symbol, Brush brush, double rotationAngle = 0)
{
 var vis = new DrawingVisual();
 using (var dc = vis.RenderOpen())
 {
  dc.DrawGlyphRun(brush, GetGlyphRun(size, fontFamily, symbol));
  dc.Close();
 }/*CreateGlyphRun(symbol, fontFamily, FontStyles.Normal, FontWeights.Normal, FontStretches.Normal)*/
 if (Math.Abs(rotationAngle) > .1)
   vis.Transform = new RotateTransform(rotationAngle, size / 2, size / 2);
 var renderTargetBitmap = new RenderTargetBitmap(size, size, 96, 96, PixelFormats.Pbgra32);
 renderTargetBitmap.Render(vis);

 return CreateCursorObject(size, xHotPointRatio, yHotPointRatio, renderTargetBitmap);
}

Using the Code

In WPF, you would probably want to set the cursor when the Window is initialized:
public MainWindow()
{
 InitializeComponent();
 Mouse.OverrideCursor = FontSymbolCursor.CreateCursor(100, .5, .03, "arial",
  'A'.ToString, System.Windows.Media.Brushes.Black);
}

The BaseWindow Class

In the sample, the MainWindow inherits from the BaseWindow class.
<fontAwesomeImageSample:BaseWindow x:Class="FontAwesomeImageSample.MainWindow"

        xmlns="<a href="http://schemas.microsoft.com/winfx/2006/xaml/presentation">http://schemas.microsoft.com/winfx/2006/xaml/presentation</a>"
        xmlns:x="<a href="http://schemas.microsoft.com/winfx/2006/xaml">http://schemas.microsoft.com/winfx/2006/xaml</a>"
        xmlns:d="<a href="http://schemas.microsoft.com/expression/blend/2008">http://schemas.microsoft.com/expression/blend/2008</a>"
        xmlns:fontAwesomeImageSample="clr-namespace:FontAwesomeImageSample"
        xmlns:mc="<a href="http://schemas.openxmlformats.org/markup-compatibility/2006">http://schemas.openxmlformats.org/markup-compatibility/2006</a>"
        Title="Font Awesome Icon Image &amp; Cursor"
        Width="525"
        Height="350"
        mc:Ignorable="d">
 <Grid>
  <Button Margin="50"

          HorizontalAlignment="Center"

          VerticalAlignment="Center"

          Click="ButtonBase_OnClick">
   <fontAwesomeImageSample:FontSymbolImage Foreground="HotPink"

                                           FontFamily="{StaticResource FontAwesomeTtf}"

                       Flip="Horizontal"

                       Rotation="10"

                                           FontAwesomeSymbol="fa_bar_chart_o" />
  </Button>
 </Grid>
</fontAwesomeImageSample:BaseWindow>
The BaseWindow class has the IsBusy DependencyProperty and creates an Arrow cursor, and several Busy cursors. When the IsBusy changes from true to false, the Cursor changes from its normal Arrow to the Hourglass which empties and then rotates. To accomplish the animation a DispatchTimer is used. It is started when the IsBusy DependencyProperty is set to true, and Stopped when it is set to false:
 public class BaseWindow : Window
 {
  private const int CursorSize = 32;
  private readonly DispatcherTimer _updateTimer;
  private readonly System.Windows.Input.Cursor _normalCursor;
  private readonly System.Windows.Input.Cursor _busyCursor;
  private readonly System.Windows.Input.Cursor[] _busyCursors;
  private int _busyCursorNumber;  public BaseWindow()
  {
   _updateTimer = new DispatcherTimer { Interval = new TimeSpan(0, 0, 1) };
   _updateTimer.Tick += UpdateBusyCursor;
   System.Windows.Input.Mouse.OverrideCursor = _normalCursor 
        = FontSymbolCursor.CreateCursor(CursorSize, .2, 0, "FontAwesome",
          FontSymbolImage.FontAwesomeSymbols.fa_mouse_pointer, System.Windows.Media.Brushes.Black);
   _busyCursors = new[] {
     FontSymbolCursor.CreateCursor(CursorSize, .5, .5, "FontAwesome",
       FontSymbolImage.FontAwesomeSymbols.fa_hourglass_start, System.Windows.Media.Brushes.Black),
     FontSymbolCursor.CreateCursor(CursorSize, .5, .5, "FontAwesome",
       FontSymbolImage.FontAwesomeSymbols.fa_hourglass_half, System.Windows.Media.Brushes.Black),
     FontSymbolCursor.CreateCursor(CursorSize, .5, .5, "FontAwesome",
       FontSymbolImage.FontAwesomeSymbols.fa_hourglass_end, System.Windows.Media.Brushes.Black),
     FontSymbolCursor.CreateCursor(CursorSize, .5, .5, "FontAwesome",
       FontSymbolImage.FontAwesomeSymbols.fa_hourglass_end, System.Windows.Media.Brushes.Black, 90.0)};
  }

  public static readonly DependencyProperty IsBusyProperty =
    DependencyProperty.Register("IsBusy", typeof(bool), typeof(BaseWindow), 
    new PropertyMetadata(false, PropertyChangedCallback));
  public bool IsBusy { 
    get { return (bool)GetValue(IsBusyProperty); } 
    set { SetValue(IsBusyProperty, value); } 
  }

  private static void PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
  {
   var window = (BaseWindow)d;
   if (window.IsBusy)
   {
    window._busyCursorNumber = 0;
    window._updateTimer.Start();
    System.Windows.Input.Mouse.OverrideCursor = window._busyCursors[0];
   }
   else
   {
    window._updateTimer.Stop();
    System.Windows.Input.Mouse.OverrideCursor = window._normalCursor;
   }
  }

  private void UpdateBusyCursor(object sender, EventArgs e)
  {
   _busyCursorNumber = ++_busyCursorNumber % _busyCursors.Length;
   System.Windows.Input.Mouse.OverrideCursor = _busyCursors[_busyCursorNumber];
  }
 }
The sample is a simple form with a single large Button containing a Font Awesome character. If this Button is clicked, the cursor changes to a rotating hourglass for two seconds.
The actual XAML for the above cursor is:
<Button Margin="50"
        HorizontalAlignment="Center"
        VerticalAlignment="Center"
        Click="ButtonBase_OnClick">
 <fontAwesomeImageSample:FontSymbolImage Foreground="HotPink"
                                         FontFamily="{StaticResource FontAwesomeTtf}"
                                 Flip="Horizontal"
                                 Rotation="10"
                                         FontAwesomeSymbol="fa_bar_chart_o" />
</Button>

Extra

The sample includes the code to create a WPF Image from a font symbol. This is documented in Creating an Image from a Font Symbol (Font Awesome) for WPF

History

  • 03/23/2016: Initial version
  • 03/26/2016: Code update
  • 04/14/2016: Full Code Awesome 4.5 enumeration
  • 04/27/2016: Font Awesome 4.6 update
  • 05/12/2015: Updated Sample to change cursor when button clicked
  • 05/17/2016: Updated code with improved wait cursor implementation
  • 05/20/2016: Updated code with new BaseWindow control

Comments

Popular posts from this blog

C++ Program to Find Quotient and Remainder

C++ Program to Find All Roots of a Quadratic Equation