问题
Before I pass a string to Graphics.DrawString(), I'd like to know exactly where each character will be - not just it's offset and width, but its exact bound box with ascent/descent figured in so that I can do collision detection at the character level rather than using the entire line-height that Graphics.MeasureString() is giving me.
I can't seem to find any examples for this that accurately return the bounding box for each character. How can this be done in c#/GDI+ ?
Ideally, I'd like to get each of the gray Rectangles as in this image:
回答1:
I don't think it's possible to accurately measure characters within a string in GDI+. Even if you forget subpixel positioning and kerning, etc, which mean that characters in the drawn string will be different sizes depending on their context in the string and position on screen and ClearType settings, MeasureString has a habit of adding padding and fudge factors so you can't easily get the desired info out of it.
One approach that sortof works is to Measure the first character, then the first two characters, then the first three characters, and so on, so that you get a rough idea where they will all end up when drawn in a single hit. Then you'll probably need to Measure each char again to determine its height. If MeasureString doesn't give the char height but gives the font height instead, you might have to resort to rendering each character to an offscreen bitmap and then scanning the pixels to determine the true height of the character.
At that point you may find it's better to p/invoke off to a lower level font API that just gives you the information you need directly from the font. But then you'll probably need to use something other than GDO+ to render it as the chances of it lining up with what you've worked out may not be good.
(can you tell that I have a trust issue with the quality off the GDI+ font renderer yet?)
回答2:
EDIT: This is the solution I used, building on @Sayka's example and a trick to get actual char width:
If you're drawing a string at X, Y using Font in Graphics g1, you can draw each char using these Rectangles:
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);
}
}
Looks like this:
回答3:
Here's the code
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;
}
. .
.
.
.
.
Measured first black point from the left, then frm right, then frm up and frm bottom.. Rate if this helps..
回答4:
Create an invisible window for measurement purposes with exactly the same size and font settings what you have in the "real" window. After that you can deduct the individual bounding boxes of the characters by blending together more data:
- rectangle of the selection of the individual character, that can give indication of the left and right bounds of the character. Unfortunately the height of this is the full height of the row
- height from data from MeasureCharacterRanges() or similar technique
I agree with @Jason Williams though. "padding and fudge factors" can distort the results a little.
回答5:
If performance isn't critical, I would think you could get the most accurate results by converting the characters to paths and examining those. I'm not sure the best way to handle kerning issues, but for fonts without ligatures one could probably figure out the bounding boxes for characters in isolation, measured relative to their starting points, and then determining the starting points of each character in the kerned string. For fonts with ligatures, individual characters may not always have distinct glyphs. For example, if a font has an "ffi" ligature, the word "offing" may contain only four glyphs: "o", "ffi", "n", and "g"; it would be meaningless to ask about the bounding box of the "i".
回答6:
For me, the solution was to stop using Graphics.DrawString. I measured the characters by adding them to a GraphicsPath and getting the bounds from the GraphicsPath object. Then I draw the characters with Graphics.FillPath.
DrawString routine is optimized for drawing (more) text in a box. If you want to draw characters more precisely then DrawString is not sufficient. It adds space in front and after the text.
If you need precise character drawing, measure with GraphicsPath and drawing with FillPath is the way to go. Compared to looking up pixels as suggested in this thread and elsewhere GraphicsPath operations are blazingly fast.
If you also need Kerning then load the Kerning table from the font using DllImport on GetKerningPairs. For this, you also need the font in a Win32 friendly way. For that I suggest CreateFont.
Good luck!
来源:https://stackoverflow.com/questions/18497792/c-sharp-drawstring-measure-bounding-boxes-for-each-character