本文章是由機器翻譯。

DirectX 要素

誰是害怕的標誌符號運行?

Charles Petzold

下载代码示例

我第一次遇到幾年前在Windows Presentation Foundation(WPF) 中的字元標誌符號的概念。排版,在"字形"是指一個字元,而不是其功能的特定的書面語言,由字元的 Unicode 值表示的圖形表示形式。我知道 WPF 字形元素和 GlyphRun 類表示另一種方法來顯示文本,但他們似乎添加一個圖層的不必要的複雜性。最大的奇異涉及引用所需的字體,不由一個字型家族名稱,但由實際字體檔的檔路徑。我已經被編碼為 Windows 自 Beta 版本的 1.0,和以前從未不得不由檔案名引用的字體。它根本就不這樣。

我迅速得出結論字形和 GlyphRun 是多太深奧為主流的程式師,包括我自己。僅幾年後當我開始工作與 Microsoft XML 文件規格 (XPS) 沒有明顯的標誌符號背後的理念。

基本的區別

通常當程式請求從 Windows 的一種字體,它會使用如世紀教科書和屬性如斜體或加粗的字型家族名稱。在運行時,Windows 發現與字體家族名稱相匹配的使用者的系統上的字體檔。斜體和粗體或者可以是固有的這種字體,或由系統合成。文本的佈局,Windows 或應用程式訪問字體度量資訊,描述字體的字元的大小。

如果兩個不同的使用者有兩個不同的字體檔都包含帶有家族名稱世紀教科書,但有些不同字體度量標準的字體在系統上時,會發生什麼呢?這不是一個真正的問題。因為文本是格式和佈局在運行時,這些兩個字體的實際呈現可能略有不同,但它真的不重要。應用程式被設計為以這種方式靈活。

XPS 文檔是不同的然而。多像 PDF、 XPS 描述一個固定頁的文檔。每個頁面上的所有文本是已經規定,準確地定位。如果這種檔在呈現時的字體,用來設計頁面的字體稍有不同,不會看起來很正確。這就是為什麼 XPS 文檔通常包含嵌入的字體檔,以及為什麼 XPS 頁使用的標誌符號元素來引用這些字體檔並呈現文本。完全消除任何含糊之處。

通常我們提到的文本字元的字元代碼。我們通常不需要知道的事的發生,由於採用了與特定字體的字元代碼顯示的精確標誌符號。但有時它是重要的是要有更好地控制自己的標誌符號。

您可能認為有一對一的對應關係之間的字元代碼和特定字體中的字形和通常是這樣。但也有非常重要的例外情況:某些字體檔包含連筆字,是對應于字元對,如 fi 或 fl 的單個符號。一些非拉丁字元集需要多個字形要呈現的單個字元。此外,某些字體檔包含某些字元替代字形 — — 例如,一個以通過它,小型大寫的字母為小寫或字母與文體花飾字使用斜杠零。這些文體的替代品是一個特定的字體檔,而不是一種字體的特點。這就是為什麼獲取正確的字體檔是關鍵。

