Saturday, October 27, 2007

WPF: A fast font drop down list

Writing a drop down list for fonts is easy in WPF. Just create a data template that binds the text and the font, and bind the ComboBox against Fonts.SystemFontFamilies:

        <ComboBox x:Name="fontComboSlow">

            <ComboBox.ItemTemplate>

                <DataTemplate>

                    <TextBlock Text="{Binding}" FontFamily="{Binding}" FontSize="15" Height="20"/>

                </DataTemplate>

            </ComboBox.ItemTemplate>

        </ComboBox>

 

and

fontComboSlow.ItemsSource = Fonts.SystemFontFamilies;

gives you a nice drop down list with font preview:

image_thumb6

However there is one caveat: The first time you run this and click on the drop down arrow, you will have to wait for some annoying time. The exact time depends on your hardware, and on how many fonts you have installed. If you click on the drop down arrow again, it is fast. If you close the program, and run the program again, it is still fast.

Why is it still fast if you run the program the second time? If you go to the task manager, and show the processes of all users, you can find that WPF is running its own process, called the "Windows Presentation Foundation Font Cache Service":

image_thumb5 

If you kill this process, and run the software again, the first click on the drop down arrow is slow again. I have been looking for a solution to this for some time. Recently, the WPF SDK blog had a post about improving the performance of the ComboBox, recommending a VirtualizingStackPanel as ItemsPanel for the ComboBox in such cases. They also explain the delay:

This reason for the delay is that by default, the ComboBox uses a StackPanel, and a visual that represents each item is created at the same time, which is processor-intensive and memory-intensive.

Sure enough, if you change the XAML for the font drop down list to utilize the VirtualStackPanel as recommended:

        <ComboBox x:Name="fontComboFast">

            <ComboBox.ItemsPanel>

                <ItemsPanelTemplate>

                    <VirtualizingStackPanel />

                </ItemsPanelTemplate>

            </ComboBox.ItemsPanel>

            <ComboBox.ItemTemplate>

                <DataTemplate>

                    <TextBlock Text="{Binding}" FontFamily="{Binding}" FontSize="15" Height="20"/>

                </DataTemplate>

            </ComboBox.ItemTemplate>

        </ComboBox>

the annoying delay is gone. If you scroll fast through the list of fonts, you can instead notice sometimes a very brief delay for a fraction of a second, when the system is loading a new font that was not shown so far.

You can download a sample program, where you can compare the two approaches.

8 Comments:

Anonymous Anonymous said...

Good Stuff! Binding in WPF always surprises me.

I noticed when using the VirtualizingStackPanel that if your comboBox is narrow (130px) the drop panel changes it's width as new fonts come into view if the fonts are too wide for the current combo width. Its a little annoying if you are confined in your combo Box width.

4:22 PM  
Anonymous Anonymous said...

Great tip. I have been looking for a workaround for this.

PresentationFontCache process, that I didnt know!

Put a fixed width on the VitualizingStackPanel to stop the width of the popup changing for long fonts e.g.
VirtualizingStackPanel Width="200"
thx.

12:56 AM  
Anonymous James Hurst said...

Hi - thanks for an excellent tip!

On my box I ran into a problem: it blows up with a FileFormatException when trying to load an Adobe font, generic.otf. Is there some event or method one can overload to put a wrapper around the binding process to catch that exception, that you know of?

2:33 PM  
Blogger AaronTFoley said...

To get rid of the FileFormatException create a list that contains all your font families and remove the one causing the error.

So with the adobe one, you can use this.

private List-FontFamily- GetFonts()
{
List-FontFamily- fonts = new List-FontFamily-();
foreach (FontFamily font in Fonts.SystemFontFamilies)
{
if (!font.Source.ToString().Contains("ExtraGlyphlets")) fonts.Add(font);
}
return fonts;
}

9:19 AM  
Blogger Josef said...

As a newbie I don't know where to place and how to use the "private List-FontFamily- GetFonts()"-Code to get rid of the generic.otf exception. Could you give me an example for this, please?

1:05 AM  
Anonymous Anonymous said...

Goes to codebehind file eg. Window1.cs

1:32 AM  
Blogger ItellStacy said...

Wow, you just saved me a TON of time. Thanks a ton for posting this.

9:00 PM  
Blogger chaiguy said...

Just what I needed--thanks!

1:42 PM  

Post a Comment

<< Home