其他语言

位置:IT落伍者 >> 其他语言 >> 浏览文章

使用 DirectWrite 和最新 C++ 管理字体


发布日期:2023年12月23日
 
使用 DirectWrite 和最新 C++ 管理字体

DirectWrite 是一种相当强大的文本布局 API 它支持从 XAML 和 Office 的 Windows 运行时 (WinRT) 实现到 Internet Explorer 和更高版本的几乎所有领先 Windows 应用程序和技术 它本身并不是呈现引擎但与 DirectD 有很近的关系是 DirectD 在 DirectX 系列中的同级产品 当然DirectD 是首要的硬件加速即时模式图形 API

您可以将 DirectWrite 与 DirectD 结合使用以提供硬件加速的文本呈现 说明一下之前我在 DirectWrite 方面的着述并不多 我不希望您认为 DirectD 只是 DirectWrite 呈现引擎 DirectD 远不只如此 DirectWrite 还有其他很多功能在本月的专栏中我将演示一些使用 DirectWrite 可以完成的任务看看最新 C++ 是如何帮助简化编程模型的
DirectWrite API

我将使用 DirectWrite 探究系统字体集 首先我需要获取 DirectWrite 工厂对象 这是编写任何要使用 DirectWrite 的出色排版功能的应用程序的第一步 与大多数 Windows API 相同DirectWrite 也依赖于 COM 基础内容 我需要调用 DWriteCreateFactory 函数来创建 DirectWrite 工厂对象 此函数返回一个指向该工厂对象的 COM 接口

ComPtr<IDWriteFactory> factory;
      

IDWriteFactory 接口是今年早些时候随 Windows 和 DirectX 推出的最新版本 DirectWrite 工厂接口 IDWriteFactory 继承自 IDWrite­Factory而 IDWrite­Factory 继承自 IDWriteFactory 后者是原始的 DirectWrite 工厂接口它公开了大部分工厂功能

我将基于前面的 ComPtr 类模板调用 DWriteCreateFactory 函数

HR(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED
   __uuidof(factory)
   reinterpret_cast<IUnknown **>(factoryGetAddressOf())));
      

DirectWrite 包含一项名为 Windows 字体缓存服务 (FontCache) 的 Windows 服务 第一个参数指示获得的工厂是否参与此跨过程缓存的字体使用 有 DWRITE_FACTORY_TYPE_SHARED 和 DWRITE_FACTORY_TYPE_ISOLATED 两个选项 SHARED 和 ISOLATED 工厂都可以利用已缓存的字体数据 只有 SHARED 工厂向缓存返回字体数据 第二个参数具体指示我希望在第三个和最后一个参数中返回哪个版本的 DirectWrite 工厂接口

在 DirectWrite 工厂对象给定的情况下我可以直接要求其提供系统字体集

ComPtr<IDWriteFontCollection> fonts;
  HR(factory>GetSystemFontCollection(fontsGetAddressOf()));
      

GetSystemFontCollection 方法的第二个参数是可选的指示其是否检查已安装字体集的更新或更改 幸运的是这个参数默认为 false因此除非要确保结果集反映最近的更改否则不必考虑它 在字体集给定的情况下我可以获取集合中的字体系列数如下所示

unsigned const count = fonts>GetFontFamilyCount();
      

然后我使用 GetFontFamily 方法通过从零开始的索引检索单个字体系列对象 一个字体系列对象表示这样一组字体它们共享一个名称当然也是一种设计但粗细样式和拉伸并不相同

ComPtr<IDWriteFontFamily> family;
  HR(fonts>GetFontFamily(index familyGetAddressOf()));
      

IDWriteFontFamily 接口继承自 IDWriteFontList 接口因此我可以枚举该字体系列中的各种字体 能够检索字体系列名称合乎情理并且非常有用 不过系列名称已经过本地化所以它并不像您期待的那样简单直接 首先我需要字体系列提供一个本地化字符串对象该对象针对每种支持的区域设置均包含一个系列名称

