【问题标题】:WinForms RichTextBox : how to reformat asynchronously, without firing TextChanged eventWinForms RichTextBox:如何在不触发 TextChanged 事件的情况下异步重新格式化
【发布时间】:2009-09-21 23:28:48
【问题描述】:

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

的跟进

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

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

在执行此操作时,用户界面仍保持响应。

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

后台工作方法如下所示:

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 的文本更改?

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

【问题讨论】:

    标签: .net winforms multithreading richtextbox manualresetevent


    【解决方案1】:

    我采用的方法是在 BackgroundWorker 中运行格式化程序逻辑。我之所以选择这个,是因为该格式需要“长”时间,超过 1 秒或 2 秒,所以我无法在 UI 线程上执行此操作。

    只是为了重申问题:BackgroundWorker 对 RichTextBox.SelectionColor 上的设置器的每次调用都会再次触发 TextChanged 事件,这将重新启动 BG 线程。在 TextChanged 事件中,我无法区分“用户已键入内容”事件和“程序已格式化文本”事件。所以你可以看到这将是一个无限的变化过程。

    简单的方法行不通

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

    检测用户发起的更改的其他尝试

    我尝试使用 RichTextBox.Text.Length 来区分源自我的格式化程序线程的 Richtextbox 中的更改与用户在 RichTextBox 中所做的更改。如果长度没有改变,我推断,那么改变是我的代码完成的格式改变,而不是用户编辑。但是检索 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 时,keypress 处理程序设置该事件(将其打开)。后台工作人员正在等待同一事件。当它打开时,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。如果在格式化期间发生另一个按键事件,则该事件再次进入信号状态。当格式化程序看到它被重新发出信号时,格式化程序会退出并从文本的开头重新开始格式化,就像西西弗斯一样。一种更智能的机制会从文档中发生更改的位置重新开始格式化。

    延迟起始格式

    另一个转折点:我不希望格式化程序立即对每个 KeyPress 开始其格式化工作。作为人类类型,击键之间的正常停顿小于 600-700 毫秒。如果格式化程序立即开始格式化,那么它将尝试在击键之间开始格式化。很没有意义。

    因此,格式化程序逻辑只有在检测到超过 600 毫秒的击键暂停时才开始进行格式化工作。收到信号后,它等待 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 对选定的文本进行调用。因为格式化是在用户专注于控件、阅读和编辑文本的同时发生的,所以我需要一种抑制滚动的方法。我找不到使用 RTB 的公共界面阻止滚动的方法,尽管我确实发现很多人在 intertubes 中询问它。经过一番实验,我发现使用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 秒。如果用户编辑文档,则正在进行的任何格式化都将中止,并且重新开始格式化。


    呸!

    令我惊讶的是,在用户输入内容时格式化富文本框这样看似简单的事情却如此复杂。但我想不出任何更简单的方法,既不锁定文本框,又避免奇怪的滚动行为。


    你可以view the code 我上面描述的事情。

    【讨论】:

      【解决方案2】:

      通常,当我在事件处理程序中以可能导致再次触发相同事件的方式做出反应时,我设置了一个标志,表明我已经在处理事件处理程序,检查事件处理程序顶部的标志,如果设置了标志,则立即返回:

      bool processing = false;
      
      TextChanged(EventArgs e)
      {
          if (processing) return;
      
          try
          {
              processing = true;
              // You probably need to lock the control here briefly in case the user makes a change
              // Do your processing
          }
          finally
          {
              processing = false;
          }
      }
      

      如果在执行处理时锁定控件是不可接受的,您可以检查控件上的 KeyDown 事件并在收到它时清除处理标志(如果可能很长,也可能终止当前的 TextChanged 处理)。

      编辑:

      完整的工作代码

      using System;
      using System.Collections.Generic;
      using System.Text;
      using System.Windows.Forms;
      using System.ComponentModel;
      
      namespace BgWorkerDemo
      {
          public class FormatRichTextBox : RichTextBox
          {
              private bool processing = false;
      
              private BackgroundWorker worker = new BackgroundWorker();
      
              public FormatRichTextBox()
              {
                  worker.DoWork += new DoWorkEventHandler(worker_DoWork);
              }
      
              delegate void SetTextCallback(string text);
              private void SetText(string text)
              {
                  Text = text;
              }
      
              delegate string GetTextCallback();
              private string GetText()
              {
                  return Text;
              }
      
              void worker_DoWork(object sender, DoWorkEventArgs e)
              {
                  try
                  {
                      GetTextCallback gtc = new GetTextCallback(GetText);
                      string text = (string)this.Invoke(gtc, null);
      
                      StringBuilder sb = new StringBuilder();
                      for (int i = 0; i < text.Length; i++)
                      {
                          sb.Append(Char.ToUpper(text[i]));
                      }
      
                      SetTextCallback stc = new SetTextCallback(SetText);
                      this.Invoke(stc, new object[]{ sb.ToString() });
                  }
                  finally
                  {
                      processing = false;
                  }
              }
      
              protected override void OnTextChanged(EventArgs e)
              {
                  base.OnTextChanged(e);
      
                  if (processing) return;
      
                  if (!worker.IsBusy)
                  {
                      processing = true;
                      worker.RunWorkerAsync();
                  }
              }
      
              protected override void OnKeyDown(KeyEventArgs e)
              {
                  if (processing)
                  {
                      BeginInvoke(new MethodInvoker(delegate { this.OnKeyDown(e); }));
                      return;
                  }
      
                  base.OnKeyDown(e);
              }
      
          }
      }
      

      【讨论】:

      • 如果格式化是异步完成的,这将不起作用。
      • 会的。您需要在格式化期间锁定输入控件(否则冲突的更改可能来自消息队列线程和格式化线程)。您可以通过使控件暂时只读或通过挂钩可能导致其更改的事件(与键相关的事件、一些鼠标事件 [因为它们可以选择区域] 和拖放事件(如果您允许)来锁定它那个)。
      • 使用我的方法用完整的工作代码更新了我的答案。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多