I was building a custom control in C# to allow a user to
identify the positions of data fields in a flat text file. So one of the things
the control needs to know is where a given character position is in a line of
text.
Easy peasy you may say – make the font Courier New or GenericMonospace (of course) and
use the Graphics.MeasureString()
function.
Here is some test code:
private void Dummy2_Load(object sender, EventArgs e)
{
Font mFont = new Font(FontFamily.GenericMonospace, (float)12);
this.Font = mFont;
Graphics g = this.CreateGraphics();
label1.Text += Convert.ToString(g.MeasureString("A", mFont));
label2.Text += Convert.ToString(g.MeasureString("AA", mFont));
label3.Text += Convert.ToString(g.MeasureString("AbCdEfGhIj", mFont));
g.Dispose();
mFont.Dispose();
}
Which gives a result like:
So is a character 15.2 pixels wide or 12.5 pixels wide or
10.4 pixels wide or...?
If I grab a screen shot and count the pixels I can see ALL
of the characters are drawn with a width of 10 pixels. [I am currently ignoring
the supposed character height as I assume that is a notional line height (but
w.t.f?).]
So if you or I want to determine the character width on an unknown
device with unknown settings we have a bit of a problem.
One might wonder if the function returns a notional “overhead”
for any given string. But if we subtract the width returned for the single character from
the width of 10 characters and then divide by 9 – we get 9.89 pixels. So any overhead is not consistent.
The reducing “error” returned by MeasureString() as the length
of the initial test strings increased might make you wonder if a very long
string would give an accurate result. Sadly a string of 100 characters measures
at 9.94 pixels per character which at least could “round up” to the correct
value but raises new questions about just what is being measured.
Perhaps there is an optimal string length that could be
applied?
A bit more code came up with a value of 48 characters for an
optimal result on my development system – but I have no idea if that is fully “portable” for the full
range of font sizes and screen resolutions.
N.B. Just in case you were wondering, and even though you know
the code is using a monospaced font – you get identical results substituting
lower case i's into any or all of the test strings.
Addendum
After posting this I built a little class to measure the true font size. The class creates a bitmap and then draws the characters q and j onto the bitmap and scans the pixels to find the first and last rows of pixels that contain part of one of the characters - this gives the true height. Then the bitmap is cleared and two j's drawn next to each other. The class then measures the number of pixels between the first column containing a pixel for the first character and the first column of pixels used to draw the second. This gives the true width.
The code is more than a bit scrappy so feel free to correct/optimise. Please treat it as a prototype (which it is) and not production quality code.
Addendum
After posting this I built a little class to measure the true font size. The class creates a bitmap and then draws the characters q and j onto the bitmap and scans the pixels to find the first and last rows of pixels that contain part of one of the characters - this gives the true height. Then the bitmap is cleared and two j's drawn next to each other. The class then measures the number of pixels between the first column containing a pixel for the first character and the first column of pixels used to draw the second. This gives the true width.
The code is more than a bit scrappy so feel free to correct/optimise. Please treat it as a prototype (which it is) and not production quality code.
class MeasureMonoFont : IDisposable
{
private Graphics g;
private Bitmap bm;
private SolidBrush br;
StringFormat drawFormat;
private const int defBitmapSize = 100;
public MeasureMonoFont()
{
bm = new Bitmap(defBitmapSize, defBitmapSize);
g = Graphics.FromImage(bm);
br = new SolidBrush(Color.Black);
drawFormat = new StringFormat();
}
public SizeF GetCharSize(Font withFont)
{
SizeF cSize = new SizeF();
g.Clear(Color.White);
PointF cPoint = new PointF(0, 0);
g.DrawString("qj", withFont, br, cPoint, drawFormat);
Color tPixel;
int firstRow = -1;
int lastRow = -1;
for (var yp = 0; yp < defBitmapSize; yp++)
{
for (var xp = 0; xp < defBitmapSize; xp++)
{
tPixel = bm.GetPixel(xp, yp);
if (!(tPixel.ToArgb() == Color.White.ToArgb()))
{
if(firstRow == -1) { firstRow = yp; }
if(yp > lastRow) { lastRow = yp; }
}
}
}
cSize.Height = ++lastRow - firstRow;
g.Clear(Color.White);
g.DrawString("jj", withFont, br, cPoint, drawFormat);
firstRow = -1;
lastRow = -1;
bool secondChar = false;
bool firstChar = false;
bool emptyColumn = false;
for (var xp = 0; xp < defBitmapSize && lastRow == -1; xp++)
{
emptyColumn = true;
for (var yp = 0; yp < defBitmapSize && lastRow == -1; yp++)
{
tPixel = bm.GetPixel(xp, yp);
if (!(tPixel.ToArgb() == Color.White.ToArgb()))
{
if (!firstChar)
{
firstChar = true;
firstRow = xp;
}
if (secondChar) { lastRow = xp; }
emptyColumn = false;
}
}
if(firstChar && emptyColumn) { secondChar = true; }
}
cSize.Width = lastRow - firstRow;
return cSize;
}
public void Dispose()
{
g.Dispose();
bm.Dispose();
br.Dispose();
drawFormat.Dispose();
}
}
No comments:
Post a Comment