DirectWrite 是一種相當強大的文本布局 API。 它支持從 XAML 和 Office 2013 的 Windows 運行 時 (WinRT) 實現到 Internet Explorer 11 和更高版本的幾乎所有領先 Windows 應用程序和技術。 它 本身並不是呈現引擎,但與 Direct2D 有很近的關系,是 Direct2D 在 DirectX 系列中的同級產品。 當然,Direct2D 是首要的硬件加速即時模式圖形 API。
您可以將 DirectWrite 與 Direct2D 結合使用,以提供硬件加速的文本呈現。 說明一下,之前我 在 DirectWrite 方面的著述並不多。 我不希望您認為 Direct2D 只是 DirectWrite 呈現引擎。 Direct2D 遠不只如此。 DirectWrite 還有其他很多功能,在本月的專欄中,我將演示一些使用 DirectWrite 可 以完成的任務,看看最新 C++ 是如何幫助簡化編程模型的。
DirectWrite API
我將使用 DirectWrite 探究系統字體集。 首先,我需要獲取 DirectWrite 工廠對象。 這是編寫 任 何要使用 DirectWrite 的出色排版功能的應用程序的第一步。 與大多數 Windows API 相同, DirectWrite 也依賴於 COM 基礎內容。 我需要調用 DWriteCreateFactory 函數來創建 DirectWrite 工廠對象。 此函數返回一個指向該工廠對象的 COM 接口:
ComPtr<IDWriteFactory2> factory;
IDWriteFactory2 接口是今年早些時候隨 Windows 8.1 和 DirectX 11.2 推出的最新版本 DirectWrite 工廠接口。 IDWriteFactory2 繼承自 IDWriteFactory1,而 IDWriteFactory1 繼承自 IDWriteFactory。 後者是原始的 DirectWrite 工廠接口,它公開了大部分工廠功能。
我將基於前面的 ComPtr 類模板調用 DWriteCreateFactory 函數:
HR(DWriteCreateFactory (DWRITE_FACTORY_TYPE_SHARED,
__uuidof(factory),
reinterpret_cast<IUnknown **>(factory.GetAddressOf())));
DirectWrite 包含一項名為 Windows 字體緩存服務 (FontCache) 的 Windows 服務。 第一個參數 指 示獲得的工廠是否參與此跨過程緩存的字體使用。 有 DWRITE_FACTORY_TYPE_SHARED 和 DWRITE_FACTORY_TYPE_ISOLATED 兩個選項。 SHARED 和 ISOLATED 工廠都可以利用已緩存的字體數據 。 只有 SHARED 工廠向緩存返回字體數據。 第二個參數具體指示我希望在第三個和最後一個參數中返回 哪 個版本的 DirectWrite 工廠接口。
在 DirectWrite 工廠對象給定的情況下,我可以直接要求其提供系統字體集:
ComPtr<IDWriteFontCollection> fonts;
HR(factory->GetSystemFontCollection(fonts.GetAddressOf()));
GetSystemFontCollection 方法的第二個參數是可選的,指示其是否檢查已安裝字體集的更新或更 改 。 幸運的是,這個參數默認為 false,因此,除非要確保結果集反映最近的更改,否則不必考慮它。 在字體集給定的情況下,我可以獲取集合中的字體系列數,如下所示:
unsigned const count = fonts- >GetFontFamilyCount();
然後我使用 GetFontFamily 方法通過從零開始的索引檢索單個字體系列對象。 一個字體系列對象 表 示這樣一組字體:它們共享一個名稱,當然也是一種設計,但粗細、樣式和拉伸並不相同:
ComPtr<IDWriteFontFamily> family;
HR(fonts->GetFontFamily(index, family.GetAddressOf()));
IDWriteFontFamily 接口繼承自 IDWriteFontList 接口,因此,我可以枚舉該字體系列中的各種字 體。 能夠檢索字體系列名稱合乎情理並且非常有用。 不過,系列名稱已經過本地化,所以它並不像您 期待的那樣簡單直接。 首先,我需要字體系列提供一個本地化字符串對象,該對象針對每種支持的區 域 設置均包含一個系列名稱:
ComPtr<IDWriteLocalizedStrings> names;
HR(family->GetFamilyNames(names.GetAddressOf()));
我也可以枚舉系列名稱,但一般只查找用戶默認區域設置對應的名稱。 事實上, 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));
如果請求的區域設置在集中不存在,我可能會回到一些默認設置,如“en-us”。假設存 在,就可以使用 IDWriteLocalizedStrings GetString 方法獲取副本:
if (exists)
{
wchar_t name[64];
HR(names->GetString(index, name, _countof(name)));
}
如果擔心長度,可以先調用 GetStringLength 方法。 只要確保緩沖區足夠大就可以。 圖 1 提供了一個完整列表,顯示所有內容是如何共同枚舉已安裝字體的。
圖 1 使用 DirectWrite API 枚舉字體
ComPtr<IDWriteFactory2> factory;
HR(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED,
__uuidof(factory),
reinterpret_cast<IUnknown **>(factory.GetAddressOf())));
ComPtr<IDWriteFontCollection> fonts;
HR(factory->GetSystemFontCollection(fonts.GetAddressOf()));
wchar_t locale[LOCALE_NAME_MAX_LENGTH];
VERIFY(GetUserDefaultLocaleName(locale, _countof(locale)));
unsigned const count = fonts->GetFontFamilyCount();
for (unsigned familyIndex = 0; familyIndex != count; ++familyIndex)
{
ComPtr<IDWriteFontFamily> family;
HR(fonts->GetFontFamily(familyIndex, family.GetAddressOf()));
ComPtr<IDWriteLocalizedStrings> names;
HR(family->GetFamilyNames(names.GetAddressOf()));
unsigned nameIndex;
BOOL exists;
HR(names->FindLocaleName(locale, &nameIndex, &exists));
if (exists)
{
wchar_t name[64];
HR(names->GetString(nameIndex, name, countof(name)));
wprintf(L"%s\n", name);
}
}
最新 C++ 淺談
如果您經常閱讀本雜志,就會知道我已經介紹過 DirectX,特別是 Direct2D 的最新 C++ 處理。 dx.h 頭文件 (dx.codeplex.com) 也包含 DirectWrite。 可以用它大大簡化我在上面提供的代碼。 不 必調用 DWriteCreateFactory,我可以從 DirectWrite 命名空間直接調用 CreateFactory 函數:
auto factory = CreateFactory();
獲取系統字體集同樣簡單:
auto fonts = factory.GetSystemFontCollection();
枚舉此字體集是 dx.h 的真正亮點。 我不必編寫傳統的 for 循環, 也不必調用 GetFontFamilyCount 和 GetFontFamily 方法, 我只需要編寫一個最新的基於范圍的 for 循環:
for (auto family : fonts)
{
...
}
這些代碼與以往相同。 編譯器(在 dx.h 的幫助下)進行生成,我使用了一個更為自然的編程模型 ,編寫正確有效的代碼更為簡單。 前面的 GetSystemFontCollection 方法返回一個 FontCollection 類,其中包含一個迭代器,它將延遲提取字體系列對象。 這使得編譯器能夠有效地實現基於范圍的循 環 。 圖 2 提供了完整列表。 與圖 1 中的代碼比較,它更為清晰,效率更高。
圖 2 使用 dx.h 枚舉字體
auto factory = CreateFactory();
auto fonts = factory.GetSystemFontCollection();
wchar_t locale[LOCALE_NAME_MAX_LENGTH];
VERIFY(GetUserDefaultLocaleName(locale, _countof(locale)));
for (auto family : fonts)
{
auto names = family.GetFamilyNames();
unsigned index;
if (names.FindLocaleName(locale, index))
{
wchar_t name[64];
names.GetString(index, name);
wprintf(L"%s\n", name);
}
}
字體浏覽器與 Windows 運行時
DirectWrite 的作用遠不只是枚舉字體。 我將前面這些內容與 Direct2D 結合起來,創建一個簡單 的字體浏覽器應用程序。 在我 2013 年 8 月的專欄 (http://msdn.microsoft.com/zh- cn/magazine/dn342867.aspx) 中,介紹了如何用標准 C++ 編寫基本的 WinRT 應用程序模型。 在我 2013 年 10 月的專欄 (msdn.microsoft.com/magazine/dn451437) 中,介紹了如何通過 DirectX(具 體 而言是 Direct2D)在此基於 CoreWindow 的應用程序中進行呈現。 現在介紹如何在 DirectWrite 的 幫 助下擴展代碼,從而使用 Direct2D 呈現文本。
這些專欄發表後,Windows 8.1 發布了,它對 DPI 縮放在最新應用程序和桌面應用程序中的處理方 式有一些改動。 以後我會詳細介紹 DPI,這裡暫不討論這些更改。 我們只重點擴展我在八月提出,在 十月擴展過的 SampleWindow 類,以便支持文本呈現並簡化字體浏覽。
首先,將 DirectWrite Factory2 類添加為成員變量:
DirectWrite::Factory2 m_writeFactory;
在 SampleWindow CreateDeviceIndependentResources 方法內,可以創建 DirectWrite 工廠:
m_writeFactory = DirectWrite::CreateFactory();
在這裡我還可以獲取系統字體集和用戶的默認區域設置,為枚舉字體系列做准備:
auto fonts = m_writeFactory.GetSystemFontCollection();
wchar_t locale[LOCALE_NAME_MAX_LENGTH];
VERIFY(GetUserDefaultLocaleName(locale, _countof(locale)));
我將使應用程序在用戶按下向上和向下箭頭鍵時循環切換字體。 我不通過 COM 接口不斷枚舉字體 集 ,而只是將字體系列名稱復制到一個標准 set 容器:
set<wstring> m_fonts;
現在,我可以在 CreateDeviceIndependentResources 中使用圖 2 中基於范圍的 for 循環將名稱 添 加到 set:
m_fonts.insert(name);
set 填充後,使用一個指向 set 開頭的迭代器開始應用程序。 將迭代器存儲為成員變量:
set<wstring>::iterator m_font;
SampleWindow CreateDeviceIndependentResources 方法初始化迭代器並調用 CreateTextFormat 方 法,定義如下:
m_font = begin(m_fonts);
CreateTextFormat();
在 Direct2D 繪制文本前,需要創建一個文本格式對象。 這需要字體系列名稱和所需字號。 我允 許 用戶使用向左和向右箭頭鍵更改字號,因此添加一個成員變量來跟蹤字號:
float m_size;
Visual C++ 編譯器很快會允許我初始化像這樣的類內非靜態數據成員。 現在,需要在 SampleWindow 的構造函數中將它設置為合理的默認值。 接下來,需要定義 CreateTextFormat 方法。 這只是 DirectWrite 工廠方法的同名包裝,但它更新了一個成員變量,Direct2D 可以使用該變量定義 要繪制的文本的格式:
TextFormat m_textFormat;
然後,CreateTextFormat 方法從 set 迭代器檢索字體系列名稱,將其與當前字號組合,創建一個 新 的文本格式對象:
void CreateTextFormat()
{
m_textFormat = m_writeFactory.CreateTextFormat(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 (!status.WasKeyDown)
{
m_accelerate = 1;
}
else
{
m_accelerate += 2;
m_accelerate = std::min(20U, m_accelerate);
}
我設置了上限,因此加速不會太過。 現在可以分別處理不同的按鍵。 首先是向左箭頭鍵,用於縮 小 字號:
if (VirtualKey_Left == key)
{
m_size = std::max(1.0f, 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_target.DrawText(text, _countof(text) - 1,
m_textFormat,
RectF(10.0f, 10.0f, size.Width - 10.0f, size.Height - 10.0f),
m_brush);
Direct2D 呈現目標的 DrawText 方法直接支持 DirectWrite。現在 DirectWrite 就可以處理文本 布 局了,而且呈現速度非常快。這就是該腳本所執行的所有操作。圖 3 是執行效果。 我可以按向上和向下箭頭鍵遍歷字體系列,按向左和向右箭頭鍵調整字號。Direct2D 根據當前選擇自 動 重新呈現。
圖 3 字體浏覽器
Windows 8.1 引入了一個稱為彩色字體的新功能,去掉了一些實現彩色字體的次優解決方案。當然 , 這都是 DirectWrite 和 Direct2D 促成的。令人高興的是,調用 Direct2D DrawText 方法就像使用 D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT 常量一樣簡單。我可以將 SampleWindow 的 Draw 方法更新為使用相應范圍的枚舉值:
m_target.DrawText(text, _countof(text) - 1,
m_textFormat,
RectF(10.0f, 10.0f, size.Width - 10.0f, size.Height - 10.0f),
m_brush);
DrawTextOptions::EnableColorFont);
圖 4 也是字體浏覽器,這次是一些 Unicode 表情。
圖 4 彩色字體
彩色字體的亮點在於可以自動縮放,而不影響質量。我可以在字體浏覽器應用程序中按向右箭頭鍵 , 更近地查看細節。結果如圖 5 所示。
圖 5 放大的彩色字 體
通過提供彩色字體、硬件加速文本呈現以及流暢有效的代碼,DirectWrite 在 Direct2D 和最新 C++ 的幫助下煥發出生命力。