如果您編寫 Windows 8 的一個 Windows 應用商店的應用程式,您可以使用 Windows 運行時 API 通過這些文體的替代品。"程式設計視窗,第 6 版"的第 16 章 (O'Reilly 媒體,2012年) 有一個專案叫 TypographyDemo,演示如何做到這一點。但在 Windows 8 字形的基礎支援在 DirectX,實施的字形本身力量透露最在該環境中。

DirectX 標誌符號支援

探索如何在 DirectX 中顯示字形涉及跟蹤路徑通過幾個介面、 方法和結構。

讓我們開始與 ID2D1RenderTarget 介面,其中包含要顯示 2D 圖形和文本的基本方法。此介面定義了一個 DrawGlyphRun 方法。這種方法需要一個結構類型描述要顯示的標誌符號的 DWRITE_GLYPH_RUN 和物件的引用也鍵入 IDWriteFontFace。

獲取一個 IDWriteFontFace 物件的一種方法是通過 IDWriteFactory 的 CreateFontFace 方法。這種方法需要一個 IDWriteFontFile 物件,它通過 IDWriteFactory 的 CreateFontFileReference 方法獲得的。

CreateFontFileReference 方法引用字體檔使用的路徑和檔案名。然而,如果使用 DirectX 在 Windows 存儲應用程式中,您的應用程式可能不會有全面和自由進入到使用者的硬碟上,並通常不能訪問字體以這種方式。你用 CreateFontFileReference 引用任何字體檔將有可能被定義為應用程式資源,並將綁定到應用程式的包。

然而,您不能在您的應用程式的包中包括只是任何字體檔。如果字體是您的應用程式的一部分,您必須將分發該字體的許可證。幸運的是,微軟已許可從字母上伸部分公司的幾個字體檔。為使您可以將它們與應用程式一起分發的明確目的。這些字體檔已主要用於 XNA 應用程式,但是他們也有趣從字形的角度。

可下載代碼之間為此列是一個程式,稱為 GlyphDump,我創建了基於 DirectX App (XAML) 範本Visual Studio快遞 2013年預覽中。此範本創建的應用程式呈現上 SwapChainPanel 的 DirectX 圖形。

我在命名的字體的 GlyphDump 專案中創建新的篩選器 (和相應的資料夾中),從字母上伸部分公司領有牌照的 10 字體檔添加在 DirectXPage.xaml 中的清單方塊顯式列出的這些字體家族名稱與標記屬性引用的檔案名。當其中一個選擇時,該程式構造的路徑和名稱的字體檔就像這樣:

Package::Current->InstalledLocation->Path +
  "\\Fonts\\" + filename;

GlyphDumpRenderer 類然後使用該路徑創建一個 IDWriteFontFile 物件和一個 IDWriteFontFace 物件。

其他的工作發生在 Render 方法中,如中所示圖 1

圖 1 中 GlyphDump 的渲染方法

bool GlyphDumpRenderer::Render()
{
  if (!m_needsRedraw)
      return false;
  ID2D1DeviceContext* context = m_deviceResources->GetD2DDeviceContext();
  context->SaveDrawingState(m_stateBlock.Get());
  context->BeginDraw();
  context->Clear(ColorF(ColorF::White));
  context->SetTransform(m_deviceResources->GetOrientationTransform2D());
  if (m_fontFace != nullptr)
  {
    Windows::Foundation::Size outputBounds = m_deviceResources->GetOutputBounds();
    uint16 glyphCount = m_fontFace->GetGlyphCount();
    int rows = (glyphCount + 16) / 16;
    float boxHeight = outputBounds.Height / (rows + 1);
    float boxWidth = outputBounds.Width / 17;
    // Define entire structure except glyphIndices
    DWRITE_GLYPH_RUN glyphRun;
    glyphRun.fontFace = m_fontFace.Get();
    glyphRun.fontEmSize = 0.75f * min(boxHeight, boxWidth);
    glyphRun.glyphCount = 1;
    glyphRun.glyphAdvances = nullptr;
    glyphRun.glyphOffsets = nullptr;
    glyphRun.isSideways = false;
    glyphRun.bidiLevel = 0;
    for (uint16 index = 0; index < glyphCount; index++)
    {
      glyphRun.glyphIndices = &index;
      context->DrawGlyphRun(Point2F(((index) % 16 + 0.5f) * boxWidth,
                                    ((index) / 16 + 1) * boxHeight),
                                    &glyphRun,
                                    m_blackBrush.Get());
    }
  }
  // We ignore D2DERR_RECREATE_TARGET here.
This error indicates
  // that the device is lost.
It will be handled during
  // the next call to Present.
HRESULT hr = context->EndDraw();
  if (hr != D2DERR_RECREATE_TARGET)
  {
    DX::ThrowIfFailed(hr);
  }
  context->RestoreDrawingState(m_stateBlock.Get());
  m_needsRedraw = false;
  return true;
}

此渲染方法顯示所有的標誌符號中的所選的字體檔中每個 16 標誌符號的行所以它嘗試計算足夠小,顯示的 fontEmSize 值。(對於一些與很多字形的字體,這不會工作很好)。DWRITE_GLYPH_RUN 結構的 glyphIndices 欄位通常的標誌符號索引陣列。在這裡我僅要一次顯示一個標誌符號。

DrawGlyphRun 方法需要一個座標點、 DWRITE_GLYPH_RUN 結構和畫筆。座標指示標誌符號的基線的左邊的緣在哪裡進行定位。注意我說的"基線"。這是不同于大多數文本顯示方法,需要指定左上角的第一個字元。

圖 2 顯示也許最有趣的這批,有一個家族名稱的佩斯卡德羅和 Pesca.ttf 字體檔案名的字體。


圖 2 顯示佩斯卡德羅字體,GlyphDump 程式

在編寫此程式之前, 我從未見過像這樣的顯示。它開始看起來有點像一個傳統的 ASCII 表,但是然後有一堆的數位下標和上標,和整個集合的各種排序,再加上大寫字母與裝飾花飾字連筆字的字形。

很明顯,DWRITE_GLYPH_RUN 的 glyphIndices 欄位不是一個字元代碼相同。這些都是引用的字體檔中實際的字型的指數,這些標誌符號甚至不需要在任何一種理性的秩序。

標誌符號的文本

您可能已經看到一些實用程式中能夠顯示使用特定字體檔標誌符號索引,而不是字元代碼的文本。你可以指定你想要的到底是什麼標誌符號。

FancyTitle 專案演示此操作。這裡的想法是你有一個程式,主要是基於 XAML,但你想要一個花哨的標題從佩斯卡德羅字體使用的一些連字和花飾字。該專案包括的 Pesca.ttf 檔和 XAML 檔定義了 SwapChainPanel 有的 778 的寬度和高度 54,我選擇了經驗主義地基於中呈現的文本的大小的值。

因為這一專案的顯示要求很簡單,但我刪除的 FancyTitleMain 類和呈現類,離開要呈現到 SwapChainPanel 的 DirectXPage 類。(不過,我不得不修改 DeviceResources 略,使 IDeviceNotify 一個 ref 類介面 DirectXPage 可以實現設備) 時­通知時的輸出裝置是丟失了,重新創建通知.)

