自定义 RichTextBox 用于文本提示(如屏幕提示)、受保护文本等等!
C# Forms RichTextEditor,支持自定义超链接(类似 Outlook 地址的文本实体)、自定义弹出列表框以及屏幕提示。
引言
本文旨在帮助那些迫切寻找 C# 基于窗体的 RichTextBox 的人,该 RichTextBox 具有以下功能:
- 特定文本在富文本框中的屏幕提示。我称之为文本提示!
- 类似 Outlook 电子邮件 ID 的只读文本。用户只能添加或删除文本,但不能修改其内容。
- 借助第一项和第二项,可以实现第三项功能,即拥有一个辅助文本。当用户将鼠标悬停在只读文本上时,将显示此辅助文本。
- 一个多列列表下拉框,用于添加只读文本。当用户键入“$”和“{”时,将弹出此下拉框。
背景
某些软件可能需要文本框支持只读文本并具有我上面提到的附加功能。在我的情况下,我们的软件包含技术术语的文本框,这些术语不应被编辑。由于某些非技术用户可能发现处理这些技术词汇有困难,因此建议支持友好名称。因此,我开始实施了该功能。
所以,现在,技术术语将被隐藏在友好名称后面。当鼠标悬停在友好名称上时,将显示技术术语作为屏幕提示。
使用代码
随附的 zip 文件包含一个解决方案文件,其中有两个项目:DemoForm 和 MuchRichTextBox。DemoForm 使用 MuchRichTextBox 用户控件来演示 MuchRichTextBox 的功能。
因此,要将此控件集成到您的项目中,您只需引用 MuchRichTextBox 的 dll 即可。有关用法,请检查演示源代码。
以下将解释 MuchRichTextBox 的设计。
下面是代码的大纲设计。
在上述所有类中,MuchRichTextBox 类是包含文本提示、受保护文本和下拉列表所有功能的主要组件。
ExRichTextBox 类来自另一篇 CodeProject 文章 https://codeproject.org.cn/Articles/4544/Insert-Plain-Text-and-Images-into-RichTextBox-at-R。感谢 Khendy 提供了插入 RTF 文本的函数,使工作更轻松。
PopupListView 类是用于选择友好名称的弹出控件。它本质上是一个列表视图。
代码详解
在本节中,我将逐一介绍各项功能,并解释其实现方式。因此,我不会解释每个函数。而是会选择一项功能,深入探讨它,展示哪些函数是如何实现该功能的一部分的。
概念
首先,让我告诉您这个概念。
- 富文本框中将存在一些类似只读文本的内容。
- 这个只读文本后面隐藏着另一个文本,我称之为辅助文本。
- 当鼠标悬停在只读文本上时,将显示辅助文本。
- 通过按顺序键入 ${ 可以插入只读文本。这将打开一个包含只读文本列表的下拉框。
- 稍后,您可以调用 muchrichtextbox 上的解析器函数,它将返回那些具有辅助名称而不是只读名称的文本。
- 另外,由于只读文本必须严格保持只读,尽管用户尝试编辑它,我不得不编写一些代码来处理这种情况。假设用户只选择了只读文本中的两个字母然后按删除键,它会删除整个单词。
功能:屏幕提示
鼠标移动时,会调用 richtextbox1_MouseMove。 如果鼠标在某个文本区域上,此函数将显示屏幕提示。代码有详细的注释,因此很容易理解。
步骤如下:
- 确定鼠标是否在某个文本区域内。如果不在,则返回。
- 如果鼠标在文本区域内,通过调用 ExtractWord 函数获取最接近的文本。
- 检查此文本是只读文本还是普通文本。如果只是普通文本,则忽略并从函数返回。
- 如果文本是只读文本,则调用 GetLinkedName 和 GetLinkedValue 函数获取关联的辅助文本。
- 将关联文本显示为屏幕提示!搞定。看起来很简单。但请查看实际代码,其中包含大量数学计算。我不想深入研究。您可以查看代码自行了解。 不过,我将在本文的最后以零散的形式讨论实现过程中遇到的最棘手的问题。
代码
以下是此功能的一些主要代码片段:
private void OnMouseMove(object sender, MouseEventArgs e)
{
int nCharIndexMax = richTextBox1.Text.Length - 1;
Point pointMax = richTextBox1.GetPositionFromCharIndex(nCharIndexMax);
Point nMousePositionCoordinate = new Point(e.X, e.Y);
int nCharIndexWrtMousePosition = richTextBox1.GetCharIndexFromPosition(nMousePositionCoordinate);
string strTip = "";
int nIndexOfLinkedDelimiter = -1;
string strLinkedNameInDelimiter = "";
int nPositionOfLinkedNameInDisplayText = -1;
bool bFound = false;
// This if block will make function to just return if the mouse position currently pointing is
// just on the text region where is no text (simply which is white)
if ((pointMax.Y + 10) < (e.Location.Y))
{
m_ToolTip.RemoveAll();
m_ToolTip.Hide(richTextBox1);
return;
}
// Get the close word depending on the current mouse position which is over some text
strTip = ExtractWord(richTextBox1.Text, nCharIndexWrtMousePosition);
if (strTip != null)
{
// Now its time to find the linked value associated with this
nIndexOfLinkedDelimiter = richTextBox1.Text.IndexOf(STRLINKEDTEXTDELIMITER1FORSEARCH, nCharIndexWrtMousePosition);
// If -1, then the word found is just a regular text and not a linked text
if (nIndexOfLinkedDelimiter != -1)
{
// We got some linked name following the text hovered. Extract the linked value of the linked name.
strLinkedNameInDelimiter = GetLinkedName(richTextBox1.Text, nIndexOfLinkedDelimiter);
if (strLinkedNameInDelimiter != null)
{
// We reverse find the existence of this word
nPositionOfLinkedNameInDisplayText = richTextBox1.Text.LastIndexOf(strLinkedNameInDelimiter, nIndexOfLinkedDelimiter);
if (nPositionOfLinkedNameInDisplayText != -1)
{
int nIndexOfLinkedName = strTip.IndexOf(strLinkedNameInDelimiter);
if( nIndexOfLinkedName == -1 )
{
nIndexOfLinkedName = strLinkedNameInDelimiter.IndexOf(strTip);
if( nIndexOfLinkedName != -1 )
{
bFound = true;
}
}
else
{
bFound = true;
}
if( bFound )
{
strTip = GetLinkedValue(richTextBox1.Text, nIndexOfLinkedDelimiter);
// Check if the pointed text lies in the range of this linked name.
// Basically this is to avoid showing the screen tip when the mouse is just in
// text region where there is no text.
if (nPositionOfLinkedNameInDisplayText <= nCharIndexWrtMousePosition)
{
// If the previous tool tip is same, then dont show the tooltip
if( m_ToolTip.GetToolTip(this.richTextBox1) != strTip )
m_ToolTip.Show(strTip, richTextBox1);
}
}
}
}
}
}
}
/// <summary>
/// Extract the word based on the position specified
/// </summary>
private string ExtractWord(string sText, int iPos)
{
// get the position of the beginning of the word
// (if no separator found, this gives zero)
int iStart = sText.LastIndexOfAny(m_charArraySeparators, iPos) + 1;
// get the position of the separator after the word
int iEnd = sText.IndexOfAny(m_charArraySeparators, iPos);
if (iEnd < 0)
iEnd = sText.Length;
if (iEnd < iStart)
return "";
return sText.Substring(iStart, iEnd - iStart);
}
功能:只读文本和辅助文本(与只读文本关联)
在本节中,我将告诉您如何将辅助文本与只读文本关联。当用户在下拉列表中选择只读文本时,将添加只读文本和关联的辅助文本。但只有只读文本会显示出来,而辅助文本则使用富文本格式中可用的可见性文本标签隐藏起来。
通过使用富文本格式中可用的保护标签,只读文本被设置为不可编辑。
示例如下:
只读文本:MyName
关联的辅助文本:Vinayaka
以下是 rtf 数据的一些示例提取:
\protect\f0\fs16 MyName\v \'a7\'a5\'a7\'a5APLT\'a7\'a5MyName\'a7\'a5Vinayaka\'a5\'a7\'a5\'a7 \cf0\highlight0\ulnone\b0\protect0\v0
在这里,您可以看到像“\protect”和“\protect0”、“\v”和“\v0”这样的标签,它们分别是保护标签和可见性标签。正如我之前所说,保护用于只读,可见性用于辅助文本。
字符串“MyName”的第一个出现被包含在“\protect”和“\protect0”标签之间,以实现只读属性。
接下来的字符串出现,即“MyName”和“Vinayaka”,被包含在“\v”和“\v0”标签之间,用于隐藏它们。
此外,您可以看到“MyName”重复出现。这是为了确保我们读取的是属于鼠标悬停的只读文本的正确辅助文本。这可能不是必需的,但已采取额外预防措施。
我将解释这些 APLT 字符串,您可能会想为什么会有这个!请看下面的示例:
MyName §¥§¥APLT§¥MyName§¥Vinayaka¥§¥§
这是我上面显示的富文本提取的纯文本版本。字符串 "§¥§¥"、"APLT" 只是我在这里用作分隔符,以将它们视为辅助文本。
代码
相关代码不言自明,无需详尽解释。
以下是相关函数。
/// Extracts the linked value in the associated text of the linked name
/// </summary>
/// <param name="nIndexToStart"> index to start from </param>
/// <param name="strSource"> the source string </param>
string GetLinkedValue(string strSource, int nIndexToStart)
{
// Format "§¥§¥APLT§¥<LinkedName>§¥<LinkedValue>¥§¥§"
string strResult = "";
int nIndex = -1;
int nLinkedValueStartIndex = -1;
int nLastIndex = -1;
nIndex = strSource.IndexOf(STRLINKEDTEXTDELIMITER1FORSEARCH, nIndexToStart);
nIndex += STRLINKEDTEXTDELIMITER1.Length;
nLinkedValueStartIndex = strSource.IndexOf(STRLINKEDTEXTDELIMITER2, nIndex);
nIndex += STRLINKEDTEXTDELIMITER2.Length;
nLinkedValueStartIndex += STRLINKEDTEXTDELIMITER2.Length;
nLastIndex = strSource.IndexOf(STRLINKEDTEXTDELIMITER3FORSEARCH, nIndex);
if (nLastIndex != -1)
strResult = strSource.Substring(nLinkedValueStartIndex, nLastIndex - nLinkedValueStartIndex);
return strResult;
}
/// <summary>
/// Extracts the linked name in the associated text of the linked name
/// </summary>
/// <param name="nIndexToStart"> index to start from </param>
/// <param name="strSource"> the source string </param>
string GetLinkedName(string strSource, int nIndexToStart)
{
// Format "§¥§¥APLT§¥<LinkedName>§¥<LinkedValue>¥§¥§"
int nLastIndex = -1;
int nIndex = strSource.IndexOf(STRLINKEDTEXTDELIMITER1FORSEARCH, nIndexToStart);
string strResult = "";
nIndex += STRLINKEDTEXTDELIMITER1FORSEARCH.Length;
nLastIndex = strSource.IndexOf(STRLINKEDTEXTDELIMITER2, nIndex);
if( nLastIndex != -1 )
strResult = strSource.Substring(nIndex, nLastIndex-nIndex);
return strResult;
}
已解析文本
已解析文本是其中所有只读文本都替换为辅助文本的字符串。在开头的动画图像的底部窗口中可以看到此已解析文本。
执行此操作的步骤:
1. 单击窗体上的解析按钮,调用 GetResolvedText 函数。
2. 在循环中,不断调用 GetIndexOfRTFTag 函数,直到找到可见性开始(“\v”)和结束(“\v0”)标签。GetIndexOfRTFTag 函数将找到传递给它的正确输入标签。
3. 因此,在“\v”和“\v0”之间,您得到的内容将是被处理过的,辅助文本将被提取并替换到只读文本的位置。
4. 最后,将返回所有只读文本都替换为辅助文本的最终字符串。
代码
/// <summary>
/// Returns the resolved text. This means all the text will be converted to regular text.
/// :) This also means that, in fact, all the linked names will be replaced with their corresponding linked values :)
/// </summary>
public string GetResolvedRTFText()
{
string strResolvedText = richTextBox1.Rtf;
string strHiddenText = "";
int nStartIndex = 0;
int nHiddenTagStartIndex = 0;
int nHiddenTagEndIndex = 0;
int nIndexFormatDelim1 = -1;
int nIndexFormatDelim2 = -1;
int nIndexFormatDelim3 = -1;
string strLinkedName = "";
string strLinkedText = "";
int nIndexDisplayLinkedName = -1;
while (nHiddenTagStartIndex != -1 && nHiddenTagEndIndex != -1)
{
nHiddenTagStartIndex = GetIndexOfRTFTag(strResolvedText, STRHIDDENBEGINTAG, nStartIndex);
if( nHiddenTagStartIndex == -1 )
break;
nStartIndex = nHiddenTagStartIndex + STRHIDDENBEGINTAG.Length;
nHiddenTagEndIndex = GetIndexOfRTFTag(strResolvedText, STRHIDDENENDTAG, nStartIndex);
if (nHiddenTagEndIndex == -1)
break;
nHiddenTagEndIndex += STRHIDDENENDTAG.Length;
nStartIndex = nHiddenTagEndIndex + STRHIDDENENDTAG.Length;
strHiddenText = strResolvedText.Substring(nHiddenTagStartIndex, nHiddenTagEndIndex - nHiddenTagStartIndex);
nIndexFormatDelim1 = strHiddenText.IndexOf(STRFORMATDELIM1ENCODED, 0);
nIndexFormatDelim2 = strHiddenText.IndexOf(STRFORMATDELIM2ENCODED,
nIndexFormatDelim1 + STRFORMATDELIM1ENCODED.Length);
nIndexFormatDelim3 = strHiddenText.IndexOf(STRFORMATDELIM3ENCODED,
nIndexFormatDelim2 + STRFORMATDELIM2ENCODED.Length);
strLinkedName = strHiddenText.Substring(nIndexFormatDelim1 + STRFORMATDELIM1ENCODED.Length,
nIndexFormatDelim2 - (nIndexFormatDelim1 + STRFORMATDELIM1ENCODED.Length));
strLinkedText = strHiddenText.Substring(nIndexFormatDelim2 + STRFORMATDELIM2ENCODED.Length,
nIndexFormatDelim3 - (nIndexFormatDelim2 + STRFORMATDELIM2ENCODED.Length));
strResolvedText = strResolvedText.Remove(nHiddenTagStartIndex, nHiddenTagEndIndex - nHiddenTagStartIndex);
nIndexDisplayLinkedName = strResolvedText.LastIndexOf(strLinkedName, nHiddenTagStartIndex);
strResolvedText = strResolvedText.Remove(nIndexDisplayLinkedName, strLinkedName.Length);
strResolvedText = strResolvedText.Insert(nIndexDisplayLinkedName, strLinkedText);
nStartIndex = 0;
nHiddenTagStartIndex = 0;
nHiddenTagEndIndex = 0;
}
return strResolvedText;
}
/// <summary>
/// Returns the index of the specified rtf tag (strRtfTag) in the rtf text
/// (strRtfText) starting from the specified start index (nStartIndex)
/// </summary>
int GetIndexOfRTFTag(string strRtfText, string strRtfTag, int nStartIndex)
{
int nIndex = strRtfText.IndexOf(strRtfTag, nStartIndex);
if (nIndex == -1)
return nIndex;
// Validate if that is a proper rtf tag
// Look reverse for properness
// occurs at very beginning, then this is a proper rtf tag
bool bReverseValid = false;
if (nIndex == 0)
bReverseValid = true;
// Previous character is '\', so this is not a rtf tag, but its a display text
if (strRtfText[nIndex - 1] == '\\')
return GetIndexOfRTFTag(strRtfText, strRtfTag, nIndex + strRtfTag.Length);
else
bReverseValid = true;
// Look forward for properness
// occurs at the last and no more characters are present except this, so this is a valid rtf tag
bool bForwardValid = false;
if ((nIndex + strRtfTag.Length) == (strRtfText.Length - 1))
bForwardValid = true;
// The following character after this tag is niether ' ' nor '\\', so its not a valid rtf tag
if (strRtfText[nIndex + strRtfTag.Length] != ' ' &&
strRtfText[nIndex + strRtfTag.Length] != '\\')
return GetIndexOfRTFTag(strRtfText, strRtfTag, nIndex + strRtfTag.Length);
else
bForwardValid = true;
if (bForwardValid && bReverseValid)
return nIndex;
return -1;
}
功能:处理只读文本的修改
感谢富文本框中的受保护文本标签。它不允许用户编辑内容,也不允许删除内容。它还有一个回调函数,当尝试修改文本时会被触发。因此,在我们的例子中,将调用 richTextBox1_Protected。不幸的是,此函数不提供尝试修改的受保护文本的字符索引信息。
所以,这很麻烦。我不得不自己编写代码来处理它。在我们的例子中,当用户尝试剪切、删除、退格或任何其他导致部分或全部文本被删除的操作时,我们希望像 Outlook 电子邮件 ID 中那样删除整个单词。
步骤:
1. 每次按下按键时,调用并存储击键信息。 这将在 richTextBox1_Protected 调用之前执行。
2. 在 richTextBox1_Protected 函数中,根据按下的按键类型,调用 HandleTextOnCut / HandleTextOnBacKSpace / HandleTextOnDelete 中的一个。这三个函数共同点是它们都调用 ProcessSelectionOfText 函数。
ProcessSelectionOfText 函数实际上做的是:它以编程方式选择文本,这些文本可能是用户部分选择的。如果用户同时选择了普通文本和只读文本的某个部分,则此函数将继续选择整个只读文本单词,然后上述三个函数(剪切、退格和删除)将相应地处理文本。
代码
/// <summary>
/// Do proper handling when user has selected to delete the linked text.
/// If the partial linked text is selected, then this function will first select full linked text
/// and then delete it.
/// </summary>
void HandleTextOnDelete()
{
int nSelectionIncrement = 0;
// If no linked text is selected and just the delete key is pressed, then
// programatically select atleast one character and then call other function to select full.
if (richTextBox1.SelectionLength == 0)
{
nSelectionIncrement = 1;
while (richTextBox1.SelectionLength == 0)
{
richTextBox1.Select(richTextBox1.SelectionStart, nSelectionIncrement);
nSelectionIncrement++;
}
}
ProcessSelectionOfText();
richTextBox1.SelectionProtected = false;
richTextBox1.SelectedRtf = "";
}
/// <summary>
/// Do proper handling when user has selected to delete the linked text by hitting backspace.
/// If the partial linked text is selected, then this function will first select full linked text
/// and then delete it.
/// </summary>
void HandleTextOnBacKSpace()
{
// If nothing is selected, then select atleast one character,
// rest of them will be taken care by ProcessSelection... function
if (richTextBox1.SelectionLength == 0)
{
int nSelectionIncrement = 1;
while (richTextBox1.SelectionLength == 0)
{
richTextBox1.Select(richTextBox1.SelectionStart -
nSelectionIncrement, nSelectionIncrement);
nSelectionIncrement++;
}
}
ProcessSelectionOfText();
richTextBox1.SelectionProtected = false;
richTextBox1.SelectedRtf = "";
}
/// <summary>
/// Process the selected linked text and Do programmatical cut operation
/// </summary>
void HandleTextOnCut()
{
ProcessSelectionOfText();
string str = richTextBox1.SelectedRtf;
Clipboard.SetText(str, TextDataFormat.Rtf);
richTextBox1.SelectionProtected = false;
richTextBox1.SelectedRtf = "";
}
/// <summary>
/// Function to smartly select the group of text when user has selected some text which may
/// include both regular text and linked text.
/// Since linked text are treated as single entity, user cant modify the content of the linked text
/// So, in some cases where user select partial linked text and attempts to delete / backspace / cut
/// we will programatically select the whole linked text and erase it.
/// </summary>
void ProcessSelectionOfText()
{
// get the start index of selected text
int nStartIndex = richTextBox1.SelectionStart;
string strLinkedName = "";
int nLastIndex = -1;
// get the selected text
string strSelected = richTextBox1.SelectedText;
int nFinalEndIndex = -1;
int nFirstLinkedNamePosition = -1;
int nFinalFirstIndex = -1;
int nSelectionLength = 0;
int nIndexWhereFullLinkedTextContentEnds = -1;
// Find the last linked value occurance
// Get the end index of selected text
int nEndIndex = richTextBox1.SelectionStart + richTextBox1.SelectionLength;
// Get the index of the arriving delimiter of the linked value
int newOccurence = richTextBox1.Text.IndexOf(STRLINKEDTEXTDELIMITER1FORSEARCH, nEndIndex);
if (newOccurence == -1)
{
// No linked text are there further. So ending selection is just the normal text
}
else
{
// Verify if partial linked text is selected.
strLinkedName = GetLinkedName(richTextBox1.Text, newOccurence);
nLastIndex = richTextBox1.Text.LastIndexOf(strLinkedName, newOccurence);
nIndexWhereFullLinkedTextContentEnds =
richTextBox1.Text.IndexOf(STRLINKEDTEXTDELIMITER3FORSEARCH, newOccurence);
if (nLastIndex >= (richTextBox1.SelectionStart + richTextBox1.SelectionLength))
{
// This means linked text is not selected
nFinalEndIndex = richTextBox1.SelectionStart + richTextBox1.SelectionLength;
}
else
{
// This means linked text is partially selected
nFinalEndIndex = nIndexWhereFullLinkedTextContentEnds + LINKEDTEXTDELIMITER3LENGTH;
}
richTextBox1.Select(richTextBox1.SelectionStart, nFinalEndIndex - richTextBox1.SelectionStart);
}
// Find the first linked text occurence
int nFirstLinkedTextOccurenceIndex =
richTextBox1.Text.IndexOf(STRLINKEDTEXTDELIMITER1FORSEARCH, richTextBox1.SelectionStart);
if (nFirstLinkedTextOccurenceIndex == -1)
{
// User has not selected any linked text.
}
else
{
strLinkedName = GetLinkedName(richTextBox1.Text, nFirstLinkedTextOccurenceIndex);
// Now search left first occurence
nFirstLinkedNamePosition = richTextBox1.Text.LastIndexOf(
strLinkedName, nFirstLinkedTextOccurenceIndex);
if (nFirstLinkedNamePosition == -1)
{
// No linked names are present. This is mistake. This should not happen.
}
else
{
// Verify if partial linked text is selected.
if (nFirstLinkedNamePosition >= richTextBox1.SelectionStart)
{
// Full linked text is selected. So no problem. Leave it intact
}
else
{
// Partial linked text is selected. Select it to full
nSelectionLength = richTextBox1.SelectionStart - nFirstLinkedNamePosition;
nSelectionLength += richTextBox1.SelectionLength;
nFinalFirstIndex = nFirstLinkedNamePosition;
richTextBox1.Select(nFinalFirstIndex, nSelectionLength);
}
}
}
}
关注点
在实现此功能时,我遇到了以下挑战:
- 在互联网上找不到适用于 RichTextBox 控件中文本的屏幕提示功能。尽管有人为此提出了问题,但没有一个答案提供了实际的解决方案。因此,我不得不编写所有这些代码来完成此任务。
- 只读文本的概念开发也很有挑战性。因为当用户按下退格键、删除键、剪切键等时,需要显式处理。尽管 RichTextBox 类在尝试修改受保护文本时会触发事件,但它不提供所讨论的受保护文本的索引。
限制
以下是一些局限性。这些局限性将来可以修复。但我不确定何时可能做到。
- 仅使用键盘选择文本框中的文本非常困难!因为一旦您选择了只读文本,其余选定的文本就会失去选择。
- 如果您不在下拉列表中选择任何内容,然后按 Enter 键,应用程序将会崩溃。这是一个简单的问题,可以修复。但我现在太懒了,懒得修复它
。
- 我最近得知,当 RichTextBox 在 Office 插件 COM 环境中使用时存在一些限制。因此,如果您将此控件用于 Office 项目,它可能不会显示屏幕提示。
如果您有任何修复方案,请分享。我将非常乐意将其纳入文章。
历史
- 首次提交于 2012 年 6 月 17 日。