Why Is Graphics.Measurestring() Returning a Higher Than Expected Number

Why is Graphics.MeasureString() returning a higher than expected number?

From WindowsClient.net:

GDI+ adds a small amount (1/6 em) to each end of every string displayed. This 1/6 em allows for glyphs with overhanging ends (such as italic 'f'), and also gives GDI+ a small amount of leeway to help with grid fitting expansion.

The default action of DrawString will work against you in displaying adjacent runs:

  • Firstly the default StringFormat adds an extra 1/6 em at each end of each output;
  • Secondly, when grid fitted widths are less than designed, the string is allowed to contract by up to an em.

To avoid these problems:

  • Always pass MeasureString and DrawString a StringFormat based on the typographic string format (StringFormat.GenericTypographic).

    Set the Graphics TextRenderingHint to TextRenderingHintAntiAlias. This rendering method uses anti-aliasing and sub-pixel glyph positioning to avoid the need for grid-fitting, and is thus inherently resolution independent.

There are two ways of drawing text in .NET:

  • GDI+ (graphics.MeasureString and graphics.DrawString)
  • GDI (TextRenderer.MeasureText and TextRenderer.DrawText)

From Michael Kaplan's (rip) excellent blog Sorting It All Out, In .NET 1.1 everything used GDI+ for text rendering. But there were some problems:

  • There are some performance issues caused by the somewhat stateless nature of GDI+, where device contexts would be set and then the original restored after each call.
  • The shaping engines for international text have been updated many times for Windows/Uniscribe and for Avalon (Windows Presentation Foundation), but have not been updated for GDI+, which causes international rendering support for new languages to not have the same level of quality.

So they knew they wanted to change the .NET framework to stop using GDI+'s text rendering system, and use GDI. At first they hoped they could simply change:

graphics.DrawString

to call the old DrawText API instead of GDI+. But they couldn't make the text-wrapping and spacing match exactly as what GDI+ did. So they were forced to keep graphics.DrawString to call GDI+ (compatiblity reasons; people who were calling graphics.DrawString would suddenly find that their text didn't wrap the way it used to).

A new static TextRenderer class was created to wrap GDI text rendering. It has two methods:

TextRenderer.MeasureText
TextRenderer.DrawText

Note: TextRenderer is a wrapper around GDI, while graphics.DrawString is still a wrapper around GDI+.


Then there was the issue of what to do with all the existing .NET controls, e.g.:

  • Label
  • Button
  • TextBox

They wanted to switch them over to use TextRenderer (i.e. GDI), but they had to be careful. There might be people who depended on their controls drawing like they did in .NET 1.1. And so was born "compatible text rendering".

By default controls in application behave like they did in .NET 1.1 (they are "compatible").

You turn off compatibility mode by calling:

Application.SetCompatibleTextRenderingDefault(false);

This makes your application better, faster, with better international support. To sum up:

SetCompatibleTextRenderingDefault(true)  SetCompatibleTextRenderingDefault(false)
======================================= ========================================
default opt-in
bad good
the one we don't want to use the one we want to use
uses GDI+ for text rendering uses GDI for text rendering
graphics.MeasureString TextRenderer.MeasureText
graphics.DrawString TextRenderer.DrawText
Behaves same as 1.1 Behaves *similar* to 1.1
Looks better
Localizes better
Faster

It's also useful to note the mapping between GDI+ TextRenderingHint and the corresponding LOGFONT Quality used for GDI font drawing:

TextRenderingHint           mapped by TextRenderer to LOGFONT quality
======================== =========================================================
ClearTypeGridFit CLEARTYPE_QUALITY (5) (Windows XP: CLEARTYPE_NATURAL (6))
AntiAliasGridFit ANTIALIASED_QUALITY (4)
AntiAlias ANTIALIASED_QUALITY (4)
SingleBitPerPixelGridFit PROOF_QUALITY (2)
SingleBitPerPixel DRAFT_QUALITY (1)
else (e.g.SystemDefault) DEFAULT_QUALITY (0)


Samples

Here's some comparisons of GDI+ (graphics.DrawString) verses GDI (TextRenderer.DrawText) text rendering:

GDI+: TextRenderingHintClearTypeGridFit, GDI: CLEARTYPE_QUALITY:

