I'm trying to use Visual Studio 2012 to create a Windows Forms application that can place the caret at the current position within a owner-drawn string. However, I've been unable to find a way to accurately calculate that position.
I've done this successfully before in C++. I've tried numerous methods in C# but have not yet been able to position the caret accurately. Originally, I tried using .NET classes to determine the correct position, but then I tried accessing the Windows API directly. In some cases, I came close, but after some time I still cannot place the caret accurately.
I've created a small test program and posted key parts below. I've also posted the entire project here.
The exact font used is not important to me; however, my application assumes a mono-spaced font. Any help is appreciated.
Form1.cs This is my main form.
public partial class Form1 : Form
{
private string TestString;
private int AveCharWidth;
private int Position;
public Form1()
{
InitializeComponent();
TestString = "123456789012345678901234567890123456789012345678901234567890";
AveCharWidth = GetFontWidth();
Position = 0;
}
private void Form1_Load(object sender, EventArgs e)
{
Font = new Font(FontFamily.GenericMonospace, 12, FontStyle.Regular, GraphicsUnit.Pixel);
}
protected override void OnGotFocus(EventArgs e)
{
Windows.CreateCaret(Handle, (IntPtr)0, 2, (int)Font.Height);
Windows.ShowCaret(Handle);
UpdateCaretPosition();
base.OnGotFocus(e);
}
protected void UpdateCaretPosition()
{
Windows.SetCaretPos(Padding.Left + (Position * AveCharWidth), Padding.Top);
}
protected override void OnLostFocus(EventArgs e)
{
Windows.HideCaret(Handle);
Windows.DestroyCaret();
base.OnLostFocus(e);
}
protected override void OnPaint(PaintEventArgs e)
{
e.Graphics.DrawString(TestString, Font, SystemBrushes.WindowText,
new PointF(Padding.Left, Padding.Top));
}
protected override bool IsInputKey(Keys keyData)
{
switch (keyData)
{
case Keys.Right:
case Keys.Left:
return true;
}
return base.IsInputKey(keyData);
}
protected override void OnKeyDown(KeyEventArgs e)
{
switch (e.KeyCode)
{
case Keys.Left:
Position = Math.Max(Position - 1, 0);
UpdateCaretPosition();
break;
case Keys.Right:
Position = Math.Min(Position + 1, TestString.Length);
UpdateCaretPosition();
break;
}
base.OnKeyDown(e);
}
protected int GetFontWidth()
{
int AverageCharWidth = 0;
using (var graphics = this.CreateGraphics())
{
try
{
Windows.TEXTMETRIC tm;
var hdc = graphics.GetHdc();
IntPtr hFont = this.Font.ToHfont();
IntPtr hOldFont = Windows.SelectObject(hdc, hFont);
var a = Windows.GetTextMetrics(hdc, out tm);
var b = Windows.SelectObject(hdc, hOldFont);
var c = Windows.DeleteObject(hFont);
AverageCharWidth = tm.tmAveCharWidth;
}
catch
{
}
finally
{
graphics.ReleaseHdc();
}
}
return AverageCharWidth;
}
}
Windows.cs Here are my Windows API declarations.
public static class Windows
{
[Serializable, StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct TEXTMETRIC
{
public int tmHeight;
public int tmAscent;
public int tmDescent;
public int tmInternalLeading;
public int tmExternalLeading;
public int tmAveCharWidth;
public int tmMaxCharWidth;
public int tmWeight;
public int tmOverhang;
public int tmDigitizedAspectX;
public int tmDigitizedAspectY;
public short tmFirstChar;
public short tmLastChar;
public short tmDefaultChar;
public short tmBreakChar;
public byte tmItalic;
public byte tmUnderlined;
public byte tmStruckOut;
public byte tmPitchAndFamily;
public byte tmCharSet;
}
[DllImport("user32.dll")]
public static extern bool CreateCaret(IntPtr hWnd, IntPtr hBitmap, int nWidth, int nHeight);
[DllImport("User32.dll")]
public static extern bool SetCaretPos(int x, int y);
[DllImport("User32.dll")]
public static extern bool DestroyCaret();
[DllImport("User32.dll")]
public static extern bool ShowCaret(IntPtr hWnd);
[DllImport("User32.dll")]
public static extern bool HideCaret(IntPtr hWnd);
[DllImport("gdi32.dll", CharSet = CharSet.Auto)]
public static extern bool GetTextMetrics(IntPtr hdc, out TEXTMETRIC lptm);
[DllImport("gdi32.dll")]
public static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);
[DllImport("GDI32.dll")]
public static extern bool DeleteObject(IntPtr hObject);
}
Edit
The code I've posted has an issue that makes it even more inaccurate. This is a result of trying many different approaches, some more accurate than this. What I'm looking for is a fix that makes it "fully accurate", as it is in my MFC Hex Editor Control in C++.
I tried out your GetFontWidth()
, and the width of a character returned was 7.
I then tried out TextRenderer.MearureText
on varying lengths of text and had values ranging from 14 through to 7.14 for text of length 1 to 50 respectively with an average character width of 7.62988874736612.
Here is the code I used:
var text = "";
var sizes = new System.Collections.Generic.List<double>();
for (int i = 1; i <= 50; i++)
{
text += (i % 10).ToString();
var ts = TextRenderer.MeasureText(text, this.Font);
sizes.Add((ts.Width * 1.0) / text.Length);
}
sizes.Add(sizes.Average());
Clipboard.SetText(string.Join("\r\n",sizes));
Not satisfied with the results of my little 'experiment', I decided to see how the text was rendered onto the form. Below is a screen capture of the form (magnified 8x).
On close inspection, I observed that
1234567890
) is 74 pixels long.TextRenderer.DrawText
can give you varying character widths rendering it quite uselesss.You can use the System.Windows.Forms.TextRenderer
to in order to draw the string as well as to calculate its metrics. Various method overloads for both operations exist
TextRenderer.DrawText(e.Graphics, "abc", font, point, Color.Black);
Size measure = TextRenderer.MeasureText(e.Graphics, "1234567890", font);
I have made good experiences with TextRenderer
and its accuracy.
UPDATE
I determined the font size like this in one of my applications and it worked perfectly
const TextFormatFlags textFormatFlags =
TextFormatFlags.NoPadding | TextFormatFlags.NoPrefix |
TextFormatFlags.PreserveGraphicsClipping;
fontSize = TextRenderer.MeasureText(this.g, "_", font,
new Size(short.MaxValue, short.MaxValue),
textFormatFlags);
height = fontSize.Height;
width = fontSize.Width;
Make sure to use the same format flags for both drawing and measuring.
(This way of determining the font size of cause works only for monospaced fonts.)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With