本文章是由機器翻譯。

DirectX 要素

透過 DirectWrite 格式化及捲動文字

Charles Petzold

下载代码示例

Charles Petzold在電腦圖形中的顯示一直是文本的相當尷尬。很大的説明 ' 要顯示的文本作為其他兩個-盡可能輕鬆地­三維圖形,如幾何形狀和點陣圖,但文本附帶了數百年的行李和一些非常明確的需求 — — 這一關鍵的可讀性,例如要求。

認識圖形內的文字的特殊性質,DirectX 拆分成兩個主要的子系統,Direct2D 和 DirectWrite 處理文本的工作。ID2D1RenderTarget 介面聲明的方法用於顯示文本,以及其他 2D 圖形,而以 IDWrite 開頭的介面説明準備顯示的文本。

在圖形和文本之間的邊界是一些有趣的技術,如獲得輪廓的文本字元,或實現 IDWriteTextRenderer 介面用於攔截和操作上其方式到顯示的文本。但必要的初步是扎實的瞭解基本的文本格式的 — — 換句話說,旨在將讀取而不是審美狂喜地敬佩的文本的顯示。

最簡單的文本輸出

最基本的文本顯示介面樣式 (斜體或傾斜),重量 (加粗或光),拉伸 (窄或擴大),和字體大小是 IDWriteTextFormat,將一個字型家族 (Times New Roman 或 arial 字體,例如) 結合在一起。可能會作為一個私有成員在一個標頭檔中定義的 IDWriteTextFormat 物件的引用計數指標:

Microsoft::WRL::ComPtr<IDWriteTextFormat> m_textFormat;

與由 IDWriteFactory 定義的一種方法被創建該物件:

dwriteFactory->CreateTextFormat(
  L"Century Schoolbook", nullptr,
  DWRITE_FONT_WEIGHT_NORMAL,
  DWRITE_FONT_STYLE_ITALIC,
  DWRITE_FONT_STRETCH_NORMAL,
  24.0f, L"en-US", &m_textFormat);

在一般情況下,您的應用程式可能將創建幾個 IDWriteTextFormat 物件為不同的字型家族、 大小和樣式。這些是獨立于設備的資源,所以您可以在任何時間調用 DWriteCreateFactory 獲取 IDWriteFactory 的物件,並讓他們為您的應用程式的持續時間之後創建它們。

如果你不正確拼寫家族名稱 — — 或者如果具有該名稱的字體在您的系統不是 — — 你會得到一個預設字型。第二個參數指示要在其中搜索,對這種字體的字體集合。指定 nullptr 指示系統字體集合。你也可以有專用字體集合。

大小是與裝置獨立單位基於解析度為每英寸 96 個單位,因此一個大小為 24 等同于 18 點字體。語言指示器指的是語言的字型家族名稱,並可保留為空字串。

一旦您已經創建一個 IDWriteTextFormat 物件,您已經指定的所有資訊都是不可變的。如果您需要更改的字體、 樣式或大小,您需要重新創建該物件。

還有什麼需要呈現文本超出 IDWriteText­物件格式?很明顯,文本本身,但還的位置在螢幕上才可以顯示的位置和顏色。這些項被指定時呈現的文本:不是一個點而帶有類型 D2D1_RECT_F 的矩形表示文本的目標。用畫筆,可以是任何類型的畫筆,如漸層筆刷或影像筆刷指定文本的顏色。

這裡是典型的 DrawText 調用:

deviceContext->DrawText(
  L"This is text to be displayed",
  28,    // Characters
  m_textFormat.Get(),
  layoutRect,
  m_blackBrush.Get(),
  D2D1_DRAW_TEXT_OPTIONS_NONE,
  DWRITE_MEASURING_MODE_NATURAL);

預設情況下,文本是破碎成線和包基於矩形 (或從頂部到底部閱讀的語言的矩形的高度) 的寬度。如果文本太長,無法顯示該矩形範圍內,它將繼續超越底部。倒數第二的參數可以指示可選標誌到剪輯文本下降以外矩形,或以不對齊圖元邊界上的字元 (這是很有用的如果你會表演動畫文本上),或在 Windows 8.1,以啟用彩色字體字元。

