Introduction
An editor supporting smileys is a very important feature nowadays. Any chat editor should render smileys from a piece of text. For example, 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:
- If the
TextPointer
is within a word or at start of a word boundary, the containing word will be returned.
- If the
TextPointer
is between two words, the following (next) word will be returned.
- 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;
wordEndPosition = GetPositionAtWordBoundary(
position, LogicalDirection.Forward);
if (wordEndPosition != null)
{
wordStartPosition = GetPositionAtWordBoundary(
wordEndPosition, 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)
{
if (_timer == null)
{
_timer = new DispatcherTimer(DispatcherPriority.Background);
_timer.Interval = TimeSpan.FromSeconds(0.5);
_timer.Tick += LookUp;
}
_timer.Stop();
_timer.Start();
base.OnTextChanged(e);
}