向 SharpDX.DirectWrite.Factory 添加嵌入字体资源

问题描述 投票:0回答:1

我的 dll 由

.Net Framework 4.8
WPF 应用程序加载。它包含作为
.ttf
.otf
格式的嵌入资源的字体文件,我想以一种格式或另一种格式将该字体添加到托管 WPF 应用程序已创建的
SharpDX.DirectWrite.Factory
实例中。

应用程序是

NinjaTrader 8
,字体是
FontAwesome
,但想必这个问题可以适用于任何 WPF 应用程序和任何字体。

所需用途: (就我而言,所需的用法位于 NinjaTrader 8 中

OnRender
Indicator
方法内。)

using System.Windows.Media;
using SharpDX;
using SharpDX.DirectWrite;

// "Globals.DirectWriteFactory" is the `SharpDX.DirectWrite.Factory` instance provided by the hosting WPF application
// "Font Awesome 6 Free Regular" is the name of the font embedded in my dll
// "\uf1c0" is a string representing one of thousands of icons available in this font.
// "RenderTarget" is a `SharpDX.Direct2D1.RenderTarget` object provided by the hosting WPF application

using var format = new TextFormat(Globals.DirectWriteFactory, "Font Awesome 6 Free Regular", 12);
using var layout = new TextLayout(Globals.DirectWriteFactory, "\uf1c0", format, 1000, 1000);
using var brush = Brushes.WhiteSmoke.ToDxBrush(RenderTarget);
RenderTarget.DrawTextLayout(new Vector2(100, 100), layout, brush);

然而,结果不是渲染图标,而是渲染了一个代表未知字符的小正方形。我认为问题在于未使用所需的字体,而是替换了默认字体。当我渲染英文字符串时,它看起来像 Arial。

这是我用来尝试将 FontAwesome 添加到

Globals.DirectWriteFactory
的代码:

由于

ResourceFontLoader
类中的断点永远不会被命中,我相信
DirectWrite.Factory
对象会忽略在
FontAwesome
实用程序类的静态构造函数中进行的新注册。

internal static class FontAwesome
{
  public static TextFormat CreateTextFormat(float fontSize)
    => new TextFormat(Globals.DirectWriteFactory, "Font Awesome 6 Free Regular", fontSize);

  static FontAwesome()
  {
    var loader = new ResourceFontLoader();
    Globals.DirectWriteFactory.RegisterFontFileLoader(loader);
    Globals.DirectWriteFactory.RegisterFontCollectionLoader(loader);
  }

  private sealed class ResourceFontLoader : CallbackBase, FontCollectionLoader, FontFileLoader
  {
    private readonly List<ResourceFontFileStream> _fontStreams = new List<ResourceFontFileStream>();
    private readonly List<ResourceFontFileEnumerator> _enumerators = new List<ResourceFontFileEnumerator>();
    private readonly DataStream _keyStream;

    public ResourceFontLoader()
    {
      foreach (var name in typeof(ResourceFontLoader).Assembly.GetManifestResourceNames())
      {
        // I have tried using both .otf and .ttf formats.

        //if (name.EndsWith(".otf") && name.IndexOf("awesome", StringComparison.OrdinalIgnoreCase) >= 0)
        if (name.EndsWith(".ttf") && name.IndexOf("fa-", StringComparison.OrdinalIgnoreCase) >= 0)
        {
          var fontBytes = Utilities.ReadStream(typeof(ResourceFontLoader).Assembly.GetManifestResourceStream(name));
          var stream = new DataStream(fontBytes.Length, true, true);
          stream.Write(fontBytes, 0, fontBytes.Length);
          stream.Position = 0;
          _fontStreams.Add(new ResourceFontFileStream(stream));
        }
      }

      _keyStream = new DataStream(sizeof(int) * _fontStreams.Count, true, true);
      for (int i = 0; i < _fontStreams.Count; i++)
        _keyStream.Write((int)i);
      _keyStream.Position = 0;
    }

    public DataStream Key
    {
      get
      {
        // Breakpoints here are never hit! It's simnply not used!

        return _keyStream;
      }
    }

    FontFileEnumerator FontCollectionLoader.CreateEnumeratorFromKey(Factory factory, DataPointer collectionKey)
    {
      // Breakpoints here are never hit! It's simnply not used!

      var enumerator = new ResourceFontFileEnumerator(factory, this, collectionKey);
      _enumerators.Add(enumerator);

      return enumerator;
    }

