Article: Graphics.DrawString() and MeasureString()

Home Page


Consultancy

  • Service Vouchers
  • Escrow Service

Shop



Programming
  • Articles
  • Tools
  • Links

Search

 

Contact

 

PHPinfo


$_SERVER







Getting text and boxes positioned accurately

category 'KB', language C#, created 06-May-2025, version V1.0, by Luc Pattyn

with the assistance of GPT-3, a large language model from Google AI


License: The author hereby grants you a worldwide, non-exclusive license to use and redistribute the files and the source code in the article in any way you see fit, provided you keep the copyright notice in place; when code modifications are applied, the notice must reflect that. The author retains copyright to the article, you may not republish or otherwise make available the article, in whole or in part, without the prior written consent of the author.

Disclaimer: This work is provided as is, without any express or implied warranties or conditions or guarantees. You, the user, assume all risk in its use. In no event will the author be liable to you on any legal theory for any special, incidental, consequential, punitive or exemplary damages arising out of this license or the use of the work or otherwise.


Understanding and Mitigating the Hidden Offset in Graphics.DrawString

1. Introduction

The Graphics class in .NET provides several Draw methods for rendering various shapes and images on a drawing surface like a Panel. For applications involving text display and manipulation, such as text editors or components with syntax highlighting, DrawString is the primary method for drawing text.

Single string drawn with DrawString
private void DrawSingleString(Graphics g, string text, Font font, PointF location, Brush brush) {
	g.DrawString(text, font, brush, location);
}

private void pan_Paint1(object sender, PaintEventArgs e) {
	Graphics g = e.Graphics;
	float x = 10;
	string text = "01234";
	using (Font font = new Font("Consolas", 24f)) {
		PointF pt = new PointF(x, 20);
		DrawSingleString(e.Graphics, text, font, pt, Brushes.Black);
	}
}

However, a problem arises when attempting to precisely position or decorate individual text elements due to an unexpected offset in the rendering.

2. The Problem: Rightward Offset Explained

The discrepancy becomes apparent when attempting to draw adjacent text strings without any gaps. As illustrated in the figure below, while a single string drawn with DrawString appears as expected, attempting to draw the same string immediately after it, using the width reported by Graphics.MeasureString to determine the starting position, results in a noticeable gap between the two text segments.

Two strings drawn with DrawString, showing the offset
private void pan_Paint2(object sender, PaintEventArgs e) {
	Graphics g = e.Graphics;
	float x = 10;
	string text = "01234";
	using (Font font = new Font("Consolas", 24f)) {
		PointF pt = new PointF(x, 20);
		DrawSingleString(e.Graphics, text, font, pt, Brushes.Black);
		// trying to append
		float w = g.MeasureString(text, font).Width;
		pt = new PointF(x+w, 20);
		DrawSingleString(e.Graphics, text, font, pt, Brushes.Red);
	}
}

This is because MeasureString returns a bounding box that includes extra padding around the text, and DrawString doesn't start rendering at the box's left edge.

Instead, it seems to indent the text, effectively shifting it to the right.

3. Implications of the Offset

This seemingly minor offset has notable consequences. Besides the concatenation problem we've already observed, there may be more issues arising in text-centric scenarios.

For instance, if the size returned by MeasureString is used to define the visual boundaries of a text element, the rendered text might unexpectedly extend beyond these boundaries.

Similarly, if one attempts to precisely position a cursor or an indicator based on character measurements, the offset can lead to the cursor appearing slightly to the right of the expected location.

4. A Method for Precise Text Placement

As clearly demonstrated in the above figure, directly using the widths obtained from Graphics.MeasureString to position consecutive text elements results in unwanted gaps. To achieve precise concatenation, as seen in the next figure, we can utilize a method that calculates a more accurate width for a given text string.

