.net - WinForms RichTextBox : how to reformat asynchronously, 没有触发 TextChanged 事件

标签 .net winforms multithreading richtextbox manualresetevent

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

后台工作方法如下所示:
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 中运行格式化程序逻辑。我选择这个是因为格式会花费“很长时间”,超过 1 秒或 2 秒,所以我无法在 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 时,按键处理程序设置该事件(将其打开)。后台工作人员正在等待同一事件。当它打开时,BG 线程将其关闭,并开始格式化。

但是如果 BG worker 已经在格式化呢?在这种情况下,新的按键可能已经更改了文本框的内容,并且到目前为止所做的任何格式化现在都可能无效,因此必须重新启动格式化。我真正想要的格式化线程是这样的:
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 to automatically scroll当 RichTextBox 具有焦点时,到选定的文本。因为格式化是在用户专注于控件、阅读和可能编辑文本的同时发生的,所以我需要一种方法来抑制滚动。我找不到使用 RTB 的公共(public)界面阻止滚动的方法,尽管我确实在 intertubes 中找到了很多人询问它。经过一些实验,我发现使用Win32 SendMessage()调用(来自 user32.dll),发送 WM_SETREDRAW Select()前后,可以防止RichTextBox在调用Select()时滚动。

因为我使用 pinvoke 来防止滚动,所以我还在 SendMessage 上使用 pinvoke 来获取或设置文本框中的选择或插入符号( EM_GETSELEM_SETSEL ),并设置选择的格式( EM_SETCHARFORMAT )。 pinvoke 方法最终比使用托管接口(interface)稍快。

响应性批量更新

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

结果是,当没有打字时,文档会自动格式化并以离散的块着色。如果用户按键之间经过了足够的时间,整个文档最终将被格式化。对于 1k XML 文档,这不到 200 毫秒,对于 30k 文档可能需要 2 秒,对于 100k 文档可能需要 10 秒。如果用户编辑文档,则正在进行的任何格式设置都会中止,格式设置会重新开始。

呼!

让我感到惊讶的是,像格式化富文本框这样看似简单而用户输入的内容却如此复杂,这让我感到惊讶。但我想不出更简单的方法,既不锁定文本框,又避免了奇怪的滚动行为。

You can view the code for the thing I described above.

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

相关文章:

c# - 在 WinForms 中数据绑定(bind)一组单选按钮的最佳方法

c# - 代码页问题

java - Java 信号量的确定性如何保证?

c - threadpools - 老板/ worker 与同行(工作人员)模型

.net - 为什么 InlineCollection 不提供索引器(无需转换)?

winforms - 如何将 app.config 移动到解决方案资源管理器中的其他文件夹?

c# - 多个物理服务器上的单个应用程序中的 Hangfire

c++ - MongoDB C++ 驱动程序中关于通过游标间接连接使用的线程安全性

.net - 在 Azure 上使用 Serilog 过滤身份验证消息

c# - 为 EntityFramework6.Npgsql 指定数据库连接字符串 - Code First