使用GraphicsPath正确绘制文本

6
如下图所示,PictureBox中的文本与TextBox中的文本不同。如果我使用Graphics.DrawString(),它可以正常工作,但是当我使用Graphics Path时,它会截断并且无法显示完整的文本。您认为我的代码哪里出了问题?

enter image description here

这是我的代码:

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);
}

1
请看这里:https://stackoverflow.com/questions/52234681/c-sharp-winforms-region-isvisible/52234895?s=1|40.1040#52234895 - TaW
2
Graphics.MeasureString() 只能告诉你使用 Graphics.DrawString() 时字符串的大小,它无法预测 Graphics.DrawPath() 的效果。可以考虑使用 GraphicsPath.GetBounds() 重载函数并传入画笔参数。 - Hans Passant
2个回答

9
GraphicsPath类以不同的方式计算文本对象的大小(如评论中已经指出)。文本大小是使用Ems边界矩形大小计算的。
Em是与目标设备上下文无关的印刷度量单位。它指的是字体最宽字母通常是字母“M”(发音为em)所占据的矩形区域。
目标Em大小可以通过两种不同的方式计算:一种包括Font descent,另一种则不包括。
float EMSize = (Font.SizeInPoints * [FontFamily].GetCellAscent([FontStyle]) 
                                  / [FontFamily].GetEmHeight([FontStyle])

或者

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

请参阅以下文档:
FontFamily.GetEmHeightFontFamily.GetCellAscentFontFamily.GetCellDescent

我在此处插入了您可以在文档中找到的图像。

Font Ascent-Descent

请参考此处包含的一般信息:
使用字体和文本 (MSDN)

该文档报告了如何翻译点、像素和 Ems 的详细信息:
如何:获取字体度量 (MSDN)


我假设您已经有一个包含/引用来自 UI 控件和所需调整的字体设置的类对象。
我在此处添加了一个类,其中包含一些与问题相关的属性的子集。

此类根据用户选择的字体大小执行一些计算。
字体大小通常以点为单位引用。然后使用当前屏幕的 DPI 分辨率将此度量转换为像素 (或从像素维度转换为点)。每个度量也转换为 Ems,如果必须使用 GraphicsPath 绘制文本,则这很方便。

计算 Ems 大小时考虑了字体的 AscentDescent
由于混合文本可以具有两个部分,如果没有,则该部分为 = 0,因此 GraphicsPath 类使用此度量更好。

要计算使用特定字体和字体大小绘制的文本的容器框,请使用 GraphicsPath.GetBounds() 方法:
([Canvas] 是提供 Paint 事件的 e.Graphics 对象的控件)

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;
}

[Canvas] 设备上绘制文本:

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);
        }
    }
}

使用此类及相关方法的可视化效果: 字体轮廓示例 作为参考使用的类对象:
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,并将其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";

事件处理程序:

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();
}

1
没错。GraphicsPath 是一个很棒的工具。经常被低估。但是一旦你理解了它的特殊性,你就可以创造奇迹(好吧,我是个粉丝 :)。看看实际的 Ems 计算。Em 大小与 Point 不同。如果你在 GraphicsPath 中使用 Points,你的测量可能会有偏差。不多,但是有偏差。 - Jimi
我有另一个问题。你是如何在组合框中加载字体系列及其自己的字体样式的? - TerribleDog
那么我可以删除SelectedIndexChanged事件,否则就像你的一样使用SelectedChangeComitted吗? - TerribleDog
1
我不知道那个。你在SelectedindexChanged中有代码吗?如果是这样,如果它们有不同的逻辑执行,您可以决定保留两者。我通常只有SelectionChangeCommitted。但那是我的习惯。无论如何,只需检查当前项目的索引是否大于等于0,并且如果没有其他阻碍,您就可以开始了。 - Jimi
明白了。谢谢。 - TerribleDog
显示剩余3条评论

6

看起来您首先提供了错误的字体大小测量,然后又增加了刷子的额外厚度。尝试改为以下方式:

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);
}

1
让我们把它变成72f,好吗? - TaW
1
还有@TaW。你总是潜伏在阴影中。哈哈 - TerribleDog

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接