The GetStringWidth method, shown below, determines the actual width the string will occupy when drawn with Graphics.DrawString. While MeasureString returns the size of a rectangle that is sufficiently wide to hold the text, it is typically wider than the text itself. In contrast, GetStringWidth calculates the exact horizontal advance of the drawing cursor after DrawString renders the specified string.

public static float GetStringWidth(Graphics g, string text, Font font) {
	int repeat = 10;
	string textLong = string.Concat(Enumerable.Repeat(text, repeat + 1));
	SizeF sizeShort = g.MeasureString(text, font);
	SizeF sizeLong = g.MeasureString(textLong, font);
	return (sizeLong.Width - sizeShort.Width) / repeat;
}

The following code snippet demonstrates how to draw multiple text strings without the extra padding, using the accurate width to calculate the starting position of each subsequent string directly within the Paint event:

Two strings drawn with GetStringWidth, showing precise concatenation
private void pan_Paint3(object sender, PaintEventArgs e) {
	Graphics g = e.Graphics;
	float x = 10;
	string text = "01234";
	using (Font font = new Font("Consolas", 24f)) {
		PointF pt = new PointF(x, 20);
		DrawSingleString(e.Graphics, text, font, pt, Brushes.Black);
		float w = GetStringWidth(g, text, font);
		pt = new PointF(x + w, 20);
		DrawSingleString(e.Graphics, text, font, pt, Brushes.LimeGreen);
	}
}

In this example, we first draw the initial text segment. Then, we calculate its accurate width using GetStringWidth and update the pt (PointF) to the starting position of the next segment. This ensures that the subsequent text is drawn immediately after the first, achieving precise concatenation without the gaps caused by the inherent padding in MeasureString.

However, as will be illustrated further on, while the text concatenation is precise, accurately drawing a bounding box around these individual text chunks still requires addressing the inherent left padding of DrawString.

5. Addressing the Bounding Box Issue

To visualize the bounding box reported by Graphics.MeasureString, one might naively attempt to draw a rectangle using the measured width and height, with the top-left corner aligned with the DrawString call as demonstrated in the pan_Paint4:

Single string with a naive bounding box
private void pan_Paint4(object sender, PaintEventArgs e) {
	Graphics g = e.Graphics;
	float x = 10;
	string text = "01234";
	using (Font font = new Font("Consolas", 24f)) {
		PointF pt = new PointF(x, 20);
		DrawSingleString(g, text, font, pt, Brushes.Black);
		SizeF size = g.MeasureString(text, font);
		g.DrawRectangle(Pens.Blue, pt.X, pt.Y, size.Width, size.Height);
	}
}

However this approach fails to produce accurate results when dealing with concatenated text due to DrawString's inherent left padding, as shown here:

Concatenated strings with a naive bounding box
private void pan_Paint5(object sender, PaintEventArgs e) {
	Graphics g = e.Graphics;
	float x = 10;
	string text = "01234";
	using (Font font = new Font("Consolas", 24f)) {
		PointF pt = new PointF(x, 20);
		DrawSingleString(e.Graphics, text, font, pt, Brushes.Black);
		SizeF size = g.MeasureString(text, font);
		g.DrawRectangle(Pens.Red, pt.X, pt.Y, size.Width, size.Height);
		float w = GetStringWidth(g, text, font);
		pt = new PointF(x + w, 20);
		DrawSingleString(e.Graphics, text, font, pt, Brushes.LimeGreen);
	}
}

To accurately draw visual boundaries, such as rectangles, around precisely positioned text elements, we need to account for this padding.

The GetStringPaddingEstimate method provides an estimate of this padding:

public static float GetStringPaddingEstimate(Graphics g, Font font) {
	int repeat = 10;
	string text = new string('=', 20);
	string textLong = string.Concat(Enumerable.Repeat(text, repeat + 1));
	SizeF sizeShort = g.MeasureString(text, font);
	SizeF sizeLong = g.MeasureString(textLong, font);
	float stringWidth = (sizeLong.Width - sizeShort.Width) / repeat;
	return (sizeShort.Width - stringWidth) / 2;
}

