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

扩展的 RichTextBox, 用于保存和加载“HTML Lite”文件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (43投票s)

2005年11月5日

4分钟阅读

viewsIcon

452517

downloadIcon

13093

此控件提供了一种直接保存和加载 HTML 文件的方法,避免使用 RTF 代码。

引言

在我开发一个聊天应用程序时,我发现 .NET 的 RichTextBox 控件只允许我使用 RTF 代码或纯文本文件保存和加载文件(天哪)。

我还想要一种将图像和 ActiveX 控件插入 RichTextBox 控件的方法,请参阅我的文章:通过 OLE 方法将图像插入 RichTextBox 控件

好吧,我决定实现一个成功的解决方案,将“HTML lite”文本保存到 RichTextBox 控件中。它被称为“HTML lite”,因为我只处理其中一小部分 HTML 标签,并且有一些限制。但是,可以根据您的需求扩展该控件以包含其他功能和 HTML 标签处理器。

背景

我使用 Win32 API 来获取字符和段落格式结构。这应该比调用本地 RichTextBox 方法更有效,因为我相信每次调用 RichTextBox 方法都会进行一次系统 SendMessage 调用,并且我可以使用 PARAFORMAT 和 CHARFORMAT 结构一次性获取有关 RichTextBox 内容的更多信息。许多互联网网站和博客都采用了这种方法。

API

请参阅源代码了解更多详情。

[StructLayout( LayoutKind.Sequential )]
    public struct PARAFORMAT
{
    public int cbSize;
    public uint dwMask;
    ...
}

[ StructLayout( LayoutKind.Sequential )]
    public struct CHARFORMAT
{
    public int      cbSize; 
    public UInt32   dwMask; 
    public UInt32   dwEffects; 
    ...
}
//Constants
...

添加 HTML

要将 HTML 内容插入控件,我使用 AddHTML 方法。在此函数中,我查找起始 HTML 标签标记“<”并根据以下内容进行处理:

b    = bold
i    = italic
u    = underline
s    = strikeout
sup  = superscript
sub  = subscript
p    = paragraph (attributes: align="alignment")
font = font (attributes: face="facename" 
                      color="#rrggbb" size="NN")
li   = list item

这是该方法的源代码。请看一下我是如何使用 API 应用格式的,以及我如何忽略未处理的标签。我还尝试调整字体大小的值到最接近的值,因为它应该是 1 到 7 之间的数字。

