Properly draw text using Graphics Path

前端 未结 2 1144
一整个雨季
一整个雨季 2020-11-27 22:37

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

相关标签:
2条回答
  • 2020-11-27 23:01

    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);
    }
    
    0 讨论(0)
  • 2020-11-27 23:08

    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.

    Refer to the general informations 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;
            e.Graphics.TextRenderingHint = TextRenderingHint.ClearTypeGridFit;
            // The composition properties are useful when drawing on a composited surface
            // when we simply draw on a Control's surface, these are useless
            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 realted methods:

    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)) {
                this.currentScreenDPI = g.DpiY; 
            }
            this.Text = text;
            this.FontFamily = fontFamily;
            this.SizeInPoints = FontSize;
            this.FillColor = Color.Black;
            this.Outline = new Pen(Color.Black, 1);
            this.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 => this.m_SizeInPoints;
            set {  this.m_SizeInPoints = value;
                   this.m_SizeInPixels = (value * 72F) / this.currentScreenDPI;
                   this.SizeInEms = GetEmSize();
            }
        }
        public float SizeInPixels {
            get => this.m_SizeInPixels;
            set {  this.m_SizeInPixels = value;
                   this.m_SizeInPoints = (value * this.currentScreenDPI) / 72F;
                   this.SizeInEms = GetEmSize();
            }
        }
    
        public float SizeInEms { get; private set; }
        public PointF Location { get; set; }
        public RectangleF DrawingBox { get; set; }
    
        private float GetEmSize()
        {
            return (this.m_SizeInPoints * 
                   (this.FontFamily.GetCellAscent(this.FontStyle) +
                    this.FontFamily.GetCellDescent(this.FontStyle))) /
                    this.FontFamily.GetEmHeight(this.FontStyle);
        }
    }
    

    Edit:

    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 ((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();
    }
    
    0 讨论(0)
提交回复
热议问题