检测 RichTextBox 中的强制分行

Detecting forced line split in RichTextBox

我有一个分析管道格式 (HL7) 数据消息的应用程序,为此,它有一个与 RichTextBox 同步的 DataGridView。具体来说,当你点击DataGridView中的一个属性时,它会跳转到RichTextBox中的相应位置,反之亦然。

RichTextBox 禁用自动换行,因此我可以轻松地将编辑器中的行与实际数据中的行匹配。

但是,我目前必须处理其中某些部分包含二进制文件的 Base64 转储的消息,并且大内容使得富文本框无论如何都会换行。这使计算变得混乱,当匹配实际消息文本中的 returned 位置时,我得到错误的数据,分析失败,并且通常,当实际的下一行时,我得到一个 ArgumentOutOfRangeException比该行上的点击位置短。

这是代码:

/// <summary>Gets the cursor position as Point, with Y as line number and X as index on that line.</summary>
/// <returns>The cursor position as Point, with Y as line number and X as index on that line</returns>
protected Point GetCursorPosition()
{
    Int32 selectionStart = this.rtxtMessage.SelectionStart;
    Int32 currentLine = this.rtxtMessage.GetLineFromCharIndex(selectionStart);
    Int32 currentPos = selectionStart - this.rtxtMessage.GetFirstCharIndexFromLine(currentLine);
    return new Point(currentPos, currentLine);
}

正确的行为:

单击此函数时,函数将 return 指向 [28, 4]。

强制换行的错误行为:

单击此函数时,函数将 return 指向 [6,5],而实际上它应该是 [2813,4]。这会导致它显示对下一行的分析,并且如前所述,如果单击该行中超出下一个分析行末尾的位置,则会导致 ArgumentOutOfRangeException.

有什么办法可以弥补这种强制分线吗?我需要能够准确判断在实际文本中的位置来做分析。

请注意,分割线似乎无法预测;我不知道它尝试拆分后的最大长度是多少,或者它决定拆分的字符是可能的。

另请注意,两个称为 RichTextBox 的函数,即 GetLineFromCharIndexGetFirstCharIndexFromLine,正确对应于屏幕上实际显示的内容...但屏幕上显示的是真实数据的错误表示。事实上,它甚至不对应 RichTextBox 自己的 .Lines 属性 的输出,它以纯文本行数组的形式给出了内容。

虽然我宁愿避免使用 .Lines 属性,因为我注意到一般来说,从富文本框中提取文本的功能相当慢。

在我寻找解决方案的过程中,我发现,正如我担心的那样,RichTextBox.Lines 并不是一个简单的无害数组指针,而是一个将富文本框内容转换为纯文本的复杂操作, 在这种情况下使用它会严重影响性能。

然而,这让我意识到在整个项目中我的代码 很多 已经 访问 属性随机操作,尤其是在任何行更改时发生的分析部分。

我决定为那个行数组创建一个缓存变量,它在 RichTextBoxTextChanged 事件中被清除。所有在 RichTextBox 上获取行的实例都被调用这个小函数所取代:

private String[] GetTextboxLines()
{
    if (this.m_LineCache != null)
        return this.m_LineCache;
    String[] lines = this.rtxtMessage.Lines;
    this.m_LineCache = lines;
    return lines;
}

当简单地在富文本编辑器中输入文本时,这仍然相当繁重,因为任何击键基本上都会清除数组,然后分析器操作会再次获取它,但由于该工具首先是分析器,而且仅其次是编辑,这不是一个大问题。即便如此,RichTextBox.Lines 在这样一个击键后的分析中被多次调用,所以总体而言,结果仍然 得到了极大的优化。

有了这个系统,lines 数组的使用在我的小 GetCursorPosition() 函数中又变得可行了,所以我也调整它以利用新的缓存值:

    /// <summary>Gets the cursor position as Point, with Y as line number and X as index on that line.</summary>
    /// <returns>The cursor position as Point, with Y as line number and X as index on that line</returns>
    protected Point GetCursorPosition()
    {
        Int32 selectionStart = this.rtxtMessage.SelectionStart;
        String[] lines = this.GetTextboxLines();
        Int32 nrOfLines = lines.Length;
        Int32 y;
        for (y = 0; y < nrOfLines; y++)
        {
            Int32 lineLen = lines[y].Length;
            // Can be equal if at the very end of a line.
            if (selectionStart <= lineLen)
                return new Point(selectionStart, y);
            // +1 to compensate for the line break character,
            // which is only one byte in a rich text box.
            selectionStart -= (lineLen + 1);
        }
        return new Point(0, nrOfLines - 1);
    }

看来,如果您通过发送 EM_SETTYPOGRAPHYOPTIONS message 启用 richedit 控件的高级排版选项,则长文本行的强制换行不会发生 RichTextBox.WordWrap 属性 设置为假的。

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
        richTextBox1.HandleCreated += RTBHandledCreated;
        FillRTB();
    }

    [DllImport("user32", CharSet = CharSet.Auto)]
    private static extern int SendMessage(IntPtr hWnd, int msg, int wParam, int lParam);

    private void RTBHandledCreated(object sender, EventArgs e)
    {
        const Int32 WM_USER = 0x400;
        const Int32 EM_SETTYPOGRAPHYOPTIONS = WM_USER + 202;
        const Int32 EM_GETTYPOGRAPHYOPTIONS = WM_USER + 203;
        const Int32 TO_ADVANCEDTYPOGRAPHY = 1;
        const Int32 TO_SIMPLELINEBREAK = 2;
        SendMessage(richTextBox1.Handle, EM_SETTYPOGRAPHYOPTIONS, TO_ADVANCEDTYPOGRAPHY, TO_ADVANCEDTYPOGRAPHY);
    }

    private void FillRTB()
    {
        for (Int32 i = 0; i <= 3; i++)
        {
            richTextBox1.AppendText($"Line {i}: ");
            if (i == 1 || i == 3 )
            {
                StringBuilder sb = new StringBuilder(100000);
                for (Int32 j = 0; j < sb.Capacity; j += 10)
                {
                    for (Int32 k = 0; k <= 9; k++)
                    {
                        sb.Append(k.ToString());
                    }
                }
                richTextBox1.AppendText(sb.ToString());
            }
            if (i != 3)
            {
                richTextBox1.AppendText($"{Environment.NewLine}");
            }
        }
        richTextBox1.SelectionStart = 0;
    }

    private void richTextBox1_SelectionChanged(object sender, EventArgs e)
    {
        label1.Text = richTextBox1.GetLineFromCharIndex(richTextBox1.SelectionStart).ToString();
    }
}

请注意,我最初在对 OP 的自我回答的评论中提到,使用 "Text Object Model" 的解决方案是可能的。在对这项技术进行更彻底的测试时,我发现它只对第一行强制换行文本是准确的,之后它在确定行位置时包括了之前的换行。因此,我没有展示该方法。