ComPtr<IDWriteLocalizedStrings> names;
  HR(family>GetFamilyNames(namesGetAddressOf()));
      

我也可以枚举系列名称但一般只查找用户默认区域设置对应的名称 事实上IDWriteLocalizedStrings 接口提供了 FindLocaleName 方法来检索本地化系列名称的索引 我从调用 GetUserDefaultLocaleName 函数以获取用户默认区域设置开始

wchar_t locale[LOCALE_NAME_MAX_LENGTH];
  VERIFY(GetUserDefaultLocaleName(locale countof(locale)));
      

然后我将默认区域设置传递给 IDWriteLocalizedStrings FindLocaleName 方法确定该字体系列是否有针对当前用户本地化的名称

unsigned index;
  BOOL exists;
  HR(names>FindLocaleName(locale &index &exists));
      

如果请求的区域设置在集中不存在我可能会回到一些默认设置enus假设存在就可以使用 IDWriteLocalizedStrings GetString 方法获取副本

if (exists)
  {
   wchar_t name[];
   HR(names>GetString(index name _countof(name)));
  }
      

如果担心长度可以先调用 GetString­Length 方法 只要确保缓沖区足够大就可以 提供了一个完整列表显示所有内容是如何共同枚举已安装字体的

使用 DirectWrite API 枚举字体

ComPtr<IDWriteFactory> factory;
  HR(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED
   __uuidof(factory)
   reinterpret_cast<IUnknown **>(factoryGetAddressOf())));
  ComPtr<IDWriteFontCollection> fonts;
  HR(factory>GetSystemFontCollection(fontsGetAddressOf()));
  wchar_t locale[LOCALE_NAME_MAX_LENGTH];
  VERIFY(GetUserDefaultLocaleName(locale _countof(locale)));
  unsigned const count = fonts>GetFontFamilyCount();
  for (unsigned familyIndex = ; familyIndex != count; ++familyIndex)
  {
   ComPtr<IDWriteFontFamily> family;
   HR(fonts>GetFontFamily(familyIndex familyGetAddressOf()));
   ComPtr<IDWriteLocalizedStrings> names;
   HR(family>GetFamilyNames(namesGetAddressOf()));
   unsigned nameIndex;
   BOOL exists;
   HR(names>FindLocaleName(locale &nameIndex &exists));
   if (exists)
   {
    wchar_t name[];
    HR(names>GetString(nameIndex name countof(name)));
    wprintf(L"%sn" name);
   }
  }
      

最新 C++ 浅谈

如果您经常阅读本杂志就会知道我已经介绍过 DirectX特别是 DirectD 的最新 C++ 处理 dxh 头文件 (d) 也包含 DirectWrite 可以用它大大简化我在上面提供的代码 不必调用 DWriteCreateFactory我可以从 DirectWrite 命名空间直接调用 CreateFactory 函数

auto factory = CreateFactory();
      

获取系统字体集同样简单

auto fonts = factoryGetSystemFontCollection();
      

枚举此字体集是 dxh 的真正亮点 我不必编写传统的 for 循环 也不必调用 GetFontFamilyCount 和 GetFontFamily 方法 我只需要编写一个最新的基于范围的 for 循环

for (auto family : fonts)
  {
   
       }
      

这些代码与以往相同 编译器(在 dxh 的帮助下)进行生成我使用了一个更为自然的编程模型编写正确有效的代码更为简单 前面的 GetSystemFontCollection 方法返回一个 FontCollection 类其中包含一个迭代器它将延迟提取字体系列对象 这使得编译器能够有效地实现基于范围的循环 提供了完整列表 与图 中的代码比较它更为清晰效率更高

使用 dxh 枚举字体

auto factory = CreateFactory();
  auto fonts = factoryGetSystemFontCollection();
  wchar_t locale[LOCALE_NAME_MAX_LENGTH];
  VERIFY(GetUserDefaultLocaleName(locale _countof(locale)));
  for (auto family : fonts)
  {
   auto names = familyGetFamilyNames();
   unsigned index;
   if (namesFindLocaleName(locale index))
   {
    wchar_t name[];
    namesGetString(index name);
    wprintf(L"%sn" name);
   }
  }
      