    FontFileStream FontFileLoader.CreateStreamFromKey(DataPointer fontFileReferenceKey)
    {
      // Breakpoints here are never hit! It's simnply not used!

      var index = Utilities.Read<int>(fontFileReferenceKey.Pointer);
      return _fontStreams[index];
    }
  }
  private sealed class ResourceFontFileStream : CallbackBase, FontFileStream
  {
    private readonly DataStream _stream;

    public ResourceFontFileStream(DataStream stream)
    {
      this._stream = stream;
    }

    /// <summary>
    /// Reads a fragment from a font file.
    /// </summary>
    /// <param name="fragmentStart">When this method returns, contains an address of a  reference to the start of the font file fragment.  This parameter is passed uninitialized.</param>
    /// <param name="fileOffset">The offset of the fragment, in bytes, from the beginning of the font file.</param>
    /// <param name="fragmentSize">The size of the file fragment, in bytes.</param>
    /// <param name="fragmentContext">When this method returns, contains the address of</param>
    /// <remarks>
    /// Note that ReadFileFragment implementations must check whether the requested font file fragment is within the file bounds. Otherwise, an error should be returned from ReadFileFragment.   {{DirectWrite}} may invoke <see cref="SharpDX.DirectWrite.FontFileStream"/> methods on the same object from multiple threads simultaneously. Therefore, ReadFileFragment implementations that rely on internal mutable state must serialize access to such state across multiple threads. For example, an implementation that uses separate Seek and Read operations to read a file fragment must place the code block containing Seek and Read calls under a lock or a critical section.
    /// </remarks>
    /// <unmanaged>HRESULT IDWriteFontFileStream::ReadFileFragment([Out, Buffer] const void** fragmentStart,[None] __int64 fileOffset,[None] __int64 fragmentSize,[Out] void** fragmentContext)</unmanaged>
    void FontFileStream.ReadFileFragment(out IntPtr fragmentStart, long fileOffset, long fragmentSize, out IntPtr fragmentContext)
    {
      lock (this)
      {
        fragmentContext = IntPtr.Zero;
        _stream.Position = fileOffset;
        fragmentStart = _stream.PositionPointer;
      }
    }

    /// <summary>
    /// Releases a fragment from a file.
    /// </summary>
    /// <param name="fragmentContext">A reference to the client-defined context of a font fragment returned from {{ReadFileFragment}}.</param>
    /// <unmanaged>void IDWriteFontFileStream::ReleaseFileFragment([None] void* fragmentContext)</unmanaged>
    void FontFileStream.ReleaseFileFragment(IntPtr fragmentContext)
    {
      // Nothing to release. No context are used
    }

    /// <summary>
    /// Obtains the total size of a file.
    /// </summary>
    /// <returns>the total size of the file.</returns>
    /// <remarks>
    /// Implementing GetFileSize() for asynchronously loaded font files may require downloading the complete file contents. Therefore, this method should be used only for operations that either require a complete font file to be loaded (for example, copying a font file) or that need to make decisions based on the value of the file size (for example, validation against a persisted file size).
    /// </remarks>
    /// <unmanaged>HRESULT IDWriteFontFileStream::GetFileSize([Out] __int64* fileSize)</unmanaged>
    long FontFileStream.GetFileSize()
    {
      return _stream.Length;
    }

    /// <summary>
    /// Obtains the last modified time of the file.
    /// </summary>
    /// <returns>
    /// the last modified time of the file in the format that represents the number of 100-nanosecond intervals since January 1, 1601 (UTC).
    /// </returns>
    /// <remarks>
    /// The "last modified time" is used by DirectWrite font selection algorithms to determine whether one font resource is more up to date than another one.
    /// </remarks>
    /// <unmanaged>HRESULT IDWriteFontFileStream::GetLastWriteTime([Out] __int64* lastWriteTime)</unmanaged>
    long FontFileStream.GetLastWriteTime()
    {
      return 0;
    }
  }

  private sealed class ResourceFontFileEnumerator : CallbackBase, FontFileEnumerator
  {
    private Factory _factory;
    private FontFileLoader _loader;
    private DataStream keyStream;
    private FontFile _currentFontFile;

