这是对
WinForms RichTextBox: how to perform a formatting on TextChanged?

我有一个带有RichTextBox的Winforms应用程序,该应用程序自动突出显示所述框的内容。由于大型文档的格式化可能要花费很长时间(10秒或更长时间),因此我设置了BackgroundWorker来对RichTextBox进行重新格式化。
它遍历文本并执行以下一系列操作:

rtb.Select(start, length);
rtb.SelectionColor = color;

在执行此操作时,UI保持响应。

BackgroundWorker从TextChanged事件开始。像这样:
private ManualResetEvent wantFormat = new ManualResetEvent(false);
private void richTextBox1_TextChanged(object sender, EventArgs e)
{
    xpathDoc = null;
    nav = null;
    _lastChangeInText = System.DateTime.Now;
    if (this.richTextBox1.Text.Length == 0) return;
    wantFormat.Set();
}

后台 worker 方法如下所示:
private void DoBackgroundColorizing(object sender, DoWorkEventArgs e)
{
    do
    {
        wantFormat.WaitOne();
        wantFormat.Reset();

        while (moreToRead())
        {
            rtb.Invoke(new Action<int,int,Color>(this.SetTextColor,
                      new object[] { start, length, color} ) ;
        }

    } while (true);
}

private void SetTextColor(int start, int length, System.Drawing.Color color)
{
   rtb.Select(start, length);
   rtb.SelectionColor= color;
}

但是,对SelectionColor的每次分配都会引发TextChanged事件:一个无尽的循环。

如何区分源自外部文本更改和源自BackgroundWorker进行格式化的文本更改?

如果我可以独立于文本格式更改而检测到文本内容更改,也可以解决此问题。

最佳答案

我采用的方法是在BackgroundWorker中运行格式化程序逻辑。我之所以选择这种格式,是因为该格式将花费很长的时间,超过一秒或两秒,因此我无法在UI线程上执行此操作。

只是要重申一下这个问题:BackgroundWorker对RichTextBox.SelectionColor上的setter的每次调用都再次触发了TextChanged事件,这将重新启动BG线程。在TextChanged事件中,我找不到将“用户已键入内容”事件与“程序已格式化文本”事件区分开的方法。因此,您可以看到这将是变化的无限进展。

简单方法不起作用

一种常见的方法(as suggested by Eric)是在文本更改处理程序中运行时“禁用”文本更改事件处理。但是,这当然不适用于我的情况,因为文本更改(SelectionColor更改)是由后台线程生成的。它们不在文本更改处理程序的范围内执行。因此,在后台线程进行更改的情况下,用于过滤用户启动事件的简单方法不起作用。

其他尝试检测用户启动的更改

我尝试使用RichTextBox.Text.Length作为一种方法,以区分来自我的格式化程序线程的richtextbox中的更改与用户所做的richtextbox中的更改。我推断,如果Length不变,则该变化是由我的代码完成的格式更改,而不是用户编辑。但是,检索RichTextBox.Text属性是昂贵的,并且对于每个TextChange事件,这样做都会使整个UI变慢,这是令人无法接受的。即使这足够快,在一般情况下也不起作用,因为用户也会更改格式。并且,如果是一种类型转换操作,则用户编辑可能会产生相同长度的文本。

我希望仅捕获并处理TextChange事件以检测源自用户的更改。由于无法执行此操作,因此将应用程序更改为使用KeyPress事件和Paste事件。结果,由于格式更改(例如RichTextBox.SelectionColor = Color.Blue),我现在不会收到虚假的TextChange事件。

通知工作线程执行其工作

好的,我正在运行一个可以进行格式更改的线程。从概念上讲,它是这样做的:

while (forever)
    wait for the signal to start formatting
    for each line in the richtextbox
        format it
    next
next

如何告诉BG线程开始格式化?

我使用了ManualResetEvent。当检测到KeyPress时,按键处理程序将设置该事件(将其打开)。后台 worker 正在等待同一事件。打开它时,BG线程将其关闭,并开始格式化。

但是,如果BG工作人员已经在格式化了,该怎么办?在这种情况下,新的按键可能已更改了文本框的内容,并且到目前为止所做的任何格式化现在都可能无效,因此必须重新启动格式化。我真正想要的格式化程序线程是这样的:
while (forever)
    wait for the signal to start formatting
    for each line in the richtextbox
        format it
        check if we should stop and restart formatting
    next
next

使用此逻辑,当设置(打开)ManualResetEvent时,格式化程序线程将对其进行检测,然后对其进行重置(将其关闭),然后开始进行格式化。它遍历文本并决定如何设置其格式。格式化程序线程会定期定期检查ManualResetEvent。如果在格式化过程中发生另一个按键事件,则该事件将再次进入信号状态。当格式化程序看到重新签名后,格式化程序将退出工作并从文本开头开始重新格式化,例如Sisyphus。一种更智能的机制将从文档中发生更改的位置重新开始格式化。

延迟的开始格式设置

另一个变化:我不希望格式化程序对每个KeyPress都立即开始其格式化工作。作为人类类型,击键之间的正常停顿时间小于600-700ms。如果格式化程序立即开始进行格式化,则它将尝试在击键之间开始格式化。毫无意义。

因此,格式化程序逻辑仅在检测到击键时间超过600ms时才开始进行格式化工作。收到信号后,它将等待600毫秒,如果没有中间的按键,则表示打字已停止,应开始格式化。如果进行了中间更改,那么格式化程序将不执行任何操作,从而得出用户仍在键入的结论。在代码中:
private System.Threading.ManualResetEvent wantFormat = new System.Threading.ManualResetEvent(false);

按键事件:
private void richTextBox1_KeyPress(object sender, KeyPressEventArgs e)
{
    _lastRtbKeyPress = System.DateTime.Now;
    wantFormat.Set();
}

在colorizer方法中,该方法在后台线程中运行:
....
do
{
    try
    {
        wantFormat.WaitOne();
        wantFormat.Reset();

        // We want a re-format, but let's make sure
        // the user is no longer typing...
        if (_lastRtbKeyPress != _originDateTime)
        {
            System.Threading.Thread.Sleep(DELAY_IN_MILLISECONDS);
            System.DateTime now = System.DateTime.Now;
            var _delta = now - _lastRtbKeyPress;
            if (_delta < new System.TimeSpan(0, 0, 0, 0, DELAY_IN_MILLISECONDS))
                continue;
        }

        ...analyze document and apply updates...

        // during analysis, periodically check for new keypress events:
        if (wantFormat.WaitOne(0, false))
            break;

用户体验是键入时不会发生格式化。输入暂停后,格式化即会开始。如果再次开始键入,格式化将停止并再次等待。

在格式更改期间禁用滚动

最后一个问题是:格式化RichTextBox中的文本格式需要调用RichTextBox.Select(),当RichTextBox具有焦点时,这会导致RichTextBox to automatically scroll成为所选文本。因为格式化是在用户集中精力控制,阅读甚至编辑文本的同时进行的,所以我需要一种抑制滚动的方法。尽管我确实在intertube中发现很多人对此进行了询问,但我找不到使用RTB的公共(public)界面阻止滚动的方法。经过一些试验,我发现使用Win32 SendMessage()调用(来自user32.dll),在Select()之前和之后发送WM_SETREDRAW,可以防止在调用Select()时在RichTextBox中滚动。

因为我求助于pinvoke来防止滚动,所以我还在SendMessage上使用了pinvoke来获取或设置文本框中的选择或插入符(EM_GETSELEM_SETSEL),并设置选择的格式(EM_SETCHARFORMAT)。最终,pinvoke方法比使用托管界面要快一些。

批量更新响应性

而且由于阻止滚动会产生一些计算开销,因此我决定将对文档所做的更改分批处理。逻辑不突出显示一个连续的部分或单词,而是保留要突出显示的列表或格式更改。每隔一段时间,它可能一次对文档应用30个更改。然后,它清除列表,然后返回到分析和排队,需要进行哪些格式更改。应用这些批次的更改时,它的速度足够快,不会打扰文档的输入。

结果是,当不进行键入操作时,文档将自动格式化并以不连续的块进行着色。如果在用户按键之间经过足够的时间,则最终将格式化整个文档。对于1k XML文档,此时间不到200毫秒;对于30k文档,则可能不到2毫秒;对于100k文档,可能不到10毫秒。如果用户编辑文档,则任何正在进行的格式化都将中止,并且格式化将重新开始。

ew!

令我惊讶的是,似乎很简单,就像在用户键入格式文本框时格式化格式文本框一样。但是我无法提出没有锁定文本框但又避免了奇怪的滚动行为的任何更简单的方法。

关于.net - WinForms RichTextBox : how to reformat asynchronously,而不触发TextChanged事件,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/1457411/

10-17 02:37