中顯示的文本輸出圖 3 具有 24 個字元,但只有 22 標誌符號。你就會認識到連字和花飾字從圖 2


圖 3 FancyTitle 輸出

我做了 DirectX 背景愛麗絲藍色,所以你可以看到 SwapChainPanel 是幾乎不大於文本。當然,它是可能預測輸出的大小,正是因為該字體檔是應用程式的一部分和標誌符號,將被直接存取。

你可以連字和替代字形,而無需使用 DrawGlyphRun。你也可以讓他們在 Windows 運行時,使用的 TextBlock 元素作為印刷術在精度較低­從我的 Windows 8 本書的演示程式演示。在 DirectWrite,你可以使用 IDWriteTextLayout 的 SetTypography 方法與一個 IDWriteTypography 物件,最終引用廣泛的 DWRITE_FONT_FEATURE_TAG 枚舉的成員。但這些技術不是很像某些作為精確地指定標誌符號。

斜體和粗體與 DrawGlyphRun 如何?在許多情況下,不同的字體檔將包含斜體和粗體變化的一種字體。在 GlyphDump 程式所包含的字體是加粗字體和光的字體的幾個。然而,您還可以選擇以類比斜和加粗字元在 CreateFontFace 中使用 DWRITE_FONT_SIMULATIONS 標誌。

進展和偏移量

在 GlyphDump 和 FancyTitle 的兩個欄位中設置 DWRITE_GLYPH_RUN 為 nullptr。這兩個欄位被命名為 glyphAdvances 和 glyphOffsets。

當您顯示陣列的標誌符號時,您指定的第一個字元左基線的起源。為每個連續的字元的水準座標的原點是自動增加基於字元的寬度。(類似的過程發生時顯示橫向文本)。這種增加是稱為"前進"。

