Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Custom Richtextbox for text tip (like screen tip), protected text and much more !

0.00/5 (No votes)
17 Jun 2012 2  
C# Forms RichTextEditor with custom hyperlink with Outlook address like text entities, custom popup listbox, and a screen tip.

Introduction

This article is aimed to help people who are looking desperately for, c# form based, richtextbox which would have following functionality:

  1. Screen tip for a specific text in rich text box. I call it as text tip!
  2. Outlook email-id like readonly text. User can only add or remove the text but cant modify the contents.
  3. 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.
  4. 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.

  1. There will be something like readonly text in the richtext box. 
  2. This readonly text will have another text hidden behind it which I call it as secondary text.
  3. The secondary text will be shown when the mouse is hovered on the readonly text. 
  4. Readonly text can be inserted by typing ${ in order. This will open up a drop down containing list of readonly text. 
  5. Later you can call resolver function on the muchrichtextbox which will give you the text which has secondary names instead of readonly names. 
  6. 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:
  1. Determine if the mouse is somewhere in the region where some text is there. If not, just return.
  2. If the mouse is in text region, get the close text based on the mouse co-ordinates calling ExtractWord function.
  3. 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.
  4. If text is readonly text, then call GetLinkedName and GetLinkedValue functions to get the associated secondary text.
  5. 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;
    
        // 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);
}

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 . 

/// 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;
} 

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 
/// <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;
}

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 
/// <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);
            }
        }
    }
}

Points of Interest

While implementing this functionality, i have faced following challenges:

  1. 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.
  2. 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.

  1. 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.
  2. 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 Smile | :)
  3. 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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here