无法计算Owner-Draw文本内的位置

7
我正在尝试使用Visual Studio 2012创建一个Windows Forms应用程序,该程序可以将插入符号放置在自绘字符串的当前位置。但是,我一直无法找到一种准确计算该位置的方法。 我以前在C++中成功完成了这个任务。我已经尝试了许多C#方法,但尚未能够准确放置插入符号。最初,我尝试使用.NET类来确定正确的位置,但后来我尝试直接访问Windows API。在某些情况下,我接近成功,但是经过一段时间后,我仍然不能准确地定位插入符号。
我创建了一个小型测试程序,并发布了以下关键部分。我还发布了整个项目在这里
精确使用的字体对我来说并不重要;但是,我的应用程序假定使用等宽字体。感谢任何帮助。
Form1.cs 这是我的主窗体。
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 这里是我的Windows API声明。

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);
}

编辑

我发布的代码存在一个问题,使其更加不准确。这是尝试许多不同方法的结果,其中一些方法比这个更准确。我正在寻找一个修复方案,使其变得“完全准确”,就像在我的 MFC Hex Editor Control in C++ 中一样。

2个回答

3
我尝试了你的GetFontWidth(),一个字符的宽度返回为7
然后我尝试使用TextRenderer.MearureText测量不同长度的文本,长度为1到50的文本值分别为147.14,平均字符宽度为7.62988874736612

以下是我使用的代码:

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));

为了进一步了解文本在表单上的渲染效果,我对其进行了实验。以下是表单的屏幕截图(放大了8倍)。

字体放大测量

经过仔细检查,我注意到:

  1. 字符之间存在间隔,因此文本块(1234567890)的长度为74像素。
  2. 尽管左填充为0,但在绘制文本时前面有一些间隔(3px)。

这对您意味着什么?

  • 如果您使用代码计算字体字符的宽度,则无法考虑到两个字符之间的间隔。
  • 使用 TextRenderer.DrawText 可能会给出变化的字符宽度,从而使其毫无用处。

您还有哪些选择?

  • 我认为最好的方法是硬编码文本的位置。这样您就可以知道每个字符的位置,并可以准确地将光标放置在任何所需位置。
    不用说,这可能需要很多代码。
  • 您的第二个选择是进行像我一样的测试,以查找文本块的长度,然后将其除以块的长度以查找平均字符宽度。
    问题在于,您的代码可能无法适当地缩放。例如,更改字体大小或用户屏幕DPI可能会对程序造成很多麻烦。

我观察到的其他事项

  • 插入文本前面的间隔等于插入符的宽度(2px 在我的例子中)加上1px(总共3px)。
  • 将每个字符的宽度硬编码为7.4可以完美解决问题。

谢谢。我会尝试并使用其中一些想法。正如我所说,当我在C++中完成它时,它完美地运行,所以我不确定为什么它需要这么难。问题似乎是打印方法,它正在干扰间距。 - Jonathan Wood
是的,没错。看起来文本渲染出现了间距问题。 - Alex Essilfie

3
你可以使用 System.Windows.Forms.TextRenderer 来绘制字符串并计算其度量值。存在多种方法重载来执行这两个操作。
TextRenderer.DrawText(e.Graphics, "abc", font, point, Color.Black);
Size measure = TextRenderer.MeasureText(e.Graphics, "1234567890", font);

我在使用TextRenderer时获得了良好的使用体验,它非常精确。


更新

在我的某个应用程序中,我是通过以下方式确定字体大小的,并且它完美地工作:

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;

请确保在绘制和测量时使用相同的格式标志。

(这种确定字体大小的方法仅适用于等宽字体。)


谢谢,但我已经尝试过了。出于某种原因,我的插入符号并不总是停留在完全正确的位置。我怀疑这可能与我测量字体的方式无关,而是与我解释它的方式有关。 - Jonathan Wood
也许文本格式标志 TextFormatFlags.NoPaddingTextFormatFlags.NoPrefix 会有所帮助。 - Olivier Jacot-Descombes
是的,我明白它是一个“Graphics”对象。我只是好奇你是如何获取它的,因为在测试中我已经尝试了大约六种方法。但是,再次说明,对我来说它并没有产生准确的结果。也许这与我某种程度上的理解有关。 - Jonathan Wood
你可以在窗体的加载事件中调用CreateGraphics,而不是在每次Paint事件中重新计算字体大小。我在UserControl的构造函数中调用了它,在InitializeComponent之后。 - Olivier Jacot-Descombes
我创建的简单测试项目上没有滚动条,您可以自行查看。 - Jonathan Wood
显示剩余8条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接