Introduction
This article is aimed to help people who are looking desperately for, c# form based, richtextbox which would have following functionality:
- Screen tip for a specific text in rich text box. I call it as text tip!
- Outlook email-id like readonly text. User can only add or remove the text but cant modify the contents.
- With the help of 1st and 2nd items, third functionality is added to have a secondary text. This secondary text will be displayed when user hovers over the readonly text.
- A multi column list dropdown to add the readonly text. This dropdown will be popped up when user types '$' and '{'.
Background
Some software may require textboxes to support read-only text with additional features as I have mentioned above. In my case, our software contains textboxes which
will have technical keywords which is not supposed to be edited. Since some nontechnical users may find it difficult to deal with these technical words,
it was suggested to support friendly names. Hence, I started implementing the same.
So, now, the technical keywords will be pushed behind the friendly name.
The technical keywords will be shown as a screen tip when hovered over the friendly name.
Using the code
The attached zip contains a solution file which has two projects DemoForm and MuchRichTextBox. DemoForm uses MuchRichTextBox user control
to demo the functionality of MuchRichTextBox.
So, to integrate this control in your project, all you need to do is to reference the dll of MuchRichTextBox. Check the demo source code for the usage.
Following will explain the design of MuchRichTextBox.
Below is the outline design of the code.
Of all the above classes, MuchRichTextBox class is the main component which contains all the features of text tip, protected text & drop down.
The class ExRichTextBox is used from the other code project article
http://www.codeproject.com/Articles/4544/Insert-Plain-Text-and-Images-into-RichTextBox-at-R .
Thanks to Khendy who made life easier by providing functions to insert RTF text.
The class PopupListView is the control that pops up for selecting friendly names. This is basically a list view.
Code Walk-through
In this section, I will take each feature and explain how it is achieved. So I wont explain each function.
Rather I will pick up a feature and drive into it by showing what functions does what to achieve a part of the feature.
Concept
First let me tell you the concept.
- There will be something like readonly text in the richtext box.
- This readonly text will have another text hidden behind it which I call it as secondary text.
- The secondary text will be shown when the mouse is hovered on the readonly text.
- Readonly text can be inserted by typing ${ in order. This will open up a drop down containing list of readonly text.
- Later you can call resolver function on the muchrichtextbox which will give you the text which has secondary names instead of readonly names.
- Also, since readonly text should strictly be kept as readonly despite of the attempts made to edit it, I had to write some code to handle this one.
Suppose if user selects only two letters in the readonly text and hits delete, then it deletes the whole word.
Feature: Screen tip
On mouse move, richtextbox1_MouseMove is called. This function will show the screen tip if the mouse is over the readonly text.
The code is well documented and hence can be understood easily looking into it.
Following are the steps:
- Determine if the mouse is somewhere in the region where some text is there. If not, just return.
- If the mouse is in text region, get the close text based on the mouse co-ordinates calling ExtractWord function.
- Check if this text is a readonly text or just the regular text. If the text is just a regular text, just ignore and return from the function.
- If text is readonly text, then call GetLinkedName and GetLinkedValue functions to get the associated secondary text.
- Display the associated text as a screen tip ! Bingo. so simple as it seems to be. But see the actual code, there is lot of mathematics. I dont want to dig into it deeply.
You can figure it out looking into the code. However, I will discuss the most toughest things I have come across during implementation at the end of this article as a bits and pieces.
Code
Following is some of the major code snippets of this feature:
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;
if ((pointMax.Y + 10) < (e.Location.Y))
{
m_ToolTip.RemoveAll();
m_ToolTip.Hide(richTextBox1);
return;
}
strTip = ExtractWord(richTextBox1.Text, nCharIndexWrtMousePosition);
if (strTip != null)
{
nIndexOfLinkedDelimiter = richTextBox1.Text.IndexOf(STRLINKEDTEXTDELIMITER1FORSEARCH, nCharIndexWrtMousePosition);
if (nIndexOfLinkedDelimiter != -1)
{
strLinkedNameInDelimiter = GetLinkedName(richTextBox1.Text, nIndexOfLinkedDelimiter);
if (strLinkedNameInDelimiter != null)
{
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);
if (nPositionOfLinkedNameInDisplayText <= nCharIndexWrtMousePosition)
{
if( m_ToolTip.GetToolTip(this.richTextBox1) != strTip )
m_ToolTip.Show(strTip, richTextBox1);
}
}
}
}
}
}
}
private string ExtractWord(string sText, int iPos)
{
int iStart = sText.LastIndexOfAny(m_charArraySeparators, iPos) + 1;
int iEnd = sText.IndexOfAny(m_charArraySeparators, iPos);
if (iEnd < 0)
iEnd = sText.Length;
if (iEnd < iStart)
return "";
return sText.Substring(iStart, iEnd - iStart);
}
Feature : READONLY AND SECONDARY TEXT (associated with the readonly text)
In this section, I am going to tell you how secondary text will be associated with the readonly text. When user selects the read only text in the drop down,
both the readonly text and associated secondary text will be added. But only readonly text will be made visible and the secondary text will be
hidden using visibility text tag available in the rich text format.
The readonly text is made uneditable by using the protected tag available
in the rich text format.
Following is the example :
Readonly Text : MyName
Associated Secondary Text : Vinayaka
Following is the sample extract from rtf data :
\protect\f0\fs16 MyName\v \'a7\'a5\'a7\'a5APLT\'a7\'a5MyName\'a7\'a5Vinayaka\'a5\'a7\'a5\'a7 \cf0\highlight0\ulnone\b0\protect0\v0
In here, you can observe tags like "\protect" and "\protect0", "\v" and "\v0"
which are protected tags and visibility tags respectively. As I said earlier, protected is for readonly
and visibility for secondary text.
The first occurence of the string "MyName" is enclosed between protected tags
"\protect" and "\protect0" tag to achieve readonly property.
The next string occurences viz., "MyName" and "Vinayaka" present
are enclosed between "\v" and "\v0" tag for hiding them1.
Also, you can see that "MyName" is repeated. This is because to make sure we are reading the proper secondary text that belongs to the hovered readonly text. This may not be required, but extra care is taken.
I will explain this APLT strings which you may be wondering why the hell is this ! See the sample below :
MyName §¥§¥APLT§¥MyName§¥Vinayaka¥§¥§
This is the plain text version of the richtext extract that i have shown above. The strings "§¥§¥", "APLT" are just the delimiters I am using here for treating them as a secondary text.
Code
The related code is self-evident and need not require comprehensive explanation.
Following are the related functions .
string GetLinkedValue(string strSource, int nIndexToStart)
{
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;
}
string GetLinkedName(string strSource, int nIndexToStart)
{
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;
}
Resolved Text
Resolved text is the string which has all the readonly text replaced with the secondary text. This resolved text can be seen in the bottom window in the animated image at the beginning.
steps to do this :
1. On clicking the resolve button in the form, call the GetResolvedText function.
2. In a loop, keep calling GetIndexOfRTFTag function untill you find visibility begin("\v") and end("\v0") tag . The GetIndexOfRTFTag function will find the proper input tag passed to it.
3. So between '\v' and '\v0', whatever you get, it will be the processed and the secondary text is extracted and replaced in place of the readonly text.
4. Finally, the resultant string with all the readonly text replaced with secondary text will be returned.
Code
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;
}
int GetIndexOfRTFTag(string strRtfText, string strRtfTag, int nStartIndex)
{
int nIndex = strRtfText.IndexOf(strRtfTag, nStartIndex);
if (nIndex == -1)
return nIndex;
bool bReverseValid = false;
if (nIndex == 0)
bReverseValid = true;
if (strRtfText[nIndex - 1] == '\\')
return GetIndexOfRTFTag(strRtfText, strRtfTag, nIndex + strRtfTag.Length);
else
bReverseValid = true;
bool bForwardValid = false;
if ((nIndex + strRtfTag.Length) == (strRtfText.Length - 1))
bForwardValid = true;
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;
}
Feature : HANDLING MODIFICATION OF READ-ONLY TEXT
Thanks to protected text tag in rich text box. It does not allow user to edit the contents nor to delete the contents. It also has a callback which will be triggered when the text is attempted to modify. So, in our case, richTextBox1_Protected will be called. Unfortunately, this function does not give any information of the char index of the attempted protected text.
So, this was a pain. I had to write my own code to handle this. In our case, when user attempts to cut, delete, backspace or anyother actions made that leads to erase partial or full text, we want to get the whole word erased similar to one we see in the outlook email ids.
Steps :
1. Whenever a key is pressed, call, and store the key stroke info. This will be called before the richTextBox1_Protected is called.
2. In richTextBox1_Protected function, depending on the type of key pressed, we call one among HandleTextOnCut / HandleTextOnBacKSpace / HandleTextOnDelete. Common of all these three functions is they all call the function ProcessSelectionOfText.
What actually the function ProcessSelectionOfText does is it programmaticaly selects the text which may be partially selected by the user. If the user has selected both the ordinary text and some partial part of the readonly text, then this function will goahead and selects the full word of readonly text and then the above three functions (on cut, backspace and delete) will handle the text accordingly.
Code
void HandleTextOnDelete()
{
int nSelectionIncrement = 0;
if (richTextBox1.SelectionLength == 0)
{
nSelectionIncrement = 1;
while (richTextBox1.SelectionLength == 0)
{
richTextBox1.Select(richTextBox1.SelectionStart, nSelectionIncrement);
nSelectionIncrement++;
}
}
ProcessSelectionOfText();
richTextBox1.SelectionProtected = false;
richTextBox1.SelectedRtf = "";
}
void HandleTextOnBacKSpace()
{
if (richTextBox1.SelectionLength == 0)
{
int nSelectionIncrement = 1;
while (richTextBox1.SelectionLength == 0)
{
richTextBox1.Select(richTextBox1.SelectionStart -
nSelectionIncrement, nSelectionIncrement);
nSelectionIncrement++;
}
}
ProcessSelectionOfText();
richTextBox1.SelectionProtected = false;
richTextBox1.SelectedRtf = "";
}
void HandleTextOnCut()
{
ProcessSelectionOfText();
string str = richTextBox1.SelectedRtf;
Clipboard.SetText(str, TextDataFormat.Rtf);
richTextBox1.SelectionProtected = false;
richTextBox1.SelectedRtf = "";
}
void ProcessSelectionOfText()
{
int nStartIndex = richTextBox1.SelectionStart;
string strLinkedName = "";
int nLastIndex = -1;
string strSelected = richTextBox1.SelectedText;
int nFinalEndIndex = -1;
int nFirstLinkedNamePosition = -1;
int nFinalFirstIndex = -1;
int nSelectionLength = 0;
int nIndexWhereFullLinkedTextContentEnds = -1;
int nEndIndex = richTextBox1.SelectionStart + richTextBox1.SelectionLength;
int newOccurence = richTextBox1.Text.IndexOf(STRLINKEDTEXTDELIMITER1FORSEARCH, nEndIndex);
if (newOccurence == -1)
{
}
else
{
strLinkedName = GetLinkedName(richTextBox1.Text, newOccurence);
nLastIndex = richTextBox1.Text.LastIndexOf(strLinkedName, newOccurence);
nIndexWhereFullLinkedTextContentEnds =
richTextBox1.Text.IndexOf(STRLINKEDTEXTDELIMITER3FORSEARCH, newOccurence);
if (nLastIndex >= (richTextBox1.SelectionStart + richTextBox1.SelectionLength))
{
nFinalEndIndex = richTextBox1.SelectionStart + richTextBox1.SelectionLength;
}
else
{
nFinalEndIndex = nIndexWhereFullLinkedTextContentEnds + LINKEDTEXTDELIMITER3LENGTH;
}
richTextBox1.Select(richTextBox1.SelectionStart, nFinalEndIndex - richTextBox1.SelectionStart);
}
int nFirstLinkedTextOccurenceIndex =
richTextBox1.Text.IndexOf(STRLINKEDTEXTDELIMITER1FORSEARCH, richTextBox1.SelectionStart);
if (nFirstLinkedTextOccurenceIndex == -1)
{
}
else
{
strLinkedName = GetLinkedName(richTextBox1.Text, nFirstLinkedTextOccurenceIndex);
nFirstLinkedNamePosition = richTextBox1.Text.LastIndexOf(
strLinkedName, nFirstLinkedTextOccurenceIndex);
if (nFirstLinkedNamePosition == -1)
{
}
else
{
if (nFirstLinkedNamePosition >= richTextBox1.SelectionStart)
{
}
else
{
nSelectionLength = richTextBox1.SelectionStart - nFirstLinkedNamePosition;
nSelectionLength += richTextBox1.SelectionLength;
nFinalFirstIndex = nFirstLinkedNamePosition;
richTextBox1.Select(nFinalFirstIndex, nSelectionLength);
}
}
}
}
Points of Interest
While implementing this functionality, i have faced following challenges:
- The screen tip functionality for text in the richtextbox control was not directly available in internet. Although questions were asked for the same, none of the answers
compelled the actual solution. Hence I had to write the whole junk code to do this.
- The read only text concept development was also challenging. Because when user hits backspace, delete, cut, etc.,
needed to be handled explicitly. Although, richtextbox class does triggers event when protected text is attempted for modification,
it does not give the index of the protected text in question.
Limitations
Following are some of the limitations. These limitations can be fixed in future. But I am not sure when it would be possible for me.
- Selecting the text in the text box with just keyboard is challenging ! Because the rest of the text selected looses its selection once you select readonly text.
- The application will crash if you dont select anything in the dropdown and just press enter. This is simple issue and can be fixed. But currently I am too lazy to fix
this
- I recently came to know that there are some limitations of rich text box when it is used in office pluggin COM environment. So, if you use this control for office project,
it may not show you screen tips.
If you have some fixes, please share it. I will be very happy to incorporate into article.
History
- First submitted on 17 June 2012.