65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.44/5 (9投票s)

2006年3月27日

7分钟阅读

viewsIcon

82882

downloadIcon

3447

一篇关于使用富文本编辑控件 (CRichEditCtrl) 实现快速 HTML 语法高亮的文章。

引言

该控件是 CRichEditCtrl 的一个扩展,基于 Derek Lakin 的 CHtmlRichEditCtrlSSL,旨在为 HTML 代码提供快速的语法高亮。显然,CHtmlRichEditCtrlSSL 对于大型文件来说速度太慢。由于我的文件很大,我决定接受挑战,开发一个快速且可扩展的语法高亮控件。该控件可以有两种使用方式:

  1. 加载并显示大量 HTML 代码(来自文件、剪贴板,发送 WM_SENDTEXT),以及
  2. 实时在线解析。

规则

该控件遵循一些简单的规则来执行语法高亮,如下所示:

  1. 任何以 '<!--' 开头并以 '-->' 结尾的内容都是 注释
  2. 任何在两个双引号 ("") 之间的内容都是 引用的文本
  3. 除注释外,任何介于 '<' 和 '>' 之间的内容都是 标签
  4. 其他所有内容都是 普通文本

策略

为了提供快速的语法高亮,该控件通过施加一个“裁剪区域”(即可见行范围)来最小化着色请求(SetSel/SetSelectionCharFormat 对的调用)。控件还维护一个逐行字节向量,其中包含逐行的精简状态(已着色/未着色,以及行上最后一个字符的着色状态),以便快速着色。当一行已着色状态的行变得可见时 - 无需做任何操作。当一行未着色的行变得可见时 - 可以轻松地使用行上第一个字符的“着色状态”对其进行着色。关于这个“着色状态”:我们逐行遍历 HTML,并计算最后一个字符的着色状态(注释/引用/标签/普通)(这当然是下一行的第一个字符的状态)。这个计算非常快,因为我们只遍历一遍 HTML,而无需任何着色。

实时更新颜色

当涉及“实时”解析和着色时,情况会更复杂。通常,只要用户键入“常规字符”(不改变任何颜色状态的字符),就不会采取特殊操作,因为 CRichEditCtrl 会自行维护当前的 CHARFORMAT

有两种例外情况:

  1. 用户将插入符号放置在行开头。我们需要显式设置正确的颜色,否则 CRichEditCtrl 将使用下一个字符的颜色。例如,当用户键入 <a>,然后将插入符号移回开头并按下一个键 - 这个字符将被着色为标签而不是普通文本。
  2. 用户键入一个完成/中断注释开始/结束组合的键。我们需要使最多三个前面的字符无效。例如,当用户在 <!-- 内部按下一个键时,他可能会破坏一个注释开始组合。

当用户键入“特殊字符”(改变颜色状态的字符,如‘<’或‘”’)时,从当前插入符号位置开始的所有字符的颜色都会被无效化,并且颜色状态会被重新计算(为了优化,如果达到一条颜色状态未修改的行 - 计算会停止,因为后续行保持有效)。‘!’ 和 ‘-’ 也被包括在此类别中,因为它们会影响注释的开始/结束组合,从而使最多三个前面的字符无效。

注意,无效化总是针对下一行进行的,而前面的行永远不受影响!

这些都通过 WM_CHAR 处理程序完成。

当用户删除文本(VK_DELETEVK_BACK 或粘贴文本覆盖选区)时,我们希望在文本被删除之前检查它。这样,我们可以查找被删除文本中的“特殊字符”,并相应地使行无效。我们还处理删除文本完成/中断注释开始/结束组合的情况。

文本删除和粘贴由 WM_KEYDOWNEN_CHANGE 处理程序处理,因为我们希望在文本被删除之前访问它。

注意,要使用 EN_CHANGE,我们必须设置事件掩码位 ENM_CHANGE

滚动

当视图区域改变时(通过滚动条,或按 Home、End、Page Up、Page Down、向上箭头或向下箭头键),可见行范围会改变。该控件处理 EN_VSCROLLWM_VSCROLL 事件,为可见范围内的未着色行进行着色(EN_VSCROLL 通知处理除点击滚动条鼠标本身之外的所有情况,而 WM_VSCROLL 通知处理这种情况)。请注意,要使用 EN_VSCROLL,我们必须设置事件掩码位 ENM_SCROLL

计算可见行范围

可见行范围是指第一个可见行和最后一个可见行之间的范围(包括它们)。很简单,对吧?嗯,不完全是。CRichEditCtrl 友好地通过 EM_GETFIRSTVISIBLELINE 告知您第一个可见行,但不会告诉您最后一个可见行,因为没有 EM_GETLASTVISIBLELINE。我在网上找到了一种解决方法,使用了另外三个消息:EM_GETRECTEM_CHARFROMPOSEM_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 对话框,它根据单选按钮的选择,显示 CFastHtmlRichEditCtrlCHtmlRichEditCtrlSSL。该应用程序用于比较两者的性能,通过调用 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 窗口类—分别为 RichEdit20ARichEdit20W。根据 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:
    • 原始文章。
© . All rights reserved.