Most modern day applications use glyphs these days. A glyph in developer lingo is a monochromatic icon. In technologies like WPF that use render engines that can easily work with vectors, these glyphs tend to be in the form of vectors, so that UI designers and developers can use them in any form of scale without destroying any of the looks of the glyph.
While using glyphs, one of the things I wanted to do was bind some of them to boolean properties and have the glyph in it’s full colour when it was true, and revert back to an outlined form when it was false.
The example application has 2 glyphs and a CheckBox, both glyphs Outlined property are bound to the CheckBox’s IsChecked property, so that we can go from this state
To this state
The glyphs
The glyphs in this example were made in Adobe Illustrator, then imported into blend and the corresponding Path’s generated. They were then inserted into a Viewbox to allow scaling while retaining Blend’s absolute margin values for alignment. In the end what we have is a WPF vector glyph.
<l:BaseGlyph x:Class="GlyphOutline.Bell" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:l="clr-namespace:GlyphOutline" mc:Ignorable="d" d:DesignHeight="200" d:DesignWidth="200"> <Viewbox Stretch="Fill"> <Canvas x:Name="Icon" Height="64" Width="64"> <Path Data="F1M32,6C34.205,6,36,7.797,36,10C36,12.203,34.205,14,32,14C29.795,14,28,12.203,28,10C28,7.797,29.795,6,32,6 M53,32C53,21.5,47.217,15.563,41.719,12.266C41.889,11.535,42,10.781,42,10C42,4.477,37.523,0,32,0C26.477,0,22,4.477,22,10C22,10.781,22.111,11.535,22.281,12.266C16.783,15.563,11,21.5,11,32C11,52,4,40,4,56L60,56C60,40,53,52,53,32" Height="56" Canvas.Left="0" Stretch="Fill" Canvas.Top="0" Width="64"/> <Path Data="F1M32,64C35.723,64,38.824,61.445,39.717,58L24.283,58C25.176,61.445,28.277,64,32,64" Height="6" Canvas.Left="21.783" Stretch="Fill" Canvas.Top="58" Width="18.434"/> </Canvas> </Viewbox> </l:BaseGlyph>
<l:BaseGlyph x:Class="GlyphOutline.Calculator" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:l="clr-namespace:GlyphOutline" mc:Ignorable="d" d:DesignHeight="200" d:DesignWidth="200"> <Viewbox Stretch="Fill"> <Canvas x:Name="Icon" Height="64" Width="64"> <Path Data="F1M48,16C48,16.547,47.555,17,47,17L17,17C16.453,17,16,16.547,16,16L16,9C16,8.445,16.453,8,17,8L47,8C47.555,8,48,8.445,48,9z M44,31.5C42.07,31.5,40.5,29.938,40.5,28C40.5,26.07,42.07,24.5,44,24.5C45.937,24.5,47.5,26.07,47.5,28C47.5,29.938,45.937,31.5,44,31.5 M44,43.5C42.07,43.5,40.5,41.934,40.5,40C40.5,38.066,42.07,36.5,44,36.5C45.937,36.5,47.5,38.066,47.5,40C47.5,41.934,45.937,43.5,44,43.5 M44,55.5C42.07,55.5,40.5,53.934,40.5,52C40.5,50.066,42.07,48.5,44,48.5C45.937,48.5,47.5,50.066,47.5,52C47.5,53.934,45.937,55.5,44,55.5 M32,31.5C30.07,31.5,28.5,29.938,28.5,28C28.5,26.07,30.07,24.5,32,24.5C33.937,24.5,35.5,26.07,35.5,28C35.5,29.938,33.937,31.5,32,31.5 M32,43.5C30.07,43.5,28.5,41.934,28.5,40C28.5,38.066,30.07,36.5,32,36.5C33.937,36.5,35.5,38.066,35.5,40C35.5,41.934,33.937,43.5,32,43.5 M32,55.5C30.07,55.5,28.5,53.934,28.5,52C28.5,50.066,30.07,48.5,32,48.5C33.937,48.5,35.5,50.066,35.5,52C35.5,53.934,33.937,55.5,32,55.5 M20,31.5C18.07,31.5,16.5,29.938,16.5,28C16.5,26.07,18.07,24.5,20,24.5C21.937,24.5,23.5,26.07,23.5,28C23.5,29.938,21.937,31.5,20,31.5 M20,43.5C18.07,43.5,16.5,41.934,16.5,40C16.5,38.066,18.07,36.5,20,36.5C21.937,36.5,23.5,38.066,23.5,40C23.5,41.934,21.937,43.5,20,43.5 M20,55.5C18.07,55.5,16.5,53.934,16.5,52C16.5,50.066,18.07,48.5,20,48.5C21.937,48.5,23.5,50.066,23.5,52C23.5,53.934,21.937,55.5,20,55.5 M49,0L15,0C11.148,0,8,3.148,8,7L8,57C8,60.852,11.148,64,15,64L49,64C52.852,64,56,60.852,56,57L56,7C56,3.148,52.852,0,49,0" Height="64" Canvas.Left="9" Stretch="Fill" Canvas.Top="0" Width="48"/> </Canvas> </Viewbox> </l:BaseGlyph>
The MainWindow
The layout of the main window is simple, it’s a stackpanel nested inside a Grid for aligning the glyphs and the CheckBox and a Viewbox to scale the CheckBox to a nice size.
<Window x:Class="GlyphOutline.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:GlyphOutline="clr-namespace:GlyphOutline" Title="MainWindow" Height="140" Width="150"> <Grid> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition /> </Grid.RowDefinitions> <StackPanel Grid.Row="0" Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center"> <GlyphOutline:Bell Height="40" Width="40" Outlined="{Binding ElementName=OutlineCheck, Path=IsChecked}" /> <GlyphOutline:Calculator Height="40" Width="40" Margin="20 0 0 0" Outlined="{Binding ElementName=OutlineCheck, Path=IsChecked}" /> </StackPanel> <Viewbox Grid.Row="1" Height="40" Width="40" HorizontalAlignment="Center" VerticalAlignment="Center"> <CheckBox Name="OutlineCheck" /> </Viewbox> </Grid> </Window>
The Outlining
To implement the Outline feature I created a custom ContentControl that I called the “BaseGlyph”. This control hosts the Outlined property and it’s implementation.
First there’s the Outline property implementation with a callback defined to handle the conversion of styles on the paths when the property is either true or false.
public static readonly DependencyProperty OutlinedProperty = DependencyProperty.Register( "Outlined", typeof(Boolean), typeof(BaseGlyph), new UIPropertyMetadata( false, // default value for this DP OnOutlinedChanged)); // callback on change private static void OnOutlinedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var caller = (BaseGlyph)d; if ((bool)e.NewValue) { foreach (var item in d.GetVisualDescendents().OfType<Path>()) item.Style = (Style)caller.FindResource("OutlinePath"); } else { foreach (var item in d.GetVisualDescendents().OfType<Path>()) item.Style = (Style)caller.FindResource(typeof(Path)); } } public Boolean Outlined { get { return (Boolean)GetValue(OutlinedProperty); } set { SetValue(OutlinedProperty, value); } }
There’s a bit of code on the constructor to create the ResourceDictionary that needs to be present to transform the Paths in the glyph in Shapes with thickness.
public BaseGlyph() { // Build the Resource Dictionary Resources = new ResourceDictionary(); var binding = new Binding(); var style = new Style(); // Ancestor Foreground Binding, binding.RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(BaseGlyph), 1); binding.Path = new PropertyPath(ForegroundProperty); style.TargetType = typeof(Path); style.Setters.Add(new Setter(Shape.FillProperty, binding)); var transparentBrush = new SolidColorBrush(Colors.White) {Opacity = 0}; var outline = new Style {TargetType = typeof (Path)}; outline.Setters.Add(new Setter(Shape.StrokeThicknessProperty, 2d)); outline.Setters.Add(new Setter(Shape.StrokeProperty, binding)); outline.Setters.Add(new Setter(Shape.FillProperty, transparentBrush)); Resources.Add(typeof(Path), style); Resources.Add("OutlinePath", outline); // Register event handlers Loaded += BaseIconLoaded; }
To support the callback method in the Outlined property I used some Bag-of-Tricks visual helpers and isolated them in the static class VisualHelper
namespace GlyphOutline { #region Using Declarations using System; using System.Collections.Generic; using System.Linq; using System.Windows; using System.Windows.Media; #endregion public static class VisualHelper { public static IEnumerable<DependencyObject> GetVisualDescendents(this DependencyObject source) { return source .GetVisualChildren() .SelectRecursive(element => element.GetVisualChildren()); } public static IEnumerable<DependencyObject> GetVisualChildren(this DependencyObject source) { var count = VisualTreeHelper.GetChildrenCount(source); for (var i = 0; i < count; i++) yield return VisualTreeHelper.GetChild(source, i); } public static IEnumerable<TSource> SelectRecursive<TSource>(this IEnumerable<TSource> source, Func<TSource, IEnumerable<TSource>> recursiveSelector) { var stack = new Stack<IEnumerator<TSource>>(); stack.Push(source.GetEnumerator()); try { while (stack.Any()) { if (stack.Peek().MoveNext()) { var current = stack.Peek().Current; yield return current; stack.Push(recursiveSelector(current).GetEnumerator()); } else { stack.Pop().Dispose(); } } } finally { while (stack.Any()) { stack.Pop().Dispose(); } } } } }
The full source of BaseGlyph is listed below
namespace GlyphOutline { #region Using Declarations using System; using System.Linq; using System.Windows.Controls; using System.Windows; using System.Windows.Media; using System.Windows.Shapes; using System.Windows.Data; #endregion public class BaseGlyph : ContentControl { #region Dependency Properties public static readonly DependencyProperty OutlinedProperty = DependencyProperty.Register( "Outlined", typeof(Boolean), typeof(BaseGlyph), new UIPropertyMetadata( false, // default value for this DP OnOutlinedChanged)); // callback on change private static void OnOutlinedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var caller = (BaseGlyph)d; if ((bool)e.NewValue) { foreach (var item in d.GetVisualDescendents().OfType<Path>()) item.Style = (Style)caller.FindResource("OutlinePath"); } else { foreach (var item in d.GetVisualDescendents().OfType<Path>()) item.Style = (Style)caller.FindResource(typeof(Path)); } } public Boolean Outlined { get { return (Boolean)GetValue(OutlinedProperty); } set { SetValue(OutlinedProperty, value); } } #endregion #region Constructors public BaseGlyph() { // Build the Resource Dictionary Resources = new ResourceDictionary(); var binding = new Binding(); var style = new Style(); // Ancestor Foreground Binding, binding.RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(BaseGlyph), 1); binding.Path = new PropertyPath(ForegroundProperty); style.TargetType = typeof(Path); style.Setters.Add(new Setter(Shape.FillProperty, binding)); var transparentBrush = new SolidColorBrush(Colors.White) {Opacity = 0}; var outline = new Style {TargetType = typeof (Path)}; outline.Setters.Add(new Setter(Shape.StrokeThicknessProperty, 2d)); outline.Setters.Add(new Setter(Shape.StrokeProperty, binding)); outline.Setters.Add(new Setter(Shape.FillProperty, transparentBrush)); Resources.Add(typeof(Path), style); Resources.Add("OutlinePath", outline); // Register event handlers Loaded += BaseIconLoaded; } #endregion #region Event Handlers private void BaseIconLoaded(object sender, RoutedEventArgs e) { // if it's Outlined (set in XAML) need to raise the event at the end of the Load // to actually load it as Outlined if (Outlined) OnOutlinedChanged(this, new DependencyPropertyChangedEventArgs(OutlinedProperty, false, true)); } #endregion } }
The full project can be downloaded here.