7

I want to measure the height of the text given a certain width of available canvas. The text that I pass in is really long and I know will wrap. To that end, I call the following:

using System.Windows.Forms;
...
string text = "Really really long text that is sure to wrap...";
Font font = new Font("Arial", 14);
Size canvas = new Size(1100, 850);
Size size = TextRenderer.MeasureText(text, font, canvas);

No matter what I pass in for canvas, it always returns 14 for size.Height.

Am I missing something simple?

Matthew Strawbridge
  • 19,940
  • 10
  • 72
  • 93
AngryHacker
  • 59,598
  • 102
  • 325
  • 594

2 Answers2

8

Please, use the TextFormatFlags measure parameter as shown below:

Size size = TextRenderer.MeasureText(text, font, canvas, TextFormatFlags.WordBreak);
DmitryG
  • 17,677
  • 1
  • 30
  • 53
  • 2
    This gets me 99% there, but the result is not very accurate and the last line of the string ends up being cut off slightly (like 33% from the bottom). – AngryHacker Dec 02 '11 at 22:10
  • Unfortunately, I cannot reproduce any line-cutting on my side. Please, post the real string and the real size, that gets a problem on your side. Or please also try to combine the TextFormatFlags.WordBreak with the TextFormatFlags.ExternalLeading. – DmitryG Dec 05 '11 at 08:35
  • I tried it, but no luck. Still cutting off. I can't say that it's the fault of the API. I am measuring the text and handing it off to a 3rd party control - it could be that it uses a different rendering technique. – AngryHacker Dec 05 '11 at 22:33
1

DimitryG's solution seems to work great, but only when there is no word big enough to fill more than an entire row. If such word exists, the width will be bigger than the proposed width. There is the flag TextFormatFlags.EndEllipsis for this case, however I didn't manage to combine the flags in a way so the output is correct (if I use TextFormatFlags.WordEllipsis | TextFormatFlags.WordBreak the width is correct, but the height is not updated when Word Ellipsis takes place, which means that the big word will be trimmed, but the height will be the same as if it's not trimmed). I also tried the flag TextFormatFlags.EndEllipsis but with no results.

So until someone makes this clear, I propose using a TextBox for word wrap, and then multiply the number of lines in the TextBox with the Font's height.

Code:

int MeasureMultilineTextHeigh(string text, Font font, int proposedWidth)
{
    // Exception handling.

    TextBox textBox = new TextBox()
    {
        Multiline = true,
        BorderStyle = BorderStyle.None,
        Width = proposedWidth,
        Font = font,
        Text = text,
    };

    int lineCount = textBox.GetLineFromCharIndex(int.MaxValue) + 1;
    int fontHeight = TextRenderer.MeasureText("X", font).Height;

    return lineCount * fontHeight;
}

However this approach has one problem: If, and only if Multiline property of a TextBox is set to true, every Font will have its own left and right padding. See this stackoverflow.com question and this social.msdn.microsoft.com question for more details. So this means that in some situations the returned value might be bigger than expected. To solve this problem you can use the SetPadding function to remove the padding (you can find the method as an answer in the first question), code:

private const int EM_SETRECT = 0xB3;

[DllImport(@"User32.dll", EntryPoint = @"SendMessage", CharSet = CharSet.Auto)]
private static extern int SendMessageRefRect(IntPtr hWnd, uint msg, int wParam, ref RECT rect);

[StructLayout(LayoutKind.Sequential)]
private struct RECT
{
    public readonly int Left;
    public readonly int Top;
    public readonly int Right;
    public readonly int Bottom;

    private RECT(int left, int top, int right, int bottom)
    {
        Left = left;
        Top = top;
        Right = right;
        Bottom = bottom;
    }

    public RECT(Rectangle r) : this(r.Left, r.Top, r.Right, r.Bottom) { }
}

public void SetPadding(TextBox textBox, Padding padding)
{
    var rect = new Rectangle(padding.Left, padding.Top, textBox.ClientSize.Width - padding.Left - padding.Right, textBox.ClientSize.Height - padding.Top - padding.Bottom);
    RECT rc = new RECT(rect);
    SendMessageRefRect(textBox.Handle, EM_SETRECT, 0, ref rc);
}

int MeasureMultilineTextHeigh(string text, Font font, int proposedWidth)
{
    // Exception handling.

    TextBox textBox = new TextBox()
    {
        Multiline = true,
        BorderStyle = BorderStyle.None,
        Width = proposedWidth,
        Font = font,
    };
    SetPadding(textBox, Padding.Empty);
    textBox.Text = text;

    int lineCount = textBox.GetLineFromCharIndex(int.MaxValue) + 1;
    int fontHeight = TextRenderer.MeasureText("X", font).Height;

    return lineCount * fontHeight;
}

Needed using statements:

using System;
using System.Windows.Forms;
using System.Drawing;
using System.Runtime.InteropServices;

I hope this helps. Sorry for my English, if I made any mistake.

dCake
  • 47
  • 6
  • 1
    "DimitryG's solution seems to work great, but only when there is no word big enough to fill more than an entire row." - Try using `TextFormatFlags.TextBoxControl | TextFormatFlags.WordBreak` to format it like a multi-line TextBox. A long first word will wrap to the next line. – TnTinMn Nov 07 '20 at 01:04