Click here to Skip to main content
15,867,488 members
Articles / Desktop Programming / WPF

Creating a Cursor from a Font Symbol in a WPF Application

Rate me:
Please Sign up or sign in to vote.
5.00/5 (17 votes)
2 Apr 2018CPOL3 min read 32.7K   551   20   8
This gives the code to create a cursor from a character in a font.

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 FontSyle, FontWeight, 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 MemoryStream method 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.

Image 1

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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Clifford Nelson Consulting
United States United States
Has been working as a C# developer on contract for the last several years, including 3 years at Microsoft. Previously worked with Visual Basic and Microsoft Access VBA, and have developed code for Word, Excel and Outlook. Started working with WPF in 2007 when part of the Microsoft WPF team. For the last eight years has been working primarily as a senior WPF/C# and Silverlight/C# developer. Currently working as WPF developer with BioNano Genomics in San Diego, CA redesigning their UI for their camera system. he can be reached at qck1@hotmail.com.

Comments and Discussions

 
QuestionWhat am I doing wrong? Pin
Tokinabo24-Mar-16 4:44
professionalTokinabo24-Mar-16 4:44 
AnswerRe: What am I doing wrong? Pin
Clifford Nelson24-Mar-16 5:26
Clifford Nelson24-Mar-16 5:26 
AnswerRe: What am I doing wrong? Pin
Clifford Nelson24-Mar-16 5:36
Clifford Nelson24-Mar-16 5:36 
GeneralRe: What am I doing wrong? Pin
Tokinabo24-Mar-16 7:06
professionalTokinabo24-Mar-16 7:06 
QuestionInteresting idea... PinPopular
Brisingr Aerowing23-Mar-16 18:16
professionalBrisingr Aerowing23-Mar-16 18:16 
AnswerRe: Interesting idea... Pin
Clifford Nelson24-Mar-16 9:34
Clifford Nelson24-Mar-16 9:34 
GeneralRe: Interesting idea... Pin
Brisingr Aerowing24-Mar-16 15:04
professionalBrisingr Aerowing24-Mar-16 15:04 
AnswerRe: Interesting idea... Pin
Clifford Nelson25-Mar-16 8:04
Clifford Nelson25-Mar-16 8:04 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.