WPF:仅将使用“AddFontMemResourceEx”安装的字体用于进程

Posted

技术标签:

【中文标题】WPF:仅将使用“AddFontMemResourceEx”安装的字体用于进程【英文标题】:WPF: Use font installed with 'AddFontMemResourceEx' for process only 【发布时间】:2018-11-30 13:34:53 【问题描述】:

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

没有详细的解决方案:

How to include external font in WPF application without installing it

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

WPF TextBlock memory leak when using Font

只能通过AddFontMemResourceEx 在 GDI 中从内存和进程中安装字体。由于这会为进程安装字体,因此它也应该适用于 WPF,但在通过 AddFontMemResourceEx 安装字体后,我们得到的 FontFamily 似乎存在问题。例如:

var font = new FontFamily("Roboto");

这样做的原因在于它不会给出任何错误,但实际上并未更改字体,更改了一些行间距和其他指标,但出于某种原因,字体看起来与Segoe UI 完全相同。

那么问题来了,如何在 WPF 中使用AddFontMemResourceEx 安装的字体?

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

【问题讨论】:

如果你只加载一次字体(作为共享资源),你不应该泄漏那么多。您的程序只会消耗更多内存(如果内存泄漏问题仍然存在,则复制项目已与 Microsoft 连接站点一起消失) 【参考方案1】:

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

    将你想要的所有fonts声明为Resource。 自定义MarkupExtension,称为FontExplorer 试试我的XAML 示例

application 启动并且第一次使用FontExplorer 时,它会缓存您作为资源拥有的所有fonts。之后,每次需要其中一个时,都会使用缓存将其归还。

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


预览

正如您在预览中看到的那样,memory usageGC 完成工作后从 83.2MB 减少到 82.9MB。

【讨论】:

以上是关于WPF:仅将使用“AddFontMemResourceEx”安装的字体用于进程的主要内容,如果未能解决你的问题,请参考以下文章

仅底部带有圆角的 WPF 弹出窗口

Wpf在运行时加载dll

如何使用mailgun php API仅将邮件发送到密件抄送? [复制]

OutputStreamWriter 仅将一项写入文件

有没有办法使用 RedShiftCopyActivity 仅将特定列从 RedShift 复制到 S3?

使用 *ngFor 时如何仅将类设置为特定元素?