Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Directwrite: Getting a font's height

My objective: I want to get the height of an IDWriteTextFormat's font so I can calculate how many lines of text can fit in an IDWriteTextLayout of a certain height.

My problem: Right now I'm using this code to calculate the visible number of lines:

inline int kmTextCtrl::GetVisLines() const
{

    /* pTextFormat is an IDWriteTextFormat pointer, dpi_y is the desktop's vertical dpi,
       and GetHeight() returns the height (in pixels) of the render target. */
    float size = (pTextFormat->GetFontSize()/72.0f)*dpi_y;
    return (int)(GetHeight()/size);
}

The calculation seems to be accurate for some fonts, but not for any of the TrueType fonts (e.g.: Courier New, Arial, Times New Roman). For these fonts, the text shown is clipped well short of the lower vertical boundary of the render target.

Some context: I am making a text scroll back buffer control which uses an IDWriteTextLayout to put text to the control's render target. I use the result of GetVisLines() to determine how many lines of text from a circular buffer (which stores text in std::strings by the line) to pull into the layout, and recreate it every time the window is scrolled or resized.

This is being done using "native" Win32 API C++.

like image 765
kittykitty Avatar asked Apr 08 '11 02:04

kittykitty


Video Answer


1 Answers

The simplest and most robust approach is to just ask the layout itself for text metrics, as that's one of the two things it was designed for, drawing and measurement. You would create an IDWriteTextLayout using the text format and call GetMetrics to get the DWRITE_TEXT_METRICS::height. I'm guessing you're using ID2D1RenderTarget::DrawText and passing a text format, so you may not have created a layout directly, but calling DrawText is just like calling CreateTextLayout yourself followed by DrawTextLayout.

Beware that going through the lower layers to get this answer (IDWriteFontFace and the like) makes certain assumptions that a generic world ready text control should not assume, such as assuming the base font will be used and that all the lines being the same height. So long as all characters are present in the given base font, this happens to work out (chances are you're mostly displaying English which is why all appears well), but throw in some CJK or RTL languages or emoji (which a base font like Times New Roman certainly doesn't support), and the line height will grow or shrink accordingly to the substituted fonts. GDI rescales substituted fonts such that they fit into the base font's height, but this leads to poorly scrunched letters in languages like Thai and Tibetan which need more breathing room for ascenders and descenders. IDWriteTextLayout and other layouts like those in WPF/Word keep all the font glyphs at the same em size, which means they line up more nicely when adjacent to each other; but it does mean the line height is variable.

If you do just draw each line of text as if they were all the same height, you can see overlap between glyphs and non-uniform baselines between lines, or clipping at the top and bottom of the control. So the ideal thing to do is to use the actual height of each line; but if you need them to all be the same height (or if it complicates the control too much), then at least set an explicit line spacing using SetLineSpacing with DWRITE_LINE_SPACING_UNIFORM to that of the base font - that way the baselines are uniformly spaced.

Though for the curious, IDWriteTextLayout computes the line height as the maximum of all run heights on that line, and the height of a single run (same font and em size) just uses the design metrics: ascent + descent, plus any lineGap that happens to be present (most fonts set this to zero, but Gabriola is a good example of large line gap). Note all em sizes are in DIP's (which at typical 96DPI means 1:1, DIP's exactly == pixels), not points (1/72 inch).

(ascent + descent + lineGap) * emSize / designUnitsPerEm

like image 196
Dwayne Robinson Avatar answered Sep 18 '22 08:09

Dwayne Robinson