使用 Rich Edit 控件进行快速 HTML 语法高亮






4.44/5 (9投票s)
2006年3月27日
7分钟阅读

82882

3447
一篇关于使用富文本编辑控件 (CRichEditCtrl) 实现快速 HTML 语法高亮的文章。
引言
该控件是 CRichEditCtrl
的一个扩展,基于 Derek Lakin 的 CHtmlRichEditCtrlSSL,旨在为 HTML 代码提供快速的语法高亮。显然,CHtmlRichEditCtrlSSL
对于大型文件来说速度太慢。由于我的文件很大,我决定接受挑战,开发一个快速且可扩展的语法高亮控件。该控件可以有两种使用方式:
- 加载并显示大量 HTML 代码(来自文件、剪贴板,发送
WM_SENDTEXT
),以及 - 实时在线解析。
规则
该控件遵循一些简单的规则来执行语法高亮,如下所示:
- 任何以 '
<!--
' 开头并以 '-->
' 结尾的内容都是 注释。 - 任何在两个双引号 ("") 之间的内容都是 引用的文本。
- 除注释外,任何介于 '
<
' 和 '>
' 之间的内容都是 标签。 - 其他所有内容都是 普通文本。
策略
为了提供快速的语法高亮,该控件通过施加一个“裁剪区域”(即可见行范围)来最小化着色请求(SetSel
/SetSelectionCharFormat
对的调用)。控件还维护一个逐行字节向量,其中包含逐行的精简状态(已着色/未着色,以及行上最后一个字符的着色状态),以便快速着色。当一行已着色状态的行变得可见时 - 无需做任何操作。当一行未着色的行变得可见时 - 可以轻松地使用行上第一个字符的“着色状态”对其进行着色。关于这个“着色状态”:我们逐行遍历 HTML,并计算最后一个字符的着色状态(注释/引用/标签/普通)(这当然是下一行的第一个字符的状态)。这个计算非常快,因为我们只遍历一遍 HTML,而无需任何着色。
实时更新颜色
当涉及“实时”解析和着色时,情况会更复杂。通常,只要用户键入“常规字符”(不改变任何颜色状态的字符),就不会采取特殊操作,因为 CRichEditCtrl
会自行维护当前的 CHARFORMAT
。
有两种例外情况:
- 用户将插入符号放置在行开头。我们需要显式设置正确的颜色,否则
CRichEditCtrl
将使用下一个字符的颜色。例如,当用户键入<a>
,然后将插入符号移回开头并按下一个键 - 这个字符将被着色为标签而不是普通文本。 - 用户键入一个完成/中断注释开始/结束组合的键。我们需要使最多三个前面的字符无效。例如,当用户在
<!--
内部按下一个键时,他可能会破坏一个注释开始组合。
当用户键入“特殊字符”(改变颜色状态的字符,如‘<’或‘”’)时,从当前插入符号位置开始的所有字符的颜色都会被无效化,并且颜色状态会被重新计算(为了优化,如果达到一条颜色状态未修改的行 - 计算会停止,因为后续行保持有效)。‘!’ 和 ‘-’ 也被包括在此类别中,因为它们会影响注释的开始/结束组合,从而使最多三个前面的字符无效。
注意,无效化总是针对下一行进行的,而前面的行永远不受影响!
这些都通过 WM_CHAR
处理程序完成。
当用户删除文本(VK_DELETE
、VK_BACK
或粘贴文本覆盖选区)时,我们希望在文本被删除之前检查它。这样,我们可以查找被删除文本中的“特殊字符”,并相应地使行无效。我们还处理删除文本完成/中断注释开始/结束组合的情况。
文本删除和粘贴由 WM_KEYDOWN
和 EN_CHANGE
处理程序处理,因为我们希望在文本被删除之前访问它。
注意,要使用 EN_CHANGE
,我们必须设置事件掩码位 ENM_CHANGE
。
滚动
当视图区域改变时(通过滚动条,或按 Home、End、Page Up、Page Down、向上箭头或向下箭头键),可见行范围会改变。该控件处理 EN_VSCROLL
和 WM_VSCROLL
事件,为可见范围内的未着色行进行着色(EN_VSCROLL
通知处理除点击滚动条鼠标本身之外的所有情况,而 WM_VSCROLL
通知处理这种情况)。请注意,要使用 EN_VSCROLL
,我们必须设置事件掩码位 ENM_SCROLL
。
计算可见行范围
可见行范围是指第一个可见行和最后一个可见行之间的范围(包括它们)。很简单,对吧?嗯,不完全是。CRichEditCtrl
友好地通过 EM_GETFIRSTVISIBLELINE
告知您第一个可见行,但不会告诉您最后一个可见行,因为没有 EM_GETLASTVISIBLELINE
。我在网上找到了一种解决方法,使用了另外三个消息:EM_GETRECT
、EM_CHARFROMPOS
和 EM_EXLINEFROMCHAR
。基本思想是获取 CRichEditCtrl
的格式矩形,然后检索有关编辑控件客户端区域中指定点附近的字符的信息,并确定其所在的行。
int CFastHtmlRichEditCtrl::GetLastVisibleLine() { // The EM_GETRECT message retrieves //the formatting rectangle of an edit control: RECT rfFormattingRect = {0}; GetRect(&rfFormattingRect); rfFormattingRect.left++; rfFormattingRect.bottom -= 2; // The EM_CHARFROMPOS message retrieves // information about the character // closest to a specified point // in the client area of an edit control int nCharIndex = CharFromPos(CPoint(rfFormattingRect.left, rfFormattingRect.bottom)); //The EM_EXLINEFROMCHAR message determines which //line contains the specified character in a rich edit control return LineFromChar(nCharIndex); }
演示程序
演示程序是一个 MFC 对话框,它根据单选按钮的选择,显示 CFastHtmlRichEditCtrl
或 CHtmlRichEditCtrlSSL
。该应用程序用于比较两者的性能,通过调用 ParseAllLines
API 并显示调用持续时间。作为基准测试,我使用了一个大小为 164480 字节、包含 3838 行的随机 XML 文件,名为 ADO.XML。
运行程序,然后单击“Refresh”(刷新)以解析和着色控件的窗口文本,或单击省略号以加载、解析和着色文件(如果您愿意,可以使用我的 ADO.XML)。
我在我的机器上获得的 ADO.XML 的结果是:
CFastHtmlRichEditCtrl
:2 秒CHtmlRichEditCtrlSSL
:169 秒
差异很大,对吧?
滚动问题
着色行范围可能会改变可见行范围,因为选择第一行和最后一行的单词会滚动这些行以使其完全可见。我通过在着色后调用 LineScroll
来解决这个问题。
当垂直滚动条可见但未获得焦点时,用户拖动其滑块 - 滑块会跳到边缘然后返回,导致闪烁。在处理 OnVScroll
事件之前将焦点设置到 CRichEditCtrl
似乎可以解决这个问题。但是,当插入符号不位于行开头时,水平滚动条可能会在滚动时向右和向左移动。这可能是因为当插入符号不位于开头时,其位置因行而异。然而,我可以忍受这样的缺陷。
另一个问题与我尝试实现的一个功能有关:后台着色。
后台着色
为了进一步提高速度,我添加了一个名为“后台着色”的功能。后台着色是通过一个定期运行一小段时间的计时器实现的,它着色几行,直到所有行都被着色。然而,如上所述的拖动垂直滑块时的水平滚动缺陷在此处也会发生,即使没有用户活动,这非常恼人!缓存水平滑块位置、着色行并恢复它没有帮助。然而,当插入符号位于行开头且没有选择时 - 水平滚动不会闪烁(请注意,这对“RichEdit20W
”类是如此。“RICHEDIT
”类仍然会闪烁)。
总结一下这个问题,我的 OnTimer
处理程序在以下情况下应用后台着色:
- 控件具有焦点(防止垂直滚动条闪烁),并且插入符号位于行开头且没有进行选择,或者
- 控件窗口完全被遮挡(此时没有任何限制)。
关于 Unicode 的说明
Rich Edit 2.0 同时具有 ANSI 和 Unicode 窗口类—分别为 RichEdit20A
和 RichEdit20W
。根据 MSDN,您可以指定 RICHEDIT_CLASS
常量,它会根据 UNICODE 编译标志的定义扩展到正确的类。然而,在演示中,我自己编写了 #ifdef _UNICODE
。
类接口(基于 CHtmlRichEditCtrlSSL)
// Construction/Destruction public: // Default constructor CHtmlRichEditCtrlSSL(); // Default destructor virtual ~CHtmlRichEditCtrlSSL(); public: // Character format functions // Sets the character format to be used for Tags void SetTagCharFormat(int nFontHeight = 8, COLORREF clrFontColour = RGB(128, 0, 0), CString strFontFace = _T("Courier New"), bool bParse = true); // Sets the character format to be used for Tags void SetTagCharFormat(CHARFORMAT& cfTags, bool bParse = true); // Sets the character format to be used for Quoted text void SetQuoteCharFormat(int nFontHeight = 8, COLORREF clrFontColour = RGB(0, 128, 128), CString strFontFace = _T("Courier New"), bool bParse = true); // Sets the character format to be used for Quoted text void SetQuoteCharFormat(CHARFORMAT& cfQuoted, bool bParse = true); // Sets the character format to be used for Comments void SetCommentCharFormat(int nFontHeight = 8, COLORREF clrFontColour = RGB(0, 128, 0), CString strFontFace = _T("Courier New"), bool bParse = true); // Sets the character format to be used for Comments void SetCommentCharFormat(CHARFORMAT& cfComments, bool bParse = true); // Sets the character format to be used for Normal Text void SetTextCharFormat(int nFontHeight = 8, COLORREF clrFontColour = RGB(0, 0, 0), CString strFontFace = _T("Courier New"), bool bParse = true); // Sets the character format to be used for Normal Text void SetTextCharFormat(CHARFORMAT& cfText, bool bParse = true); // Parsing functions // Parses all lines in the control, // colouring each line accordingly. void ParseAllLines(); // Miscellaneous functions // Loads the contents of the specified // file into the control. Replaces // the existing contents and parses all lines. void LoadFile(CString& strPath); // Enables/disables the background // coloring timer. If enabled, event // is raised every uiInterval millis // and nNumOfLines uncolored lines are colored. void SetBckgdColorTimer UINT uiInterval = 1000, int nNumOfLines = 10; // Overrides // ClassWizard generated virtual function overrides //{{AFX_VIRTUAL(CHtmlRichEditCtrlSSL) protected: virtual void PreSubclassWindow(); //}}AFX_VIRTUAL
结论
在开发 CFastHtmlRichEditCtrl
的过程中,出现了几种关于如何创建最佳和最快的 HTML 语法高亮控件的想法。如果您有任何其他想法、建议和改进,请告诉我,以便我可以更新本文并将其实现到下一个版本中。
历史
- 22-3-2006:
- 原始文章。