The following examples demonstrate how to use this padding estimate to draw correctly aligned bounding boxes around the segments of precisely concatenated text.

The first example shows a green bounding box correctly enclosing the first "01234" segment.

Concatenated strings with a correct bounding box around the first segment
private void pan_Paint6(object sender, PaintEventArgs e) {
	Graphics g = e.Graphics;
	float x = 10;
	string text = "01234";
	using (Font font = new Font("Consolas", 24f)) {
		float padding = GetStringPaddingEstimate(g, font);

		PointF pt1 = new PointF(x, 20);
		DrawSingleString(g, text, font, pt1, Brushes.Black);
		float w = GetStringWidth(g, text, font);
		PointF pt2 = new PointF(pt1.X + w, pt1.Y);
		DrawSingleString(g, text, font, pt2, Brushes.Black);
		PointF pt3 = new PointF(pt2.X + w, pt2.Y);
		DrawSingleString(g, text, font, pt3, Brushes.Black);

		// Correct box around the first chunk
		SizeF size = g.MeasureString(text, font);
		g.DrawRectangle(Pens.LimeGreen, pt1.X + padding, pt1.Y, w, size.Height);
	}
}

The second example shows a green bounding box correctly enclosing the middle "01234" segment.

Concatenated strings with a correct bounding box around the middle segment
private void pan_Paint7(object sender, PaintEventArgs e) {
	Graphics g = e.Graphics;
	float x = 10;
	string text = "01234";
	using (Font font = new Font("Consolas", 24f)) {
		float padding = GetStringPaddingEstimate(g, font);

		PointF pt1 = new PointF(x, 20);
		DrawSingleString(g, text, font, pt1, Brushes.Black);
		float w = GetStringWidth(g, text, font);
		PointF pt2 = new PointF(pt1.X + w, pt1.Y);
		DrawSingleString(g, text, font, pt2, Brushes.Black);
		PointF pt3 = new PointF(pt2.X + w, pt2.Y);
		DrawSingleString(g, text, font, pt3, Brushes.Black);

		// Correct box around the middle chunk
		SizeF size = g.MeasureString(text, font);
		g.DrawRectangle(Pens.LimeGreen, pt2.X + padding, pt2.Y, w, size.Height);
	}
}

The final example shows a green bounding box correctly enclosing the last "01234" segment, with added padding on the right.

Concatenated strings with a correct bounding box around the last segment
private void pan_Paint8(object sender, PaintEventArgs e) {
	Graphics g = e.Graphics;
	float x = 10;
	string text = "01234";
	using (Font font = new Font("Consolas", 24f)) {
		float padding = GetStringPaddingEstimate(g, font);

		PointF pt1 = new PointF(x, 20);
		DrawSingleString(g, text, font, pt1, Brushes.Black);
		float w = GetStringWidth(g, text, font);
		PointF pt2 = new PointF(pt1.X + w, pt1.Y);
		DrawSingleString(g, text, font, pt2, Brushes.Black);
		PointF pt3 = new PointF(pt2.X + w, pt2.Y);
		DrawSingleString(g, text, font, pt3, Brushes.Black);

		// Correct box around the last chunk
		SizeF size = g.MeasureString(text, font);
		g.DrawRectangle(Pens.LimeGreen, pt3.X + padding, pt3.Y, w + padding, size.Height);
	}
}

6. Refactoring with Extension Methods

This refactoring is optional; it does improve code organization and makes the GetStringWidth and GetStringPaddingEstimate methods more discoverable and accessible.

Refactoring them into extension methods allows us to call these methods directly on the Graphics object, as if they were built-in methods.

First, we define a static class to hold the extension methods:

