【问题标题】:c# DrawString - measure bounding boxes for each characterc# DrawString - 测量每个字符的边界框
【发布时间】:2013-09-01 02:39:07
【问题描述】:

在我将字符串传递给 Graphics.DrawString() 之前,我想知道每个字符的确切位置 - 不仅仅是它的偏移量和宽度,还包括它的确切边界框以及上升/下降,以便我可以在字符级别进行碰撞检测,而不是使用 Graphics.MeasureString() 给我的整个行高。

我似乎找不到任何可以准确返回每个字符的边界框的示例。如何在 c#/GDI+ 中做到这一点?

理想情况下,我希望得到如下图中的每个灰色矩形:

【问题讨论】:

  • 你的character画在哪里?我希望这不是 RichTextBoxTextBox 中的字符,因为处理文本框中的字符并不容易。
  • 看看codeproject.com/Articles/2118/…。您似乎在寻找graphics.MeasureCharacterRanges
  • 我找到了这篇文章,但 MeasureCharacterRanges() 不会随字符串中每个字符的高度而变化 - 它会给出每个字符的整个行高。
  • 啊。对不起。我会继续寻找。
  • 为什么不单独测量每个字符?不要让问题变得更复杂。

标签: c# gdi+


【解决方案1】:

对我来说,解决方案是停止使用 Graphics.DrawString。我通过将字符添加到 GraphicsPath 并从 GraphicsPath 对象获取边界来测量字符。然后我用 Graphics.FillPath 绘制字符。

DrawString 例程针对在框中绘制(更多)文本进行了优化。如果你想更精确地绘制字符,那么 DrawString 是不够的。它在文本前后添加空格。

如果您需要精确的字符绘制,请使用 GraphicsPath 进行测量,使用 FillPath 进行绘制是不错的选择。与此线程和其他地方建议的查找像素相比,GraphicsPath 操作非常快。

如果您还需要字距调整,则使用 GetKerningPairs 上的 DllImport 从字体加载字距调整表。为此,您还需要 Win32 友好的字体。为此,我建议使用 CreateFont。

祝你好运!

