最近在設計的應用程式,需要一個有 syntax highlighting 功能的 RichTextBox。後來在 Code Project 上面找到一個陽春的 RichTextBox 元件,這個元件可以針對字串、整數...等各種語法文字顯示自訂的顏色。不過我的需求更陽春,我只需要把類似 XML 的標籤用特殊顏色顯示就行了,像這樣:

因 此自己動手修改了一下,把程式碼簡化,並將類別名稱改為 RichTextBoxHL,再加個小圖示,以便安裝到 VS2005 的 Toolbox 時有個比較像樣的圖案。原始程式碼附在最後。這當中為了尋找成對的起始標籤和結束標籤,又 Goggle 了一下,看看有沒有現成的 regular expression,結果不只找到了:"<(?<tag>\\w*)>(?<text>.*)</\\k<tag>>",還發現另一個好用的 Regular Expression 的編輯與測試工具:Expresso。雖然早知道 regular expression 強大好用,但怎麼也耐不住性子去學那一大串的語法符號。有了這個工具,倒是增加不少學習和使用上的方便。另外,RegExpLib.com 也是可以去尋寶的地方。

RichTextBoxHL 原始程式碼:

using System;
using System.ComponentModel;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Drawing;
using System.Windows.Forms;
using System.Text.RegularExpressions;
using Huanlin.Helpers;
namespace Huanlin.WinForms
{
  /// <summary>
  /// 能夠將標籤以特殊顏色顯示的 RichTextBox。
  /// 此元件參考自 Patrik Svensson 的文章:Enabling syntax highlighting in a RichTextBox。
  /// URL: http://www.codeproject.com/cs/miscctrl/SyntaxRichTextBox.asp。
  /// </summary>
  [ToolboxBitmap(typeof(RichTextBoxHL), "RichTextBoxHL.bmp")]
  public class RichTextBoxHL : RichTextBox
  {
    private bool m_SkipTextChanged;
    private bool m_EnableUpdate;
    private string m_Line;
    private int m_ContentLength;
    private int m_LineStartIndex;
    private int m_LineEndIndex;
    private int m_LineLength;
    private int m_CurrentSelection;
    private bool m_EnableTagColor;
    private Color m_TagColor;
    public RichTextBoxHL()
      : base()
    {
      m_EnableUpdate = true;
      m_SkipTextChanged = false;
      m_EnableTagColor = true;
      m_TagColor = Color.Maroon;
    }
    public new void LoadFile(string path)
    {
      m_SkipTextChanged = true;

      try
      {
        base.LoadFile(path);
        ProcessAllLines();
      }
      finally
      {
        m_SkipTextChanged = false;
      }
    }
    public new void LoadFile(string path, RichTextBoxStreamType fileType)
    {
      m_SkipTextChanged = true;
      try
      {
        base.LoadFile(path, fileType);
        ProcessAllLines();
      }
      finally 
      {
        m_SkipTextChanged = false;
      }
    }
    public new void LoadFile(Stream data, RichTextBoxStreamType fileType)
    {
      m_SkipTextChanged = true;

      try
      {
        base.LoadFile(data, fileType);
        ProcessAllLines();
      }
      finally
      {
        m_SkipTextChanged = false;
      }
    }
    protected override void WndProc(ref Message m)
    {
      if (m.Msg == 0x00f)
      {
        if (m_EnableUpdate)
          base.WndProc(ref m);
        else
          m.Result = IntPtr.Zero;
      }
      else
        base.WndProc(ref m);
    }
    protected override void OnTextChanged(EventArgs e)
    {
      if (m_SkipTextChanged)
        return; 
      m_EnableUpdate = false;
      try
      {
        // Calculate sh*t here.
        m_ContentLength = this.TextLength;
        int currSelectionStart = SelectionStart;
        int currSelectionLength = SelectionLength;
        // Find the start of the current line.
        m_LineStartIndex = currSelectionStart;
        while ((m_LineStartIndex > 0) && (Text[m_LineStartIndex - 1] != '\n'))
          m_LineStartIndex--;
        // Find the end of the current line.
        m_LineEndIndex = currSelectionStart;

        while ((m_LineEndIndex < Text.Length) && (Text[m_LineEndIndex] != '\n'))
          m_LineEndIndex++;
        // Calculate the length of the line.
        m_LineLength = m_LineEndIndex - m_LineStartIndex;
        // Get the current line.
        m_Line = Text.Substring(m_LineStartIndex, m_LineLength);
        ProcessLine();
      }
      finally
      {
        m_EnableUpdate = true;
      }
    }
    private void ProcessLine()
    {
      // Save the position and make the whole line black

      int pos = base.SelectionStart;
      base.SelectionStart = m_LineStartIndex;
      base.SelectionLength = m_Line.Length;
      base.SelectionColor = Color.Black;
      ProcessTagColor();
      base.SelectionStart = pos;
      base.SelectionLength = 0;
      base.SelectionColor = Color.Black;
      m_CurrentSelection = pos;
    }
    private void ProcessTagColor()
    {
      if (!m_EnableTagColor)
        return;
      MatchCollection matches = StrHelper.FindTagPairs(m_Line);
      int start;
      int end;
      foreach (Match match in matches)
      {
        // 每個找到的 Match 物件都包含一對標籤: <xxx>abc</xxx>
        // 起始標籤
        start = match.Index;
        end = m_Line.IndexOf('>', start + 1);
        base.SelectionStart = m_LineStartIndex + start;
        base.SelectionLength = end - start + 1;
        base.SelectionColor = m_TagColor;
        // 結束標籤
        start = m_Line.IndexOf('<', end + 1);
        end = m_Line.IndexOf('>', start + 1);
        base.SelectionStart = m_LineStartIndex + start;
        base.SelectionLength = end - start + 1;
        base.SelectionColor = m_TagColor;
      }
    }
    private void ProcessAllLines()
    {
      m_Line = base.Text;
      m_LineStartIndex = 0;
      m_LineEndIndex = m_LineStartIndex + m_Line.Length;
      ProcessLine();
    }
    #region 屬性
    public new string Text
    {
      get
      {
        return base.Text;
      }
      set
      {
        m_SkipTextChanged = true;
        try
        {
          base.Text = value;
          ProcessAllLines();
        }
        finally
        {
          m_SkipTextChanged = false;
        }
      }
    }
    public new string[] Lines
    {
      get
      {
        return base.Lines;
      }
      set
      {
        m_SkipTextChanged = true;
        try
        {
          base.Lines = value;
          ProcessAllLines();
        }
        finally
        {
          m_SkipTextChanged = false;
        }
      }
    }
    [Browsable(true), Description("是否將標籤顯示成特殊顏色。")]
    public bool EnableTagColor
    {
      get
      {
        return m_EnableTagColor;
      }
      set
      {
        if (m_EnableTagColor != value)
        {
          m_EnableTagColor = value;
          ProcessAllLines();
        }       
      }
    }
    [Browsable(true), Description("標籤顏色。")]
    public Color TagColor
    {
      get
      {
        return m_TagColor;
      }
      set
      {
        if (m_TagColor != value)
        {
          m_TagColor = value;
          ProcessAllLines();
        }
      }
    }
    #endregion
  }
}

其中呼叫到的 StrHelper.FindTagPairs() 程式碼為: 

    public static MatchCollection FindTagPairs(string s)
    {
      string pattern = "<(?<tag>\\w*)>(?<text>.*)</\\k<tag>>";
      return Regex.Matches(s, pattern);
    }

如果需要為讓關鍵字或語法顯示特殊顏色,只要修改 ProcessLine(),利用 regular expression 把找到的字串"上色"就行了(就像上面的 ProcessTagColor 方法一樣)。