您可以獲得 DirectX 為間距字元使用通過調用 IDWriteFontFace 的 GetDesignGlyphMetrics 方法的進展。結果是一個 DWRITE_GLYPH_METRICS 結構,一個為您感興趣的每個標誌符號索引陣列。這一結構的 advanceWidth 欄位指示與 designUnitsPerEm 領域的 GetMetrics 方法的 IDWriteFontFace,從獲得的 DWRITE_FONT_METRICS 結構的進展和其中還包括適用于特定字體中的所有標誌符號的垂直量度。

或者,您可以調用 GetDesignGlyphAdvances 方法的 IDWriteFontFace1,還提供了相對於 designUnitsPerEm 值的進步。值除以 designUnitsPerEm (這通常是一個不錯的倒圓角值,如 2,048),然後乘以在 DWRITE_GLYPH_RUN 中指定的 em 大小。

GlyphAdvances 陣列常用空間更接近的字元在一起或分開進一步比設計指標顯示。如果您決定使用它,你需要將 glyphAdvances 設置為至少是的標誌符號索引,減去一陣列的大小值的陣列。提前不需要上的最後的標誌符號,因為之外,則不顯示。

GlyphOffsets 欄位是一個 DWRITE_GLYPH_OFFSET 結構,另一個用於每個字元陣列。這兩個欄位是 advanceOffset 和 ascenderOffset,指示所需的偏移量 (向右,分別和向上) 相對於字元的正常位置。這種設施在 XPS 檔通常用於整頁都指定特定字體的多個標誌符號的位置。

CenteredGlyphDump 程式演示如何使用 glyphOffsets 欄位來顯示整個陣列的字形從單個 DrawGlyphRun 調用的特定字體檔:

context->DrawGlyphRun(Point2F(), &m_glyphRun, m_blackBrush.Get());

傳遞給 DrawGlyphRun 的座標是 (0,0),和 glyphAdvances 設置為零值的陣列,以抑制推進連續標誌符號。每個標誌符號的位置完全受 glyphOffsets。這一立場基於字形度量標準來中心每個標誌符號在其列。結果如 [圖 4] 所示。


圖 4 CenteredGlyphDump 程式

提供您自己的字體載入程式

如果一個字體檔是一個應用程式包的一部分,它是相當容易,從中你可以使用 CreateFontReference 的檔路徑。但如果您有坐落在 XPS 或 EPUB 包中的字體或也許在獨立存儲中或在雲計算嗎?

只要您可以編寫代碼來訪問字體檔,DirectX 可以訪問它。您需要提供兩個類,一個實現 IDWriteFontFileLoader,另一種實現 IDWriteFontFileStream。一般情況下,實現 IDWriteFontFileLoader 的類是單身人士,訪問您的應用程式將需要,並將他們每個人都分配一個鍵的所有字體檔。您的 IDWriteFontFileLoader 實現中的 CreateStreamFromKey 方法返回的每個字體檔 IDWriteFontStream 的實例。

若要使用這兩個類,第一次具現化的實現 IDWriteFontFileLoader 的類的單個實例中,並將它傳遞給你的 IDWriteFactory 物件的 RegisterFontFileLoader。然後,而不是調用 CreateFontFileReference 獲取 IDWriteFontFile 的物件,調用 CreateCustomFontFileReference 與此 IDWriteFontFileLoader 實例和一個金鑰識別所需的特定的字體檔。

CenteredGlyphDump 程式中演示了這種技術。該專案包括兩個類 — — PrivateFontFileLoader 和 PrivateFontFileStream — — 實現兩個介面。類訪問字體檔在應用程式包中,但他們可能會用於其他目的的適應。

很可能您的 IDWriteFontFileLoader 實現將需要進行檔的 I/O 調用,並在 Windows 存儲應用程式中,這些調用必須是非同步。它使得有很多意義為 IDWriteFontFileLoader 類定義的所有字體都載入到記憶體中,非同步方法和 IDWriteFontFileStream 簡單地返回到這些區塊的指標。這是我帶 PrivateFontFileLoader 和 PrivateFontFileStream 後仔細審查微軟示例代碼之間的 Direct2D 雜誌 App 示例 Windows 8.1, 的辦法。

