7

Visual Studio 2012 を使用して、所有者が描画した文字列内の現在の位置にキャレットを配置できる Windows フォーム アプリケーションを作成しようとしています。しかし、その位置を正確に計算する方法を見つけることができませんでした。

以前に 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);
}

編集

私が投稿したコードには、さらに不正確になる問題があります。これは、さまざまなアプローチを試した結果であり、これよりも正確なものもあります。私が探しているのは、C++ の MFC Hex Editor Control にあるように、「完全に正確」にする修正です。

4

2 に答える 2

3

あなたの を試してみたところGetFontWidth()、返された文字の幅は7でした。次に、さまざまな長さのテキストを
試してみたところ、平均文字幅が7.62988874736612で、長さが 1 から 50 のテキストに対してそれぞれ14から7.14の範囲の値がありました。TextRenderer.MearureText

使用したコードは次のとおりです。

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) があります。

これはあなたにとって何を意味しますか?

  • コードを使用してフォント文字の幅を計算すると、2 つの文字間の区切りスペースを考慮できなくなります。
  • を使用すると、TextRenderer.DrawTextさまざまな文字幅が得られ、まったく役に立たなくなります。

残りのオプションは何ですか?

  • これからわか​​る最善の方法は、テキストの配置をハードコーディングすることです。そうすれば、各文字の位置がわかり、カーソルを任意の場所に正確に配置できます。
    言うまでもなく、これには多くのコードが必要になる可能性があります。
  • 2 番目のオプションは、私が行ったようにテストを実行して、テキストのブロックの長さを見つけ、ブロックの長さで割って平均文字幅を見つけることです。
    これの問題は、コードが適切にスケーリングされない可能性があることです。たとえば、フォントのサイズやユーザーの画面 DPI を変更すると、プログラムに多くの問題が発生する可能性があります。

その他気になったこと

  • テキストの前に挿入されるスペースは、キャレットの幅 (私の場合は 2px) に 1px (合計 3px) を加えたものに相当します
  • 各文字の幅を 7.4 にハードコーディングすると、完全に機能します。
于 2012-11-30T18:31:03.943 に答える
3

to を使用しSystem.Windows.Forms.TextRendererて、文字列を描画したり、そのメトリックを計算したりできます。両方の操作にさまざまなメソッドのオーバーロードが存在します

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

TextRenderer私はその正確さと良い経験をしました。


アップデート

アプリケーションの 1 つでこのようなフォント サイズを決定したところ、完全に機能しました

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;

描画と測定の両方で同じフォーマット フラグを使用するようにしてください。

(原因のフォント サイズを決定するこの方法は、等幅フォントの場合にのみ機能します。)

于 2012-11-23T17:11:07.870 に答える