Properly draw text using GraphicsPath

As you can see in the image below, the text on the PictureBox is different from the one in the TextBox. It is working alright if I use Graphics.DrawString() but when I use the Graphics Path, it truncates and doesn't show the whole text. What do you think is wrong in my code?

enter image description here

Here is my code:

public LayerClass DrawString(LayerClass.Type _text, string text, RectangleF rect, Font _fontStyle, PaintEventArgs e)
{
    using (StringFormat string_format = new StringFormat())
    {
        rect.Size = e.Graphics.MeasureString(text, _fontStyle);
        rect.Location = new PointF(Shape.center.X - (rect.Width / 2), Shape.center.Y - (rect.Height / 2));
        if(isOutlinedOrSolid)
        {
            using (GraphicsPath path = new GraphicsPath())
            {
                path.AddString(text, _fontStyle.FontFamily, (int)_fontStyle.Style, rect.Size.Height, rect, string_format);
                e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
                e.Graphics.CompositingQuality = CompositingQuality.HighQuality;
                e.Graphics.CompositingMode = CompositingMode.SourceOver;
                e.Graphics.DrawPath(new Pen(Color.Red, 1), path);
            }
        }
        else
        {
            e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
            e.Graphics.CompositingQuality = CompositingQuality.HighQuality;
            e.Graphics.CompositingMode = CompositingMode.SourceOver;
            e.Graphics.DrawString(text,_fontStyle,Brushes.Red, rect.Location);
        }
    }

    this._Text = text;
    this._TextRect = rect;
    return new LayerClass(_text, this, text, rect);
}

Solution 1:

The GraphicsPath class calculates the size of a Text object in a different way (as already noted in the comments). The Text Size is calculated using the Ems bounding rectangle size.
An Em is a typographic measure that is independent from a destination Device context.
It refers to the rectangle occupied by a font's widest letter, usually the letter "M" (pronounced em).

The destination Em size can be calculated in 2 different ways: one includes a Font descent, the other doesn't include it.

