6

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);
}
Jimi
  • 29,621
  • 8
  • 43
  • 61
TerribleDog
  • 1,237
  • 1
  • 8
  • 31
  • 1
    [See here](https://stackoverflow.com/questions/52234681/c-sharp-winforms-region-isvisible/52234895?s=1|40.1040#52234895) – TaW Oct 30 '18 at 10:07
  • 2
    Graphics.MeasureString() can only tell you about the size of the string when it is drawn with Graphics.DrawString(), it is not useful to predict what Graphics.DrawPath() will do. Consider the GraphicsPath.GetBounds() overload that takes a Pen. – Hans Passant Oct 30 '18 at 10:49

2 Answers2

8

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();
}
Jimi
  • 29,621
  • 8
  • 43
  • 61
  • Thanks @Jimi!!! now I've learned something. here's my output https://imgur.com/a/C0FLWcs – TerribleDog Oct 31 '18 at 00:29
  • 1
    @untargeted Yep. `GraphicsPath` is a great tool. Often underestimated. But once you understand its peculiarity, you can do wonders (well, ok, I'm a fan :). Take a look at the actual `Ems` calculations. The `Em` size is different from a `Point`. If you use `Points` with `GraphicsPath`, your measures can be off. Not much, but off. – Jimi Oct 31 '18 at 00:36
  • ,I have another question. How did you load the font family in the combobox with its own font style? – TerribleDog Oct 31 '18 at 00:48
  • Did you use `SelectionChangeCommitted` or `SelectedIndexChanged`? `Index = -1` happens when `SelectedIndex` is set the first time, when you fill the Items list. It's always -1. That's why I used `SelectionChangeCommitted`. This event is raised only after someone has *manually* selected something. If you have some other code in `SelectedIndexChanged`, the add a check in `DrawItem` and `return` if `e.Index < 0`. – Jimi Oct 31 '18 at 01:12
  • So i can delete the SelectedindexChanged and otherwise, use the SelectedChangeComitted just like yours? – TerribleDog Oct 31 '18 at 01:18
  • 1
    I don't know that. Do you have code in `SelectedindexChanged`? If so, you can decide to keep both if they have different logic to perform. I usually have only `SelectionChangeCommitted`. But that's me, my habits. Anyway, just check out that the current item `Index >= 0` and, if there' nothing else that gets in the way, you're good to go. – Jimi Oct 31 '18 at 01:25
  • Got it. Thanks. – TerribleDog Oct 31 '18 at 01:27
  • This answer is incredible, but it could be simplified by using `Font.SizeInPoints`. I believe it makes a very good and precise calculation!: `float pixelsPerPoint = (float) (graphics.DpiY / 72.0); float lineSpacingInPixels = this.GetHeight(graphics); float emHeightInPixels = lineSpacingInPixels * FontFamily.GetEmHeight(Style) / FontFamily.GetLineSpacing(Style); emHeightInPoints = emHeightInPixels / pixelsPerPoint;` – Loudenvier May 07 '20 at 17:22
6

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);
}
roozbeh S
  • 1,084
  • 1
  • 9
  • 16