Search code examples
c#wpfrichtextboxemoticons

RichTextBox replace string with emoticon / image


Within a RichtTextBox I want to automatically replace emoticon strings (e.g. :D) with an emoticon image. I got it working so far, except that when I write the emoticon string between existing words / strings, the image gets inserted at the end of the line.

For example: hello (inserting :D here) this is a message
results in: hello this is a message ☺ << image

Another (tiny) problem is, that the caret position is set before the image after inserting.

This is what I already got:

public class Emoticon
{
    public Emoticon(string key, Bitmap bitmap)
    {
        Key = key;
        Bitmap = bitmap;
        BitmapImage = bitmap.ToBitmapImage();
    }

    public string Key { get; }
    public Bitmap Bitmap { get; }
    public BitmapImage BitmapImage { get; }
}

public class EmoticonRichTextBox : RichTextBox
{
    private readonly List<Emoticon> _emoticons;

    public EmoticonRichTextBox()
    {
        _emoticons = new List<Emoticon>
        {
            new Emoticon(":D", Properties.Resources.grinning_face)
        };
    }

    protected override void OnTextChanged(TextChangedEventArgs e)
    {
        base.OnTextChanged(e);
        Dispatcher.InvokeAsync(Look);
    }

    private void Look()
    {
        const string keyword = ":D";

        var text = new TextRange(Document.ContentStart, Document.ContentEnd);
        var current = text.Start.GetInsertionPosition(LogicalDirection.Forward);

        while (current != null)
        {
            var textInRun = current.GetTextInRun(LogicalDirection.Forward);
            if (!string.IsNullOrWhiteSpace(textInRun))
            {
                var index = textInRun.IndexOf(keyword, StringComparison.Ordinal);
                if (index != -1)
                {
                    var selectionStart = current.GetPositionAtOffset(index, LogicalDirection.Forward);
                    if (selectionStart == null)
                        continue;

                    var selectionEnd = selectionStart.GetPositionAtOffset(keyword.Length, LogicalDirection.Forward);
                    var selection = new TextRange(selectionStart, selectionEnd) { Text = string.Empty };

                    var emoticon = _emoticons.FirstOrDefault(x => x.Key.Equals(keyword));
                    if (emoticon == null)
                        continue;

                    var image = new System.Windows.Controls.Image
                    {
                        Source = emoticon.BitmapImage,
                        Height = 18,
                        Width = 18,
                        Margin = new Thickness(0, 3, 0, 0)
                    };

                    // inserts at the end of the line
                    selection.Start?.Paragraph?.Inlines.Add(image);

                    // doesn't work
                    CaretPosition = CaretPosition.GetPositionAtOffset(1, LogicalDirection.Forward);
                }
            }

            current = current.GetNextContextPosition(LogicalDirection.Forward);
        }
    }
}

public static class BitmapExtensions
{
    public static BitmapImage ToBitmapImage(this Bitmap bitmap)
    {
        using (var stream = new MemoryStream())
        {
            bitmap.Save(stream, ImageFormat.Png);
            stream.Position = 0;

            var image = new BitmapImage();
            image.BeginInit();
            image.CacheOption = BitmapCacheOption.OnLoad;
            image.DecodePixelHeight = 18;
            image.DecodePixelWidth = 18;
            image.StreamSource = stream;
            image.EndInit();
            image.Freeze();

            return image;
        }
    }
}

Solution

  • The faulty line is selection.Start?.Paragraph?.Inlines.Add(image);. You append the image to the end of the paragraph. You should use one of the InsertBefore or InsertAfter methods.

    But to use these methods you should iterate over the Inlines and find the proper inline to insert before or after. This is not so difficult. You can determine the inline by comparing selectionStart and selectionEnd to ElementStart and ElementEnd properties of the inline.

    One other tricky posibility is that the position you want to insert may fall within an inline. Then you should split that inline and create three others:

    • One containing the elements before the insertion position
    • One containing the image
    • One containing the elements after insertion position.

    Then, you can remove the inline and insert new three inlines to the proper position.

    Wpf's RichTextBox does not have the most beautiful API. Sometimes it can be hard to work with. There is another control called AvalonEdit. It is much easier to use than the RichTextBox. You may want to consider it.