字体浏览器与 Windows 运行时

DirectWrite 的作用远不只是枚举字体 我将前面这些内容与 DirectD 结合起来创建一个简单的字体浏览器应用程序 在我 月的专栏 () 中介绍了如何用标准 C++ 编写基本的 WinRT 应用程序模型 在我 月的专栏 (msd/magazine/dn) 中介绍了如何通过 DirectX(具体而言是 DirectD)在此基于 CoreWindow 的应用程序中进行呈现 现在介绍如何在 DirectWrite 的帮助下扩展代码从而使用 DirectD 呈现文本

这些专栏发表后Windows 发布了它对 DPI 缩放在最新应用程序和桌面应用程序中的处理方式有一些改动 以后我会详细介绍 DPI这里暂不讨论这些更改 我们只重点扩展我在八月提出在十月扩展过的 SampleWindow 类以便支持文本呈现并简化字体浏览

首先将 DirectWrite Factory 类添加为成员变量

DirectWrite::Factory m_writeFactory;
      

在 SampleWindow CreateDeviceIndependentResources 方法内可以创建 DirectWrite 工厂

m_writeFactory = DirectWrite::CreateFactory();
      

在这里我还可以获取系统字体集和用户的默认区域设置为枚举字体系列做准备

auto fonts = m_writeFactoryGetSystemFontCollection();
  wchar_t locale[LOCALE_NAME_MAX_LENGTH];
  VERIFY(GetUserDefaultLocaleName(locale _countof(locale)));
      

我将使应用程序在用户按下向上和向下箭头键时循环切换字体 我不通过 COM 接口不断枚举字体集而只是将字体系列名称复制到一个标准 set 容器

set<wstring> m_fonts;
      

现在我可以在 CreateDeviceIndependentResources 中使用图 中基于范围的 for 循环将名称添加到 set

m_fontsinsert(name);
      

set 填充后使用一个指向 set 开头的迭代器开始应用程序 将迭代器存储为成员变量

set<wstring>::iterator m_font;
      

SampleWindow CreateDeviceIndependentResources 方法初始化迭代器并调用 CreateTextFormat 方法定义如下

m_font = begin(m_fonts);
  CreateTextFormat();
      

在 DirectD 绘制文本前需要创建一个文本格式对象 这需要字体系列名称和所需字号 我允许用户使用向左和向右箭头键更改字号因此添加一个成员变量来跟蹤字号

float m_size;
      

Visual C++ 编译器很快会允许我初始化像这样的类内非静态数据成员 现在需要在 SampleWindow 的构造函数中将它设置为合理的默认值 接下来需要定义 CreateTextFormat 方法 这只是 DirectWrite 工厂方法的同名包装但它更新了一个成员变量DirectD 可以使用该变量定义要绘制的文本的格式

TextFormat m_textFormat;
      

然后CreateTextFormat 方法从 set 迭代器检索字体系列名称将其与当前字号组合创建一个新的文本格式对象

void CreateTextFormat()
  {
   m_textFormat = m_writeFactoryCreateTextFormat(m_font>c_str()m_size);
  }
      

我已经把它包装起来因此除了最初在 CreateDeviceIndependentResources 末尾调用它外我还可以在每次用户按下一个箭头键更改字体系列或字号时调用它 这引出了在 WinRT 应用程序模型中如何处理按键的问题 在桌面应用程序中这与 WM_KEYDOWN 消息处理有关 好在 CoreWindow 提供了 KeyDown 事件它是这一消息的最新等效项 我从定义 IKeyEventHandler 接口开始SampleWindow 需要实现该接口

typedef ITypedEventHandler<CoreWindow * KeyEventArgs *> IKeyEventHandler;
      

然后可以将此接口添加到继承接口的 SampleWindow 列表中并相应地更新 QueryInterface 实现 接下来只需提供它的 Invoke 实现

