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

WPF RichTextBox supporting smileys

0.00/5 (No votes)
28 May 2013 1  
The article describes the way to create an extended RichTextBox which supports smileys.

Introduction 

An editor supporting smileys is a very important feature nowadays. Any chat editor should render smileys from a piece of text. For example, Smile | <img src= denotes a smiling face. This article helps you to create an extended rich text editor which supports smileys.

Background 

Before going to create an extended editor, it is a must to understand the structure of the WPF RichTextBox. The WPF editor usually works on flow document objects. In order to replace a text in RTB with a UI container say image in our case, we should iterate all words first. 

Basically we need a helper method which takes the TextPointer position as an argument and returns the word start and end positions. The following scenarios need to be considered on word iteration:

  1. If the TextPointer is within a word or at start of a word boundary, the containing word will be returned.
  2. If the TextPointer is between two words, the following (next) word will be returned.
  3. If the TextPointer is at trailing word boundary, the following (next) word will be returned.  

It is convenient to return the word start and end positions as a TextRange object. Also, our implementation of GetWordRange will return the range covering just the word, excluding any extra trailing whitespaces.

public static TextRange GetWordRange(TextPointer position)
{
    TextRange wordRange = null;
    TextPointer wordStartPosition = null;
    TextPointer wordEndPosition = null;
    // Go forward first, to find word end position.
    wordEndPosition = GetPositionAtWordBoundary(
      position, /*wordBreakDirection*/LogicalDirection.Forward);
    if (wordEndPosition != null)
    {
        // Then travel backwards, to find word start position.
        wordStartPosition = GetPositionAtWordBoundary(
          wordEndPosition, /*wordBreakDirection*/LogicalDirection.Backward);
    }
    if (wordStartPosition != null)
    {
        wordRange = new TextRange(wordStartPosition, wordEndPosition);
    }
    return wordRange;
} 

More details: http://blogs.msdn.com/b/prajakta/archive/2006/11/01/navigate-words-in-richtextbox.aspx 

Using the code 

Our extended editor will contain a collection property of type EmoticonMapper. It has a Text property and a ImageSource property. The text denotes the piece of text from which the smiley should render. ImageSource denotes the corresponding icon for the text. 

public class EmoticonMapper
{
    public ImageSource Icon { get; set; }
    public string Text { get; set; }
}

The usage of this collection property in XAML will look like below:

<local:RichTextBoxExt Margin="5">
    <local:RichTextBoxExt.Emoticons>
        <local:EmoticonMapper Text=":)" Icon="Smileys/1.png"/>
        <local:EmoticonMapper Text=":-c" Icon="Smileys/101.png"/>
        <local:EmoticonMapper Text="B-)" Icon="Smileys/16.png"/>
        <local:EmoticonMapper Text=":D" Icon="Smileys/4.png"/>
        <local:EmoticonMapper Text=":(" Icon="Smileys/2.png"/>
    </local:RichTextBoxExt.Emoticons>
</local:RichTextBoxExt>  

Our control will look for a word matching with any of the given text and render a corresponding smiley at the cursor position. The look up will trigger on the OnTextChanged override. So for every text change the following method will be invoked:

private void UpdateSmileys()
{
    var tp = Document.ContentStart;
    var word = WordBreaker.GetWordRange(tp);
    while (word.End.GetNextInsertionPosition(LogicalDirection.Forward) != null)
    {
        word = WordBreaker.GetWordRange(
          word.End.GetNextInsertionPosition(LogicalDirection.Forward));
        var smileys = from smiley in Emoticons
                      where smiley.Text == word.Text
                      select smiley;
        var emoticonMappers = smileys as IList<EmoticonMapper> ?? smileys.ToList();
        if (emoticonMappers.Any())
        {
            var emoticon = emoticonMappers.FirstOrDefault();
            var img = new Image(){Stretch = Stretch.None};
            if (emoticon != null) img.Source = emoticon.Icon;
            ReplaceTextRangeWithImage(word, img);
        }
    }
} 

We iterate through all the words in the editor and look for a text exactly matching any of the emoticon mapper text. Once we find such a word, an image will be created and inserted at the position of the text. The ReplacingTextRangeWithImage method is used to insert the smiley image at the particular text range.

public void ReplaceTextRangeWithImage(TextRange textRange, Image image)
{
    if (textRange.Start.Parent is Run)
    {
        var run = textRange.Start.Parent as Run;
        var runBefore =
            new Run(new TextRange(run.ContentStart, textRange.Start).Text);
        var runAfter =
            new Run(new TextRange(textRange.End, run.ContentEnd).Text);

        if (textRange.Start.Paragraph != null)
        {
            textRange.Start.Paragraph.Inlines.Add(runBefore);
            textRange.Start.Paragraph.Inlines.Add(image);
            textRange.Start.Paragraph.Inlines.Add(runAfter);
            textRange.Start.Paragraph.Inlines.Remove(run);
        }
        CaretPosition = runAfter.ContentEnd;
    }
} 

Performance 

Triggering look up on text changed will kill the typing speed. So I suggest to start look up on idle time. A dispatcher timer is used, which will re-start at text changed with a 0.5 seconds interval. It allows the user to type faster and never start the look up. Once the user is idle for 0.5 seconds, it will start look up in the entire editor. The interval can be adjusted if more performance is needed. 

protected override void OnTextChanged(TextChangedEventArgs e)
{
    //Looking for an idle time to start look up..
    if (_timer == null)
    {
        _timer = new DispatcherTimer(DispatcherPriority.Background);
        _timer.Interval = TimeSpan.FromSeconds(0.5);
        _timer.Tick += LookUp;
    }
    //Restart timer here...
    _timer.Stop();
    _timer.Start();
    base.OnTextChanged(e);
}  

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