Sample Image

GDI+: TextRenderingHintAntiAlias, GDI: ANTIALIASED_QUALITY:

Sample Image

GDI+: TextRenderingHintAntiAliasGridFit, GDI: not supported, uses ANTIALIASED_QUALITY:

Sample Image

GDI+: TextRenderingHintSingleBitPerPixelGridFit, GDI: PROOF_QUALITY:

Sample Image

GDI+: TextRenderingHintSingleBitPerPixel, GDI: DRAFT_QUALITY:

Sample Image

i find it odd that DRAFT_QUALITY is identical to PROOF_QUALITY, which is identical to CLEARTYPE_QUALITY.

See also

  • UseCompatibleTextRendering - Compatible with whaaaaaat?
  • Sorting it all out: A quick look at Whidbey's TextRenderer
  • MSDN: LOGFONT Structure
  • AppCompat Guy: GDI vs. GDI+ Text Rendering Performance
  • GDI+ Text, Resolution Independence, and Rendering Methods.
    Or - Why does my text look different in GDI+ and in GDI?

What happens when Graphics.MeasureString() is called?

public SizeF MeasureString(String text, Font font, SizeF layoutArea, StringFormat stringFormat) 
{
if (text == null || text.Length == 0)
{
return new SizeF(0, 0);
}

if (font == null)
{
throw new ArgumentNullException("font");
}

GPRECTF grfLayout = new GPRECTF(0, 0, layoutArea.Width, layoutArea.Height);
GPRECTF grfboundingBox = new GPRECTF();

int a, b;
int status = SafeNativeMethods.Gdip.GdipMeasureString(new HandleRef(this, this.NativeGraphics), text, text.Length, new HandleRef(font, font.NativeFont),
ref grfLayout,
new HandleRef(stringFormat, (stringFormat == null) ? IntPtr.Zero : stringFormat.nativeFormat),
ref grfboundingBox, out a, out b);

if (status != SafeNativeMethods.Gdip.Ok)
{
throw SafeNativeMethods.Gdip.StatusException(status);
}

return grfboundingBox.SizeF;
}

So no, no rendering occurs. Even the documentation says so:

Measures the specified string when drawn with the specified Font.

TextRenderer.MeasureText and Graphics.MeasureString mismatch in size

TextRenderer uses GDI to render the text, whereas Graphics uses GDI+. The two use a slightly different method for laying out text so the sizes are different.

Which one you should use depends on what will eventually be used to actually draw the text. If you are drawing it with GDI+ Graphics.DrawString, measure using Graphics.MeasureString. If you are drawing using GDI TextRenderer.DrawText, measure using TextRenderer.MeasureText.

If the text will be displayed inside a Windows Forms control, it uses TextRenderer if UseCompatibleTextRendering is set to false (which is the default).

Reading between the lines of your question, you seem to be using TextRenderer because you don't have a Graphics instance outside the Paint event. If that's the case, you can create one yourself to do the measuring:

using( Graphics g = someControl.CreateGraphics() )
{
SizeF size = g.MeasureString("some text", SystemFonts.DefaultFont);
}

If you don't have access to a control to create the graphics instance you can use this to create one for the screen, which works fine for measurement purposes.

using( Graphics g = Graphics.FromHwnd(IntPtr.Zero) )
{
SizeF size = g.MeasureString("some text", SystemFonts.DefaultFont);
}

Measure a String without using a Graphics object?

If you have a reference to System.Windows.Forms, try using the TextRenderer class. There is a static method (MeasureText) which takes the string and font and returns the size. MSDN Link

Graphics.MeasureString() - Finding the relative X,Y coordinates

I can feel your pain as I have been there before. Actually, I wanted to design my own Textbox not inheriting from Microsoft.Textbox control and when I researched on-line, I sort of discouraged to learn 1000 reasons why one must not reinvent the wheel and why it is so difficult to do from scratch. Highlighting selection text was one of the major challenge among the others like right-to-left, caret positioning, non-fixed fonts etc. But I decided to fly against the wind because I had my reasons to do so and finally got what I wanted. Since my text selection code was for TextBox, I had to change it to suit your requirement as you are dealing with ListBox.

Following is the code snippet:

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

