在指定字体中确定精确的字形高度

11

我搜索了很多并尝试了很多,但我找不到合适的解决方案。

我想知道是否有一种方法可以确定指定字体中精确字形高度?

我的意思是,当我想要确定DOT字形的高度时,我应该得到小的高度,而不是带有填充或字体大小的高度。

我已经找到了确定精确字形宽度的解决方案(在这里)(我使用了第二种方法),但它对于高度不起作用。

更新: 我需要.NET 1.1的解决方案。


如果这对你有所帮助 - Nikhil Agrawal
1
如果您喜欢CodeProject上的解决方案(您提供了链接),您可以绘制一个字形并旋转图形对象,这样高度就会变成宽度,并使用相同的方法。 - cookieMonster
嗯。。如果可能的话那会很好。但是,你怎么建议“绘制和旋转”字形呢?这是否可能?你能通过回答这个问题并提供示例来分享你的解决方案吗? - Michael Z
这似乎比我预期的要棘手一些,但我想我们可以处理好这个问题 - 你使用链接文章中的哪种方法? - cookieMonster
我尝试了这个(来自链接文章的第一种方法): int width = MeasureDisplayStringWidth(g, ".", new Font("Arial", 12.0f)); //它给了我8,也许我漏掉了什么,但似乎不正确。 - cookieMonster
我在提供的文章中使用了第二种方法。是的,我认为8不是像DOT(.)这样可怜字形的正确高度。 - Michael Z
3个回答

7
这并不难获取字符指标。GDI 包含一个函数GetGlyphOutline,您可以使用 GGO_METRICS 常量调用该函数,以获取在呈现时包含字形的最小外接矩形的高度和宽度。例如,Arial 字体中的点的 10 点字形将得到一个 1x1 像素的矩形,而对于字母 I,如果字体大小为 100 点,则为 95x.14。
这些是 P/Invoke 调用的说明:
// the declarations
public struct FIXED
{
    public short fract;
    public short value;
}

public struct MAT2
{
    [MarshalAs(UnmanagedType.Struct)] public FIXED eM11;
    [MarshalAs(UnmanagedType.Struct)] public FIXED eM12;
    [MarshalAs(UnmanagedType.Struct)] public FIXED eM21;
    [MarshalAs(UnmanagedType.Struct)] public FIXED eM22;
}

[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
    public int x;
    public int y;
}

[StructLayout(LayoutKind.Sequential)]
public struct POINTFX
{
    [MarshalAs(UnmanagedType.Struct)] public FIXED x;
    [MarshalAs(UnmanagedType.Struct)] public FIXED y;
}

[StructLayout(LayoutKind.Sequential)]
public struct GLYPHMETRICS
{

    public int gmBlackBoxX;
    public int gmBlackBoxY;
    [MarshalAs(UnmanagedType.Struct)] public POINT gmptGlyphOrigin;
    [MarshalAs(UnmanagedType.Struct)] public POINTFX gmptfxGlyphOrigin;
    public short gmCellIncX;
    public short gmCellIncY;

}

private const int GGO_METRICS = 0;
private const uint GDI_ERROR = 0xFFFFFFFF;

[DllImport("gdi32.dll")]
static extern uint GetGlyphOutline(IntPtr hdc, uint uChar, uint uFormat,
   out GLYPHMETRICS lpgm, uint cbBuffer, IntPtr lpvBuffer, ref MAT2 lpmat2);

[DllImport("gdi32.dll", ExactSpelling = true, PreserveSig = true, SetLastError = true)]
static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);

实际的代码相当简单,如果您不考虑P / Invoke冗余的话。我测试了这段代码,它可以工作(您也可以调整从GLYPHMETRICS获取宽度)。
注意:这是临时代码,在现实世界中,您应该使用ReleaseHandleDeleteObject清理HDC和对象。感谢用户2173353的评论指出这一点。
// if you want exact metrics, use a high font size and divide the result
// otherwise, the resulting rectangle is rounded to nearest int
private int GetGlyphHeight(char letter, string fontName, float fontPointSize)
{
    // init the font. Probably better to do this outside this function for performance
    Font font = new Font(new FontFamily(fontName), fontPointSize);
    GLYPHMETRICS metrics;

    // identity matrix, required
    MAT2 matrix = new MAT2
        {
            eM11 = {value = 1}, 
            eM12 = {value = 0}, 
            eM21 = {value = 0}, 
            eM22 = {value = 1}
        };

    // HDC needed, we use a bitmap
    using(Bitmap b = new Bitmap(1,1))
    using (Graphics g = Graphics.FromImage(b))
    {
        IntPtr hdc = g.GetHdc();
        IntPtr prev = SelectObject(hdc, font.ToHfont());
        uint retVal =  GetGlyphOutline(
             /* handle to DC   */ hdc, 
             /* the char/glyph */ letter, 
             /* format param   */ GGO_METRICS, 
             /* glyph-metrics  */ out metrics, 
             /* buffer, ignore */ 0, 
             /* buffer, ignore */ IntPtr.Zero, 
             /* trans-matrix   */ ref matrix);

        if(retVal == GDI_ERROR)
        {
            // something went wrong. Raise your own error here, 
            // or just silently ignore
            return 0;
        }


        // return the height of the smallest rectangle containing the glyph
        return metrics.gmBlackBoxY;
    }    
}