此列的可下載代碼包含 Windows 8.1 的程式,使用 IDWriteTextFormat 和 DrawText 顯示路易士 · 卡洛的第 7 章"在仙境中的愛麗絲的冒險"(我從網站專案古騰堡,獲得文本但稍,使它更符合原始版的版式作了修改它)。該計畫被稱為 PlainTextAlice,並創建在 Windows 8.1 的Visual Studio快遞 2013年預覽中使用 DirectX App (XAML) 範本。此專案範本會生成一個包含 SwapChainPanel 和在它上顯示 DirectX 圖形的所有必要開銷的 XAML 檔。

帶有文本的檔是專案內容的一部分。每一段是一條線和每個由一個空行分隔。DirectXPage 類載入一個載入的事件處理常式中的文本並將其傳送到 PlainTextAliceMain 類 (作為專案的一部分創建),將它傳輸到 PlainTextAliceRenderer 類 — — 我也有參與到專案的類。

因為此程式所顯示的圖形是相當靜態的我通過不附加 CompositionTarget::Rendering 事件的處理常式禁用在 DirectXPage 中的呈現迴圈。相反,PlainTextAliceMain 確定何時應繪圖形,這是僅在載入文本時或在應用程式視窗的大小或方向的更改時。在這些時間,PlainTextAliceMain 調用 Render 方法中 PlainTextAlice 的­渲染器和 DeviceResources 中的本方法。

PlainTextAliceRenderer 類的 c + + 部分所示圖 1。為清楚起見,我已刪除了 HRESULT 檢查。

圖 1 PlainTextAliceRenderer.cpp 檔

#include "pch.h"
#include "PlainTextAliceRenderer.h"
using namespace PlainTextAlice;
using namespace D2D1;
using namespace Platform;
PlainTextAliceRenderer::PlainTextAliceRenderer(
  const std::shared_ptr<DeviceResources>& deviceResources) :
  m_text(L""),
  m_deviceResources(deviceResources)
{
  m_deviceResources->GetDWriteFactory()->
    CreateTextFormat(L"Century Schoolbook",
                     nullptr,
                     DWRITE_FONT_WEIGHT_NORMAL,
                     DWRITE_FONT_STYLE_NORMAL,
                     DWRITE_FONT_STRETCH_NORMAL,
                     24.0f,
                     L"en-US",
                     &m_textFormat);
  CreateDeviceDependentResources();
}
void PlainTextAliceRenderer::CreateDeviceDependentResources()
{
  m_deviceResources->GetD2DDeviceContext()->
    CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Black),
                          &m_blackBrush);
  m_deviceResources->GetD2DFactory()->
    CreateDrawingStateBlock(&m_stateBlock);
}
void PlainTextAliceRenderer::CreateWindowSizeDependentResources()
{
  Windows::Foundation::Size windowBounds =
    m_deviceResources->GetOutputBounds();
  m_layoutRect = RectF(50, 0, windowBounds.Width - 50,
      windowBounds.Height);
}
void PlainTextAliceRenderer::ReleaseDeviceDependentResources()
{
  m_blackBrush.Reset();
  m_stateBlock.Reset();
}
void PlainTextAliceRenderer::SetAliceText(std::wstring text)
{
  m_text = text;
}
void PlainTextAliceRenderer::Render()
{
  ID2D1DeviceContext* context = 
    m_deviceResources->GetD2DDeviceContext();
  context->SaveDrawingState(m_stateBlock.Get());
  context->BeginDraw();
  context->Clear(ColorF(ColorF::White));
  context->SetTransform(m_deviceResources->GetOrientationTransform2D());
  context->DrawText(m_text.c_str(),
                    m_text.length(),
                    m_textFormat.Get(),
                    m_layoutRect,
                    m_blackBrush.Get(),
                    D2D1_DRAW_TEXT_OPTIONS_NONE,
                    DWRITE_MEASURING_MODE_NATURAL);
  HRESULT hr = context->EndDraw();
  context->RestoreDrawingState(m_stateBlock.Get());
}