對於鍵來標識每個檔,我用檔案名。PrivateFontFileLoader 中的非同步載入方法完成後,CenteredGlyphDump 程式為清單方塊中獲取這些檔案名。這就是為什麼只有檔案名顯示在圖 4。該程式是無知的每個檔關聯的字型家族名稱。

從到的標誌符號的字元

當然,如果通過引用字形顯示文本則很好你知道到底什麼你需要的字形但您可以顯示正常的 Unicode 字元,使用 DrawGlyphRun 嗎?

可以,因為 IDWriteFontFace 有一個 GetGlyphIndices 方法,將字元代碼轉換為特定的字體檔的標誌符號索引。這樣做,它會接收預設字形為每個字元的代碼,所以你不會得到任何花哨的替代品。

但您將傳遞給 GetGlyphIndices 的字元代碼中的所有視窗也許是獨一無二的表單中。而不是 16 位字元代碼 (如你通常工作與 Unicode 字元的字串的時候是這種情況),您需要創建一個陣列的 32 位字元代碼。如你所知,Unicode 實際上 21 位字元集,但字元通常存儲為 16 位值 (稱為 UTF-16),這意味著一些字元組成兩個 16 位值。但 GetGlyphIndices 想要 32 位值。除非您的字元字串有個字元與 0xFFFF 上面的代碼,你可以簡單地傳送的值從一個陣列到另一個。

如果你在處理正常字元從拉丁字母,您還可以假定是有一對一的對應關係之間的字元和字形。否則,您可能需要創建一個更大的輸出陣列來接收指數的。

HelloGlyphRun 專案,足以證明這一簡單技術。圖 5 顯示載入的字體檔並設置一個 DWRITE_GLYPH_RUN 結構的代碼。更新方法調整標誌符號偏移量以實現一種漣漪效應的文本。

圖 5 定義 DWRITE_GLYPH_RUN 從一個字元的字串

// Convert string to glyph indices
std::wstring str = L"Hello, Glyph Run!";
uint32 glyphCount = str.length();
std::vector<uint32> str32(glyphCount);
for (uint32 i = 0; i < glyphCount; i++)
     str32[i] = str[i];
m_glyphIndices = std::vector<uint16>(glyphCount);
m_fontFace->GetGlyphIndices(str32.data(), glyphCount, m_glyphIndices.data());
// Allocate array for offsets (set during Update)
m_glyphOffsets = std::vector<DWRITE_GLYPH_OFFSET>(glyphCount);
// Get output bounds
Windows::Foundation::Size outputBounds = m_deviceResources->GetOutputBounds();
// Define fields of DWRITE_GLYPH_RUN structure
m_glyphRun.fontFace = m_fontFace.Get();
m_glyphRun.fontEmSize = outputBounds.Width / 8; // Empirical
m_glyphRun.glyphCount = glyphCount;
m_glyphRun.glyphIndices = m_glyphIndices.data();
m_glyphRun.glyphAdvances = nullptr;
m_glyphRun.glyphOffsets = m_glyphOffsets.data();
m_glyphRun.isSideways = false;
m_glyphRun.bidiLevel = 0;

雖然您很可能將使用 DrawGlyphRun 只與你熟悉的字體檔,它是可能確定在運行時特定的字體檔中包含什麼類型的標誌符號。IDWriteFontFace 定義訪問表中的 OpenFont 檔的 TryGetFontTable 方法。使用的"cmap"標記為字元到標誌符號表和"GSUB"為標誌符號替換表,但要準備要成功讀取這些表的 OpenType 規範許多難以忍受小時。

標誌符號運行使用系統字體

只是 DrawGlyphRun 為您提供自己的字體檔好嗎?起初似乎是這樣,但您可以使用它,以及系統字體。這裡是的過程:使用 IDWriteFactory 的 GetSystemFontCollection 方法獲取一個 IDWriteFontCollection 物件。該物件允許您查找在系統上安裝的字體相關聯的所有家庭名稱。IDWriteFontCollection 的 GetFontFamily 方法返回一個 IDWriteFontFamily 類型的物件。從那,你可以打電話 GetMatchingFonts 或 GetFirstMatchingFont,字型家族結合斜體、 粗體和拉伸屬性以獲得 IDWriteFont

