【问题标题】:Unable to Calculate Position within Owner-Draw Text无法计算所有者绘制文本中的位置
【发布时间】:2012-11-12 01:23:42
【问题描述】:

我正在尝试使用 Visual Studio 2012 创建一个 Windows 窗体应用程序,该应用程序可以将插入符号放置在所有者绘制的字符串中的当前位置。但是,我一直无法找到准确计算该位置的方法。

I've done this successfully before in C++。我在 C# 中尝试了许多方法,但还不能准确定位插入符号。最初,我尝试使用 .NET 类来确定正确的位置,但后来我尝试直接访问 Windows API。在某些情况下,我接近了,但一段时间后我仍然无法准确地放置插入符号。

我创建了一个小型测试程序并在下面发布了关键部分。我还发布了整个项目here

使用的确切字体对我来说并不重要;但是,我的应用程序采用等距字体。任何帮助表示赞赏。

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++ 中一样。

【问题讨论】:

    标签: c# winforms winapi gdi+ gdi


    【解决方案1】:

    我试用了你的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++ 中完成它时,这非常有效,所以我不确定为什么它需要这么难。问题似乎是打印方法弄乱了间距。
    • 是的,这是真的。看起来文本渲染弄乱了间距。
    【解决方案2】:

    您可以使用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;
    

    确保对绘图和测量使用相同的格式标志。

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

    【讨论】:

    • 谢谢,但我已经尝试过了。出于某种原因,我的插入符号并不总是在完全正确的位置结束。我怀疑这可能与我测量字体的方式无关,而是与我解释它的方式有关。
    • 也许文本格式标志 TextFormatFlags.NoPaddingTextFormatFlags.NoPrefix 会有所帮助。
    • 你是如何获得this.g的?
    • 无论哪种方式,如果我创建一个图形对象并运行此代码,然后使用结果来定位我的插入符号,它就会大大偏离。
    • 它是Graphics 对象。上面显示的代码来自我称为“FontDescriptor”的帮助程序类。 g 只是这个类的成员。通常你会从OnPaint 方法的参数e.GraphicsPaint 事件处理程序中得到它。
    猜你喜欢
    • 2011-12-20
    • 1970-01-01
    • 2011-04-08
    • 1970-01-01
    • 2016-01-28
    • 2014-02-18
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多