public static class GraphicsExtensions {
    public static float GetStringWidth(this Graphics g, string text, Font font) {
        int repeat = 10;
        string textLong = string.Concat(Enumerable.Repeat(text, repeat + 1));
        SizeF sizeShort = g.MeasureString(text, font);
        SizeF sizeLong = g.MeasureString(textLong, font);
        return (sizeLong.Width - sizeShort.Width) / repeat;
    }

    public static float GetStringPaddingEstimate(this Graphics g, Font font) {
        int repeat = 10;
        string text = new string('=', 20);
        string textLong = string.Concat(Enumerable.Repeat(text, repeat + 1));
        SizeF sizeShort = g.MeasureString(text, font);
        SizeF sizeLong = g.MeasureString(text, font);
        float stringWidth = (sizeLong.Width - sizeShort.Width) / repeat;
        return (sizeShort.Width - stringWidth) / 2;
    }
}

Note the this keyword before the Graphics g parameter in both method signatures. This signifies that these are extension methods for the Graphics class.

With these extension methods in place, we can now rewrite the pan_Paint8 method (and all other similar methods) in a more concise and readable manner:

With these extension methods in place, we can now rewrite the pan_Paint8 method (and all other similar methods) in a more concise and readable manner:

private void pan_Paint8_WithExtensions(object sender, PaintEventArgs e) {
	Graphics g = e.Graphics;
	float x = 10;
	string text = "01234";
	using (Font font = new Font("Consolas", 24f)) {
		float padding = g.GetStringPaddingEstimate(font);
		PointF pt1 = new PointF(x, 20);
		DrawSingleString(g, text, font, pt1, Brushes.Black);
		float w = g.GetStringWidth(text, font);
		PointF pt2 = new PointF(pt1.X + w, pt1.Y);
		DrawSingleString(g, text, font, pt2, Brushes.Black);
		PointF pt3 = new PointF(pt2.X + w, pt2.Y);
		DrawSingleString(g, text, font, pt3, Brushes.Black);
		// Correct box around the last chunk
		SizeF size = g.MeasureString(text, font);
		g.DrawRectangle(Pens.Green, pt3.X + padding, pt3.Y, w + padding, size.Height);
	}
}

The functionality remains the same, but the code is now cleaner and more expressive.

7. Limitations and Further Considerations

While the GetStringWidth and GetStringPaddingEstimate methods provide a practical solution for addressing the Graphics.DrawString offset and accurately positioning text and bounding boxes, it's important to acknowledge their limitations and potential areas for further consideration.

First, the GetStringPaddingEstimate method relies on the assumption that the left padding applied by DrawString is consistent across all characters within a given font and size. While this holds true for most monospaced fonts, it might not be entirely accurate for proportional fonts, where character spacing can vary. In such cases, the estimated padding might need to be adjusted on a per-character basis for the highest precision.

Second, the examples provided in this article focus on horizontal concatenation and simple rectangular bounding boxes. Applying these methods to more complex layouts, such as vertical or curved text, or more intricate visual decorations, might require further refinement or adaptation of the padding calculation.

Finally, it's worth noting that the observed offset behavior of Graphics.DrawString might vary slightly across different operating systems, graphics drivers, or .NET Framework versions. While the methods presented here offer a robust workaround, it's always advisable to perform thorough testing in the target environment to ensure consistent results.

8. Conclusion

The inherent rightward offset in Graphics.DrawString can complicate precise text layout in .NET, leading to gaps in concatenation and misaligned bounding boxes.

This article presented GetStringWidth for accurate text width measurement and GetStringPaddingEstimate to estimate DrawString's left padding. Using these methods allows for finer control over text positioning and the accurate drawing of bounding boxes, crucial for applications demanding precise text rendering like editors and code viewers.

While acknowledging certain limitations, these workarounds offer a significant improvement over Graphics.MeasureString alone, empowering developers to create more visually accurate and polished user interfaces.



Perceler

Copyright © 2012, Luc Pattyn

Last Modified 04-May-2025