Search code examples
c#.netwpfwpf-controlsrichtextbox

WPF RichTextBox text navigation


I am developing test application where have RichTextBox are and few buttons. That control contains text and whenever caret position is located when I am clicking button I want to count next 3 words from current caret position and after third word insert some text.

When I am trying to do this with WPF RichTextBox it comes comlicated to do this. During investigation, I found RichTextBox from WinForms namespace handle this task easier.

Does anyone know is there an extension, package for WPF RichTextBox that works in similar way as RichTextBox from WinForms?

P.S. If there is no such solution, please share location where I can lear how to work with WPF RichTextBox deeply.

Please, do not suggest to use RichTextBox from Winforms in WPF


Solution

  • See the following InsertSomeText(this RichTextBox rtb, int skip, string text) extension method below.

    Example how to use it:

    rtb.InsertSomeText(3, "<Text>");
    

    The rtb is a reference to the RichTextBox control. The first parameter skip defines how much words from the current caret position will be passed and then some text specified by the text parameter will be inserted after the last word.

    The regular expression is used to analyze a text. Currently the following characters is defined as word delimiters: ,, ., ;, \s (space) and \t (tab). This list can be updated depend on requirements. Additional information about this pattern you can see here: https://regexr.com/7c53c.

    public static class RichTextBoxEx
    {
        public static void InsertSomeText(this RichTextBox rtb, int skip, string text)
        {
            var textRange = new TextRange(rtb.CaretPosition, rtb.Document.ContentEnd);
    
            // Build list of Word/TextRange pairs
            var words = CalculateTextRange(textRange, @"[^,.;\s\t]+", skip);
    
            if (words.Count >= skip)
            {
                var required = words[skip - 1];
                if (required.Item2 is TextRange tr && required.Item1 is string word)
                {                  
                    var position = tr.Start.GetPositionAtOffset(word.Length);
                    position.InsertTextInRun(text);
                    rtb.CaretPosition = position.GetPositionAtOffset(text.Length);
                }
            }
        }
    
    
        private static IList<(string, TextRange)> CalculateTextRange(TextRange range, string pattern, int maxwords)
        {
            TextRange search = range;
            int correction = 0;
            var result = new List<(string, TextRange)>();
    
            // Enumerate all found mathes and creating list of Word/TextRange pairs
            var regExp = new Regex(pattern.ToString(), RegexOptions.IgnoreCase);
            foreach (Match match in regExp.Matches(range.Text))
            {
                if (CalculateTextRange(search, match.Index - correction, match.Length) is TextRange tr)
                {
                    result.Add((match.Value, tr));
                    correction = match.Index + match.Length;
                    search = new TextRange(tr.End, search.End);
                    if (--maxwords <= 0)
                        break;
                }
            }
            return result;
        }
    
        // Returns a `TextRange` of the string started from `iStart` index
        // and having `length` characters or `null` if no string found.
        private static TextRange CalculateTextRange(TextRange search, int iStart, int length)
        {
            return (GetTextPositionAtOffset(search.Start, iStart) is TextPointer start)
                ? new TextRange(start, GetTextPositionAtOffset(start, length))
                : null;
        }
    
        private static TextPointer GetTextPositionAtOffset(TextPointer position, int offset)
        {
            for (TextPointer current = position; current != null; current = position.GetNextContextPosition(LogicalDirection.Forward))
            {
                position = current;
                var adjacent = position.GetAdjacentElement(LogicalDirection.Forward);
                var context = position.GetPointerContext(LogicalDirection.Forward);
                switch (context)
                {
                    case TextPointerContext.Text:
                        int count = position.GetTextRunLength(LogicalDirection.Forward);
                        if (offset <= count)
                        {
                            return position.GetPositionAtOffset(offset);
                        }
                        offset -= count;
                        break;
                    case TextPointerContext.ElementStart:
                        if (adjacent is InlineUIContainer)
                            offset--;
                        break;
                    case TextPointerContext.ElementEnd:
                        if (adjacent is Paragraph)
                            offset -= 2;
                        break;
                    default:
                        break;
                }
            }
            return position;
        }
    }