views:

174

answers:

2

I am trying to show a Font picker list similar to the one in Blend:

Blend Font Picker

Like Blend, I am seeing performance issues when the FontFamilies are not loaded in the FontCache.

It seems that the penalty I am paying is the time it takes to actually render the FontFamily for the given FontSize and save it into the FontCache. Once the rendered font is in the cache the problem goes away.

I have tried iterating the Fonts.SystemFontFamilies collection on a background thread and dispatching a call to the UI which causes a hidden TextBlock to update (which should cause the font to render).

Of course, since the dispatch calls happen in succession it just pounds the UI and I get the same net result of a blocking UI until all the fonts have been rendered and loaded into the FontCache.

Does anybody have a good solution to this issue? They didn't seem to find a fix for it in Blend so I am thinking there isn't a good solution.

+1  A: 

A couple of ideas for you

Idea 1: Do the font fetching entirely on the background thread.

The fonts are actually loaded into the system font cache (inter-process) and then part of the font information is copied into the thread-specific font cache. It is possible that filling the system font cache would result in a good enough increase in speed. This could be done by a low priority background thread that starts running the instant your app is started. So by the time the user drops down the font list the system font cache should be fully populated.

Idea 2: Cache the rendered font geometry yourself

Instead of using TextBlocks, use ContentPresenter objects in your ComboBox's DataTemplate with the content bound to a PriorityBinding. The lower priority would produce a TextBlock using the default font, and the higher priority would be an IsAsync binding that would create a GlyphRun with the appropriate parameters, call BuildGeometry() on it, and return the Geometry inside a Path object. The created Geometry objects can be cached and returned again for future accesses to the same font.

The result of this will be that items will initially appear in the default font, and render into the styled font as soon as the fonts can be loaded and their geometry created. Note that this can be combined with code that prefills your cache in a separate thread.

The code for Idea 2 would look something like this:

<ComboBox ItemsSource="{Binding MyFontObjects}">
  <ComboBox.ItemTemplate>
    <ContentPresenter>
      <ContentPresenter.Content>
        <PriorityBinding>
          <Binding IsAsync="True" Path="BuildStyledFontName" />
          <Binding Path="BuildTextBlock" />
        </PriorityBinding>
        ... close all tags ...

Where MyFontObjets would be a IEnumerable of objects something like this:

public class MyFontObject
{
  public FontFamily Font { get; set; }

  public object BuildTextBlock
  {
    get { return new TextBlock { Text = GetFamilyName(Font) } }
  }

  public object BuildStyledFontName
  {
    get
    {
      return new Path { Data = GetStyledFontGeometryUsingCache() };
    }
  }

  private Geometry GetStyledFontGeometryUsingCache()
  {      
    Geometry geo;
    lock(_fontGeometryCache)
      if(_fontGeometryCache.TryGetValue(Font, out geo) return geo;

    lock(_fontGeometryBuildLock)
    {
      lock(_fontGeometryCache)
        if(_fontGeometryCache.TryGetValue(Font, out geo) return geo;

      geo = BuildStyledFontGeometry();

      lock(_fontGeometryCache)
        _fontGeometryCache[Font] = geo;
    }
  }
  static object _fontGeometryCache = new Dictionary<FontFamily, Geometry>();
  static object _fontGeometryBuildLock = new object();

  private Geometry BuildStyledFontGeometry()
  {
    var run = new GlyphRun
    {
      Characters = GetFamilyName(Font),
      GlyphTypeface = GetGlyphTypeface(Font),
    }
    return run.BuildGeometry();
  }

  ... GetFamilyName ...

  ... GetGlyphTypeface ...

  // Call from low priority background thread spawned at app startup
  publc static void PrefillCache()
  {
    foreach(FontFamily font in Fonts.SystemFontFamilies)
      new MyFontObject { Font = font }.GetStyledFontGeometryUsingCache();
  }
}

Note that the Geometry objects in the cache could be saved to disk by converting them to PathGeometry and thence to strings in the PathGeometry mini-language. This would allow the font geometry cache to be filled using a single file read & parse, so the only time you would see any delay was when you first ran the app, or when you ran it with a large number of new fonts.

Ray Burns
Thanks for the detailed response. I am going to give option 2 a shot and see what happens
Foovanadil
This looks like it will work. The amount of effort to implement the GetGlyphTypeface method is beyond the scope of what I can afford to do on this project so I can't confirm for sure. But I did walk through the motions of the rest of the solution and it does seem to work. Be warned, though it is labor intensive to implement. For me the trade off in effort spent vs perceived value isn't worth it. But thank you for the solution.
Foovanadil
A: 

My solution is to not render all the fonts in advance, by using a virtualized panel for teh font list you will only load the fonts that fit into the screen, it will slow down scrolling for the first time but its almost unnoticeable by the user.

look at http://www.bennedik.de/2007/10/wpf-fast-font-drop-down-list.html

BTW, if you use a combo with a VirtualizingStackPanel you will have to set the width of the TextBlock element inside the DataTemplate, otherwise the dropdown width will change during scrolling.

Nir
I tried that first but it seems that the VirtualizingStackPanel isn't giving me the performance savings. I did exactly the same thing described in the blog you reference and the performance is still very noticable.
Foovanadil