user5856424
user5856424

Reputation:

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

Upvotes: 5

Views: 1149

Answers (2)

user5856424
user5856424

Reputation:

As @Yusuf Tarık Günaydın suggested, I looked for the AvalonEdit control which did the trick fairly easy.

With the help of this example I just needed to create an VisualLineElementGenerator which looks for the emoticon matches and inserts the images.

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

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 EmoticonTextBox : TextEditor
{
    public EmoticonTextBox()
    {
        HorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
        VerticalScrollBarVisibility = ScrollBarVisibility.Disabled;

        TextArea.TextView.ElementGenerators.Add(new ImageElementGenerator());
    }
}

public class ImageElementGenerator : VisualLineElementGenerator
{
    // To use this class:
    // textEditor.TextArea.TextView.ElementGenerators.Add(new ImageElementGenerator());

    private static readonly Regex ImageRegex = new Regex(@":D", RegexOptions.IgnoreCase);

    private readonly List<Emoticon> _emoticons;

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

    private Match FindMatch(int startOffset)
    {
        var endOffset = CurrentContext.VisualLine.LastDocumentLine.EndOffset;
        var relevantText = CurrentContext.Document.GetText(startOffset, endOffset - startOffset);

        return ImageRegex.Match(relevantText);
    }

    public override int GetFirstInterestedOffset(int startOffset)
    {
        var match = FindMatch(startOffset);
        return match.Success ? startOffset + match.Index : -1;
    }

    public override VisualLineElement ConstructElement(int offset)
    {
        var match = FindMatch(offset);
        if (!match.Success || match.Index != 0)
            return null;

        var key = match.Groups[0].Value;
        var emoticon = _emoticons.FirstOrDefault(x => x.Key.Equals(key));

        var bitmap = emoticon?.BitmapImage;
        if (bitmap == null)
            return null;

        var image = new System.Windows.Controls.Image
        {
            Source = bitmap,
            Width = bitmap.PixelWidth,
            Height = bitmap.PixelHeight
        };

        return new InlineObjectElement(match.Length, image);
    }
}

Upvotes: 1

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.

Upvotes: 2

Related Questions