一旦您有一個 IDWriteFont 物件,調用 CreateFontFace 來獲取 IDWriteFontFace 物件。這是物件的同一類型在早期版本的程式從 CreateFontFace 獲得 !從該物件,就可以開始為 DrawGlyphRun DWRITE_GLYPH_RUN 結構設置。

這表現在 SystemFontGlyphs 專案中。該專案使用在 GetSystemFontCollection 在其 DirectXPage 類中填充與系統字型家族名稱的清單方塊。當選定項,則 IDWriteFontFace 被派生的物件傳遞到渲染器。

SystemFontGlyphsRenderer 類生成一個 DWRITE_GLYPH_RUN 結構基於文本"惱人振動文字效果"。Update 方法然後不得不將標誌符號的偏移量陣列的值設置為-3 和 3,之間的亂數字,使它看起來,如果文本字串中的所有字元都獨立地都振動。

那裡似乎不是很多的 IDWriteFont 和 IDWriteFontFace 之間的概念區別除了,IDWriteFont 始終表示的字體,不是 IDWriteFontFace 需要時 (如系統字體集合) 的字體集合的一部分。IDWriteFontCollection 具有一個 GetFontFromFontFace 方法,接受 IDWriteFontFace,並返回相應的 IDWriteFont,但它只有在與 IDWriteFontFace 關聯的字體檔是字體集合中的一部分。

自訂字體集合

現在你已經看到如何使用 DrawGlyphRun 將與您的應用程式載入的字體檔和系統字體。它是可能要使用常規文本輸出方法 (DrawText 和 DrawTextLayout) 與您自己的字體檔嗎?

是的。你會記得 DrawText 和 DrawTextLayout 需要一個 IDWriteTextFormat 物件。您創建此使用 IDWriteFactory 的 CreateTextFormat 方法,通過指定一個字型家族名稱和字體集合。

通常你的字體集合參數設置為 nullptr 以指示系統字體。但你也可以通過調用 IDWriteFactory 的 CreateCustomFontCollection 方法來創建一個自訂字體集合。您需要提供您自己的類實現 IDWriteFontCollectionLoader 介面的和另一個實現的類,IDWriteFontFileEnumerator。

ResourceFontLayout 程式演示了這一點。該程式還包括介面的實現的 IDWriteFontFileLoader 和 IDWriteFontFileStream 從居中­GlyphDump。當您從該自訂字體集合獲取字型家族的清單時,你會發現多個字體檔有時被合併為一個單一的字型家族。

在框的底部獎

此時,IDWriteFontFace 的重要性應該是相當明顯的。您可以創建一個 IDWriteFontFace 物件從兩個方向:或者直接從一個字體檔,或者通過選擇特定字體的字體集合。你然後引用此 IDWriteFontFace 在 DWRITE_GLYPH_RUN 結構中,或使用它來獲取字形度量或訪問字體檔中的表。

IDWriteFontFace 還定義了一個名為 GetGlyphRun 方法­大綱。此方法的參數是相同的欄位的 DWRITE_GLYPH_RUN 結構,但一個額外的參數是 IDWriteGeometrySink,這是 ID2D1SimplifiedGeometrySink 相同。

這意味著您可以將一個文本字串轉換為 ID2D1PathGeometry,然後呈現和操作那幾何在任何你想要的方式。圖 6 是從 OutlinedCharacters 程式,可顯示字元幾何已描邊和填充 ; 截圖 用一個虛線樣式 (這動畫效果,使周遊字元的點) ; 呈現相同的幾何 和幾何擴闊和概述,實際上,其中概述了輪廓。


圖 6 OutlinedCharacters 程式

我才只是開始。

Charles Petzold 是 MSDN 雜誌和作者的"程式設計視窗,第 6 版"長期貢獻 (O'Reilly 媒體,2012年),一本關於編寫應用程式的 Windows 8 書。 他的網站是 charlespetzold.com

衷心感谢以下 Microsoft 技术专家对本文的审阅:JimGalasyn 和賈斯汀 Panian