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 usage
在 GC
完成工作后从 83.2MB 减少到 82.9MB。
【讨论】:
以上是关于WPF:仅将使用“AddFontMemResourceEx”安装的字体用于进程的主要内容,如果未能解决你的问题,请参考以下文章
如何使用mailgun php API仅将邮件发送到密件抄送? [复制]