Che*_*eso 6 .net multithreading richtextbox manualresetevent winforms
这是对WinForms RichTextBox的跟进
:如何在TextChanged上执行格式化?
我有一个带有RichTextBox的Winforms应用程序,该应用程序会自动突出显示所述框的内容.因为大型文档的格式化需要很长时间,10秒或更长时间,我已经设置了BackgroundWorker来重新格式化RichTextBox.它遍历文本并执行一系列这些:
rtb.Select(start, length);
rtb.SelectionColor = color;
Run Code Online (Sandbox Code Playgroud)
在这样做的同时,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();
}
Run Code Online (Sandbox Code Playgroud)
后台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;
}
Run Code Online (Sandbox Code Playgroud)
但是,对SelectionColor的每个赋值都会导致TextChanged事件触发:无限循环.
如何区分源自外部源自BackgroundWorker进行格式化的文本更改的外部文本更改?
如果我可以独立于文本格式更改检测到文本内容更改,我也可以解决此问题.
我采用的方法是在BackgroundWorker中运行格式化程序逻辑.我选择这个是因为格式需要"很长"的时间,超过1秒或2秒,所以我无法在UI线程上执行此操作.
只是为了重申问题:BackgroundWorker对RichTextBox.SelectionColor上的setter进行的每次调用再次触发了TextChanged事件,这将再次启动BG线程.在TextChanged事件中,我找不到区分"用户输入内容"事件和"程序格式化文本"事件.所以你可以看到它将是一个无限的变化进程.
简单方法不起作用
一种常见的方法(如Eric所建议的)是在文本更改处理程序中运行时"禁用"文本更改事件处理.但是当然这对我的情况不起作用,因为文本更改(SelectionColor更改)是由后台线程生成的.它们不在文本更改处理程序的范围内执行.因此,过滤用户启动的事件的简单方法对我的情况不起作用,后台线程正在进行更改.
检测用户启动的更改的其他尝试
我尝试使用RichTextBox.Text.Length来区分源自我的格式化程序线程的richtextbox中的更改与用户创建的richtextbox中的更改.如果长度没有改变,我推断,然后更改是我的代码完成的格式更改,而不是用户编辑.但是检索RichTextBox.Text属性很昂贵,并且为每个TextChange事件执行此操作会使整个UI变得无法接受.即使这个速度足够快,但在一般情况下也不起作用,因为用户也会进行格式更改.并且,如果用户编辑是类型操作,则用户编辑可能会生成相同长度的文本.
我希望只捕获和处理TextChange事件以检测源自用户的更改.由于我无法做到这一点,我更改了应用程序以使用KeyPress事件和粘贴事件.因此,由于格式更改,我现在不会得到虚假的TextChange事件(如RichTextBox.SelectionColor = Color.Blue).
发信号通知工作线程完成其工作
好的,我有一个可以进行格式化更改的线程运行.从概念上讲,它是这样做的:
while (forever)
wait for the signal to start formatting
for each line in the richtextbox
format it
next
next
Run Code Online (Sandbox Code Playgroud)
如何告诉BG线程开始格式化?
我使用了ManualResetEvent.当检测到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
Run Code Online (Sandbox Code Playgroud)
使用此逻辑,当设置(打开)ManualResetEvent时,格式化程序线程会检测到并重置它(关闭它),然后开始格式化.它遍历文本并决定如何格式化它.格式化程序线程会定期再次检查ManualResetEvent.如果在格式化期间发生另一个按键事件,则该事件再次进入信号状态.当格式化程序看到它被重新发出信号时,格式化程序会退出并从文本的开头再次开始格式化,比如Sisyphus.更智能的机制将从发生更改的文档中的点重新开始格式化.
延迟开始格式化
另一个转折:我不希望格式化程序立即开始每个KeyPress的格式化工作.作为人类类型,击键之间的正常暂停小于600-700ms.如果格式化程序没有延迟地开始格式化,那么它将尝试在击键之间开始格式化.非常无意义.
因此格式化程序逻辑只有在检测到按键超过600毫秒的暂停时才开始执行格式化工作.收到信号后,它会等待600毫秒,如果没有介入的按键,则输入已停止,格式化应该开始.如果存在干预性更改,则格式化程序不执行任何操作,从而断定用户仍在键入内容.在代码中:
private System.Threading.ManualResetEvent wantFormat = new System.Threading.ManualResetEvent(false);
Run Code Online (Sandbox Code Playgroud)
按键事件:
private void richTextBox1_KeyPress(object sender, KeyPressEventArgs e)
{
_lastRtbKeyPress = System.DateTime.Now;
wantFormat.Set();
}
Run Code Online (Sandbox Code Playgroud)
在后台线程中运行的着色器方法中:
....
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;
Run Code Online (Sandbox Code Playgroud)
用户体验是在键入时不会发生格式化.键入暂停后,格式化开始.如果再次键入,格式化将停止并再次等待.
格式更改期间禁用滚动
最后一个问题是:格式化RichTextBox中的文本需要调用RichTextBox.Select(),这会导致RichTextBox在RichTextBox具有焦点时自动滚动到所选文本.因为格式化是在用户专注于控件,阅读和编辑文本的同时发生的,所以我需要一种方法来抑制滚动.我找不到使用RTB公共接口阻止滚动的方法,尽管我确实在intertubes中找到了很多人询问它.经过一些实验,我发现使用Win32 SendMessage()调用(来自user32.dll),在Select()之前和之后发送WM_SETREDRAW,可以在调用Select()时阻止RichTextBox中的滚动.
因为我使用pinvoke来阻止滚动,所以我还使用SendMessage上的pinvoke来获取或设置文本框中的选择或插入符号(EM_GETSEL或EM_SETSEL),并在选择(EM_SETCHARFORMAT)上设置格式.pinvoke方法最终比使用托管接口略快.
批量更新以获得响应
并且因为阻止滚动会产生一些计算开销,所以我决定批量处理对文档所做的更改.逻辑保留了要进行的突出显示或格式更改的列表,而不是突出显示一个连续的部分或单词.每隔一段时间,它一次可以对文档应用30个更改.然后它清除列表并返回分析和排队需要进行哪些格式更改.它足够快,在应用这些批量更改时,不会中断输入文档.
结果是当没有发生打字时,文档会以离散的块自动格式化和着色.如果用户按键之间有足够的时间,整个文档最终将被格式化.对于1k XML文档,这可能低于200ms,对于30k文档可能为2s,对于100k文档可能为10s.如果用户编辑文档,则中止正在进行的任何格式设置,并且格式化将重新开始.
唷!
令我感到惊讶的是,在用户输入内容时,格式化richtextbox这样看起来非常简单.但我无法想出任何更简单的没有锁定文本框,但避免了奇怪的滚动行为.