RichEdit性能优化

RichEdit 性能优化

 

一, 问题描述

需要显示 TLV Type-length-value )数据,并且使用不同的颜色来显示 Tag Length Value 三类数据,以示区别。按照 16 进制显示,并且字节间用空白分割。如 AB CD 01 0A 等。

有可能显示几十 M 规模的二进制数据。

显示数据时, UI 应该能够响应用户操作。

因为 RichEdit 可以显示 RTF 文档,可以任意控制文字的颜色,大小,风格等。所以决定使用 RichEdit 控件来实现此功能。

 

二, 直接 Insert 数据

根据 TLV 规则,向 RichEdit 逐个项目添加数据。

Foreach tlv

    设定 RichEdit 的文字颜色为 Tag

    插入 Tag 数据

    设定 RichEdit 的文字颜色为 Length

    插入 Length 数据

    设定 RichEdit 的文字颜色为 Value

    插入 Value 数据

通过 CRichEditCtrl SetSelectionCharFormat 函数来设定文字颜色。

通过 CRichEditCtrl SetSel( -1 , -1 ) ReplaceSel 函数来插入数据。需要注意的是,应该首先设置文字颜色,然后才能插入数据。

 

效果:功能能够实现,但是速度特别慢。不可接受。因为大量调用了 SetSel SetSelectionCharFormat ,而这两个函数都比较慢。

改善:在 foreach tlv 过程中,应该关闭 RichEdit Redraw 功能,一方面提高了性能,另一方面防止了 RichEdit 控件数据的闪烁。


 

三, 使用 StreamIn

浏览了 RichEdit 控件的接口时,发现了如下方法。

long StreamIn(

   int nFormat,

   EDITSTREAM& es

);

该函数可以从 Stream 中读取数据并放入 RichEdit 控件中。

nFormat 有两种可能的值:

SF_TEXT   表示纯文本

SF_RTF   表示 RTF 文档,该文档可以设置颜色,大小等信息。

如果使用该功能的话,必须了解 RTF 文档的格式,最新的 Rich Text Format (RTF) Specification, version 1.9.1 URL :

http://www.microsoft.com/downloads/en/details.aspx?FamilyID=dd422b8d-ff06-4207-b476-6b5396a18a2b&displaylang=en

足足 200 多页,实在没有耐心读完。

好在, RichEdit 还可以将内容 StreamOut RTF 文档。可以以该文档为 Base ,生成 StreamIn 的字符串。

RichEdit 会循环调用下面的函数(该函数返回生成的 RTF 文档字符串),直到所有的数据读取完毕。但是, cb 每次只有 4K

DWORD EditStreamCallback(          DWORD_PTR dwCookie,

    LPBYTE pbBuff,

    LONG cb,

    LONG *pcb

);

效果:速度有很大提高,但是仍然是不可接受的。毕竟 foreach tlv 版本是一点一点的追加文本的,而 Stream 则是一大块一大块追加文本的。

备注:通过该函数只能全体替换 RichEdit 的内容,但是如果通过发送 Message 的话,还可以只替换选中的部分。具体可以参考 EM_STREAMIN 消息。

可能是因为即使是通过 Stream 来读取数据,但是 RichEdit 还是需要解析 RTF 文档并显示。这需要一定的时间。


 

四, TLV 作为整体追加到 RichEdit

既然一段一段数据的向 RichEdit 控件中添加比较耗时。那么是否可以首先合成字符串,然后将字符串整体添加到控件中呢。

第一种方法,

void StrPlus( char * pucData , int unDataLen )

{

       CString strResult ;

       CString str ;

 

       for ( int i = 0 ; i < unDataLen ; i++ )

       {

              str = "" ;

              str.Format("%02X " ,pucData[ i ]);

              strResult = strResult + str;

       }

}

该方法非常慢,下面的性能测试时,就不列出该函数的数据了。

应该使用 += 运算符。

void StrPlusEqual( char * pucData , int unDataLen )

{

       CString strResult ;

       CString str ;

 

       for ( int i = 0 ; i < unDataLen ; i++ )

       {

              str = "" ;

              str.Format("%02X " ,pucData[ i ]);

              strResult += str;

       }

}

该版本性能虽然提高了很多,但是还不够理想。既然数据的长度是已知的,为何不事先分配好足够的 Buffer ,然后填充该 Buffer 呢。

