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:
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);
}
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);
}
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);
}
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);
}
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:
Constructor
Destructor
QueryInterface()
AddRef()
Release()
IsPixelSnappingDisabled()
GetCurrentTransform()
GetPixelsPerDip()
DrawGlyphRun()
DrawUnderline()
DrawStrikethrough()
DrawInlineObject()
TEXT 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.
I am provided:
Context pointer (unused)
Starting point (OriginX,OriginY)
Measuring mode (unused)
Glyph run
Glyph description (unused)
I will be using (temporarily):
ID2D1PathGeometry *pPath;
ID2D1TransformedGeometry *pPathX
ID2D1GeometrySink *pSink;
Create a path geometry for the glyph outlines.
pD2DFactory->CreatePathGeometry(&pPath);
Create a GeometrySink to receive the geometry.
pPath->Open(&pSink);
Call GetGlyphRunOutline() to extract the glyph outlines into the path geometry.
pGlyph->fontFace->GetGlyphRunOutline(...)
Close the geometry sink.
pSink->Close()
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));
Create a transformed version of the outline path.
pD2DFactory->CreateTransformedGeometry(pPath,&pPathX);
Fill the geometry and draw the stroke.
pRT->FillGeometry(pPathX,pbrFill);
pRT->DrawGeometry(pPathX,pbrStroke,StrokeWidth);
Clean up
SafeRelease(pPath);
SafeRelease(pPathX);
SafeRelease(pSink);
TEXT 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.
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:
TEXT 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!
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