通知的 m_layoutRect 成員計算基於應用程式的大小在螢幕上,但與在左和右 50 圖元的邊距。結果就如 [圖 2] 所示,


圖 2 PlainTextAlice 程式

第一次了,我會找到一些好的事情,說此程式:顯然,與開銷最少,在文本正確換成很好地分隔段落。

以純文字形式的不足之處­愛麗絲專案也是顯而易見的:段落之間的間距,只是因為原始文字檔中包含的空行插入只是為此目的。如果你想要稍微更小或更廣泛的段落間距,根本不可能。此外,沒有辦法指示斜體或粗體顯示單詞的文本內。

但最大缺點是你可以看到只有一章的開頭。可以執行滾動的邏輯,但你怎麼能告訴遠如何滾動嗎?很大的問題與 IDWriteText­格式和 DrawText 是高度的格式呈現的文本只是不是可用。

最後,使用 DrawText 意義僅在文本具有統一的格式,和你知道你要指定的矩形是不足以容納文本或你不在乎是否有一個小 runover 時。為更複雜的目的需要更好的方法。

在移動之前,需要注意在 CreateTextFormat 方法中指定的資訊是不可變的在 IDWriteTextFormat 物件中,但介面聲明讓你改變文字的顯示方式的幾種方法:例如,SetParagraphAlignment 會改變在 DrawText 中指定的矩形內文本的垂直位置,雖然 SetTextAlignment 可以讓您指定是否段落的行左、 右、 居中或左右對齊的矩形內。有關這些方法的參數使用字如近遠、 前導,和尾隨來讀取從上到下或從右至左的文本為廣義。您還可以控制行間距、 文本換行和定位停駐點。

文本佈局方法

從 IDWriteTextFormat 的下一步是大一,它是理想的差不多所有標準的文本輸出需求,其中包括點擊測試。它涉及到類型 IDWriteTextLayout,不僅來自 IDWriteTextFormat,納入了一個 IDWriteTextFormat 物件,當它被具現化的物件。

這裡是引用計數指標指向一個 IDWriteTextLayout 物件,可能在一個標頭檔中聲明:

Microsoft::WRL::ComPtr<IDWriteTextLayout> m_textLayout;

創建物件就像這樣:

dwriteFactory->CreateTextLayout(
  pText, wcslen(pText),
  m_textFormat.Get(),
  maxWidth, maxHeight,
  &m_textLayout);

與不同的 IDWriteTextFormat 介面,IDWriteTextLayout 包含了文本本身和所需的高度和寬度的矩形來設置文本的格式。 顯示一個 IDWriteTextLayout 物件的 DrawTextLayout 方法需要只有 2D 點以指明格式化文字的左上角的顯示:

deviceContext->DrawTextLayout(
  point,
  m_textLayout.Get(),
  m_blackBrush.Get(),
  D2D1_DRAW_TEXT_OPTIONS_NONE);

因為 IDWriteTextLayout 物件具有計算分行符號之前呈現的文本所需的所有資訊,它也知道中呈現的文本將是多麼大。IDWriteTextLayout 有幾種方法 — — GetMetrics、 GetOverhangMetrics、 GetLineMetrics 和 GetClusterMetrics — — 提供了豐富的資訊,以説明您有效地使用此文本。例如,GetMetrics 提供的總寬度和高度格式化的文本,以及行和其他資訊的數目。