下面是第三个版本。

static const char gs_chHex[] = {'0' , '1' , '2' , '3' , '4' , '5' , '6' , '7' ,

'8' , '9' , 'A' , 'B' , 'C' , 'D' , 'E' , 'F' } ;

void Buff( char * pucData , int unDataLen )

{

       CString strResult = "" ;

       char * pszBuf = NULL ;            

       char * pszPtr = NULL ;            

       unsigned int unBufSize;           

      

       unBufSize = unDataLen * 3 + 1 ;

       pszBuf = pszPtr = (char *)new char [ unBufSize ] ;

       memset( pszBuf , ' ' , unBufSize ) ;

       if ( pszBuf == NULL )

       {

              strResult = "XX " ;        

              return ;

       }

       for ( int i = 0 ; i < unDataLen ; i++ )

       {

              char c = *(pucData+i) ;

              *pszPtr++ = gs_chHex[ ( c >> 4) & 0x0F ];

              *pszPtr++ = gs_chHex[ c & 0x0F ];

              pszPtr++ ; // skip space

       }

       *pszPtr++ = '/0' ;

       strResult = pszBuf ;

       delete [] pszBuf ;

}

虽然代码长度较前两个版本增大了很多,但是性能是最好的。

pucData 长度

+=

buffer

性能比

1M

2:359

0:16

0.6%

10M

176:985

0:125

0.07%

100M

-

1:328

 

 


 

五, 利用 80/20 原则

解决了字符串的合成问题后,添加颜色的性能还是不能接受。既然是设置颜色比较耗时,那么就应该尽量减少调用 SetSelectionCharFormat 函数的次数。 一般情况下, TLV 的三个部分中, V 的长度比较大。

如果不设置颜色,速度非常快。耗时操作还是集中在设置颜色。 TLV 中的 V 部分将占据 80% 甚至更多。如果能把该部分的时间节省出来(第一次向 RichEdit 添加数据时,默认颜色设置为 V 的颜色,这样只需要设置 T L 的颜色了,调用次数至少节省 1/3 )。性能会有好几倍的提高。

实例代码如下:

       SetSel(-1,-1);

       CHARFORMAT2 cfColor = XXX ;

       ReplaceSel( szContent ) ;

       Foreach tlv

              设定 RichEdit 的文字颜 色为色

              插入 Tag 数据

              设定 RichEdit 的文字颜 色为色

              插入 Length 数据

但是,经过这些优化之后,仍然不能满足性能要求。下面继续。

六, 利用 Lazy evaluation

既然 RichEdit 的控件大小是固定的,那么同时能够显示的最大数据也是有限的(因为没有水平滚动条)。那么就可以只把显示出的数据设定颜色。而对未显示的,不设定颜色。这样可以节约大量的时间。

首先, RichEdit 显示的第一个可视行可以通过如下函数获得, GetFirstVisibleLine 。然后再根据控件的大小,行高可以算出可显示的总行数。最后根据第一个可视行和显示的总行数,设置 TL 的颜色( V 的颜色在初始加入控件时已经设置)。

接下来要处理行的滚动了。行滚动有两种方式。

1. 通过 ScrollBar

2. 通过 MouseWheel

首先看看 ScrollBar 的处理情况。

首先处理 CRichEditCtrl 控件的 WM_VSCROLL 消息,函数如下:

void CScrollRichEdit::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)

{

       static int nOldLine =  0 ;

 

       if ( nSBCode != SB_ENDSCROLL )

       {

              nOldLine = GetFirstVisibleLine() ;

       }

      

       CRichEditCtrl::OnVScroll(nSBCode, nPos, pScrollBar);

 

       if ( nSBCode == SB_ENDSCROLL )

       {

           OnTopVisibleLineChanged( nOldLine ,  GetFirstVisibleLine() ) ;

       }

}


 

nSBCode 代表了 ScrollBar 的动作。

nSbCode

意义

SB_BOTTOM

到达滚动条的底部

SB_ENDSCROLL

滚动事件结束

SB_LINEDOWN

向下滚动一行

SB_LINEUP

向上滚动一行

SB_PAGEDOWN

向下滚动一页

SB_PAGEUP

向上滚动一页

