dev.nlited.com

>>

Direct2D Text

<<<< prev
next >>>>

2017-05-25 19:52:33 chip Page 1983 📢 PUBLIC

May 25 2017

Direct2D and DWrite provide a lot of control over how the text is rendered, but there is a steep learning curve.

Simple Text

The simplest way to render text is using ID2D1RenderTarget::DrawTextW(), which has 5 prerequisites:

  1. ID2D1Factory *pD2DFactory: This is the top-level interface used to create Direct2D objects. Only one instance should be created.

    D2D1Factory1 *pD2DFactory; int D2DGetFactory(void) { int Err= ERR_OK; HRESULT WinErr; D2D1_FACTORY_TYPE type= D2D1_FACTORY_TYPE_MULTI_THREADED; REFIID guid= __uuidof(ID2D1Factory1); D2D1_FACTORY_OPTIONS options; options.debugLevel= D2D1_DEBUG_LEVEL_INFORMATION; if(!SUCCEEDED(WinErr= D2D1CreateFactory(Type,guid,&options,(void**)&pD2DFactory))) { Err= Error(ERR_DIRECTX,"D2DGetFactory: Unable to create Direct2D factory. [%X]",WinErr); } return(Err); }

  2. IDWriteFactory *pDWriteFactory: The top level interface to create DirectWrite objects. Only one instance should be created.

    IDWriteFactory *pDWriteFactory; int D2DCreateWriteFactory(void) { int Err= ERR_OK; HRESULT WinErr; DWRITE_FACTORY_TYPE Type= DWRITE_FACTORY_TYPE_SHARED; REFIID guid= __uuidof(IDWriteFactory); if(!SUCCEEDED(WinErr= DWriteCreateFactory(Type,guid,reinterpret_cast<IUnknown>(&pDWriteFactory))) { Err= Error(ERR_DIRECTX,"D2DCreateWriteFactory: Unable to create DirectWrite factory. [%X]",WinErr); } return(Err); }

  3. ID2D1RenderTarget *pRT: This is the interface used to draw everything to the graphic "surface". The final render target for a window:

    int D2DGetWndRT(HWND hWnd, ID2D1HwndRenderTarget *&pRT, FLOAT *pPxlScaler) { int Err= ERR_OK; HRESULT WinErr; RECT rWnd; GetClientRect(hWnd,&rWnd); if(IsErr(Err= D2DGetFactory())) return(Err); const D2D1_RENDER_TARGET_PROPERTIES Prop= RenderTargetProperties(); D2D1_HWND_RENDER_TARGET_PROPERTIES WndProp= HwndRenderTargetProperties(hWnd,SizeU(RWID(rWnd),RHGT(rWnd))); //NOTE: CreateHwndRenderTarget() is not available for Microsoft Store programs. // I would need to use Device and DeviceContext instead. if(!SUCCEEDED(WinErr= pD2DFactory->CreateHwndRenderTarget(Prop,WndProp,&pWndRT))) return(Error(ERR_SYSCREATE,"D2DGetWndRT: Unable to create window RT. [%X]",WinErr)); if(pPxlScaler) { D2D1_SIZE_F PixelSz= pWndRT->GetSize(); //DIP units *pPxlScaler= (FLOAT)PixelSz.width/(FLOAT)RWID(rWnd); //Scales dialog units to DIP units. } return(Err); }

    Once I have the window RT, I can create compatible render targets that draw onto offscreen bitmaps.

    ID2D1BitmapRenderTarget *pImgRT; ID2D1Bitmap *pImgBM; if(!SUCCEEDED(WinErr= pWndRT->CreateCompatibleRenderTarget(size,&pImgRT))) { Warn(ERR_DIRECTX,"Unable to create offscreen render target. [%X]",WinErr); } else { pImgRT->GetBitmap(&pImgBM); }

  4. IDWriteTextFormat *pFmt: Describes the text font, alignment, etc.

    int D2DCreateText(IDWriteTextFormat *&pFmt, FLOAT Hgt, const WCHAR *Font) { int Err= ERR_OK; HRESULT WinErr; if(!pDWriteFactory && !SUCCEEDED(WinErr= DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED,__uuidof(IDWriteFactory),reinterpret_cast<IUnknown**>(&pDWriteFactory)))) return(Error(ERR_SYSCREATE,"D2DCreateText: Unable to create DWriteFactory [%X]",WinErr)); DWRITE_FONT_WEIGHT Weight= DWRITE_FONT_WEIGHT_NORMAL; DWRITE_FONT_STYLE Style= DWRITE_FONT_STYLE_NORMAL; DWRITE_FONT_STRETCH Stretch= DWRITE_FONT_STRETCH_NORMAL; if(!SUCCEEDED(WinErr= pDWriteFactory->CreateTextFormat(Font,0,Weight,Style,Stretch,Hgt,L"en-us",&pFmt))) return(Error(ERR_SYSCREATE,"D2DCreateText: Unable to create text '%S' [%X]",Font,WinErr)); return(Err); }

  5. ID2D1SolidColorBrush *pbrFill: Describes the color and opacity of the brush.

    ID2D1SolidColorBrush *pbrBrush; pRT->CreateSolidColorBrush(ColorF(ColorF::Azure,0.75f),&pbrBrush);

Once all the prerequisites have been created, drawing the text is easy.

int DrawImage(D2D1RenderTarget *pRT, const WCHAR *Text) { int Err= ERR_OK; HRESULT WinErr; D2D1_RECT_F rText; rText.left= rClnt.left*PxlScaler; rText.top= rClnt.top*PxlScaler; rText.right= rClnt.right*PxlScaler; rText.bottom= rClnt.bottom*PxlScaler; pRT->BeginDraw(); pRT->Clear(); pRT->DrawTextW(Text,wcslen(Text),pFmt,rText,pbrText); if(!SUCCEEEDED(WinErr= pRT->EndDraw())) { Err= Warn(ERR_DIRECTX,"DrawImage: EndDraw() failed [%X]",WinErr); } return(Err); }

Getting Fancy

I can add an outline stroke to the text by implementing my own custom text render class by deriving the IDWriteTextRenderer class. This msdn page was enough to point me in the right direction, but lacked a lot of crucial details and the referenced example code seems to have been changed to a Windows Store version that doesn't use IDWriteTextRenderer at all. I found some random examples of DWrite code that might be helpful in the future.

Fortunately, the actual code is not too complex, but there are a lot of virtual functions that must be implemented or stubbed before it will even compile. I created a D2DCustomRender class that stubs in the virtual functions with code that returns ERROR_CALL_NOT_IMPLEMENTED.

I need to provide default or stub functions for:


D2DCustomRender: class D2DCustomRender: public IDWriteTextRenderer { public: HRESULT QueryInterface(REFIID RefIID, void **pObj) { return(E_NOINTERFACE); }; ULONG AddRef(void) { return(++RefCount); }; ULONG Release(void) { if(--RefCount <= 0) { delete this; return(0); } return(RefCount); }; D2DCustomRender(ID2D1RenderTarget *pRT); virtual ~D2DCustomRender(void); protected: virtual HRESULT IsPixelSnappingDisabled(void *pContext, BOOL *pIsDisabled) { *pIsDisabled= 0; return(ERROR_SUCCESS); }; virtual HRESULT GetCurrentTransform(void *pContext, DWRITE_MATRIX *pXform) { pRT->GetTransform(reinterpret_cast<Matrix3x2F*>(pXform)); return(ERROR_SUCCESS); }; virtual HRESULT GetPixelsPerDip(void *pContext, FLOAT *pPixelsPerDip) { FLOAT DpiX,DpiY; pRT->GetDpi(&DpiX,&DpiY); *pPixelsPerDip= DpiX/96; return(ERROR_SUCCESS); } virtual HRESULT DrawGlyphRun(void *pContext, FLOAT OriginX, FLOAT OriginY ,DWRITE_MEASURING_MODE Mode ,DWRITE_GLYPH_RUN const *pGlyph ,DWRITE_GLYPH_RUN_DESCRIPTION const *pDesc ,IUnknown *pEffect ) { return(ERROR_CALL_NOT_IMPLEMENTED); }; virtual HRESULT DrawUnderline(void *pContext, FLOAT OriginX, FLOAT OriginY, DWRITE_UNDERLINE const *pUnderline, IUnknown *pEffect) { return(ERROR_CALL_NOT_IMPLEMENTED); }; virtual HRESULT DrawStrikethrough(void *pContext, FLOAT PosX, FLOAT PosY, DWRITE_STRIKETHROUGH const *pStrike, IUnknown *pEffect) { return(ERROR_CALL_NOT_IMPLEMENTED); }; virtual HRESULT DrawInlineObject(void *pContext, FLOAT PosX, FLOAT PosY, IDWriteInlineObject *pObj, BOOL IsSideways, BOOL IsRTL, IUnknown *pEffect) { return(ERROR_CALL_NOT_IMPLEMENTED); }; //Data ID2D1RenderTarget *pRT; private: int RefCount; }; D2DCustomRender::D2DCustomRender(ID2D1RenderTarget *pRT) { RefCount= 0; (this->pRT= pRT)->AddRef(); } D2DCustomRender::~D2DCustomRender(void) { SafeRelease(pRT); }

To implement a stroked outline, I just need to provide a working DrawGlyphRun() function.

  1. I am provided:
  2. I will be using (temporarily):
  3. Create a path geometry for the glyph outlines.

    pD2DFactory->CreatePathGeometry(&pPath);

  4. Create a GeometrySink to receive the geometry.

    pPath->Open(&pSink);

  5. Call GetGlyphRunOutline() to extract the glyph outlines into the path geometry.

    pGlyph->fontFace->GetGlyphRunOutline(...)

  6. Close the geometry sink.

    pSink->Close()

  7. Create a translation matrix to transform the draw operations to the requested text location.

    Matrix3x2F xform= Matrix3x2F::Identity(); xform= xform*Matrix3x2F::Translation(SizeF(OriginX,OriginY));

  8. Create a transformed version of the outline path.

    pD2DFactory-&gtCreateTransformedGeometry(pPath,&pPathX);

  9. Fill the geometry and draw the stroke.

    pRT->FillGeometry(pPathX,pbrFill); pRT->DrawGeometry(pPathX,pbrStroke,StrokeWidth);

  10. Clean up

    SafeRelease(pPath); SafeRelease(pPathX); SafeRelease(pSink);

D2DOutlineRender: class D2DOutlineRender: public D2DCustomRender { public: D2DOutlineRender(ID2D1RenderTarget *pRT,ID2D1SolidColorBrush *pbrStroke,FLOAT StrokeWid,ID2D1SolidColorBrush *pbrFill); ~D2DOutlineRender(void); protected: HRESULT DrawGlyphRun(void *pContext, FLOAT OriginX, FLOAT OriginY ,DWRITE_MEASURING_MODE Mode ,DWRITE_GLYPH_RUN const *pGlyph ,DWRITE_GLYPH_RUN_DESCRIPTION const *pDesc ,IUnknown *pEffect ); private: FLOAT StrokeWidth; ID2D1SolidColorBrush *pbrStroke; ID2D1SolidColorBrush *pbrFill; }; D2DOutlineRender::D2DOutlineRender(ID2D1RenderTarget *pRT,ID2D1SolidColorBrush *pbrStroke,FLOAT StrokeWid,ID2D1SolidColorBrush *pbrFill) :D2DCustomRender(pRT) { (this->pbrStroke= pbrStroke)->AddRef(); (this->pbrFill= pbrFill)->AddRef(); this->StrokeWidth= StrokeWid; } D2DOutlineRender::~D2DOutlineRender(void) { SafeRelease(pbrStroke); SafeRelease(pbrFill); } HRESULT D2DOutlineRender::DrawGlyphRun(void *pContext, FLOAT OriginX, FLOAT OriginY ,DWRITE_MEASURING_MODE Mode ,DWRITE_GLYPH_RUN const *pGlyph ,DWRITE_GLYPH_RUN_DESCRIPTION const *pDesc ,IUnknown *pEffect ) { ID2D1PathGeometry *pPath= 0; ID2D1TransformedGeometry *pPathX= 0; ID2D1GeometrySink *pSink= 0; HRESULT WinErr; if(!pD2DFactory) { WinErr= ERROR_NOT_FOUND; } else if(!SUCCEEDED(WinErr= pD2DFactory->CreatePathGeometry(&pPath))) { Warn(ERR_DIRECTX,"D2DOutlineRender:DrawGlyphRun: CreatePathGeometry failed. [%X]",WinErr); } else if(!SUCCEEDED(WinErr= pPath->Open(&pSink))) { Warn(ERR_DIRECTX,"D2DOutlineRender:DrawGlyphRun: Create geometry sink failed. [%X]",WinErr); } else { // Create a couple shortcuts to make the call to GetGlyphRunOutline() easier to read. IDWriteFontFace *pFont= pGlyph->fontFace; const DWRITE_GLYPH_RUN &g= *pGlyph; WinErr= pFont->GetGlyphRunOutline(g.fontEmSize,g.glyphIndices,g.glyphAdvances,g.glyphOffsets,g.glyphCount,g.isSideways,g.bidiLevel%2,pSink); if(!SUCCEEDED(WinErr)) { Warn(ERR_DIRECTX,"D2DOutlineRender:DrawGlyphRun: GetGlyphRunOutline failed. [%X]",WinErr); } else if(!SUCCEEDED(WinErr= pSink->Close())) { Warn(ERR_DIRECTX,"D2DOutlineRender:DrawGlyphRun: Sink::Close() failed. [%X]",WinErr); } else { Matrix3x2F xform= Matrix3x2F::Identity(); xform= xform*Matrix3x2F::Translation(SizeF(OriginX,OriginY)); if(!SUCCEEDED(WinErr= pD2DFactory->CreateTransformedGeometry(pPath,&xform,&pPathX))) { Warn(ERR_DIRECTX,"D2DOutlineRender:DrawGlyphRun: CreateTransformedGeometry() failed. [%X]",WinErr); } else { pRT->FillGeometry(pPathX,pbrFill); pRT->DrawGeometry(pPathX,pbrStroke,StrokeWidth); } } } SafeRelease(pPath); SafeRelease(pPathX); SafeRelease(pSink); return(WinErr); }

The final step is to provide a simple API to draw the text.

D2DDrawText: int D2DDrawText(ID2D1RenderTarget *pRT, const D2D1_RECT_F &rText, IDWriteTextFormat *pFmt, ID2D1SolidColorBrush *pbrFill, ID2D1SolidColorBrush *pbrStroke, FLOAT StrokeWidth, const WCHAR *Text, int TextCt) { int Err= ERR_OK; HRESULT WinErr; if(Text==0 || TextCt==0) return(ERR_OK); TextCt= (TextCt < 0) ? (int)wcslen(Text) : min(TextCt,(int)wcslen(Text)); //pRT->DrawTextW(Text,TextCt,pFmt,rText,pbrFill); D2DOutlineRender Outline(pRT,pbrStroke,StrokeWidth,pbrFill); IDWriteTextLayout *pLayout; if(!SUCCEEDED(WinErr= pDWriteFactory->CreateTextLayout(Text,TextCt,pFmt,RWID(rText),RHGT(rText),&pLayout))) { Err= Warn(ERR_DIRECTX,"D2DDrawText: Unable to create layout. [%X]",WinErr); } else { pLayout->Draw(0,&Outline,rText.left,rText.top); SafeRelease(pLayout); } return(Err); }

An example:

EXAMPLE: void MapView::ImgDrawTitle(void) { int Err; D2D1_RECT_F rTitle= { rDraw.left+10,rDraw.top+10,rDraw.right-10,rDraw.bottom-10 }; if(!pfmtTitle && IsErr(Err= D2DCreateText(pfmtTitle,48.0f,L"Bauhaus 93"))) { Warn(Err,"MapView:ImgDrawTitle: Unable to create title font."); } else if(!pbrTitleFill && !SUCCEEDED(pImgRT->CreateSolidColorBrush(ColorF(ColorF::LimeGreen),&pbrTitleFill))) { Warn(ERR_DIRECTX,"MapView:ImgDrawTitle: Unable to create title fill brush."); } else if(!pbrTitleStroke && !SUCCEEDED(pImgRT->CreateSolidColorBrush(ColorF(ColorF::Black),&pbrTitleStroke))) { Warn(ERR_DIRECTX,"MapView:ImgDrawTitle: Unable to create title stroke brush."); } else { pfmtTitle->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER); pfmtTitle->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_NEAR); if(IsErr(Err= D2DDrawText(pImgRT,rTitle,pfmtTitle,pbrTitleFill,pbrTitleStroke,2.0f,L"AutoBOM",-1))) { Warn(Err,"MapView:ImgDrawTitle: D2DDrawText failed."); } } }

And it all works!

Direct2D outlined text.

Now that I have access to the text outline, I could implement other fancy features like extrusions, drop shadows, etc.



WebV7 (C)2018 nlited | Rendered by tikope in 36.185ms | 3.21.247.95