WPF:仅使用通过“AddFontMemResourceEx”安装的字体进行处理

2023-12-09

在WPF中我们想使用ttf字体作为嵌入式资源,无需将它们复制或安装到系统中,也无需实际将它们写入磁盘。没有内存泄漏问题。

没有详细说明的解决方案:

如何在 WPF 应用程序中包含外部字体而不安装它

由于 WPF 内存泄漏,在这种情况下可用:

使用 Font 时 WPF TextBlock 内存泄漏

在 GDI 中只能通过以下方式从内存和进程安装字体添加FontMemResourceEx。由于这会安装该进程的字体,因此它也应该适用于 WPF,但似乎存在问题FontFamily我们通过安装字体后得到的AddFontMemResourceEx. E.g.:

var font = new FontFamily("Roboto");

这是有效的,因为它不会给出任何错误,但字体实际上没有改变,一些行间距和其他指标发生了变化,但字体看起来完全像Segoe UI因为某些原因。

那么问题是如何使用安装的字体AddFontMemResourceEx in WPF?

PS:这里是P/Invoke代码:

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);
    }
}

此代码成功并显示如下日志消息:

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

将嵌入资源加载为字节数组的支持代码:

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;
    }
}

这意味着安装嵌入字体可以像这样完成assembly是包含嵌入字体资源的程序集EMBEDDEDFONTNAMESPACE是嵌入资源的命名空间,例如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);
}

我不知道这是否正是您想要的,但我有一个解决方案,您可以将字体用作Resource在你的解决方案中。

  1. 全部申报fonts你想要作为Resource.
  2. 进行定制MarkupExtension called FontExplorer
  3. Try my XAML Example

当。。。的时候application开始并且FontExplorer第一次使用,会缓存所有fonts您拥有的资源。此后,每当您需要其中之一时,都会使用缓存将其返还。

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

正如您在预览中看到的,memory usage后从 83,2MB 减少到 82,9 MBGC做它的工作。

enter image description here

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

WPF:仅使用通过“AddFontMemResourceEx”安装的字体进行处理 的相关文章

随机推荐