SB_THUMBPOSITION

滚动到指定位置

SB_THUMBTRACK

滚动框被拖动.可利用该消息来跟踪对滚动框的拖动

SB_TOP

到达滚动条的顶部

每次滚动事件都是以 SB_ENDSCROLL 为结束的。比如按下向下箭头,处理函数会被调用两次, nSbCode 分别为 SB_LINEDOWN SB_ENDSCROLL 。拖拽滚动条的话,处理函数会被调用 N 次。 nSbCode 分别为 SB_THUMBPOSITION N-2 次), SB_THUMBTRACK SB_ENDSCROLL

在调用 CRichEditCtrl::OnVScroll(nSBCode, nPos, pScrollBar) 函数前后, CRichEditCtrl TopVisibleLine 会发生改变(但是 SB_THUMBTRACK SB_ENDSCROLL 除外,因为这两个 Code 的情况下, TopVisibleLine 不发生改变,而且是处理完毕事件之后,最终的 TopVisibleLine )。可以以该行为条件更新控件能显示的行的颜色。

因为在设定颜色前可能有被选中的部分,那么设定颜色后,不应该影响原来的选中。并且调用 SetSel 会影响 TopVisibleLine 。虽然没有 SetTopVisibleLine 函数,但是可以通过 LineScroll 函数来实现该功能。

void CScrollRichEdit::SetTopVisibleLine( int nLine )

{

       if ( nLine < 0 || nLine >= GetLineCount() )

       {

              return ;

       }

       int nPreLine =  GetFirstVisibleLine() ;

       // set top line

       this ->LineScroll(nLine - nPreLine ) ; // DO not triger vscroll event

       TRACE("SetTopVisibleLine: %d-->%d" , nPreLine , GetFirstVisibleLine()) ;

       ASSERT(nLine == GetFirstVisibleLine() );

}


 

void CScrollRichEdit::OnTopVisibleLineChanged(UINT nOldLine , UINT nNewLine)

{

       TRACE("OnTopVisibleLineChanged: %d-->%d" , nOldLine ,nNewLine) ;

 

       long begin , end ;

       // get sel

       GetSel( begin , end ) ;

 

       // Set Color for each Tag and Length in the visible lines

      

 

       // restore sel

       this ->SetSel( begin , end ) ;

       SetTopVisibleLine( nNewLine ) ;

}

OnVScroll 函数中,当最终的行通过 SB_ENDSCROLL 被确定后,调用 OnTopVisibleLineChanged ,该函数首先保存当前的选中信息,然后设定颜色。最后恢复选中信息,并恢复 TopVisibleLine 。整个过程就可以结束了。

下面讨论 MouseWheel 的情况。

首先响应消息 WM_MOUSEWHEEL ,处理函数如下:

 

BOOL CScrollRichEdit::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt)

{

       int nOldLine = GetFirstVisibleLine();

 

       // BUT nNewLine will be the first visible line after sometime

       // the time is UNKNOWN

       int nNewLine = zDelta < 0 ?

( nOldLine + m_nWheelStep) : (nOldLine - m_nWheelStep) ;

// nNewLine < 0 ??

       nNewLine = max ( 0 , nNewLine ) ; 

       // nNewLine > Max Line ??

       nNewLine = min ( nNewLine , GetLineCount() ) ;   OnTopVisibleLineChanged( nOldLine , nNewLine ) ;

 

       // restore to original state

       SetTopVisibleLine( nOldLine ) ;

       return CRichEditCtrl::OnMouseWheel(nFlags, zDelta, pt);

}

 

调用函数 CRichEditCtrl::OnMouseWheel(nFlags, zDelta, pt); 前后, TopVisibleLine 并不发生改变,原因不详。并且在 OnMouseWheel 函数返回后的某个时刻发生改变,具体时刻也不详。但是,只要在调用 CRichEditCtrl::OnMouseWheel(nFlags, zDelta, pt); 前,保持原始的 TopVisibleLine 不发生改变,就能够正常的滚动。

但是我们怎么知道每次滚动的行数及向前滚动向后滚动呢。 zDelta 小于 0 代表向下滚动, zDelta 大于 0 代表上上滚动。

通过函数        SystemParametersInfo(SPI_GETWHEELSCROLLLINES, 0, &m_nWheelStep, 0); 得到每次滚动的行数。

