Introduction
Surprisingly, the Windows Presentation Framework (WPF) comes with no predefined dialog forms, so for instance there is no Font Dialog or Color Dialog in the default toolbox. Since I needed one, I decided to write one on my own. Well, not entirely on my own: as any smart programmer knows, nothing is built from scratch anymore. So I must begin with some...
Credits
My WPF Font Picker is based on the remix of two different works: a Font Chooser in XAML and a Color Combobox control. The first one is the Pure XAML Font Chooser by Cheng Norris [1]. The second one is the Color Combobox Picker by Muhammed Sudheer [2].
Without these two noticeable pieces of software, my WPF Font Picker would have never seen the light.
Requisites
I needed in first place a pluggable WPF library, so that I can easily add it to any of my projects. I decided to build a "WPF User Control" in the form of two different reusable controls (the Color Picker and the Font Picker) and a Window Dialog in the fashion of Windows Forms Task Dialogs.
The Font Picker had to allow the user to choose a font with every option: family, style, size, weight and stretch. Also, the Font Picker had to let the user choose the font foreground color.
An important requisite, that was missing from the works quoted in the credits, was that the dialog form should be able to be initialized with a "current" font. It is easy to understand why: the typical scenario is that the user selects font, then she is not satisfied and clicks again to display the dialog: she does not want a "default" font again, she wants to modify the font she selected in the first attempt.
Using the code
The DLL library that contains Font Picker is called ColorFont and it is composed by two XAML User Controls:
- ColorFontChooser
- ColorPicker
and by one XAML Window (the Dialog):
so that it can be easily referenced in a .NET project and be used as a standard dialog form:
ColorFont.ColorFontDialog fntDialog = new ColorFont.ColorFontDialog();
fntDialog.Owner = this;
fntDialog.Font = FontInfo.GetControlFont(this.txtText);
if (fntDialog.ShowDialog() == true)
{
FontInfo selectedFont = fntDialog.Font;
if (selectedFont != null)
{
FontInfo.ApplyFont(this.txtText, selectedFont);
}
}
where FontInfo
can be seen as an equivalent of FileInfo
class, that is a class holding all the properties of a Font (color included):
public class FontInfo
{
public FontFamily Family { get; set; }
public double Size { get; set; }
public FontStyle Style { get; set; }
public FontStretch Stretch { get; set; }
public FontWeight Weight { get; set; }
public SolidColorBrush BrushColor { get; set; }
#region Static Utils
public static string TypefaceToString(FamilyTypeface ttf)
{
StringBuilder sb = new StringBuilder(ttf.Stretch.ToString());
sb.Append("-");
sb.Append(ttf.Weight.ToString());
sb.Append("-");
sb.Append(ttf.Style.ToString());
return sb.ToString();
}
public static void ApplyFont(Control control, FontInfo font)
{
control.FontFamily = font.Family;
control.FontSize = font.Size;
control.FontStyle = font.Style;
control.FontStretch = font.Stretch;
control.FontWeight = font.Weight;
control.Foreground = font.BrushColor;
}
public static FontInfo GetControlFont(Control control)
{
FontInfo font = new FontInfo();
font.Family = control.FontFamily;
font.Size = control.FontSize;
font.Style = control.FontStyle;
font.Stretch = control.FontStretch;
font.Weight = control.FontWeight;
font.BrushColor = (SolidColorBrush)control.Foreground;
return font;
}
#endregion
public FontInfo()
{
}
public FontInfo(FontFamily fam, double sz, FontStyle style,
FontStretch strc, FontWeight weight, SolidColorBrush c)
{
this.Family = fam;
this.Size = sz;
this.Style = style;
this.Stretch = strc;
this.Weight = weight;
this.BrushColor = c;
}
public FontColor Color
{
get
{
return AvailableColors.GetFontColor(this.BrushColor);
}
}
public FamilyTypeface Typeface
{
get
{
FamilyTypeface ftf = new FamilyTypeface();
ftf.Stretch = this.Stretch;
ftf.Weight = this.Weight;
ftf.Style = this.Style;
return ftf;
}
}
}
Color Picker User Control
The Color Picker User Control is nothing but a custom drop down list exposing the System.Windows.Media.Colors
enumeration. The XAML part is heavily taken from [2], so I'd send you there for the details.
I just added a custom event that the control raises each time a new color is selected, bubbling the original 'DropDownClosed
' event; and a Property, "SelectedColor
", that is able to communicate the color picked by the user.
public partial class ColorPicker : UserControl
{
private ColorPickerViewModel viewModel;
public static readonly RoutedEvent ColorChangedEvent =
EventManager.RegisterRoutedEvent(
"ColorChanged", RoutingStrategy.Bubble,
typeof(RoutedEventHandler), typeof(ColorPicker));
public static readonly DependencyProperty SelectedColorProperty =
DependencyProperty.Register(
"SelectedColor", typeof(FontColor),
typeof(ColorPicker), new UIPropertyMetadata(null));
public ColorPicker()
{
InitializeComponent();
this.viewModel = new ColorPickerViewModel();
this.DataContext = this.viewModel;
}
public event RoutedEventHandler ColorChanged
{
add { AddHandler(ColorChangedEvent, value); }
remove { RemoveHandler(ColorChangedEvent, value); }
}
public FontColor SelectedColor
{
get
{
FontColor fc = (FontColor)this.GetValue(SelectedColorProperty);
if (fc == null)
{
fc = AvailableColors.GetFontColor("Black");
}
return fc;
}
set
{
this.viewModel.SelectedFontColor = value;
SetValue(SelectedColorProperty, value);
}
}
private void RaiseColorChangedEvent()
{
RoutedEventArgs newEventArgs = new RoutedEventArgs(ColorPicker.ColorChangedEvent);
RaiseEvent(newEventArgs);
}
private void superCombo_DropDownClosed(object sender, EventArgs e)
{
this.SetValue(SelectedColorProperty, this.viewModel.SelectedFontColor);
this.RaiseColorChangedEvent();
}
private void superCombo_Loaded(object sender, RoutedEventArgs e)
{
this.SetValue(SelectedColorProperty, this.viewModel.SelectedFontColor);
}
}
Font Chooser User Control
As I said before, the Font Chooser User Control is a little modification of [1]. The original control did not sort the list of fonts. I added this feature by modifying the CollectionViewSource resource as follows:
<CollectionViewSource Source="{Binding Source={x:Static Fonts.SystemFontFamilies}}"
x:Key="familyCollection">
<CollectionViewSource.SortDescriptions>
<scm:SortDescription PropertyName="Source" Direction="Ascending" />
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
where the "scm:" namespace is defined as:
xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase"
In this case too, I added some logic to handle the "ColorChanged
" event defined above, and to expose the selected font as an object of class FontInfo
.
public partial class ColorFontChooser : UserControl
{
public ColorFontChooser()
{
InitializeComponent();
this.colorPicker.SelectedColor = new SolidColorBrush(Colors.Black);
this.txtSampleText.IsReadOnly = true;
}
public FontInfo SelectedFont
{
get
{
return new FontInfo(this.txtSampleText.FontFamily,
this.txtSampleText.FontSize,
this.txtSampleText.FontStyle,
this.txtSampleText.FontStretch,
this.txtSampleText.FontWeight,
this.colorPicker.SelectedColor);
}
}
private void colorPicker_ColorChanged(object sender, RoutedEventArgs e)
{
this.txtSampleText.Foreground = this.colorPicker.SelectedColor;
}
}
Dialog Window
The Font Picker is served as a normal Dialog window, returning "true" if the user presses "OK". In this case the programmer can fetch the selected color by retrieving the property "Font" of the Dialog class, and apply it to any WPF Control by using the utility static method "ApplyFont" defined in the FontInfo
class.
The following is the XAML source code of the dialog window:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ColorFont" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" x:Class="ColorFont.ColorFontDialog"
Title="Select Font" Height="380" Width="592" WindowStartupLocation="CenterOwner" ResizeMode="NoResize" Icon="Resources/colorfont_icon.png" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" Loaded="Window_Loaded_1">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="40" />
</Grid.RowDefinitions>
<local:ColorFontChooser x:Name="colorFontChooser" Grid.Row="0" Margin="0,0,6,0" d:LayoutOverrides="Width, Height" />
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="406"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="1" Orientation="Horizontal" d:LayoutOverrides="Margin">
<Button x:Name="btnOk" Width="85" Margin="4,8" Content="OK" IsDefault="True" Click="btnOk_Click"/>
<Button Width="70" Margin="4,8" Content="Cancel" IsCancel="True"/>
</StackPanel>
</Grid>
</Grid>
</Window>
Notice how the Window makes use of the User Controls defined above, as objects belonging to the "local:" namespace.
The C# code behind the XAML is trivial, as it handles the OK button setting the Font property that exposes the font selected by the user.
The interesting part regards how the dialog "synchronizes" its controls to display a font suggested by the user. First, the font is set calling the Font property. Then, when the window is loaded, the Font specifications are stored in the FontInfo
class, and the color, the family, the typeface and the size of the specified font are passed to the controls.
public partial class ColorFontDialog : Window
{
private FontInfo selectedFont;
public ColorFontDialog()
{
this.selectedFont = null; InitializeComponent();
}
public FontInfo Font
{
get
{
return this.selectedFont;
}
set
{
FontInfo fi = value;
this.selectedFont = fi;
}
}
private void SyncFontName()
{
string fontFamilyName = this.selectedFont.Family.Source;
int idx = 0;
foreach (var item in this.colorFontChooser.lstFamily.Items)
{
string itemName = item.ToString();
if (fontFamilyName == itemName)
{
break;
}
idx++;
}
this.colorFontChooser.lstFamily.SelectedIndex = idx;
this.colorFontChooser.lstFamily.ScrollIntoView(this.colorFontChooser.lstFamily.Items[idx]);
}
private void SyncFontSize()
{
double fontSize = this.selectedFont.Size;
this.colorFontChooser.fontSizeSlider.Value = fontSize;
}
private void SyncFontColor()
{
int colorIdx = AvailableColors.GetFontColorIndex(this.Font.Color);
this.colorFontChooser.colorPicker.superCombo.SelectedIndex = colorIdx;
this.colorFontChooser.txtSampleText.Foreground = this.Font.Color.Brush;
this.colorFontChooser.colorPicker.superCombo.BringIntoView();
}
private void SyncFontTypeface()
{
string fontTypeFaceSb = FontInfo.TypefaceToString(this.selectedFont.Typeface);
int idx = 0;
foreach (var item in this.colorFontChooser.lstTypefaces.Items)
{
FamilyTypeface face = item as FamilyTypeface;
if (fontTypeFaceSb == FontInfo.TypefaceToString(face))
{
break;
}
idx++;
}
this.colorFontChooser.lstTypefaces.SelectedIndex = idx;
}
private void btnOk_Click(object sender, RoutedEventArgs e)
{
this.Font = this.colorFontChooser.SelectedFont;
this.DialogResult = true;
}
private void Window_Loaded_1(object sender, RoutedEventArgs e)
{
this.SyncFontColor();
this.SyncFontName();
this.SyncFontSize();
this.SyncFontTypeface();
}
}
Conclusions
I personally find rather annoying that the WPF base library comes with no predefined dialog forms, and that even the basic File Dialog is a wrapper to the Windows Forms one. Is there a reason for that? I frankly do not know, anyway I am looking forward to a new WPF version with the dialogs in there. For the time being, my humble Font Picker could be of some use.
History
May 4th, 2012: Initial release.