    public ResourceFontFileEnumerator(Factory factory, FontFileLoader loader, DataPointer key)
    {
      _factory = factory;
      _loader = loader;
      keyStream = new DataStream(key.Pointer, key.Size, true, false);
    }

    /// <summary>
    /// Advances to the next font file in the collection. When it is first created, the enumerator is positioned before the first element of the collection and the first call to MoveNext advances to the first file.
    /// </summary>
    /// <returns>
    /// the value TRUE if the enumerator advances to a file; otherwise, FALSE if the enumerator advances past the last file in the collection.
    /// </returns>
    /// <unmanaged>HRESULT IDWriteFontFileEnumerator::MoveNext([Out] BOOL* hasCurrentFile)</unmanaged>
    bool FontFileEnumerator.MoveNext()
    {
      bool moveNext = keyStream.RemainingLength != 0;
      if (moveNext)
      {
        if (_currentFontFile != null)
          _currentFontFile.Dispose();

        _currentFontFile = new FontFile(_factory, keyStream.PositionPointer, 4, _loader);
        keyStream.Position += 4;
      }
      return moveNext;
    }

    /// <summary>
    /// Gets a reference to the current font file.
    /// </summary>
    /// <value></value>
    /// <returns>a reference to the newly created <see cref="SharpDX.DirectWrite.FontFile"/> object.</returns>
    /// <unmanaged>HRESULT IDWriteFontFileEnumerator::GetCurrentFontFile([Out] IDWriteFontFile** fontFile)</unmanaged>
    FontFile FontFileEnumerator.CurrentFontFile
    {
      get
      {
        ((IUnknown)_currentFontFile).AddReference();
        return _currentFontFile;
      }
    }
  }
}
fonts font-awesome sharpdx directwrite ninjatrader
1个回答
0
投票

我需要解决 3 件事:

  1. 使用上面问题中创建的 FontLoader 创建并存储一个
    FontCollection
    对象。
  2. 作为一项副任务,使用 FontCollection 获取正确的字体系列名称。
  3. 在 TextFormat 对象的构造函数中使用该 FontCollection。

这是完整的解决方案:

字体使用:

using FFT.NT8.Fonts;
using NinjaTrader.Gui;
using NinjaTrader.Gui.Chart;
using SharpDX.DirectWrite;

namespace NinjaTrader.NinjaScript.Indicators.FFT_Code;

public sealed class FontAwesomeIndicator : FFTIndicator
{
  private TextFormat _format = null!;

  protected override void FFTOnStateChange()
  {
    switch (State)
    {
      case State.SetDefaults:
        IsOverlay = true;
        Calculate = Calculate.OnBarClose;
        IsSuspendedWhileInactive = true;
        break;

      case State.DataLoaded:
        _format = FontAwesome.CreateTextFormat(FontAwesome.Families.Regular, FontWeight.Regular, FontStyle.Normal, FontStretch.Normal, 26);
        break;

      case State.Terminated:
        _format?.Dispose();
        break;
    }
  }

  protected override void FFTOnRender(ChartControl chartControl, ChartScale chartScale)
  {
    // Draw two items of text on the chart using FontAwesome. 
    {
      using var textLayout = new TextLayout(Globals.DirectWriteFactory, "hello", _format, 1000, 1000);
      using var brush = MediaBrushes.WhiteSmoke.ToDxBrush(RenderTarget);
      RenderTarget.DrawTextLayout(new Vector2(100, 100), textLayout, brush);
    }

    {
      using var textLayout = new TextLayout(Globals.DirectWriteFactory, "\uf004", _format, 1000, 1000); // database icon
      using var brush = MediaBrushes.WhiteSmoke.ToDxBrush(RenderTarget);
      RenderTarget.DrawTextLayout(new(200, 200), textLayout, brush);
    }
  }
}

FontAwesome.cs:

using System.Collections.ObjectModel;
using SharpDX;
using SharpDX.DirectWrite;

namespace FFT.NT8.Fonts;

internal static class FontAwesome
{
  public enum Families
  {
    Regular,
    Solid,
    Brands,
  }

  private static readonly FontCollection _fontCollection;
  private static readonly IReadOnlyDictionary<Families, string> _familyNames;

  public static TextFormat CreateTextFormat(Families family, FontWeight fontWeight, FontStyle fontStyle, FontStretch fontStretch, float fontSize)
    => new TextFormat(Globals.DirectWriteFactory, _familyNames[family], _fontCollection, fontWeight, fontStyle, fontStretch, fontSize);

