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






4.97/5 (43投票s)
2005年11月5日
4分钟阅读

452517

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("<", "<");
strData = strData.Replace(">", ">");
strData = strData.Replace("'", "'");
strData = strData.Replace(""", "\""");
//-------------------------------
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 = {"&", "<", ">",
""", "'"};
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 的引用,并调用 AddHTML
和 GetHTML
方法。
我使用一个带有格式按钮的工具栏。为了更新按钮状态,我处理 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
}
参考文献和致谢
- Pete Vidler 的 Extending RichTextBox。
- Richard Parsons 的 RichTextBoxExtended。
历史
- 2005年11月5日:版本 1.0
- 2005年12月5日:版本 1.1
- 添加了上标和下标样式。
注意
请提供您的评论、更正或要求致谢。您的反馈非常受欢迎!.