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);
using (var stream = new MemoryStream())
{
stream.Write(BitConverter.GetBytes((Int16) 0), 0, 2);
stream.Write(BitConverter.GetBytes((Int16) 2), 0, 2);
stream.Write(BitConverter.GetBytes((Int16) 1), 0, 2);
stream.WriteByte(32);
stream.WriteByte(32);
stream.WriteByte(0);
stream.WriteByte(0);
stream.Write(BitConverter.GetBytes((Int16) (size*xHotPointRatio)), 0, 2);
stream.Write(BitConverter.GetBytes((Int16) (size*yHotPointRatio)), 0, 2);
stream.Write(BitConverter.GetBytes(byteCount), 0, 4);
stream.Write(BitConverter.GetBytes((Int32) 22), 0, 4);
stream.Write(pngBytes, 0, byteCount);
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();
}
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:
xmlns:x="<a href="http:
xmlns:d="<a href="http:
xmlns:fontAwesomeImageSample="clr-namespace:FontAwesomeImageSample"
xmlns:mc="<a href="http:
Title="Font Awesome Icon Image & 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