至此为止,所有的 Scroll 关联的处理已经完毕。

性能很好,看不出什么延迟。

当然可以再进一步优化,比如已经设定颜色的项目,即使下次 Scroll TopVisibleLine 也不用重新设定颜色。

 

关于 Lazy evaluation 的解释,请参见如下 URL

http://en.wikipedia.org/wiki/Lazy_evaluation


 

七, 使 UI 能够响应用户操作

如果向 UI 显示数据非常耗时,那么就有必要在这段时间内,响应一下消息。否则 UI Freeze 。当然如果操作和 UI 没有关系,直接使用 WorkerThread 是一种比较好的做法。 下面讨论一种不用 Thread 的方法。 PeekMessage 技巧。 如果耗时操作可以分解为较小的大量的 Loop 的话,就可以使用该技巧。思想是每次 loop ,或者每几次 loop 之后,处理一下消息,这样保证了较好的 UI 响应速度。

PeekMessage GetMessage 的一个主要区别是, PeekMessage 通过返回值来标示 MessageQueue 中是否有 Message 。而没有 Message 时, GetMessage 并不返回。

通过 PeekMessage 来查看是否有 Message ,有的话,处理 Message ,直到所有的 Message 都被处理完毕。

但是,有时我们不想让他一直处理,而是希望有个最大时间。可以通过参数来指定。

使用 AfxGetApp()->PumpMessage() 的目的是,最大限度的和 MFC 保持兼容。否则的话可以使用 TranslateMessage(&msg); DispatchMessage(&msg);

 

// 0xFFFFFFFF はタイムアウトなしとする

BOOL    DoEvents( UINT nTimeout = 0xFFFFFFFF )

{

    BOOL  bTimeout = FALSE ;

    DWORD dwTick   = ::GetTickCount() ;

    MSG message;

 

    while ( ::PeekMessage(&message,   NULL,   0,   0,   PM_NOREMOVE) )

    {

        if ( message.message == WM_CLOSE )

        {

            // クローズメッセージを処理しないはずです。

            // 処理すると、アプリ終了するとき、異常が発生します。

            // 原因は終了後、 DoEvents は返してから、プログラムは実行し続けます。

            ::PeekMessage(&message,   NULL,   0,   0,   PM_REMOVE);

        }

        else

        {

            // PreTranslateMessage が呼び出されるために、 PumpMessage を呼び出します。

            ::AfxGetApp()->PumpMessage() ;

        }

                // タイムアウトかをチェックします。

        if ( ::GetTickCount() - dwTick >= nTimeout )

        {

            bTimeout = TRUE ;

            break ;

        }

    }

        return bTimeout ;

}

 

八, 总结

必须测量运行时间 ,才能得到性能数据,而不是靠猜测

如下函数用于测量时间,精确到 ms

用法如下:

stTickCount tc ;

BeginTickCount (tc);

XXXX

EndTickCount(tc , “XXXX” ) ;

YYYY

EndTickCount(tc , “YYYY” ) ;

 

 

inline VOID chOutputDebugString( const TCHAR * szFormat , ... )

{

       static const INT MAXLENGTH = 1024 ;

      

       va_list ArgList;

       TCHAR szInfoStr[ MAXLENGTH ] = { 0 } ;

 

       va_start(ArgList, szFormat);

       _vsnprintf( szInfoStr , MAXLENGTH , szFormat , ArgList ) ;

      

       szInfoStr[ MAXLENGTH - 1 ] = 0 ;

 

       OutputDebugString( szInfoStr ) ;

}

 

typedef struct stTickCount {

       DWORD  dwBegin ;

       DWORD  dwEnd ;

} stTickCount;

 

inline void BeginTickCount( stTickCount & tc )

{

       tc.dwBegin = ::GetTickCount() ;

}

 

inline void EndTickCount( stTickCount & tc , char * pszPrefix )

{

       tc.dwEnd = ::GetTickCount() ;

       chOutputDebugString( "%s:(%d:%d)"

              , pszPrefix

              , ( tc.dwEnd - tc.dwBegin) / 1000

              , ( tc.dwEnd - tc.dwBegin) % 1000 ) ;

       BeginTickCount( tc ) ;

}

你可能感兴趣的:(RichEdit性能优化)