【讨论】:

    【解决方案2】:

    编辑:这是我使用的解决方案,基于@Sayka 的示例和获取实际字符宽度的技巧:

    如果您使用 Graphics g1 中的字体在 X、Y 处绘制字符串,则可以使用这些矩形绘制每个字符:

    public IEnumerable<Rectangle> GetRectangles(Graphics g1)
    {
        int left = X;
        foreach (char ch in word)
        {
    
            //actual width is the (width of XX) - (width of X) to ignore padding
            var size = g1.MeasureString("" + ch, Font);
            var size2 = g1.MeasureString("" + ch + ch, Font);
    
            using (Bitmap b = new Bitmap((int)size.Width + 2, (int)size.Height + 2))
            using (Graphics g = Graphics.FromImage(b))
            {
                g.FillRectangle(Brushes.White, 0, 0, size.Width, size.Height);
                g.TextRenderingHint = g1.TextRenderingHint;
                g.DrawString("" + ch, Font, Brushes.Black, 0, 0);
                int top = -1;
                int bottom = -1;
    
                //find the top row
                for (int y = 0; top < 0 && y < (int)size.Height - 1; y++)
                {
                    for (int x = 0; x < (int)size.Width; x++)
                    {
                        if (b.GetPixel(x, y).B < 2)
                        {
                            top = y;
                        }
                    }
                }
    
                //find the bottom row
                for (int y = (int)(size.Height - 1); bottom < 0 && y > 1; y--)
                {
                    for (int x = 0; x < (int)size.Width - 1; x++)
                    {
                        if (b.GetPixel(x, y).B < 2)
                        {
                            bottom = y;
                        }
                    }
                }
                yield return new Rectangle(left, Y + top, (int)(size2.Width - size.Width), bottom - top);
            }
            left += (int)(size2.Width - size.Width);
        }
    
    }
    

    看起来像这样:

    【讨论】:

      【解决方案3】:

      这是代码

      private void saykaPanel_Paint(object sender, PaintEventArgs e)
          {
              string saykaName = "Sayka";
              Font usedFont = new Font("Tahoma", 46);
      
              int lastPoint = 0;
              for (int i = 0; i < saykaName.Length; i++)
              {
                  Rectangle rect = measureAlphabet(saykaName[i], usedFont);
                  e.Graphics.FillRectangle(Brushes.Gray, new Rectangle(lastPoint + rect.Left, rect.Top, rect.Width, rect.Height));
                  e.Graphics.DrawString(saykaName[i].ToString(), usedFont, Brushes.Black, new PointF(lastPoint, 0));
                  lastPoint += rect.Width;
      
              }        
          }
      
          Rectangle measureAlphabet(char alph, Font font)
          {
              SizeF size = CreateGraphics().MeasureString(alph.ToString(), font);
              Bitmap bmp = new Bitmap((int)size.Width, (int)size.Height);
              Graphics grp = Graphics.FromImage(bmp);
              grp.FillRectangle(Brushes.White, 0, 0, bmp.Width, bmp.Height);
              grp.DrawString(alph.ToString(), font, Brushes.Black, new PointF(0, 0));
              Bitmap img = (Bitmap)bmp;
              int x = 0, y = 0, width = 0, height = 0;
      
              for (int i = 0; i < img.Width;i++)
                  for (int j = 0; j < img.Height;j++)
                      if (img.GetPixel(i, j).B < 2)
                      {
                          x = i;
                          goto jmp1;
                      }
      
          jmp1: ;
              for (int i = img.Width - 1; i >= 0; i--)
                  for (int j = 0; j < img.Height; j++)
                      if (img.GetPixel(i, j).B < 2)
                      {
                          width = i - x;
                          goto jmp2;
                      }
      
          jmp2: ;
              for (int i = 0; i < img.Height; i++)
                  for (int j = 0; j < img.Width; j++)
                      if (img.GetPixel(j, i).B < 2)
                      {
                          y = i;
                          goto jmp3;
                      }
      
          jmp3: ;
              for (int i = img.Height - 1; i >= 0; i--)
                  for (int j = 0; j < img.Width; j++)
                      if (img.GetPixel(j, i).B < 2)
                      {
                          height = i - y;
                          goto jmp4;
                      }
      
          jmp4: ;
              Rectangle resultRectangle = new Rectangle(x, y, width, height);
              return resultRectangle;
          }
      

      。 .

      .

      .

      .

      .

      从左侧开始测量第一个黑点,然后从右侧开始测量,然后从顶部开始测量,然后从底部开始测量。 如果这有帮助,请评分..

      【讨论】:

        【解决方案4】:

        如果性能不重要,我认为您可以通过将字符转换为路径并检查它们来获得最准确的结果。我不确定处理字距问题的最佳方法,但对于没有连字的字体,可能会单独找出字符的边界框,相对于它们的起点进行测量,然后确定字距中每个字符的起点细绳。对于带有连字的字体,单个字符可能并不总是不同的字形。例如,如果字体有一个“ffi”连字,那么“offing”这个词可能只包含四个字形:“o”、“ffi”、“n”和“g”;询问“i”的边界框是没有意义的。

        【讨论】:

          【解决方案5】:

          为测量目的创建一个不可见的窗口,其大小和字体设置与“真实”窗口中的完全相同。之后,您可以通过混合更多数据来扣除字符的各个边界框:

          • 选择单个字符的矩形,可以指示字符的左右边界。不幸的是,它的高度是行的全高
          • 来自 MeasureCharacterRanges() 或类似技术的数据的高度

          我同意@Jason Williams。 “填充和软糖因素”可能会稍微扭曲结果。

          【讨论】:

            【解决方案6】:

            我认为在 GDI+ 中准确测量字符串中的字符是不可能的。即使您忘记了亚像素定位和字距调整等,这意味着绘制的字符串中的字符将根据字符串中的上下文和屏幕上的位置以及 ClearType 设置而具有不同的大小,MeasureString 有添加填充和软糖因子的习惯,因此您无法轻易从中获取所需的信息。

            一种可行的方法是测量第一个字符,然后是前两个字符,然后是前三个字符,依此类推,这样您就可以粗略地了解在一次点击中绘制它们时它们的最终位置.然后你可能需要再次测量每个字符以确定它的高度。如果 MeasureString 没有给出字符高度,而是给出了字体高度,您可能不得不求助于将每个字符渲染到屏幕外位图,然后扫描像素以确定字符的真实高度。

            此时您可能会发现最好使用较低级别的字体 API,该 API 可以直接从字体中为您提供所需的信息。但是你可能需要使用 GDO+ 以外的东西来渲染它,因为它与你的工作结果对齐的机会可能并不好。

            (你能说我对 GDI+ 字体渲染器的质量存在信任问题吗?)

            【讨论】:

            • 你不是唯一一个。 GDI+ 字体渲染器是垃圾,这就是为什么没有人使用它,除了那些不知道更好的可怜的 WinForms 程序员。 TextRenderer.DrawText 是一个很多更好的选择。它封装了 GDI,自 1987 年左右以来,它就已正确处理文本。
            • @CodyGray poor WinForms programmers who don't know any better - 这样的评论没有帮助,不应该发布在 SO 上。对于特定项目,WinForms 可能是比 WPF 等其他项目更好的选择。
            • @deegee 我一个 WinForms 程序员。我比较不喜欢 WPF,继续使用 WinForms。这不是关于 WinForms 糟糕透顶的那些恼人的问题之一。我根本不相信。关键是 WinForms Graphics 类是 GDI+(不是 GDI)的包装器,并且有一个明显命名的 DrawString 方法,不知道更好的程序员尝试使用它来绘制文本。这是一个错误,他们应该使用包含 GDI 的晦涩的 TextRenderer.DrawText 方法来绘制文本。只是他们不知道,因为它很模糊。很抱歉误导。
            • @CodyGray - 很抱歉误解了您之前的评论。
            猜你喜欢
            • 2012-11-02
            • 1970-01-01
            • 2021-04-20
            • 1970-01-01
            • 2017-06-15
            • 2017-04-27
            • 2016-07-07
            • 2012-06-29
            • 1970-01-01
            相关资源
            最近更新 更多