5
votes

In WPF we would like to use ttf fonts as embedded resources without copying or installing these to the system and without actually writing these to disk. Without memory leak issues.

None of the solutions detailed in:

How to include external font in WPF application without installing it

are useable in this scenario due to the WPF memory leak around this:

WPF TextBlock memory leak when using Font

Installing fonts from memory and process only is possible in GDI via AddFontMemResourceEx . Since this installs the font for the process, it should work for WPF as well, but there seems to be issues around the FontFamily that we get after installing the font via AddFontMemResourceEx. E.g.:

var font = new FontFamily("Roboto");

This works in that it does not give any errors, but the font isn't actually changed, some line spacing and other metrics are changed, but the font looks exactly like Segoe UI for some reason.

The question is then is it and how is it possible to use fonts installed with AddFontMemResourceEx in WPF?

PS: Here the P/Invoke code:

const string GdiDllName = "gdi32";
[DllImport(GdiDllName, ExactSpelling= true)]
private static extern IntPtr AddFontMemResourceEx(byte[] pbFont, int cbFont, IntPtr pdv, out uint pcFonts);

public static void AddFontMemResourceEx(string fontResourceName, byte[] bytes, Action<string> log)
{
    var handle = AddFontMemResourceEx(bytes, bytes.Length, IntPtr.Zero, out uint fontCount);
    if (handle == IntPtr.Zero)
    {
        log?.Invoke($"Font install failed for '{fontResourceName}'");
    }
    else
    {
        var message = $"Font installed '{fontResourceName}' with font count '{fontCount}'";
        log?.Invoke(message);
    }
}

This code succeeeds with log messages like:

Font installed 'Roboto-Regular.ttf' with font count '1'

Support code for loading embedded resource as byte array:

public static byte[] ReadResourceByteArray(Assembly assembly, string resourceName)
{
    using (var stream = assembly.GetManifestResourceStream(resourceName))
    {
        var bytes = new byte[stream.Length];
        int read = 0;
        while (read < bytes.Length)
        {
            read += stream.Read(bytes, read, bytes.Length - read);
        }
        if (read != bytes.Length)
        {
            throw new ArgumentException(
                $"Resource '{resourceName}' has unexpected length " +
                $"'{read}' expected '{bytes.Length}'");
        }
        return bytes;
    }
}

Which means installing embedded fonts can be done like, with assembly being the assembly containing the embedded font resources and EMBEDDEDFONTNAMESPACE being the namespace of the embedded resources e.g. SomeProject.Fonts:

var resourceNames = assembly.GetManifestResourceNames();

string Prefix = "EMBEDDEDFONTNAMESPACE" + ".";
var fontFileNameToResourceName = resourceNames.Where(n => n.StartsWith(Prefix))
    .ToDictionary(n => n.Replace(Prefix, string.Empty), n => n);

var fontFileNameToBytes = fontFileNameToResourceName
    .ToDictionary(p => p.Key, p => ReadResourceByteArray(assembly, p.Value));

foreach (var fileNameBytes in fontFileNameToBytes)
{
    AddFontMemResourceEx(fileNameBytes.Key, fileNameBytes.Value, log);
}
1
If you only load the font once (as a shared resource), you shouldn't leak that much. Your program will just eat a bit more memory (if that memory leak issue is still there, the reproducing project is gone with the Microsoft connect site)Simon Mourier

1 Answers

3
votes

I don't know if this is exactly what you want, but I got a solution where you can use your fonts as a Resource in your solution.

  1. Declare all fonts you want as Resource.
  2. Make a custom MarkupExtension called FontExplorer
  3. Try my XAML Example

When the application starts and the FontExplorer is used for the first time, it caches all fonts which you have as resource. After that, everytime you need one of it, the cache is used to give it back.

Resources

public class FontExplorer : MarkupExtension
{
    // ##############################################################################################################################
    // Properties
    // ##############################################################################################################################

    #region Properties

    // ##########################################################################################
    // Public Properties
    // ##########################################################################################

    public string Key { get; set; }

    // ##########################################################################################
    // Private Properties
    // ##########################################################################################

