Draw text using DirectWrite in Game UI IDWriteTextFormat / IDWriteTextLayout / IDWriteTextRenderer

這篇筆記是紀錄用 DirectWrite 在遊戲或一般 application UI 中寫文字會用到的東西。

IDWriteFactory

首先,所有 DirectWrite 的物件都建立在 IDWriteFactory 上。

IDWriteFactory* dw_factory_;
HRESULT hret = DWriteCreateFactory(
    DWRITE_FACTORY_TYPE_SHARED,
    __uuidof(IDWriteFactory),
    reinterpret_cast<IUnknown**>(&dw_factory_)
);

IDWriteTextFormat

定義了單一文字的字型、大小、Font Weight (筆觸厚重)、Font Style (斜體、義大利斜體)、Font Stretch (左右的拉長壓縮)。也可以定義一段文字要靠左靠右靠中(SetTextAlignment),與段落跟邊界的空間 (SetParagraphAlignment)。

但是文字資料,文字要畫的位置都不在這裏面。

IDWriteTextFormat* dw_textformat_;
HRESULT hret = dw_factory_->CreateTextFormat(
    L"Reem Kufi Regular", // font name
    NULL, // IDWriteFontCollection*, NULL for system font collection
    DWRITE_FONT_WEIGHT_NORMAL,
    DWRITE_FONT_STYLE_NORMAL,
    DWRITE_FONT_STRETCH_NORMAL,
    50, // font size in dip (1/96 inch)
    L"", //locale name
    &dw_textformat_
); 
hret = dw_textformat_->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_LEADING);
hret = dw_textformat_->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_NEAR);

IDWriteTextLayout

定義了要繪製的文字字串,與繪製時邊框的長寬。建立 layout 時,除了需要 factory,也需要 text format。

static const WCHAR sc_str[] = L"This is the text I want to draw!";
HRESULT hret = dw_factory_->CreateTextLayout(
    sc_str,      // The string to be laid out and formatted.
    ARRAYSIZE(sc_str)-1,  // The length of the string.
    dw_textformat_,  // The text format to apply to the string (contains font information, etc).
    300,         // The width of the layout box.
    50,        // The height of the layout box.
    &dw_textlayout_  // The IDWriteTextLayout interface pointer.
);

關於 layout width and height,它會自動換行,但不保證一定會畫在框框內。可以在繪製時,要求超出 layout 的不要繪製,但是這麼簡單的功能在製作可以 scrolling 的文字框時會無法實現。需要用 IDWriteTextRenderer callback 來自行繪製。

還有做排版時一定會需要 “事先” (pre-render) 知道文字區塊繪製後的真實長寬,下面是取得 TextLayout 的方法。

DWRITE_TEXT_METRICS textMetrics;
hr = dw_textlayout_->GetMetrics(&textMetrics);

ID2D1RenderTarget::DrawText()、只需要 text format 的文字繪製

最簡單的繪製方法,只需要 IDWriteTextFormat。因為 text format 沒有文字和位置,所以參數要提供字串與繪製的範圍 (rect),也需要額外提供 Brush (有四種,Solid/Linear Gradient/Circular Gradient/Bitmap),與如果畫到邊界之外時的處裡方法的 option。

ID2D1RenderTarget::DrawTextLayout()、繪製 text layout 的 API

不同於 text format,text layout 已經有必要的資料了,所以 DrawTextLayout() 只需要額外提供 Brush 和超出邊界處裡的 option。

IDWriteTextLayout::Draw()、提供 callback 自行處理文字的繪製

上面的 API 都是自動完成繪製的,而使用 layout 的 Draw() API 是利用 callback,在繪製過程提供文字的 Geometry (由很多線組成的文字外框)、文字座標、和其他很專業才會用的東西 (ex: effect object),讓你自己繪製文字邊框和填色。

Callback interface 是 IDWriteTextRenderer,Microsoft 有個 Sample Implementaion, named CustomTextRender 可以在 github 找到。我這裡修改了繪製最主要的 CustomTextRenderer::DrawGlyphRun() 讓繪製時不超出外框 (pCanvas) 當範例,因為這也是最常用到的功能,另一個常用的是輸出限制在一行,不過那只要看 baselineOriginY 忽略掉 Y 值太高的 function call 即可。

IFACEMETHODIMP CustomTextRenderer::DrawGlyphRun(
    __maybenull void* clientDrawingContext,
    FLOAT baselineOriginX,
    FLOAT baselineOriginY,
    DWRITE_MEASURING_MODE measuringMode,
    __in DWRITE_GLYPH_RUN const* glyphRun,
    __in DWRITE_GLYPH_RUN_DESCRIPTION const* glyphRunDescription,
    IUnknown* clientDrawingEffect
)
{
    // Create a path geometry to represent the area text should be draw, 
    // actually you can create a rect geometry and any other geometry instead
    ID2D1PathGeometry* pCanvas = NULL;
    hr = pD2DFactory_->CreatePathGeometry(&pCanvas);

    pCanvas->Open(&pSink);

    pSink->SetFillMode(D2D1_FILL_MODE_WINDING);

    pSink->BeginFigure(
        D2D1::Point2F(30, 30),
        D2D1_FIGURE_BEGIN_FILLED
    );
    D2D1_POINT_2F points[5] = {
       D2D1::Point2F(30, 30),
       D2D1::Point2F(100, 30),
       D2D1::Point2F(100, 100),
       D2D1::Point2F(30, 100),
       D2D1::Point2F(30, 30)
    };

    pSink->AddLines(points, ARRAYSIZE(points));
    pSink->EndFigure(D2D1_FIGURE_END_CLOSED);
    pSink->Close();
    SafeRelease(&pSink);
    
    //
    //
    // ... skip ...
    //
    //
    
    // Combine (intersect) final path geometric, which is 
    // pTransformedGeometry, with pCanvas 
    ID2D1PathGeometry* pPathCombine = NULL;
    hr = pD2DFactory_->CreatePathGeometry(
        &pPathCombine
    );

    pPathCombine->Open(&pSink);

    pTransformedGeometry->CombineWithGeometry(
        pCanvas,
        D2D1_COMBINE_MODE_INTERSECT,
        NULL,
        NULL,
        pSink);

    pSink->Close();
    SafeRelease(&pSink);

    // Draw the outline of the glyph run
    pRT_->DrawGeometry(
        pPathCombine,
        pOutlineBrush_
    );
    
    //
    //  ... skip ...
    //
}

Check the Direct2D Geometry section to understand how Geometry works.