吊胃口 V part I -- 用RichTextBox进行无闪烁的着色

很多时候,我们希望对用户输入的文字进行即时的分析,并对部分文字的字体颜色进行修改,让那些文字更加醒目。如果说要对部分的文字进行字体颜色的修改,我们很可能就会想到使用RichTextBox这个控件,因为这个控件至少能够让部分文字的字体颜色变得和其他部分不一样。然而实际上对于现实静态的东西,RichTextBox也许是适合的,但是对于想这种的“动态着色”则不见得有效率。最为头痛的一个问题就是,通过SelectionStart、SelectionLength的设置,然后再修改SelectionFont和SelectionColor,必然有一个短暂的时间会有文字被选中,结果就是你要加亮的文字会在一瞬间闪现蓝色背景的白色文字。这个现象并不一定每次都能够看到,但是如果在不停的进行输入的话,就一定会看到这个情况了。

这种不停的闪烁的情景比较让人感到讨厌,怎么解决呢?在没有试验之前,我想到了三种办法:
1、在进行着色的时候,让焦点从当前的RichTextBox上面转移到别的地方,同时HideSelection设置成Ture;
2、用一个后台RichTextBox进行着色,然后把着色的结果复制到前台;
3、在进行着色的时候暂时屏蔽RichTextBox的画面更新,着色完毕之后再允许画面更新。

首先说第一个,这个方法看起来很好,但事实上首先效率有问题,其次很多时候并不能够成功的转移焦点。尤其是牵扯到一个后面将会提到的快速输入引起问题的时候,为了让行为正确,设计将会变得非常复杂和麻烦,所以我放弃了这个方案。
而第二个则是我在NfaGen1里面所使用的方案,因为这个方案甚至连RichTextBox都不需要改造就能够实现,开发起来最为简单易行。代码类似于:

private   void  rtxEdit_TextChanged()
{
    
if (busy)
    
{
        busy 
= false;
        
return;
    }

    busy 
= true;
    
int oriStart, oriLength;
    rtxBuffer.Text 
= rtxEdit.Text;
    oriStart 
= rtxEdit.SelectionStart;
    oriLength 
= rtxEdit.SelectionLength;
    AnalyzeBuffer();
    rtxEdit.Rtf 
= rtxBuffer.Rtf;
    rtxEdit.SelectionStart 
= oriStart;
    rtxEdit.SelectionLength 
= oriLength;
}


上面的rtxEdit是用户输入区,rtxBuffer则是一个不可见的后台处理区。由于处理的时候在另外一个RichTextBox里面,要闪也是在那个看不见的后台空间内部“闪”,所以基本上解决了“选中”-“闪烁”的问题。但是这个方案也不见得很明智,因为需要不断的更新前台输入区的内容,需要通过busy变量来避免更新前台输入的时候引发TextChanged事件,进而引起一个死循环。而这个方案实际上并不能够完全杜绝“闪烁”问题,因为当输入的内容超过文本框的大小范围,就会引起“更新”-“闪烁”问题。因为当你从后台拷贝Rtf到前台的时候,会使得SelectionStart变成0,也就是回到了最前面的位置,而很多时候原本光标的位置里最前面的位置相差有一个RichTextBox的大小,因此画面就会短暂停留在和原来显示内容不相同的位置上,然后通过后面恢复SelectionStart来恢复光标位置以及原来显示内容的位置。这个时候就会闪现整个文本最开始的内容的画面,如果有滚动条,你还会看到滚动条的位置也会不断的来回跳跃。但是对于内容非常有限的,能够保证输入内容不会超过文本框大小的情况下,用这个方法的确是比较合适的。

实际上第三种方法才是最好的,因为不需要将文字来回的搬动,就不应该出现任何的闪烁,同时效率也应该相对较高。可这么好的方法,为什么在NfaGen1里面使用呢?因为我遇到了困难,不过幸运的是现在我已经有解决方法了。困难就是:首先override OnPaint似乎不起作用,其次截获WM_PAINT不传递给基类的WndProc函数来阻止重画会引起系统不断发送WM_PAINT消息,非常地占用CPU。而且事实上被截获的消息最后还是要发出去,也就是说本来你想阻止不让画的东西似乎还是要画出来的,或者至少背负了一些不必要的消息处理花费。解决问题的办法就是绕过去!

我猜测这个WM_PAINT的重发并不是操作系统引起的,因为在MSDN里面说,如果这个窗口处理这个消息,返回0就OK了。如果WM_PAINT不断重发,那么很可能是因为.NET Framework的底层代码发出了这个WM_PAINT来达到重画画面的目的,但是他发现并没有执行相应的操作,因此就不断的重发。当然,这个也是我的无理猜测而已,因为至少这部分代码在Managed部分找不到。既然有这个猜测,我就进行了相应的测试。不过大家想想,如果允许RichTextBox接受WM_PAINT消息固然可能不让消息重发,但是也不可能达到暂停重画的目的。在我分析过RichTextBox、TextBoxBase、Control等部分的WndProc函数之后,发现WM_PAINT的消息处理是通过非托管的代码来完成的,并且很可能是根据托管对象的类型来确定具体的行为方式,因此我就尝试对.NET进行“欺骗”。

首先我建立一个派生自Control的类,然后在这个类里面添加一个public函数,调用base.WndProc(ref m),然后所有WM_PAINT消息就转发到这个类的对象上面,结果竟然成功了!我猜测也许hWnd确实是RichTextBox的Handle,所以系统认为那个WM_PAINT消息已经被处理了。但是同时由于这个时候的处理对象并不是一个RichTextBox,因此就不会用RichTextBox的画面更新代码来进行更新,而是什么也不做,因此达到了暂时屏蔽画面显示的效果。

public   class  SuperBox : RichTextBox
{
    
private class paintHelper : Control
    
{
        
public void DefaultWndProc(ref Message m)
        
{
            
this.DefWndProc(ref m);
        }

    }


    
private const int WM_PAINT = 0x000F;
    
private int lockPaint;
    
private bool needPaint;
    
private paintHelper pHelp = new paintHelper();

    
public void BeginUpdate()
    
{
        lockPaint
++;
    }


    
public void EndUpdate()
    
{
        lockPaint
--;
        
if (lockPaint <= 0)
        
{
            lockPaint 
= 0;
            
if (needPaint)
            
{
                
this.Refresh();
                needPaint 
= false;
            }

        }

    }


    
protected override void WndProc(ref Message m)
    
{
        
switch (m.Msg)
        
{
            
case WM_PAINT:
                
if (lockPaint <= 0)
                
{
                    
base.WndProc(ref m);
                }

                
else
                
{
                    needPaint 
= true;
                    pHelp.DefaultWndProc(
ref m);
                }

                
return;
        }


        
base.WndProc (ref m);
    }

}

你可能感兴趣的:(text)