namespace HowToHighlightPortionOfText
{
public static class Helper
{
private static Rectangle dummy
{
get
{
return new Rectangle(0, 0, 10, 10);
}
}
const uint H = 0x00000000;
const uint V = 0x00000001;
const uint T = 0x00000002;

#region api functions
[DllImport("user32.dll")]
static extern int DrawText(IntPtr hdc, string lpStr, int nCount, ref Dimension lpRect, int wFormat);

[DllImport("gdi32.dll")]
public static extern IntPtr SelectObject(this IntPtr hdc, IntPtr hObject);

[DllImport("gdi32.dll")]
public static extern int DeleteObject(this IntPtr hObject);

[DllImport("gdi32.dll", EntryPoint = "GdiGradientFill", ExactSpelling = true)]
static extern bool GradientFill(IntPtr hdc, Trivertex[] pVertex,
uint dwNumVertex, uint[] pMesh, uint dwNumMesh, uint dwMode);

[DllImport("gdi32")]
public static extern int SetBkMode(this IntPtr hdc, int nBkMode);

[DllImport("gdi32.dll")]
public static extern uint SetTextColor(this IntPtr hdc, int crColor);

[DllImport("gdi32.dll")]
public static extern uint SetBkColor(this IntPtr hdc, int crColor);
#endregion

#region public methods
//use this function to hilight portion of listbox item text
public static void HilightItemText(this ListBox control, int itemIndex, int startIndex, int endIndex,
Color highlightForeColor, Color highlightBackColorStart, Color? highlightBackColorEnd = null)
{
var container = control.GetItemRectangle(itemIndex);
var text = control.GetItemText(itemIndex);

using (Graphics g = control.CreateGraphics())
{
g.HighlightText(control.Font, text, container, startIndex, endIndex,
highlightForeColor, highlightBackColorStart, highlightBackColorEnd);

}
}

public static void HighlightText(this IDeviceContext dc, Font font, string text,
Rectangle container, int start, int end, Color highlightForeColor, Color highlightBackColorStart,
Color? highlightBackColorEnd, DrawTextFlags? flags = null)
{
IntPtr hdc = dc.GetHdc();
IntPtr _font = SelectObject(hdc, font.ToHfont());

Dimension dm = container;
var flag = flags.getMeasureFlag(false);

SetBkMode(hdc, ColorTranslator.ToWin32(Color.Transparent));

//first draw whole text
DrawText(hdc, text, text.Length, ref dm, 0);

//now get the highlight rectangle which will draw the highlighted text
Rectangle textBound, uptoIndex;
var rect = hdc.rangeBound(text, container, start, end, out textBound, out uptoIndex, flags: flags);
dm = rect;

var _backColorEnd = highlightBackColorEnd ?? highlightBackColorStart;
hdc.Fill(rect, highlightBackColorStart, _backColorEnd, Angle.A0);

SetTextColor(hdc, ColorTranslator.ToWin32(highlightForeColor));

if (start < 0 || start > text.Length - 1 || end < 0 || end > text.Length - 1)
throw new Exception("start and end value must be with in text length");

var _text = text.Substring(start, end - start + 1);
DrawText(hdc, _text, _text.Length, ref dm, 0);

DeleteObject(SelectObject(hdc, _font));
dc.ReleaseHdc();
}

public static Rectangle RangeBound(this IDeviceContext dc, Font font, string text,
Rectangle container, int start, int end, DrawTextFlags? flags = null)
{
Rectangle textBound, uptoIndex;
return dc.RangeBound(font, text, container, start, end, out textBound, out uptoIndex, flags);
}

public static Rectangle GetPortionRectangleToHighlight(this ListBox control, int itemIndex, int startIndex, int endIndex)
{
var container = control.GetItemRectangle(itemIndex);
var text = control.GetItemText(itemIndex);
Rectangle rect;

using (Graphics g = control.CreateGraphics())
{
rect = g.RangeBound(control.Font, text, container, startIndex, endIndex);

}
return rect;
}

public static bool Fill(this IntPtr hdc, Rectangle rc, Color c1,
Color c2, Angle angle)
{
return hdc.Fill(rc.X, rc.Y, rc.Right, rc.Bottom, c1, c2, angle);
}

public static bool Fill(this IntPtr hdc, int x0, int y0, int x1, int y1, Color c1, Color c2, Angle angle)
{
Trivertex[] t = new Trivertex[4]
{
new Trivertex(x0, y0, c1),
new Trivertex(x1, y1, c2),
new Trivertex(x0, y1, c1, c2),
new Trivertex(x1, y0, c1, c2)
};
uint[] pMesh = new uint[] { 0, 1, 2, 0, 1, 3 };

switch ((int)angle % 180)
{
case 0:
return GradientFill(hdc, t, 2, pMesh, 1, H);
case 45:
return GradientFill(hdc, t, 4, pMesh, 2, T);
case 90:
return GradientFill(hdc, t, 2, pMesh, 1, V);
case 135:
t[0].x = x1;
t[3].x = x0;
t[1].x = x0;
t[2].x = x1;
return GradientFill(hdc, t, 4, pMesh, 2, T);
default:
return false;
}
}

#endregion

#region get the highlight rectangle
static Rectangle RangeBound(this IDeviceContext dc, Font font, string text,
Rectangle container, int start, int end, out Rectangle textBound, out Rectangle uptoIndex, DrawTextFlags? flags = null)
{
textBound = Rectangle.Empty;
uptoIndex = Rectangle.Empty;

if (string.IsNullOrEmpty(text)) return Rectangle.Empty;

IntPtr hdc = dc.GetHdc();
IntPtr _font = SelectObject(hdc, font.ToHfont());

var rc = hdc.rangeBound(text, container, start, end, out textBound, out uptoIndex, flags: flags);

DeleteObject(SelectObject(hdc, _font));
dc.ReleaseHdc();
return rc;
}

static TextMeasurement charRectangle(this IntPtr hdc, string text, Rectangle container,
string wholeText = null, Point? point = null, bool adjustByPoint = false, DrawTextFlags? flags = null)
{
if (string.IsNullOrEmpty(text)) return TextMeasurement.Default;

TextMeasurement measurement = new TextMeasurement();
Rectangle textBound;

wholeText = (wholeText ?? text);

var location = container.Location;

var measureWholeText = point == null;
measurement.UserPoint = point ?? Point.Empty;

textBound = hdc.textBound(wholeText, container, flags: flags);

var rect = textBound;
var p = point ?? new Point(container.Right, container.Y);

if (!measureWholeText)
{
if (p.X > textBound.Right)
p.X = textBound.Right;
else if (p.X < textBound.Left)
p.X = textBound.X;
}

var charIndex = 0;

var result = hdc.charRectangle(text, ref p, rect, flags, measureWholeText);

charIndex = Math.Max(0, result.Item2);
var rectangles = result.Item1;

measurement.Bounds = rectangles[0];
measurement.TextBounds = (measureWholeText) ? rectangles[1] : textBound;
rectangles[1] = measurement.TextBounds;

if (!measureWholeText && adjustByPoint && charIndex > 0)
{
float middle = (float)measurement.Bounds.Left +
measurement.Bounds.Width / 2;
if (p.X > middle - 1)
{
Rectangle r;
Dimension r1 = measurement.TextBounds;

var newresult = hdc.charBound(text, charIndex + 2, ref r1,
(int)flags.getMeasureFlag(false), out r);

if (!newresult.Equals(measurement.Bounds) &&
newresult.X > measurement.Bounds.X)
{
charIndex++;
measurement.Bounds = newresult;
}
}
}
if (measurement.Bounds.Size.Width<=0)
measurement.Bounds = new Rectangle(measurement.Bounds.Location, new Size(2, 2));

measurement.CharIndex = charIndex;
measurement.Char = '\0';
measurement.Char = text[Math.Min(charIndex, text.Length - 1)];
return measurement;
}

static Tuple<Rectangle[], int> charRectangle(this IntPtr hdc, string text, ref Point p, Rectangle rect,
DrawTextFlags? flags, bool measureWholeText = false)
{
int i = 0;

int middle = text.Length / 2, start = 0;
bool first = true;
do
{
var upto = hdc.Measure(text.Substring(0, middle), null, rect, flags);
bool found = upto.Has(p);
if (!found)
{
start = middle;
middle += (text.Length - middle) / 2;
first = false;
if (start == middle) break;
}
else break;
} while (middle > 1 && text.Length - middle > 1);

if (first)
{
return hdc.charRectangle(text.Substring(0, middle),
ref p, rect, flags);
}
else
{
Rectangle[] list = new Rectangle[2];
for (i = start; i <= middle; i++)
{
if (hdc.Measure(text, out list, p, i + 1, rect, flags))
break;
}
i = Math.Max(i, 0);
return new Tuple<Rectangle[], int>(list, i);
}
}

static Rectangle charBound(this IntPtr hdc, string text, int len,
ref Dimension bounds, int flag, out Rectangle whole)
{
DrawText(hdc, text, len, ref bounds, flag);
whole = bounds;
var rc = bounds;
if (len - 1 > 0 && len <= text.Length)
{
DrawText(hdc, text.Substring(0, len - 1), len - 1, ref rc, flag);
rc = Rectangle.FromLTRB(rc.Right, bounds.Top, bounds.Right, bounds.Bottom);
}
return rc;
}

static Rectangle rangeBound(this IntPtr hdc, string text, Rectangle container, int start, int end,
out Rectangle textBound, out Rectangle uptoIndex, DrawTextFlags? flags = null)
{
textBound = Rectangle.Empty;
uptoIndex = Rectangle.Empty;

if (string.IsNullOrEmpty(text)) return Rectangle.Empty;

var location = container.Location;
textBound = hdc.textBound(text, container, flags);

Dimension rect = textBound;
var flag = flags.getMeasureFlag(false);

start++;
var text1 = text.Substring(0, start);
var rc = hdc.charBound(text1, text1.Length, ref rect, (int)flag, out uptoIndex);

end++;
var text2 = text.Substring(0, end);
DrawText(hdc, text2, text2.Length, ref rect, (int)flag);

return Rectangle.FromLTRB(rc.Left, rect.Top, rect.Right, rect.Bottom);
}

static Rectangle textBound(this IntPtr hdc, string text, Rectangle container, DrawTextFlags? flags = null)
{
Rectangle rc = Rectangle.Empty;

if (string.IsNullOrEmpty(text)) return rc;
Point p = container.Location;

var r = hdc.Measure(text, text.Length, flags: flags);
return new Rectangle(p, r.Size);
}

static DrawTextFlags getMeasureFlag(this DrawTextFlags? flags, bool textboxControl = false)
{
DrawTextFlags flag = DrawTextFlags.CalculateArea;
if (flags != null) flag |= flags.Value;

flag |= DrawTextFlags.WordBreak | DrawTextFlags.NoPrefix
| DrawTextFlags.NoPadding | DrawTextFlags.NoClipping;

if (textboxControl) flag |= DrawTextFlags.TextBoxControl;
else flag |= DrawTextFlags.SingleLine;
return flag;
}

static Rectangle RangeBound(this IntPtr hdc, string text,
Rectangle container, int start, int end, DrawTextFlags? flags = null)
{
Rectangle textBound, uptoIndex;
return hdc.rangeBound(text, container, start, end, out textBound, out uptoIndex, flags);
}

static Rectangle Measure(this IntPtr hdc, string text, int? length = null,
Rectangle? rect = null, DrawTextFlags? flags = null)
{
if (string.IsNullOrEmpty(text)) return Rectangle.Empty;
Dimension bounds = rect ?? dummy;

var len = length ?? text.Length;
var flag = flags.getMeasureFlag(false);

var i = DrawText(hdc, text, len, ref bounds, (int)flag);

return bounds;
}

static bool Measure(this IntPtr hdc, string text, out Rectangle[] rectangles, Point p,
int? length = null, Rectangle? rect = null, DrawTextFlags? flags = null)
{
rectangles = new Rectangle[2];

if (string.IsNullOrEmpty(text)) return true;
Dimension bounds = rect ?? dummy;

var len = length ?? text.Length;
var flag = flags.getMeasureFlag(false);

Rectangle rc, rc1;
rc1 = hdc.charBound(text, len, ref bounds, (int)flag, out rc);
rectangles = new Rectangle[] { rc1, rc };
return (rectangles[0].Left < bounds.Left || rectangles[0].Has(p.X));
}

static bool Has(this Rectangle rect, int x = -1,
int y = -1, int checkRightUpto = -1, int checkBottomUpto = -1)
{
if (x == -1 && y == -1)
{
x = 0;
y = 0;
}
else
{
x = x == -1 ? rect.X : x;
y = y == -1 ? rect.Y : y;
}
if (checkRightUpto == -1)
{
checkRightUpto = rect.Width;
}
if (checkBottomUpto == -1)
{
checkBottomUpto = rect.Height;
}
return x >= rect.Left && x <= rect.Left +
checkRightUpto && y >= rect.Top &&
y <= rect.Top + checkBottomUpto;
}

static bool Has(this Rectangle rect, Point p,
int checkRightUpto = -1, int checkBottomUpto = -1)
{
return rect.Has(p.X, p.Y, checkRightUpto, checkBottomUpto);
}
#endregion
}

#region structs
[StructLayout(LayoutKind.Sequential)]
public struct Dimension
{
public int Left, Top, Right, Bottom;

public Dimension(int left, int top, int right, int bottom)
{
this.Left = left;
this.Right = right;
this.Top = top;
this.Bottom = bottom;
}
public Dimension(Rectangle r)
{
this.Left = r.Left;
this.Top = r.Top;
this.Bottom = r.Bottom;
this.Right = r.Right;
}
public static implicit operator Rectangle(Dimension rc)
{
return Rectangle.FromLTRB(rc.Left, rc.Top, rc.Right, rc.Bottom);
}
public static implicit operator Dimension(Rectangle rc)
{
return new Dimension(rc);
}

public static Dimension Default
{
get { return new Dimension(0, 0, 1, 1); }
}
}

[StructLayout(LayoutKind.Sequential)]
public struct Trivertex
{
public int x;
public int y;
public ushort Red;
public ushort Green;
public ushort Blue;
public ushort Alpha;

public Trivertex(int x, int y, Color color)
: this(x, y, color.R, color.G, color.B, color.A)
{
}
public Trivertex(int x, int y, Color color, Color other)
: this(x, y, color.R, color.G, color.B, color.A, other)
{
}
public Trivertex(int x, int y, ushort red, ushort green, ushort blue, ushort alpha)
{
this.x = x;
this.y = y;
Red = (ushort)(red << 8);
Green = (ushort)(green << 8);
Blue = (ushort)(blue << 8);
Alpha = (ushort)(alpha << 8);
}
public Trivertex(int x, int y, ushort red, ushort green, ushort blue, ushort alpha, Color other)
{
this.x = x;
this.y = y;
Red = (ushort)((red + other.R / 2) << 8);
Green = (ushort)((green + other.G / 2) << 8);
Blue = (ushort)((blue + other.B / 2) << 8);
Alpha = (ushort)((alpha + other.A / 2) << 8);
}

public static ushort R(Color c)
{
return (ushort)(c.R << 8);
}
public static ushort G(Color c)
{
return (ushort)(c.G << 8);
}
public static ushort B(Color c)
{
return (ushort)(c.B << 8);
}
public static ushort R(Color c, Color c1)
{
return (ushort)(((c.R + c1.R / 2)) << 8);
}
public static ushort G(Color c, Color c1)
{
return (ushort)(((c.G + c1.G / 2)) << 8);
}
public static ushort B(Color c, Color c1)
{
return (ushort)(((c.B + c1.B / 2)) << 8);
}
}
#endregion

#region textmeasurement interface + class
public interface ITextMeasurement : ICloneable
{
int CharIndex { get; set; }
int PreviousIndex { get; }
Rectangle Bounds { get; }
Rectangle TextBounds { get; }
char Char { get; }
Point UserPoint { get; }

void CopyFrom(ITextMeasurement other);
}
public class TextMeasurement : ITextMeasurement
{
Rectangle now, textBound;

public virtual Rectangle Bounds
{
get
{
return now;
}
set { now = value; }
}
public virtual Rectangle TextBounds
{
get
{
return textBound; ;
}
set { textBound = value; }
}

public virtual int CharIndex { get; set; }
public virtual int PreviousIndex { get; set; }
public virtual char Char { get; set; }
public Point UserPoint { get; set; }


Related Topics



Leave a reply



Submit