Unable to calculate position in Owner-Draw text

I am trying to use Visual Studio 2012 to create a Windows Forms application that can put the carriage at the current position in the line drawn by the owner. However, I could not find a way to accurately calculate this position.

I did this successfully earlier in C ++ . I tried many methods in C #, but still could not accurately position the caret. I initially tried to use .NET classes to determine the correct position, but then I tried to access the Windows API directly. In some cases, I came closer, but after a while I still can’t accurately position the carriage.

I created a small test program and posted the key parts below. I also posted the whole project here .

The exact font used is not important to me; however, my application assumes a font with a single spacing. 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 posted has a problem that makes it even more inaccurate. This is the result of trying many different approaches, more accurate than that. What I'm looking for is a fix that makes it "completely accurate", as in my MFC Hex Editor Control in C ++ .

+7
source share
2 answers

I tried your GetFontWidth() and the width of the returned character was 7 .
Then I tried TextRenderer.MearureText at different text lengths and had values ​​from 14 to 7.14 for text lengths from 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 displayed in the form. Shown below is the screen capture form (enlarged 8x).

With a private check, I noticed that

  • There was a separation between the characters. This made the text block length ( 1234567890 ) longer than 74 .
  • There is some space (3px) before the text, although the left padding is 0.

What does this mean for you?

  • If you use your code to calculate the width of a font character, you do not take into account the separation space between two characters.
  • Using TextRenderer.DrawText can give you different character widths, which makes it completely useless.

What are the remaining options?

  • The best way I can understand this is to hardcode the placement of your text. Thus, you know the position of each character and can accurately place the cursor at any desired location.
    Needless to say, this is likely to cause a lot of code.
  • The second option is to run the tests, as I did to find the length of the block of text, and then divide by the length of the block to find the average width of the character.
    The problem is that your code is unlikely to scale properly. For example, changing the font size or screen of a DPI user can cause a lot of problems for the program.

Other things that I watched

  • The space inserted in front of the text is equivalent to the width of the carriage (2px in my case) plus 1px (only 3 pixels).
  • Hard coding the width of each character in 7.4 works fine.
+3
source

You can use System.Windows.Forms.TextRenderer to draw a row, as well as to calculate its metrics. There are various method overloads for both operations.

 TextRenderer.DrawText(e.Graphics, "abc", font, point, Color.Black); Size measure = TextRenderer.MeasureText(e.Graphics, "1234567890", font); 

I did a good job with TextRenderer and its accuracy.


UPDATE

I determined the font size like this in one of my applications and it worked great

 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; 

Be sure to use the same format flags for both drawing and measurement.

(This method of determining cause font size only works for monospace fonts.)

+3
source

All Articles