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:
Hide Copy Code
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:
Hide Copy Code
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.
Hide Shrink Copy Code
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.
Hide Copy Code
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
:
Hide Copy Code
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:
Hide Copy Code
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.
Hide Shrink Copy Code
<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 & 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
:
Hide Shrink Copy Code
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:
Hide Copy Code
<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 WPFHistory
- 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
Post a Comment