  static FontAwesome()
  {
    var loader = new ResourceFontLoader();
    Globals.DirectWriteFactory.RegisterFontFileLoader(loader);
    Globals.DirectWriteFactory.RegisterFontCollectionLoader(loader);
    _fontCollection = new FontCollection(Globals.DirectWriteFactory, loader, loader.Key);
    _familyNames = MapFamilyNames();
  }

  private static IReadOnlyDictionary<Families, string> MapFamilyNames()
  {
    var familyNames = Enumerable.Range(0, _fontCollection.FontFamilyCount)
      .Select(i => _fontCollection.GetFontFamily(i).FamilyNames.GetString(0));

    var families = new Dictionary<Families, string>();
    foreach (var name in familyNames)
    {
      if (name.IndexOf("brand", StringComparison.InvariantCultureIgnoreCase) >= 0)
        families[Families.Brands] = name;
      else if (name.IndexOf("solid", StringComparison.InvariantCultureIgnoreCase) >= 0)
        families[Families.Solid] = name;
      else
        families[Families.Regular] = name;
    }

    return new ReadOnlyDictionary<Families, string>(families);
  }

  private sealed class ResourceFontLoader : CallbackBase, FontCollectionLoader, FontFileLoader
  {
    private readonly List<ResourceFontFileStream> _fontStreams = new List<ResourceFontFileStream>();
    private readonly List<ResourceFontFileEnumerator> _enumerators = new List<ResourceFontFileEnumerator>();
    private readonly DataStream _keyStream;

    public ResourceFontLoader()
    {
      foreach (var name in typeof(ResourceFontLoader).Assembly.GetManifestResourceNames())
      {
        if (name.EndsWith(".otf") && name.IndexOf("awesome", StringComparison.OrdinalIgnoreCase) >= 0)
        {
          var fontBytes = Utilities.ReadStream(typeof(ResourceFontLoader).Assembly.GetManifestResourceStream(name));
          var stream = new DataStream(fontBytes.Length, true, true);
          stream.Write(fontBytes, 0, fontBytes.Length);
          stream.Position = 0;
          _fontStreams.Add(new ResourceFontFileStream(stream));
        }
      }

      _keyStream = new DataStream(sizeof(int) * _fontStreams.Count, true, true);
      for (int i = 0; i < _fontStreams.Count; i++)
        _keyStream.Write((int)i);
      _keyStream.Position = 0;
    }

    public DataStream Key
    {
      get
      {
        return _keyStream;
      }
    }

    FontFileEnumerator FontCollectionLoader.CreateEnumeratorFromKey(Factory factory, DataPointer collectionKey)
    {
      var enumerator = new ResourceFontFileEnumerator(factory, this, collectionKey);
      _enumerators.Add(enumerator);

      return enumerator;
    }

    FontFileStream FontFileLoader.CreateStreamFromKey(DataPointer fontFileReferenceKey)
    {
      var index = Utilities.Read<int>(fontFileReferenceKey.Pointer);
      return _fontStreams[index];
    }
  }
  private sealed class ResourceFontFileStream : CallbackBase, FontFileStream
  {
    private readonly DataStream _stream;

    public ResourceFontFileStream(DataStream stream)
    {
      this._stream = stream;
    }

    /// <summary>
    /// Reads a fragment from a font file.
    /// </summary>
    /// <param name="fragmentStart">When this method returns, contains an address of a  reference to the start of the font file fragment.  This parameter is passed uninitialized.</param>
    /// <param name="fileOffset">The offset of the fragment, in bytes, from the beginning of the font file.</param>
    /// <param name="fragmentSize">The size of the file fragment, in bytes.</param>
    /// <param name="fragmentContext">When this method returns, contains the address of</param>
    /// <remarks>
    /// Note that ReadFileFragment implementations must check whether the requested font file fragment is within the file bounds. Otherwise, an error should be returned from ReadFileFragment.   {{DirectWrite}} may invoke <see cref="SharpDX.DirectWrite.FontFileStream"/> methods on the same object from multiple threads simultaneously. Therefore, ReadFileFragment implementations that rely on internal mutable state must serialize access to such state across multiple threads. For example, an implementation that uses separate Seek and Read operations to read a file fragment must place the code block containing Seek and Read calls under a lock or a critical section.
    /// </remarks>
    /// <unmanaged>HRESULT IDWriteFontFileStream::ReadFileFragment([Out, Buffer] const void** fragmentStart,[None] __int64 fileOffset,[None] __int64 fragmentSize,[Out] void** fragmentContext)</unmanaged>
    void FontFileStream.ReadFileFragment(out IntPtr fragmentStart, long fileOffset, long fragmentSize, out IntPtr fragmentContext)
    {
      lock (this)
      {
        fragmentContext = IntPtr.Zero;
        _stream.Position = fileOffset;
        fragmentStart = _stream.PositionPointer;
      }
    }