请问您能解释一下这个注释的意思吗:“// should check Marshal.GetLastError here.”?为什么需要执行这个操作,以及应该如何执行? - Michael Z
@MichaelZ:这是一个常见的做法,就像在 C# 编程中使用 try/catch 或检查结果值一样,当调用 Win32 函数时,您需要检查"LastError"。(编辑说明:实际上,如果它出现错误,这个函数并不会调用 SetLastError,而只是返回一个常量 "GDI_ERROR",没有额外的信息。这类似于 GDI+ 中可怕的"发生了通用 GDI+ 错误"。)我会更新代码以反映这一点。 - Abel
你确定输入字体大小为points时,结果是以像素为单位的吗?我认为结果也是以points为单位的。不是吗?如果是这样,那么所有的结果都会被整数舍入,但是正如你所理解的那样,这并不是非常精确的 - 我想要浮点数! - Michael Z
虽然这个回答解决了问题,但代码存在问题。引用 MSDN 的内容:“GetHdc 和 ReleaseHdc 方法的调用必须成对出现”,以及“使用此方法(ToHfont)时,你必须使用 GDI DeleteObject 方法处理生成的 Hfont,以确保资源得到释放。” - user2173353
@user2173353:我创建这篇文章已经有一段时间了,但现在浏览它,我会认为在Graphics对象周围使用using{...}应该可以处理这个问题。如果不是这样的话,你是对的,在using块的结尾处,应该调用ReleaseHdcDeleteObject。嗯,再想想这个:我认为最好使用SafeHandle衍生类,而不是IntPtr,这将自动处理清理工作。我添加了一个警告注释到这篇文章中来提醒大家注意这一点。 - Abel
显示剩余5条评论

2

以下是涉及 WPF 的解决方案。我们创建一个中间的几何对象以便获取我们文本的精确边界框。这种解决方案的优点是它实际上不会渲染任何东西。即使您没有在界面中使用 WPF,您也可以使用此代码片段进行测量,假设字体渲染大小在 GDI 中相同或足够接近。

    var fontFamily = new FontFamily("Arial");
    var typeface = new Typeface(fontFamily, FontStyles.Normal, FontWeights.Normal, FontStretches.Normal);
    var fontSize = 15;

    var formattedText = new FormattedText(
        "Hello World",
        CultureInfo.CurrentCulture,
        FlowDirection.LeftToRight,
        typeface,
        fontSize,
        Brushes.Black);

    var textGeometry = formattedText.BuildGeometry(new Point(0, 0));

    double x = textGeometry.Bounds.Left;
    double y = textGeometry.Bounds.Right;
    double width = textGeometry.Bounds.Width;
    double height = textGeometry.Bounds.Height;

这里,“Hello world”的尺寸大约为77 x 11个单位。一个点的大小为1.5 x 1.5。

作为另一种解决方案,仍然在WPF中,您可以使用GlyphRun和ComputeInkBoundingBox()。虽然它不支持自动字体替换,但它会更加复杂。它看起来像这样:

var glyphRun = new GlyphRun(glyphTypeFace, 0, false, fontSize,
            glyphIndexList,
            new Point(0, 0),
            advanceWidths,
            null, null, null, null,
            null, null);

Rect glyphInkBox = glyphRun.ComputeInkBoundingBox();

抱歉,我无法使用此解决方案,因为我的应用程序需要使用.NET 1.1编译。 - Michael Z
@Michael Z 只是好奇,为什么有这样的要求?您需要在旧系统上部署软件吗?.NET 2.0于2005年发布。此外,如果您只需要支持几种字体,可以使用最新的.NET版本在本地预先计算字形大小,并将这些预先计算的表嵌入到您的1.1版本中。 - Jem
不幸的是,我需要支持许多字体,而且我们必须为.NET 1.1支持我们的产品。 - Michael Z

2
你能否更新问题,说明你尝试过什么?
通过点形符号,我假设你指的是这里详细介绍的标点符号?
这个字形高度是在屏幕上显示还是打印出来?
我成功修改了你发布的链接中的第一种方法,以计算匹配的垂直像素。然而,除非你愿意逐个字符绘制,否则很难确定字形的最大高度,因此这不是像文章那样的通用工作解决方案。
为了得到一个通用的工作解决方案,需要确定字符/字形最大单像素垂直区域,然后计算该区域中的像素数。
我还验证了Graphics.MeasureStringTextRenderer.MeasureTextGraphics.MeasureCharacterRanges都返回边界框,给出了类似于字体高度的数字。
另一种方法是使用Glyph.ActualHeight属性,它获取框架元素的呈现高度。这部分是WPF及其相关的GlyphTypefaceGlyphRun类。由于我只有Mono,所以无法进行测试。
获取Glyph.ActualHeight的步骤如下:
  1. 初始化GlyphRun的参数
  2. 初始化GlyphRun对象
  3. 使用glyphTypeface.CharacterToGlyphMap[text[n]]或更正确的是glyphTypeface.GlyphIndices[n]访问相关的Glyph,其中glyphTypeface是你创建的GlyphTypeface,它是从Step 1中创建的Typeface对象。
相关使用它们的资源包括

关于GDI(这些类在后台使用的是GDI或GDI+)和Windows中的字体,进一步参考资料包括:


抱歉,我无法使用此解决方案,因为我的应用程序需要使用 .NET 1.1 进行编译。顺便说一下,我已经使用了提供的问题链接中的“第二”种方法。 - Michael Z

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