    private static readonly Dictionary<string, FontFamily> _CachedFonts = new Dictionary<string, FontFamily>();

    #endregion


    // ##############################################################################################################################
    // Constructor
    // ##############################################################################################################################

    #region Constructor

    static FontExplorer()
    {
        foreach (FontFamily fontFamily in Fonts.GetFontFamilies(new Uri("pack://application:,,,/"), "./Fonts/"))
        {
            _CachedFonts.Add(fontFamily.FamilyNames.First().Value, fontFamily);
        }            
    }

    #endregion

    // ##############################################################################################################################
    // methods
    // ##############################################################################################################################

    #region methods

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        return ReadFont();
    }

    private object ReadFont()
    {
        if (!string.IsNullOrEmpty(Key))
        {
            if (_CachedFonts.ContainsKey(Key))
                return _CachedFonts[Key];
        }

        return new FontFamily("Comic Sans MS");
    } 

    #endregion
}

<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        d:DataContext="{d:DesignInstance local:MainWindow}"
        Title="MainWindow" Height="450" Width="800">
    <Window.Style>
        <Style TargetType="local:MainWindow">
            <Setter Property="FontFamily" Value="{local:FontExplorer Key='Candle Mustard'}"/>
            <Style.Triggers>
                <Trigger Property="Switch" Value="True">
                    <Setter Property="FontFamily" Value="{local:FontExplorer Key=Roboto}"/>
                </Trigger>
            </Style.Triggers>
        </Style>
    </Window.Style>
    <Grid x:Name="grid">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <StackPanel Orientation="Vertical" VerticalAlignment="Center" HorizontalAlignment="Center" Grid.Column="0">
            <TextBlock Text="Hello World" FontFamily="{local:FontExplorer Key='Candle Mustard'}"/>
            <TextBlock Text="Hello World" FontFamily="{local:FontExplorer Key=Roboto}"/>
            <TextBlock Text="Hello World"/>
            <TextBlock Text="Hello World"/>
            <TextBlock Text="Hello World"/>
            <TextBlock Text="Hello World"/>
            <TextBlock Text="Hello World"/>
            <TextBlock Text="Hello World"/>
            <TextBlock Text="Hello World"/>
            <TextBlock Text="Hello World"/>
            <TextBlock Text="Hello World"/>
            <TextBlock Text="Hello World"/>
        </StackPanel>
        <StackPanel Orientation="Vertical" VerticalAlignment="Center" HorizontalAlignment="Center" Grid.Column="1" x:Name="Panel"/>
    </Grid>
</Window>

public partial class MainWindow : Window
{
    public bool Switch
    {
        get => (bool)GetValue(SwitchProperty);
        set => SetValue(SwitchProperty, value);
    }

    /// <summary>
    /// The <see cref="Switch"/> DependencyProperty.
    /// </summary>
    public static readonly DependencyProperty SwitchProperty = DependencyProperty.Register("Switch", typeof(bool), typeof(MainWindow), new PropertyMetadata(false));


    private readonly DispatcherTimer _Timer;

    public MainWindow()
    {
        InitializeComponent();
        _Timer = new DispatcherTimer();
        _Timer.Interval = TimeSpan.FromMilliseconds(50);
        _Timer.Tick += (sender, args) =>
        {
            Switch = !Switch;
            Panel.Children.Add(new TextBlock {Text = "I'm frome code behind"});
            if(Panel.Children.Count > 15)
                Panel.Children.Clear();
        };
        _Timer.Start();
    }


    // ##############################################################################################################################
    // PropertyChanged
    // ##############################################################################################################################

    #region PropertyChanged

    /// <summary>
    /// The PropertyChanged Eventhandler
    /// </summary>
    public event PropertyChangedEventHandler PropertyChanged;

    /// <summary>
    /// Raise/invoke the propertyChanged event!
    /// </summary>
    /// <param name="propertyName"></param>
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    #endregion
}

Preview

As you can see in the preview, the memory usage reduces from 83,2MB to 82,9 MB after the GC does it's job.

enter image description here