// looking for start tags
int nStart = strHTML.IndexOf('<');
if (nStart >= 0)
{
  if (nStart > 0)
  {
    // tag is not the first character, so
    // we need to add text to control and continue
    // looking for tags at the begining of the text
    strData = strHTML.Substring(0, nStart);
    strHTML = strHTML.Substring(nStart);
  }
  else
  {
    // ok, get tag value
    int nEnd = strHTML.IndexOf('>', nStart);
    if (nEnd > nStart)
    {
      if ((nEnd - nStart) > 0)
      {
        string strTag = strHTML.Substring(nStart, 
                                   nEnd - nStart + 1);
        strTag = strTag.ToLower();

        if (strTag == "<b>")
        {
          cf.dwMask |= CFM_WEIGHT | CFM_BOLD;
          cf.dwEffects |= CFE_BOLD;
          cf.wWeight = FW_BOLD;
        }
        else if (strTag == "<i>")
        {
          cf.dwMask |= CFM_ITALIC;
          cf.dwEffects |= CFE_ITALIC;
        }
        else if (strTag == "<u>")
        {
          cf.dwMask |= CFM_UNDERLINE | CFM_UNDERLINETYPE;
          cf.dwEffects |= CFE_UNDERLINE;
          cf.bUnderlineType = CFU_UNDERLINE;
        }
        else if (strTag == "<s>")
        {
          cf.dwMask |= CFM_STRIKEOUT;
          cf.dwEffects |= CFE_STRIKEOUT;
        }
        else if (strTag == "<sup>")
        {
          cf.dwMask |= CFM_SUPERSCRIPT;
          cf.dwEffects |= CFE_SUPERSCRIPT;
        }
        else if (strTag == "<sub>")
        {
          cf.dwMask |= CFM_SUBSCRIPT;
          cf.dwEffects |= CFE_SUBSCRIPT;
        }
        else if ((strTag.Length > 2) && 
                (strTag.Substring(0, 2) == "<p"))
        {
          if (strTag.IndexOf("align=\"left\"") > 0)
          {
            pf.dwMask |= PFM_ALIGNMENT;
            pf.wAlignment = (short)PFA_LEFT;
          }
          else if (strTag.IndexOf("align=\"right\"") > 0)
          {
            pf.dwMask |= PFM_ALIGNMENT;
            pf.wAlignment = (short)PFA_RIGHT;
          }
          else if (strTag.IndexOf("align=\"center\"") > 0)
          {
            pf.dwMask |= PFM_ALIGNMENT;
            pf.wAlignment = (short)PFA_CENTER;
          }
        }
        else if ((strTag.Length > 5) && 
                (strTag.Substring(0, 5) == "<font")
        {
          string strFont = new string(cf.szFaceName);
          strFont = strFont.Trim(chtrim);
          int crFont = cf.crTextColor;
          int yHeight = cf.yHeight;

          int nFace = strTag.IndexOf("face=");
          if (nFace > 0)
          {
            int nFaceEnd = strTag.IndexOf("\""", nFace + 6);
            if (nFaceEnd > nFace)
                strFont = 
                    strTag.Substring(nFace + 6, nFaceEnd - nFace - 6);
          }

          int nSize = strTag.IndexOf("size=");
          if (nSize > 0)
          {
            int nSizeEnd = strTag.IndexOf("\""", nSize + 6);
            if (nSizeEnd > nSize)
            {
              yHeight = int.Parse(strTag.Substring(nSize + 6, 
                                             nSizeEnd - nSize - 6));
              yHeight *= (20 * 5);
            }
          }

          int nColor = strTag.IndexOf("color=");
          if (nColor > 0)
          {
            int nColorEnd = strTag.IndexOf("\""", nColor + 7);
            if (nColorEnd > nColor)
            {
              if (strTag.Substring(nColor + 7, 1) == "#")
              {
                string strCr = strTag.Substring(nColor + 8, 
                                      nColorEnd - nColor - 8);
                int nCr = Convert.ToInt32(strCr, 16);

                Color color = Color.FromArgb(nCr);

                crFont = GetCOLORREF(color);
              }
              else
              {
                crFont = int.Parse(strTag.Substring(nColor + 7, 
                                         nColorEnd - nColor - 7));
              }
            }
          }

          cf.szFaceName = new char[LF_FACESIZE];
          strFont.CopyTo(0, cf.szFaceName, 0, 
                     Math.Min(LF_FACESIZE - 1, strFont.Length));
          //cf.szFaceName = strFont.ToCharArray(0, 
                         Math.Min(strFont.Length, LF_FACESIZE));
          cf.crTextColor = crFont;
          cf.yHeight = yHeight;

          cf.dwMask |= CFM_COLOR | CFM_SIZE | CFM_FACE;
          cf.dwEffects &= ~CFE_AUTOCOLOR;
        }
        else if (strTag == "<li>")
        {
          if (pf.wNumbering != PFN_BULLET)
          {
            pf.dwMask |= PFM_NUMBERING;
            pf.wNumbering = (short)PFN_BULLET;
          }
        }
        else if (strTag == "</b>")
        {
          cf.dwEffects &= ~CFE_BOLD;
          cf.wWeight = FW_NORMAL;
        }
        else if (strTag == "</i>")
        {
          cf.dwEffects &= ~CFE_ITALIC;
        }
        else if (strTag == "</u>")
        {
          cf.dwEffects &= ~CFE_UNDERLINE;
        }
        else if (strTag == "</s>")
        {
          cf.dwEffects &= ~CFM_STRIKEOUT;
        }
        else if (strTag == "</sup>")
        {
          cf.dwEffects &= ~CFE_SUPERSCRIPT;
        }
        else if (strTag == "</sub>")
        {
          cf.dwEffects &= ~CFE_SUBSCRIPT;
        }
        else if (strTag == "</font>")
        {
        }
        else if (strTag == "</p>")
        {
        }
        else if (strTag == "")
        {
        }

        //-------------------------------
        // now, remove tag from HTML
        int nStart2 = strHTML.IndexOf("<", nEnd + 1);
        if (nStart2 > 0)
        {
          // extract partial data
          strData = strHTML.Substring(nEnd + 1, nStart2 - nEnd - 1);
          strHTML = strHTML.Substring(nStart2);
        }
        else
        {
          // get remain text and finish
          if ((nEnd + 1) < strHTML.Length)
            strData = strHTML.Substring(nEnd + 1);
          else
            strData = "";

          strHTML = "";
        }
        //-------------------------------s


        //-------------------------------
        // have we any continuos tag ?
        if (strData.Length > 0)
        {
          // yes, ok, goto to reinit
          if (strData[0] == '<')
            goto reinit;
        }
        //-------------------------------
      }
      else
      {
        // we have not found any valid tag
        strHTML = "";
      }
    }
    else
    {
      // we have not found any valid tag
      strHTML = "";
    }
  }
}
else
{
  // we have not found any tag
  strHTML = "";
}

为了通过 PARAFORMAT 和 CHARFORMAT 应用格式,我使用属性(一个从互联网上学到的好技巧)。请参阅源代码了解更多详情。

public PARAFORMAT ParaFormat
{
  get
  {
    PARAFORMAT pf = new PARAFORMAT();
    pf.cbSize = Marshal.SizeOf( pf );
      
    // Get the alignment.
    SendMessage( new HandleRef( this, Handle ),
      EM_GETPARAFORMAT,
      SCF_SELECTION, ref pf );
      
    return pf;
  }
  
  set
  {
    PARAFORMAT pf = value;
    pf.cbSize = Marshal.SizeOf( pf );
      
    // Set the alignment.
    SendMessage( new HandleRef( this, Handle ),
      EM_SETPARAFORMAT,
      SCF_SELECTION, ref pf );
  }
}

public PARAFORMAT DefaultParaFormat
{  
   ...
}

public CHARFORMAT CharFormat
{
   ...
}

public CHARFORMAT DefaultCharFormat
{
   ...
}

下面是如何使用其新属性将文本格式信息写入控件。变量 strData 包含应用格式之前的纯文本。

if (strData.Length > 0)
{
  //-------------------------------
  // replace entities
  strData = strData.Replace("&", "&");
  strData = strData.Replace("&lt;", "<");
  strData = strData.Replace("&gt;", ">");
  strData = strData.Replace("&apos;", "'");
  strData = strData.Replace("&quot;", "\""");
  //-------------------------------

  string strAux = strData; // use another copy

  while (strAux.Length > 0)
  {
    //-----------------------
    int nLen = strAux.Length;
    //-----------------------

    //-------------------------------
    // now, add text to control
    int nStartCache = this.SelectionStart;
    string strt = strAux.Substring(0, nLen);

    this.SelectedText = strt;
    strAux = strAux.Remove(0, nLen);

    this.SelectionStart = nStartCache;
    this.SelectionLength = strt.Length;
    //-------------------------------

    //-------------------------------
    // apply format
    this.ParaFormat = pf;
    this.CharFormat = cf;
    //-------------------------------


    // reposition to final
    this.SelectionStart = this.TextLength+1;
    this.SelectionLength = 0;
  }

  // reposition to final
  this.SelectionStart = this.TextLength+1;
  this.SelectionLength = 0;

  //-------------------------------
  // new paragraph requires to reset alignment
  if ((strData.IndexOf("\r\n", 0) >= 0) || 
                   (strData.IndexOf("\n", 0) >= 0))
  {
    pf.dwMask = PFM_ALIGNMENT|PFM_NUMBERING;
    pf.wAlignment = (short)PFA_LEFT;
    pf.wNumbering = 0;
  }
  //-------------------------------

从控件获取 HTML 内容

要从控件获取 HTML 内容,我使用以下方法:逐个字符地获取(*如果有人知道替代方法,请告诉我*)。

我逐个字符地对控件中的字符进行格式分析,并提取有关其样式的​​信息。如果字符格式或段落格式在任何时候发生更改,我会在原始文本中添加一个 HTML 标签。

这是通过使用内部结构 cMyREFormat 来实现的,该结构存储相关信息,例如位置和应该在该位置的标签。

private enum uMyREType
{
  U_MYRE_TYPE_TAG,
  U_MYRE_TYPE_EMO,
  U_MYRE_TYPE_ENTITY,
}

private struct cMyREFormat
{
  public uMyREType nType;
  public int nLen;
  public int nPos;
  public string strValue;
}

步骤 1

查找实体(&、<、>、"、')并存储它们的位置。

char[] ch = {'&', '<', '>', '""', '\''};
string[] strreplace = {"&", "&lt;", "&gt;", 
                              "&quot;", "&apos;"};

for (i = 0; i < ch.Length; i++)
{
    char[] ch2 = {ch[i]};

    int n = this.Find(ch2, 0);
    while (n != -1)
    {
        mfr = new cMyREFormat();

        mfr.nPos = n;
        mfr.nLen = 1;
        mfr.nType = uMyREType.U_MYRE_TYPE_ENTITY;
        mfr.strValue = strreplace[i];

        colFormat.Add(mfr);

        n = this.Find(ch2, n+1);
    }
}

第二步

查找字体更改。

//-------------------------
// get format for this character
cf = this.CharFormat;
pf = this.ParaFormat;

string strfname = new string(cf.szFaceName);
strfname = strfname.Trim(chtrim);
//-------------------------


//-------------------------
// new font format ?
if ((strFont != strfname) || (crFont != cf.crTextColor) || 
                                    (yHeight != cf.yHeight))
{
    if (strFont != "")
    {
        // close previous <font> tag

        mfr = new cMyREFormat();

        mfr.nPos = i;
        mfr.nLen = 0;
        mfr.nType = uMyREType.U_MYRE_TYPE_TAG;
        mfr.strValue = "</font>";

        colFormat.Add(mfr);
    }

    //-------------------------
    // save this for cache
    strFont = strfname;
    crFont = cf.crTextColor;
    yHeight = cf.yHeight;
    //-------------------------

    //-------------------------
    // font size should be translate to 
    // html size (Approximately)
    int fsize = yHeight / (20 * 5);
    //-------------------------

    //-------------------------
    // color object from COLORREF
    color = GetColor(crFont);
    //-------------------------

    //-------------------------
    // add <font> tag
    mfr = new cMyREFormat();

    string strcolor = string.Concat("#", 
                 (color.ToArgb() & 0x00FFFFFF).ToString("X6"));

    mfr.nPos = i;
    mfr.nLen = 0;
    mfr.nType = uMyREType.U_MYRE_TYPE_TAG;
    mfr.strValue = "<font face=\"" + strFont + "\" color=\"" + 
                        strcolor + "\" size=\"" + fsize + "\">";;

    colFormat.Add(mfr);
    //-------------------------

步骤 3

查找段落格式更改,并在我们进入新段落时关闭之前的标签。这是通过使用状态来实现的。

  • none:未应用格式;
  • new:应用新的格式样式(<b>、<i>、<p>...等);
  • continue:格式与前一个相同(无更改);
  • reset:关闭并重新开始(</b>、</i>、</p>...等)。
//-------------------------
// are we in another line ?
if ((strChar == "\r") || (strChar == "\n"))
{
    // yes?
    // then, we need to reset paragraph format
    // and character format
    if (bParaFormat)
    {
        bnumbering = ctformatStates.nctNone;
        baleft = ctformatStates.nctNone;
        baright = ctformatStates.nctNone;
        bacenter = ctformatStates.nctNone;
    }

    // close previous tags

    // is italic? => close it
    if (bitalic != ctformatStates.nctNone)
    {
        mfr = new cMyREFormat();

        mfr.nPos = i;
        mfr.nLen = 0;
        mfr.nType = uMyREType.U_MYRE_TYPE_TAG;
        mfr.strValue = "</i>";

        colFormat.Add(mfr);

        bitalic = ctformatStates.nctNone;
    }

    // is bold? => close it
    if (bold != ctformatStates.nctNone)
    {
        ...
    }

    ...
}

// now, process the paragraph format,
// managing states: none, new, 
// continue {with previous}, reset
if (bParaFormat)
{
    // align to center?
    if (pf.wAlignment == PFA_CENTER)
    {
        if (bacenter == ctformatStates.nctNone)
            bacenter = ctformatStates.nctNew;
        else
            bacenter = ctformatStates.nctContinue;
    }
    else
    {
        if (bacenter != ctformatStates.nctNone)
            bacenter = ctformatStates.nctReset;
    }

    if (bacenter == ctformatStates.nctNew)
    {
        mfr = new cMyREFormat();

        mfr.nPos = i;
        mfr.nLen = 0;
        mfr.nType = uMyREType.U_MYRE_TYPE_TAG;
        mfr.strValue = "<p align='\"center\"'>";

        colFormat.Add(mfr);
    }
    else if (bacenter == ctformatStates.nctReset)
        bacenter = ctformatStates.nctNone;
    //---------------------

    //---------------------
    // align to left ?
    if (pf.wAlignment == PFA_LEFT)
    {
        ...
    }
    //---------------------

    //---------------------
    // align to right ?
    if (pf.wAlignment == PFA_RIGHT)
    {
        ...
    }
    //---------------------

    //---------------------
    // bullet ?
    if (pf.wNumbering == PFN_BULLET)
    {
        ...
    }
    //---------------------
}

步骤 4

查找样式更改:粗体、斜体、下划线、删除线(使用相同的方法,通过状态)。

//---------------------
// bold ?
if ((cf.dwEffects & CFE_BOLD) == CFE_BOLD)
{
    if (bold == ctformatStates.nctNone)
        bold = ctformatStates.nctNew;
    else
        bold = ctformatStates.nctContinue;
}
else
{
    if (bold != ctformatStates.nctNone)
        bold = ctformatStates.nctReset;
}

if (bold == ctformatStates.nctNew)
{
    mfr = new cMyREFormat();

    mfr.nPos = i;
    mfr.nLen = 0;
    mfr.nType = uMyREType.U_MYRE_TYPE_TAG;
    mfr.strValue = "<b>";

    colFormat.Add(mfr);
}
else if (bold == ctformatStates.nctReset)
{
    mfr = new cMyREFormat();

    mfr.nPos = i;
    mfr.nLen = 0;
    mfr.nType = uMyREType.U_MYRE_TYPE_TAG;
    mfr.strValue = "</b>";

    colFormat.Add(mfr);

    bold = ctformatStates.nctNone;
}
//---------------------

//---------------------
// Italic
if ((cf.dwEffects & CFE_ITALIC) == CFE_ITALIC)
{
    ...
}
//---------------------

...

步骤 5

对格式数组进行排序,并通过逐个添加字符和标签来应用样式,直到完成 HTML 文本。

//--------------------------
// now, reorder the formatting array
k = colFormat.Count;
for (i = 0; i < k - 1; i++)
{
    for (int j = i + 1; j < k; j++)
    {
        mfr = (cMyREFormat)colFormat[i];
        cMyREFormat mfr2 = (cMyREFormat)colFormat[j];

        if (mfr2.nPos < mfr.nPos)
        {
            colFormat.RemoveAt(j);
            colFormat.Insert(i, mfr2);
            j--;
        }
        else if ((mfr2.nPos == mfr.nPos) && 
                              (mfr2.nLen < mfr.nLen))
        {
            colFormat.RemoveAt(j);
            colFormat.Insert(i, mfr2);
            j--;
        }
    }
}
//--------------------------


//--------------------------
// apply format by replacing and inserting HTML tags
// stored in the Format Array
int nAcum = 0;
for (i = 0; i < k; i++)
{
    mfr = (cMyREFormat)colFormat[i];

    strHTML += 
       strT.Substring(nAcum, mfr.nPos - nAcum) + mfr.strValue;
    nAcum = mfr.nPos + mfr.nLen;
}

if (nAcum < strT.Length)
    strHTML += strT.Substring(nAcum);
//--------------------------

关注点

为了避免在应用字符和段落格式时进行不断的屏幕更新,我使用了 **Pete Vidler** 在文章 Extending RichTextBox 中提供的 **更快的更新** 方法。

这是通过向控件发送两个消息来实现的:EM_SETEVENTMASK 以防止控件引发任何事件,以及 WM_SETREDRAW 以防止控件重绘自身。

public void BeginUpdate()
{
    // Deal with nested calls.
    ++updating;
    
    if ( updating > 1 )
        return;
    
    // Prevent the control from raising any events.
    oldEventMask = SendMessage( new HandleRef( this, Handle ),
        EM_SETEVENTMASK, 0, 0 );
    
    // Prevent the control from redrawing itself.
    SendMessage( new HandleRef( this, Handle ),
        WM_SETREDRAW, 0, 0 );
}

/// <SUMMARY>
/// Resumes drawing and event handling.
/// </SUMMARY>
/// <REMARKS>
/// This method should be called every time a call is made
/// made to BeginUpdate. It resets the event mask to it's
/// original value and enables redrawing of the control.
/// </REMARKS>
public void EndUpdate()
{
    // Deal with nested calls.
    --updating;
    
    if ( updating > 0 )
        return;
    
    // Allow the control to redraw itself.
    SendMessage( new HandleRef( this, Handle ),
        WM_SETREDRAW, 1, 0 );

    // Allow the control to raise event messages.
    SendMessage( new HandleRef( this, Handle ),
        EM_SETEVENTMASK, 0, oldEventMask );
}

/// <SUMMARY>
/// Returns true when the control is performing some 
/// internal updates, specially when is reading or writing
/// HTML text
/// </SUMMARY>
public bool InternalUpdating
{
    get 
    {
        return (updating != 0);
    }
}

使用代码

要使用该代码,只需添加对 HmlRichTextBox 的引用,并调用 AddHTMLGetHTML 方法。

我使用一个带有格式按钮的工具栏。为了更新按钮状态,我处理 OnSelectionChanged 事件。请记住,在从/到 HTML 文本进行转换时,必须使用 InternalUpdating 属性来提高性能。

private void richTextBox1_SelectionChanged(object sender, 
                                         System.EventArgs e)
{
    if (!richTextBox1.InternalUpdating)
        UpdateToolbar(); //Update the toolbar buttons
}

/// <SUMMARY>
///     Update the toolbar button statuses
/// </SUMMARY>
public void UpdateToolbar()
{
    //This is done incase 2 different 
    //fonts are selected at the same time
    //If that is the case there is no 
    //selection font so I use the default
    //font instead.
    Font fnt;
    
    if (richTextBox1.SelectionFont != null)
        fnt = richTextBox1.SelectionFont;
    else
        fnt = richTextBox1.Font;

    //Do all the toolbar button checks
    tbbBold.Pushed      = fnt.Bold; //bold button
    tbbItalic.Pushed    = fnt.Italic; //italic button
    tbbUnderline.Pushed = fnt.Underline; //underline button
    tbbStrikeout.Pushed = fnt.Strikeout; //strikeout button
    tbbLeft.Pushed      = (richTextBox1.SelectionAlignment == 
                            HorizontalAlignment.Left); //justify left
    tbbCenter.Pushed    = (richTextBox1.SelectionAlignment == 
                            HorizontalAlignment.Center); //justify center
    tbbRight.Pushed     = (richTextBox1.SelectionAlignment == 
                            HorizontalAlignment.Right); //justify right
}

参考文献和致谢

历史

  • 2005年11月5日:版本 1.0
  • 2005年12月5日:版本 1.1
    • 添加了上标和下标样式。

注意

请提供您的评论、更正或要求致谢。您的反馈非常受欢迎!.

© . All rights reserved.