In this article, you will learn how to display text, measure text and enumerate installed fonts using DirectWrite.
Table of Contents
The example code is hosted on Github.
Introduction
Today's applications must support high-quality text rendering, resolution-independent outline fonts, and full Unicode text and layout support. DirectWrite, a DirectX API, provides these features and more. In this article, you will learn how to display text, measure text and enumerate installed fonts using DirectWrite. The demo code is an end credit dialog from my MandyFrenzy photo slideshow app where the user adds an end credit (such as its director or producer) to the slideshow. It is fun to see yourself listed as director/producer at the end of the slideshow video. I have added MandyFrenzy app to the article download so that you can have fun with it.
Text Display
Before displaying the text in Direct2D, the parameters, such as font name, font size, italic and bold, must be specified in a IDWriteTextFormat
object. GetTextFormat()
creates an IDWriteTextFormat
object based on those parameters.
ComPtr<IDWriteTextFormat> textFormat = GetTextFormat(m_FontFamily,
m_FontSize * m_DPIScale, m_Italic, m_Bold, m_Centerize, m_Centerize);
DrawText(m_DCTarget.Get(), textFormat.Get(), m_Text);
The DrawText()
creates a rect
from the size of the render target and calls DrawTextW()
with the text
and textFormat
. There is an overloaded function of DrawTextW()
that takes in a point, instead of a rectangle, as a starting location to display the text.
void TextDisplayStatic::DrawText(ID2D1RenderTarget* target,
IDWriteTextFormat* textFormat, const CString& text)
{
auto size = target->GetSize();
auto rect = RectF(0.0f, 0.0f, size.width, size.height);
target->DrawTextW(text, text.GetLength(), textFormat, rect, m_BlackBrush.Get());
}
As I previously mentioned, the GetTextFormat()
must be called to create TextFormat
before DrawText()
.
ComPtr<IDWriteTextFormat> GetTextFormat(const CString& fontname, float fontSize,
bool italic, bool bold, bool centerHorizontal, bool centerVertical)
{
ComPtr<IDWriteTextFormat> textFormat;
DWRITE_FONT_STYLE fontStyle = italic ?
DWRITE_FONT_STYLE_ITALIC : DWRITE_FONT_STYLE_NORMAL;
DWRITE_FONT_WEIGHT fontWeight = bold ?
DWRITE_FONT_WEIGHT_BOLD : DWRITE_FONT_WEIGHT_NORMAL;
HR(FactorySingleton::GetDWriteFactory()->CreateTextFormat((LPCTSTR)fontname,
nullptr, fontWeight, fontStyle,
DWRITE_FONT_STRETCH_NORMAL, fontSize, L"",
textFormat.GetAddressOf()));
if (centerHorizontal)
textFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER);
if (centerVertical)
textFormat->SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_CENTER);
return textFormat;
}
For the italic text, we can choose between DWRITE_FONT_STYLE_ITALIC
or DWRITE_FONT_STYLE_OBLIQUE
. What is the difference between these two styles? Well, oblique just takes in normal font and slants it by applying the skew transformation while italic is a font that is specially crafted to be slanted. As the old saying goes, a picture speaks a thousand words, please refer to the image below (Courtesy of Advanced Typography Pluralsight course by Pariah Burke). The text on the left shows the difference between a normal and italic font while the right shows the difference between a normal and oblique style. As you can see, the italic characters f,i,e,a are different and the oblique characters are just slanted versions.
Text Measurement
Sometimes, it is necessary to measure the length and height of the text before it is displayed. For example, when displaying text in a button, we want to adjust the button dimensions to accommodate the text. If the width of the given size
is insufficient to hold the text in a single line, the height will be adjusted to break the text to fit in the next line or multiple lines. Please note this GetTextSize()
is not in the source code download. You can copy GetTextSize()
from the code snippet below.
HRESULT GetTextSize
(const WCHAR* text, IDWriteTextFormat* pTextFormat, D2D1_SIZE_F& size)
{
HRESULT hr = S_OK;
ComPtr<IDWriteTextLayout> pTextLayout;
float floatDim = static_cast<float>(m_Dim);
hr = FactorySingleton::GetDWriteFactory()->CreateTextLayout(text,
static_cast<UINT32>(wcslen(text)),
pTextFormat, floatDim, floatDim, pTextLayout.GetAddressOf());
if (SUCCEEDED(hr))
{
DWRITE_TEXT_METRICS textMetrics;
hr = pTextLayout->GetMetrics(&textMetrics);
size = D2D1::SizeF(ceil(textMetrics.widthIncludingTrailingWhitespace),
ceil(textMetrics.height));
}
return hr;
}
Font Enumeration
Whenever we display text with a particular font, we must ensure that the font exists on the user system. That can be accomplished by enumerating the fonts on the system. These are the structures we needed for the font enumeration. For brevity, the constructors, setters and getters of the structures are not shown.
struct FontSubType
{
private:
std::wstring m_SubName;
DWRITE_FONT_STRETCH m_Stretch;
DWRITE_FONT_STYLE m_Style;
DWRITE_FONT_WEIGHT m_Weight;
};
struct FontInfo
{
private:
std::wstring m_OriginalName;
std::wstring m_LocalizedName;
std::vector<FontSubType> m_SubTypes;
UINT32 m_StartY;
UINT32 m_Height;
};
The EnumFont()
takes care of font enumeration for you.
void EnumFont(std::vector< std::shared_ptr<FontInfo> >& vecFont)
{
if (vecFont.empty() == false)
return;
ComPtr<IDWriteFontCollection> fontCollection;
HR(FactorySingleton::GetDWriteFactory()->GetSystemFontCollection(
fontCollection.GetAddressOf(),
TRUE
));
UINT32 familyCount = fontCollection->GetFontFamilyCount();
for (UINT32 i = 0; i < familyCount; ++i)
{
ComPtr<IDWriteFontFamily> fontFamily;
HR(fontCollection->GetFontFamily(i, fontFamily.GetAddressOf()));
if (!fontFamily)
continue;
ComPtr < IDWriteLocalizedStrings> names;
HR(fontFamily->GetFamilyNames(names.GetAddressOf()));
WCHAR wname[100];
UINT32 localizedCount = names->GetCount();
std::shared_ptr<FontInfo> info = std::make_shared<FontInfo>();
HR(names->GetString(localizedCount - 1, wname, _countof(wname)));
info->LocalizedName(wname);
HR(names->GetString(0, wname, _countof(wname)));
info->OriginalName(wname);
UINT32 fontCount = fontFamily->GetFontCount();
std::vector<FontSubType> vecSubNames;
vecSubNames.reserve(fontCount);
for (UINT32 j = 0; j < fontCount; ++j)
{
ComPtr<IDWriteFont> font;
HR(fontFamily->GetFont(j, font.GetAddressOf()));
if (!font)
continue;
ComPtr < IDWriteLocalizedStrings> faceNames;
font->GetFaceNames(faceNames.GetAddressOf());
WCHAR wface[100];
HR(faceNames->GetString(0, wface, _countof(wface)));
vecSubNames.push_back(FontSubType(wface, font->GetStretch(),
font->GetStyle(), font->GetWeight()));
}
info->SubTypes(std::move(vecSubNames));
vecFont.push_back(info);
}
std::sort(vecFont.begin(), vecFont.end(),
[](const std::shared_ptr<FontInfo>& a, const std::shared_ptr<FontInfo>& b)
{return a->OriginalName() < b->OriginalName(); });
}
The selected font and its subtypes are shown below. Subtypes deals with italic and bold types of a font family. The subtypes are not useful to me because I am unclear about some subtype names. And Microsoft Office does not expose the font subtypes for its user to select from.
Do Not Mix and Match DirectWrite and GDI+ Font Enumeration
If you use DirectWrite font enumeration, you should display the text with DirectWrite, not GDI+. Why? You may ask. One good reason is if you choose Cooper
font returned from DirectWrite font enumeration, GDI+ cannot display that font while DirectWrite has no problem. GDI+ font enumeration returns Cooper Black
font which it can display. Strictly speaking, Cooper Black
is a bold subtype of Cooper
font family. This is a quirk of how Microsoft classified its font types under two different graphical technologies.
History
- 11th March 2023: Added more information about width of
size
parameter of GetTextSize()
in Text Measurement section. - 14th January 2023: First release
Articles in the Series