儘管 CreateTextLayout 方法包括最大寬度和高度參數,這些可以被設置為其他值在以後的時間。(文本本身是不可變的然而。如果顯示區域的變化 (例如,打開您的 tablet 從橫向縱向模式),您不需要重新創建 IDWriteTextLayout 物件。只是調用的 SetMaxWidth 和 SetMaxHeight 的方法,由該介面聲明。事實上,當您首次創建 IDWriteTextLayout 物件,你可以將最大寬度和高度參數設置為零。

ParagraphFormattedAlice 專案使用 IDWriteTextLayout 和 DrawTextLayout,並且結果將顯示在圖 3。你仍然不能滾動以查看剩餘的文本,但請注意某些行居中,並且許多有首行縮進。標題使用比其他人更大的字體大小和一些單詞為斜體。


圖 3 ParagraphFormattedAlice 程式

該文字檔是在這個專案中有一點不同的第一個專案:每一段仍是單獨的行,但沒有空行分隔這些段落。而不是使用一個單一的 IDWriteTextFormat 物件的整個文本,每個段落 ParagraphFormatted­愛麗絲是用單獨的 DrawTextLayout 調用呈現一個單獨的 IDWriteTextLayout 物件。因此,可以將段落之間的間距設置為任何所需的量。

要使用的文本,我定義一個命名段落的結構:

struct Paragraph
{
  std::wstring Text;
  ComPtr<IDWriteTextLayout> TextLayout;
  float TextHeight;
  float SpaceAfter;
};

一個名為 AliceParagraphGenerator 的説明器類生成的段物件基於文本的行的集合。

IDWriteTextLayout 有一群對單個單詞或其他文字區塊設置格式的方法。 例如,在這裡是如何在文本中的 23 偏移量開始的五個字元為斜體:

DWRITE_TEXT_RANGE range = { 23, 5 };
textLayout->SetFontStyle(DWRITE_FONT_STYLE_ITALIC, range);

類似的方法是可用的字型家族、 集合、 大小、 重量、 舒展、 底線和刪除線。 在實際應用中,文本格式通常被定義的標記 (例如 HTML),但為了簡單起見,AliceParagraphGenerator 類作品與純文字的檔和包含硬編碼位置為斜體字樣。

AliceParagraphGenerator 也有 SetWidth 方法來設置新的顯示寬度,如中所示圖 4。 (為清楚起見,HRESULT 檢查已刪除從在此圖中的代碼。)顯示寬度更改當視窗大小發生變化,或當一個平板更改方向。 SetWidth 段的所有物件進行都迴圈、 SetMaxWidth 呼籲 TextLayout,然後獲取一個新的格式化的高度,它是保存在 TextHeight 段。 較早前,SpaceAfter 欄位被簡單地設置為 12 圖元的大部分段落、 標題,36 圖元和 0 為幾個行的詩歌。 這樣就可以輕鬆獲得的每一段的高度和在一章中的所有文本的總高度。

圖 4 中 AliceParagraphGenerator 的 SetWidth 方法

float AliceParagraphGenerator::SetWidth(float width)
{
  if (width <= 0)
    return 0;
  float totalHeight = 0;
  for (Paragraph& paragraph : m_paragraphs)
  {
    HRESULT hr = paragraph.TextLayout->SetMaxWidth(width);
    hr = paragraph.TextLayout->SetMaxHeight(FloatMax());
    DWRITE_TEXT_METRICS textMetrics;
    hr = paragraph.TextLayout->GetMetrics(&textMetrics);
    paragraph.TextHeight = textMetrics.height;
    totalHeight += paragraph.TextHeight + paragraph.SpaceAfter;
  }
  return totalHeight;
}

ParagraphFormattedAliceRenderer 中的渲染方法也遍歷段的所有物件,並調用 DrawTextLayout 基於文本的累積高度不同來源。

首行縮進問題

正如你可以看到在圖 3,ParagraphFormattedAlice 程式將縮進段落的第一行。 也許最簡單的方法,使這種縮進是通過插入一些空格的文本字串的開頭。 Unicode 標準定義了碼的全形空格 (其寬度等於點的大小),en 空間 (一半,),季度 em 和更小的空間,所以您可以組合這些為所需的間距。 好處是縮進量是成正比的點大小的字體。

然而,這種做法不會為負的首行縮進工作 — — 有時稱為首行凸排 — — 這是開始到左邊的段的其餘部分的第一行。 此外,首行縮進通常指定 (如半英寸) 是獨立的點大小的度量。

出於這些原因,我決定改為執行使用 IDWriteTextLayout 介面的 SetInlineObject 方法的首行縮進。 這種方法旨在允許你把任何繪圖物件嵌入的文本,所以它幾乎成為像單獨的詞,其大小考慮到換線。

SetInlineObject 方法通常用於小點陣圖插入文本。 若要使用它,或任何其他目的,您需要編寫一個類,實現 IDWriteInlineObject 介面,其中,除了三個標準 IUnknown 方法,宣佈 GetMetrics、 GetOverhangMetrics、 GetBreakConditions、 和繪製方法。 基本上,你提供的類稱為而文本正在測量或呈現。 對於我的 FirstLineIndent 類,我定義一個建構函式的參數,該值的所需的縮進,以圖元為單位),和值基本上是 GetMetrics 調用將返回指示內嵌物件的大小。 抽獎類的執行沒有任何效果。 負值為懸掛式縮進工作正常。

