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