auto __stdcall Invoke(
   ICoreWindow *IKeyEventArgs * args) > HRESULT override
  {
   
       return S_OK;
  }
      

IKeyEventArgs 接口提供的信息与 LPARAM 和 WPARAM 向 WM_KEYDOWN 消息提供的信息基本相同 它的 get_VirtualKey 方法对应于后者的 WPARAM指示按下了哪个非系统键

VirtualKey key;
  HR(args>get_VirtualKey(&key));
      

与此类似它的 get_KeyStatus 方法对应于 WM_KEYDOWN 的 LPARAM 这样可以提供有关按键事件状态的丰富信息

CorePhysicalKeyStatus status;
  HR(args>get_KeyStatus(&status));
      

为方便用户我支持在用户按住箭头键时加速以便更快地调整所呈现字体的字号 为此需要另一个成员变量

unsigned m_accelerate;
      

然后可以使用该事件的键状态来确定是将字号更改一个增量还是增加一定的大小

if (!statusWasKeyDown)
  {
   m_accelerate = ;
  }
  else
  {
   m_accelerate += ;
   m_accelerate = std::min(U m_accelerate);
  }
      

我设置了上限因此加速不会太过 现在可以分别处理不同的按键 首先是向左箭头键用于缩小字号

if (VirtualKey_Left == key)
  {
   m_size = std::max(f m_size m_accelerate);
  }
      

我很小心不会使字号无效 然后是向右箭头键用于增加字号

else if (VirtualKey_Right == key)
  {
   m_size += m_accelerate;
  }
      

接下来移到上一字体系列处理向上箭头键

if (begin(m_fonts) == m_font)
  {
   m_font = end(m_fonts);
  }
  m_font;
      

然后我仔细循环到最后一个字体看看迭代器是否会回到序列开头 接下来移到下一字体系列处理向下箭头键

else if (VirtualKey_Down == key)
  {
   ++m_font;
   if (end(m_fonts) == m_font)
   {
     m_font = begin(m_fonts);
   }
  }
      

在这里再一次仔细循环到开头看看迭代器是否会回到序列结尾 最后我可以在事件处理程序末尾调用 CreateTextFormat 方法来重新创建文本格式对象

剩下的事情是更新 SampleWindow Draw 方法用当前文本格式绘制一些文本 方法如下

wchar_t const text [] = L"The quick brown fox jumps over the lazy dog";
  m_targetDrawText(text _countof(text)
   m_textFormat
   RectF(f f sizeWidth f sizeHeight f)
   m_brush);
      

DirectD 呈现目标的 DrawText 方法直接支持 DirectWrite现在 DirectWrite 就可以处理文本布局了而且呈现速度非常快这就是该脚本所执行的所有操作 是执行效果我可以按向上和向下箭头键遍历字体系列按向左和向右箭头键调整字号DirectD 根据当前选择自动重新呈现


字体浏览器

彩色字体

Windows 引入了一个称为彩色字体的新功能去掉了一些实现彩色字体的次优解决方案当然这都是 DirectWrite 和 DirectD 促成的令人高兴的是调用 DirectD DrawText 方法就像使用 DD_DRAW_TEXT_OPTIONS_ENABLE_COLOR_­FONT 常量一样简单我可以将 SampleWindow 的 Draw 方法更新为使用相应范围的枚举值

m_targetDrawText(text _countof(text)
   m_textFormat
   RectF(f f sizeWidth f sizeHeight f)
   m_brush);
  DrawTextOptions::EnableColorFont);
      

也是字体浏览器这次是一些 Unicode 表情


彩色字体

彩色字体的亮点在于可以自动缩放而不影响质量我可以在字体浏览器应用程序中按向右箭头键更近地查看细节结果如 所示


放大的彩色字体

通过提供彩色字体硬件加速文本呈现以及流畅有效的代码DirectWrite 在 DirectD 和最新 C++ 的帮助下焕发出生命力

               

上一篇:DELPHI面试题 研发笔试试卷

下一篇:为Delphi程序添加自动滚动的工具栏