你叫 IDWriteFontLayout SetInlineObject SetFontStyle 和基於文本的範圍,其他方法一樣但這是僅當我開始使用 SetInlineObject 發現範圍不能有長度為零。 換句話說,你不能簡單地插入內嵌物件。 內聯物件都有要替換文本的至少一個字元。 為此原因,同時定義的段物件代碼插入無寬度的空白字元 ('\x200B') 有沒有可視外觀,但在調用 SetInlineObject 時可以替換的每一行的開頭。

DIY 滾動

ParagraphFormattedAlice 程式不會滾動,但它擁有所有必要的資訊來實現滾動功能,具體來說,呈現的文本的總高度。 ScrollableAlice 專案演示了一種滾動的方法:該程式仍輸出到 SwapChainPanel 的程式視窗的大小,但它偏移基於使用者的滑鼠或觸控式螢幕輸入的呈現。 我認為這種方法的"做自己"滾動。

我在Visual Studio2013年預覽 (使用相同的 DirectX App (XAML) 範本用於較早的專案,創建了 ScrollableAlice 專案卻能夠利用此範本的另一個有趣的方面。 該範本包含創建輔助執行緒的執行處理指標事件從 SwapChainPanel 的 DirectXPage.cpp 中的代碼。 這項技術可以避免遲遲具有此輸入的 UI 執行緒。

當然,引入一個輔助執行緒把事情以及變複雜。 我想要一些慣性在我滾動,這就意味著我想要操作事件,而不是指標事件。 這需要使用 DirectXPage 來創建一個 GestureRecognizer 物件 (也在該輔助執行緒),從指標的事件生成操作事件。

載入文本時,視窗的大小改變時,先前的 ParagraphFormattedAlice 程式重繪該程式的視窗。 ScrollableAlice 此外,不會的那重繪繼續在 UI 執行緒中發生。 ScrollableAlice 還重繪視窗時觸發 ManipulationUpdated 事件,但在為指標輸入創建輔助執行緒中出現這種情況。

如果你給文本好電影用你的手指所以它繼續滾動與慣性,雖然仍然滾動文本,調整視窗的大小,將會發生什麼呢? 有一種好的可能性,重疊的 DirectX 調用將由兩個不同的執行緒在同一時間,並且這是一個問題。

什麼都需要的是一些執行緒同步,和一個很好、 很容易的解決方案涉及到的併發命名空間中的 critical_section 類。 在標頭檔中,以下的聲明:

Concurrency::critical_section m_criticalSection;

Critical_section 類包含一個名為 scoped_lock 的嵌入的類。 下面的語句創建一個物件,通過調用帶有 critical_section 物件的建構函式命名的類型 scoped_lock 鎖:

critical_section::scoped_lock lock(m_criticalSection);

如果由另一個執行緒擁有的 m_criticalSection 物件,則此建構函式假定的 m_criticalSection 物件或塊執行的擁有權。 什麼是好的關於此 scoped_lock 類是析構函數釋放 m_criticalSection 的擁有權,當鎖定物件超出範圍,所以它是非常簡單,只是灑上可能同時調用的各種方法在這周圍的一群。

我確定它將在 DirectXPage,其中包含幾個關鍵電話至 DeviceResources 類 (如 UpdateForWindowSizeChanged),需要其他的執行緒被阻止持續時間內實施這關鍵一節最簡單的方法。 雖然它不是一個好主意阻止 UI 執行緒 (這種情況發生時指標事件觸發),這些塊是極短。

時候的滾動資訊被傳遞到可滾動­AliceRenderer 類,它是存儲為變數命名為 m_scrollOffset,夾緊 0 和最大值等於全章的文本高度與視窗的高度之間的差異之間的浮點值的形式。 Render 方法使用該值來確定如何開始和結束的段落,顯示中所示圖 5

圖 5 執行在 ScrollableAlice 中滾動

std::vector<Paragraph> paragraphs =
  m_aliceParagraphGenerator.GetParagraphs();
float outputHeight = m_deviceResources->GetOutputBounds().Height;
D2D1_POINT_2F origin = Point2F(50, -m_scrollOffset);
for (Paragraph paragraph : paragraphs)
{
  if (origin.y + paragraph.TextHeight + paragraph.SpaceAfter > 0)
    context->DrawTextLayout(origin, paragraph.TextLayout.Get(),
    m_blackBrush.Get());
  origin.y += paragraph.TextHeight + paragraph.SpaceAfter;
  if (origin.y > outputHeight)
    break;
}

滾動一次反彈

雖然 ScrollableAlice 程式實現滾動,有觸摸慣性,當您嘗試過去的頂部或底部滾動時,它沒有特色的 Windows 8 反彈。 (到現在) 熟悉反彈納入 ScrollViewer,,雖然它可能是有趣的嘗試重現 ScrollViewer 反彈在您自己的代碼中,那不是工作的今天。

可滾動的文本可能很長,因為它是最好不要嘗試呈現它所有關于一個表面或點陣圖。 DirectX 已經對這些表面可以有多大的限制。 ScrollableAlice 程式獲取繞過這些限制通過限制其顯示為 SwapChainPanel 的程式視窗的大小和能夠正常運行。 但為 ScrollViewer 工作,內容必須有一個大小在佈局中反映的完整格式化文字的高度。

幸運的是,Windows 8 支援不會真正需要的元素。 VirtualSurfaceImageSource 類派生從 SurfaceImageSource,後者又派生從看來,所以它可以作為一個點陣圖來源為 ScrollViewer 中的圖像元素。 VirtualSurfaceImageSource 可以是任何所需的大小 (和的大小可以調整而不重新創建),並通過虛擬化的表面面積和執行需求上繪圖獲取周圍 DirectX 大小限制。 (然而,SurfaceImageSource 和 VirtualSurfaceImageSource 是不優化的高性能基於幀的動畫。

VirtualSurfaceImageSource 是一個 Windows 運行時 ref 類。 要在與 DirectX 一起使用它,必須將其轉換為類型的物件的 IVirtualSurfaceImageSourceNative,揭示了用於實現需求上繪圖的方法。 這些方法報告矩形區域需要更新,並允許該程式通過提供一個實現 IVirtualSurfaceUpdatesCallbackNative 類新更新矩形的通知。

BounceScrollableAlice,證明這種技術的專案,因為它不需要 SwapChainPanel,我在Visual Studio2013年預覽基於空白 App (XAML) 範本中創建它。 為所需實現的類的 IVirtualSurface­UpdatesCallbackNative 我創建了一個名為 VirtualSurface 類­ImageSourceRenderer,和它還提供了很多的 DirectX 開銷。 AliceVsisRenderer 類派生從 VirtualSurface­ImageSourceRenderer 提供的愛麗絲特定繪圖。

可從 IVirtualSurfaceImageSourceNative 的更新矩形 VirtualSurfaceImageSource 的完整大小但繪圖座標是相對於更新矩形。 這意味著在 BounceScrollableAlice 的 DrawTextLayout 電話幾乎是所示的相同圖 5,除初始原點設置的負面的頂部的更新矩形,而不是滾動偏移量,和 outputHeight 的值是更新矩形的頂部和底部之間的差異。

ScrollViewer 引入混合,文本顯示真正感覺 Windows 8 樣,您甚至可以使用 pinch 手勢,使它更大或更小的強調更多的此 Windows 8 相融合的 DirectX 和 XAML 提供附近最好的兩個世界的東西。

CharlesPetzold 是 MSDN 雜誌和作者的"程式設計視窗,第 6 版"長期貢獻 (微軟出版社,2012年),一本關於編寫應用程式的 Windows 8 書。 他的網站是 charlespetzold.com

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