本文章是由機器翻譯。

Windows 與 C++

透過 DirectWrite 和現代 C++ 探索字型

Kenny Kerr

DirectWrite 是一個非常強大的文本佈局的 API。它的權力幾乎所有領先的 Windows 應用程式和技術,從 Windows 運行庫 (WinRT) 執行 XAML 和辦公室 2013 年,互聯網資源管理器中 11 和更多。它不是本身的呈現引擎,但已與 Direct2D,其同級中 DirectX 家庭的密切關係。當然,Direct2D 是總理的即時模式,硬體加速圖形 API。

你可以使用 DirectWrite 與 Direct2D 提供硬體-­加速呈現文本。為了避免任何混淆,我還沒寫太多關於 DirectWrite 在過去。我不想讓你覺得 Direct2D 是只 DirectWrite 渲染引擎。Direct2D 是那麼遠不止于此。仍然,DirectWrite 有很多提供,並在本月的專欄中我讓你們看看什麼是可能的與 DirectWrite 的和看看如何現代 c + + 可以説明簡化的程式設計模型。

API DirectWrite

我將使用 DirectWrite 來探索系統字體集合。首先,我需要拿到的 DirectWrite 工廠物件。這是想要使用令人印象深刻的 DirectWrite 版式的任何應用程式的起始點。DirectWrite,像這麼多的 Windows API,依賴于本質的 com。我需要調用 DWriteCreateFactory 函數來創建的 DirectWrite 工廠物件。此函數返回一個指向的工廠物件的 COM 介面:

ComPtr<IDWriteFactory2> factory;

IDWriteFactory2 介面是與 Windows 8.1 和 DirectX 11.2 介紹了今年早些時候的 DirectWrite 工廠介面的最新版本。 IDWriteFactory2 將繼承 IDWrite­Factory1,後者反過來又從 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 這兩個選項。 共用和隔離的工廠可以利用已緩存的字體資料。 僅共用工廠貢獻字體資料返回到緩存中。 第二個參數指示具體哪個版本的 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-我們."假設存在的我可以使用 IDWriteLocalizedStrings GetString 方法獲取一份副本:

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

如果你擔心長度,您可以先調用 GetString­長度的方法。 只是請確保您有足夠大的緩衝區。 圖 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 月專欄裡 (msdn.microsoft.com/magazine/dn342867),我向您展示如何在標準 c + + 編寫基本的 WinRT 應用程式模型水暖。 我 2013 年 10 月列中 (msdn.microsoft.com/magazine/dn451437),我向您展示如何呈現內與 DirectX 和專門 Direct2D 這個基於 CoreWindow 的應用程式。 現在我要向您展示如何擴展該代碼以使用 Direct2D 呈現 DirectWrite 的説明文本。

由於這些列被寫了,發佈了 Windows 8.1,和它改變了一些東西,DPI 縮放處理是在現代和桌面應用程式的方式有關。 我要去蓋 DPI 詳細將來,所以我就會離開這些暫時的變化。 我只去重點上充實我始于 8 月和 10 月,支援文本呈現和簡單字體流覽擴展的 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<wstring> m_fonts;

現在可以簡單地是 for 迴圈從使用相同基於範圍的圖 2 CreateDeviceIndependentResources 將名稱添加到組內:

m_fonts.insert(name);

設置填充了,我會從開始 app 一個反覆運算器指向集合的開始。 我會作為一個成員變數來存儲反覆運算器:

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 方法只需設置反覆運算器從檢索字型家族名稱,並將它與當前的字體大小,以創建一個新的文本格式物件相結合:

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 執行。 我然後只需要提供其調用執行:

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 繪製方法繪製一些與當前文本格式的文本。 這應做到:

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 讓它發生。幸運的是,它是簡單,使用 D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_­字體常數時調用 Direct2D DrawText 方法。我可以更新我 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 + + 的説明下生活。

Kenny Kerr  是設在加拿大的一位作者為 Pluralsight 和微軟最有價值球員一個電腦程式員。在他博客 kennykerr.ca 你可以跟著他在 Twitter 上 twitter.com/kennykerr

衷心感谢以下技术专家对本文的审阅: Worachai Chaoweeraprasit (Microsoft)
Worachai Chaoweeraprasit 是 Direct2D 和 DirectWrite 的發展線索。他是迷戀速度和品質的 2D 向量圖形以及螢幕上文本的可讀性。在他的業餘時間他喜歡被壟斷了,在家裡的兩個小小的人。