    /// <summary>
    /// Releases a fragment from a file.
    /// </summary>
    /// <param name="fragmentContext">A reference to the client-defined context of a font fragment returned from {{ReadFileFragment}}.</param>
    /// <unmanaged>void IDWriteFontFileStream::ReleaseFileFragment([None] void* fragmentContext)</unmanaged>
    void FontFileStream.ReleaseFileFragment(IntPtr fragmentContext)
    {
      // Nothing to release. No context are used
    }

    /// <summary>
    /// Obtains the total size of a file.
    /// </summary>
    /// <returns>the total size of the file.</returns>
    /// <remarks>
    /// Implementing GetFileSize() for asynchronously loaded font files may require downloading the complete file contents. Therefore, this method should be used only for operations that either require a complete font file to be loaded (for example, copying a font file) or that need to make decisions based on the value of the file size (for example, validation against a persisted file size).
    /// </remarks>
    /// <unmanaged>HRESULT IDWriteFontFileStream::GetFileSize([Out] __int64* fileSize)</unmanaged>
    long FontFileStream.GetFileSize()
    {
      return _stream.Length;
    }

    /// <summary>
    /// Obtains the last modified time of the file.
    /// </summary>
    /// <returns>
    /// the last modified time of the file in the format that represents the number of 100-nanosecond intervals since January 1, 1601 (UTC).
    /// </returns>
    /// <remarks>
    /// The "last modified time" is used by DirectWrite font selection algorithms to determine whether one font resource is more up to date than another one.
    /// </remarks>
    /// <unmanaged>HRESULT IDWriteFontFileStream::GetLastWriteTime([Out] __int64* lastWriteTime)</unmanaged>
    long FontFileStream.GetLastWriteTime()
    {
      return 0;
    }
  }

  private sealed class ResourceFontFileEnumerator : CallbackBase, FontFileEnumerator
  {
    private Factory _factory;
    private FontFileLoader _loader;
    private DataStream keyStream;
    private FontFile _currentFontFile;

    public ResourceFontFileEnumerator(Factory factory, FontFileLoader loader, DataPointer key)
    {
      _factory = factory;
      _loader = loader;
      keyStream = new DataStream(key.Pointer, key.Size, true, false);
    }

    /// <summary>
    /// Advances to the next font file in the collection. When it is first created, the enumerator is positioned before the first element of the collection and the first call to MoveNext advances to the first file.
    /// </summary>
    /// <returns>
    /// the value TRUE if the enumerator advances to a file; otherwise, FALSE if the enumerator advances past the last file in the collection.
    /// </returns>
    /// <unmanaged>HRESULT IDWriteFontFileEnumerator::MoveNext([Out] BOOL* hasCurrentFile)</unmanaged>
    bool FontFileEnumerator.MoveNext()
    {
      bool moveNext = keyStream.RemainingLength != 0;
      if (moveNext)
      {
        if (_currentFontFile != null)
          _currentFontFile.Dispose();

        _currentFontFile = new FontFile(_factory, keyStream.PositionPointer, 4, _loader);
        keyStream.Position += 4;
      }
      return moveNext;
    }

    /// <summary>
    /// Gets a reference to the current font file.
    /// </summary>
    /// <value></value>
    /// <returns>a reference to the newly created <see cref="SharpDX.DirectWrite.FontFile"/> object.</returns>
    /// <unmanaged>HRESULT IDWriteFontFileEnumerator::GetCurrentFontFile([Out] IDWriteFontFile** fontFile)</unmanaged>
    FontFile FontFileEnumerator.CurrentFontFile
    {
      get
      {
        ((IUnknown)_currentFontFile).AddReference();
        return _currentFontFile;
      }
    }
  }
}
© www.soinside.com 2019 - 2024. All rights reserved.