float EMSize = (Font.SizeInPoints * [FontFamily].GetCellAscent([FontStyle]) 
                                  / [FontFamily].GetEmHeight([FontStyle])

or

float EMSize = (Font.SizeInPoints * 
               ([FontFamily].GetCellAscent([FontStyle] + 
                [FontFamily.GetCellDescent([FontStyle])) / 
                [FontFamily].GetEmHeight([FontStyle])

See the Docs about:
FontFamily.GetEmHeight, FontFamily.GetCellAscent and FontFamily.GetCellDescent

I'm inserting here the figure you can find in the Docs.

Font Ascent-Descent

Refer to the general information contained here:
Using Font and Text (MSDN)

This document reports the specifics on how to translate Points, Pixels and Ems:
How to: Obtain Font Metrics (MSDN)


I assume you already have a class object that contains/references the Font settings that come from the UI controls and the required adjustments.
I'm adding one here with some properties that contain a subset of those settings, related to the question.

This class performs some calculations based on the size of a Font selected by the user.
The Font size is usually referenced in Points. This measure is then converted in Pixels, using the current screen DPI resolution (or converted in Points from a Pixel dimension). Each measure is also converted in Ems, which comes in handy if you have to use GraphicsPath to draw the Text.

The Ems size is calculated considering both the Ascent and the Descent of the Font.
The GraphicsPath class works better with this measure, since a mixed text can have both parts and if it doesn't, that part is = 0.

To calculate the container box of a Text drawn with a specific Font and Font size, use the GraphicsPath.GetBounds() method:
([Canvas] is the Control that provides the Paint event's e.Graphics object)

using (var path = new GraphicsPath())
using (var format = new StringFormat(StringFormatFlags.NoClip | StringFormatFlags.NoWrap))
{
    format.Alignment = [StringAlignment.Center/Near/Far]; //As selected
    format.LineAlignment = [StringAlignment.Center/Near/Far]; //As selected
    //Add the Text to the GraphicsPath
    path.AddString(fontObject.Text, fontObject.FontFamily, 
                   (int)fontObject.FontStyle, fontObject.SizeInEms, 
                   [Canvas].ClientRectangle, format);
    //Ems size (bounding rectangle)
    RectangleF textBounds = path.GetBounds(null, fontObject.Outline);
    //Location of the Text
    fontObject.Location = textBounds.Location;
}

Draw the Text on the [Canvas] Device context:

private void Canvas_Paint(object sender, PaintEventArgs e)
{
    using (var path = new GraphicsPath())
    using (var format = new StringFormat(StringFormatFlags.NoClip | StringFormatFlags.NoWrap))
    {
        format.Alignment = [StringAlignment.Center/Near/Far]; //As selected
        format.LineAlignment = [StringAlignment.Center/Near/Far]; //As selected

        path.AddString(fontObject.Text, fontObject.FontFamily, (int)fontObject.FontStyle, fontObject.SizeInEms, Canvas.ClientRectangle, format);

        e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
        // When text is rendered directly
        e.Graphics.TextRenderingHint = TextRenderingHint.ClearTypeGridFit;
        // The composition properties are useful when drawing on a composited surface
        // It has no effect when drawing on a Control's plain surface
        e.Graphics.CompositingMode = CompositingMode.SourceOver;
        e.Graphics.CompositingQuality = CompositingQuality.HighQuality;

        if (fontObject.Outlined) { 
            e.Graphics.DrawPath(fontObject.Outline, path);
        }
        using(var brush = new SolidBrush(fontObject.FillColor)) {
            e.Graphics.FillPath(brush, path);
        }
    }
}

Visual effect using this class and the related methods:

Font Outline Example

The class object, to use as reference:

public class FontObject
{
    private float currentScreenDPI = 0.0F;
    private float m_SizeInPoints = 0.0F;
    private float m_SizeInPixels = 0.0F;
    public FontObject() 
        : this(string.Empty, FontFamily.GenericSansSerif, FontStyle.Regular, 18F) { }
    public FontObject(string text, Font font) 
        : this(text, font.FontFamily, font.Style, font.SizeInPoints) { }
    public FontObject(string text, FontFamily fontFamily, FontStyle fontStyle, float FontSize)
    {
        if (FontSize < 3) FontSize = 3;
        using (Graphics g = Graphics.FromHwndInternal(IntPtr.Zero)) {
            currentScreenDPI = g.DpiY; 
        }
        Text = text;
        FontFamily = fontFamily;
        SizeInPoints = FontSize;
        FillColor = Color.Black;
        Outline = new Pen(Color.Black, 1);
        Outlined = false;
    }

    public string Text { get; set; }
    public FontStyle FontStyle { get; set; }
    public FontFamily FontFamily { get; set; }
    public Color FillColor { get; set; }
    public Pen Outline { get; set; }
    public bool Outlined { get; set; }
    public float SizeInPoints {
        get => m_SizeInPoints;
        set {  m_SizeInPoints = value;
               m_SizeInPixels = (value * 72F) / currentScreenDPI;
               SizeInEms = GetEmSize();
        }
    }
    public float SizeInPixels {
        get => m_SizeInPixels;
        set {  m_SizeInPixels = value;
               m_SizeInPoints = (value * currentScreenDPI) / 72F;
               SizeInEms = GetEmSize();
        }
    }

    public float SizeInEms { get; private set; }
    public PointF Location { get; set; }
    public RectangleF DrawingBox { get; set; }

    private float GetEmSize()
    {
        return (m_SizeInPoints * 
               (FontFamily.GetCellAscent(FontStyle) +
                FontFamily.GetCellDescent(FontStyle))) /
                FontFamily.GetEmHeight(FontStyle);
    }
}

ComboBox with Font families
Create a custom ComboBox and set its DrawMode = OwnerDrawVariable:

string[] fontList = FontFamily.Families.Where(f => f.IsStyleAvailable(FontStyle.Regular)).Select(f => f.Name).ToArray();

cboFontFamily.DrawMode = DrawMode.OwnerDrawVariable;
cboFontFamily.AutoCompleteMode = AutoCompleteMode.SuggestAppend;
cboFontFamily.AutoCompleteSource = AutoCompleteSource.CustomSource;
cboFontFamily.AutoCompleteCustomSource.AddRange(fontList);
cboFontFamily.DisplayMember = "Name";
cboFontFamily.Items.AddRange(fontList);
cboFontFamily.Text = "Arial";

Event handlers:

private void cboFontFamily_DrawItem(object sender, DrawItemEventArgs e)
{
    if ((cboFontFamily.Items.Count == 0) || e.Index < 0) return;
    e.DrawBackground();
   
    var flags = TextFormatFlags.Left | TextFormatFlags.VerticalCenter;
    using (var family = new FontFamily(cboFontFamily.GetItemText(cboFontFamily.Items[e.Index])))
    using (var font = new Font(family, 10F, FontStyle.Regular, GraphicsUnit.Point)) {
        TextRenderer.DrawText(e.Graphics, family.Name, font, e.Bounds, cboFontFamily.ForeColor, flags);
    }
    e.DrawFocusRectangle();
}

private void cboFontFamily_MeasureItem(object sender, MeasureItemEventArgs e)
{
    e.ItemHeight = (int)this.Font.Height + 4;
}

private void cboFontFamily_SelectionChangeCommitted(object sender, EventArgs e)
{
    fontObject.FontFamily = new FontFamily(cboFontFamily.GetItemText(cboFontFamily.SelectedItem));
    Canvas.Invalidate();
}

Solution 2:

Seems you are providing wrong measure for font size in the first place and then adding extra thickness to the brush. Try this instead:

using (GraphicsPath path = new GraphicsPath())
{
    path.AddString(
        text,                         
        _fontStyle.FontFamily,      
        (int)_fontStyle.Style,      
        e.Graphics.DpiY * fontSize / 72f,       // em size
        new Point(0, 0),                       // location where to draw text
        string_format);          

    e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
    e.Graphics.CompositingQuality = CompositingQuality.HighQuality;
    e.Graphics.CompositingMode = CompositingMode.SourceOver;
    e.Graphics.DrawPath(new Pen(Color.Red), path);
}