Reputation:
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
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
Reputation: 3305
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:
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