How do I "restore" the Caret Position in a Wpf RichTextBox?
Asked Answered
M

4

5

After setting my RichTextBox's text to the string T, the Caret Position in the RichTextBox is "lost" (it goes to the start of it). Here's what I'm doing to try to "restore" it after it is "lost":

public static int GetCaretIndex(RichTextBox C)
{
    return new TextRange(C.Document.ContentStart, C.CaretPosition).Text.Length;
}
...
int CaretIndex = GetCaretIndex(C); // Get the Caret position before setting the text of the RichTextBox
new TextRange(C.Document.ContentStart, C.Document.ContentEnd).Text = T; // Set the text of the RichTextBox
C.CaretPosition = C.Document.ContentStart.GetPositionAtOffset(CaretIndex, LogicalDirection.Forward); // Set the Caret Position based on the "Caret Index" variable

This code, however, does not work. The "restored" Caret is at a different position than the "original" one (always behind the "original" one for some reason).

"Saving" the RichTextBox's CaretPosition as a TextPointer doesn't seem to work either.

Can anyone provide me with an alternative way of "restoring" the Caret, or a way to fix the code above?

Monoicous answered 28/5, 2017 at 21:8 Comment(2)
you retrieve an index and set a position. according to documentation, they are not the same. try saving the caret position instead of the caret index. you seem to be replacing the whole content - what is the point of restoring the caret if there is new text? especially, what should happen if the caret was somwehere near the end, and the new text is shorter?Tully
@dlatikay Trying to save the CaretPosition as a TextPointer makes the "restored" pointer go to the start of the RichTextBox. I'm replacing the whole content for a undo/redo system (see: #15773102). To answer your second question, nothing "different" seems to happen, the caret just go to the Paragraph over the "original" caret line, or goes back a few characters.Monoicous
M
5

Seems to work (for me): C.CaretPosition = C.Document.ContentStart; C.CaretPosition = C.CaretPosition.GetPositionAtOffset(CaretIndex, LogicalDirection.Forward);

(I hate RichTextBox by the way.)

Marylnmarylou answered 28/5, 2017 at 21:51 Comment(2)
That (for some weird reason) "works better". Still having some trouble when the new Text changes the Text size, but I'm pretty sure that with some playing around I can make it work (will post it here as soon as I get it working). "I hate RichTextBox by the way." - you are not the only one.Monoicous
This is short, obvious, and to the point. Just wish CaretPosition wasn't set twice, but I'm willing to live with that.Encarnacion
P
4

I was dealing with a similar issue recently and there is my solution. In my case, I'm creating a new RichTextBox.Document content and when I do this, I want to keep the caret position.

My idea was that caret offset functions are biased thanks to data structures used for text representation (Paragraphs, Runs, ...) which are also somehow calculated to offset position.

TextRange is a good approach to get exact caret position in the text. The problem lays in its restoration. But it gets easy when I know from which components my document is constructed. In my case, there are just Paragraphs and Runs.

What remains is to visit document structure, find an exact run where the caret should be and set the caret to correct position of found run.

Code:

// backup caret position in text
int backPosition = 
    new TextRange(RichTextBox.CaretPosition.DocumentStart, RichTextBox.CaretPosition).Text.Length;

// set new content (caret position is lost there)
RichTextBox.Document.Blocks.Clear();
SetNewDocumentContent(RichTextBox.Document);

// find position and run to which place caret
int pos = 0; Run caretRun = null;
foreach (var block in RichTextBox.Document.Blocks)
{
    if (!(block is Paragraph para))
        continue;

    foreach (var inline in para.Inlines){
    {
        if (!(inline is Run run))
            continue;

        // find run to which place caret
        if (caretRun == null && backPosition > 0)
        {
            pos += run.Text.Length;
            if (pos >= backPosition){
                 caretRun = run;
                 break;
            }
        }
    }

    if (caretRun!=null)
        break;
}

// restore caret position
if (caretRun != null)
    RichTextBox.CaretPosition = 
        caretRun.ContentEnd.GetPositionAtOffset(backPosition - pos, LogicalDirection.Forward);

The code is not tested. I assembled it from various parts of my application. Let me know if you find any issue.

Pathless answered 16/5, 2018 at 17:11 Comment(0)
C
0

In my situation I have a RichTextBox with a single Paragraph that only allows entering text and line breaks. I change the structure of the RichTextBox ( by creating different coloured Run instances ) but not the text and restore after the change.

public static class CaretRestorer
{
    public static void Restore(RichTextBox richTextBox, Action changer)
    {
        var caretPosition = GetCaretPosition(richTextBox);
        changer();
        Restore(richTextBox, caretPosition);
    }
    private static string GetFullText(RichTextBox richTextBox)
    {
        return new TextRange(richTextBox.Document.ContentStart, richTextBox.Document.ContentEnd).Text;
    }
    private static int GetInlineTextLength(Inline inline)
    {
        if(inline is LineBreak)
        {
            return 2;
        }
        return new TextRange(inline.ContentStart, inline.ContentEnd).Text.Length;
    }
    private static void Restore(RichTextBox richTextBox,int caretPosition)
    {
        var inlines = GetInlines(richTextBox);
        var accumulatedTextLength = 0;
        foreach (var inline in inlines)
        {
            var inlineTextLength = GetInlineTextLength(inline);
            var newAccumulatedTextLength = accumulatedTextLength + inlineTextLength;
            if (newAccumulatedTextLength >= caretPosition)
            {
                TextPointer newCaretPosition = null;
                if(inline is LineBreak)
                {
                    newCaretPosition = inline.ContentEnd;
                }
                else
                {
                    var diff = caretPosition - accumulatedTextLength;
                    newCaretPosition = inline.ContentStart.GetPositionAtOffset(diff);
                }
                
                richTextBox.CaretPosition = newCaretPosition;
                break;
            }
            else
            {
                accumulatedTextLength = newAccumulatedTextLength;
            }
        }
    }
    private static int GetCaretPosition(RichTextBox richTextBox)
    {
        return new TextRange(richTextBox.Document.ContentStart, richTextBox.CaretPosition).Text.Length;
    }

    

    private static Paragraph GetParagraph(RichTextBox RichTextBox)
    {
        return RichTextBox.Document.Blocks.FirstBlock as Paragraph;
    }
    private static InlineCollection GetInlines(RichTextBox RichTextBox)
    {
        return GetParagraph(RichTextBox).Inlines;
    }
}
Crankshaft answered 16/7, 2020 at 14:8 Comment(0)
M
0

I found the simplest solution was just to compare the text before and after the change.

Here's what that looks like:

string _preText = "";
private void SaveCursor()
{
    _preText = new TextRange(RTB.Document.ContentStart, RTB.CaretPosition).Text;
}
private void RestoreCursor()
{
    var startPos = RTB.Document.ContentStart;
    var newPos = RTB.Document.ContentStart;
    string _postText = "";
    while (newPos != null)
    {
        _postText = new TextRange(startPos, newPos).Text;
        if (_preText == _postText)
            break;

        newPos = newPos.GetNextContextPosition(LogicalDirection.Forward);
    }
    RTB.CaretPosition = newPos;
}

Then in practice, you would just sandwich the two methods around your update.

private void KeyUse_Editor(object sender, System.Windows.Input.KeyEventArgs e)
{
    SaveCursor();
    //Whatever your update method is
    UpdateText();
    RestoreCursor();
}

This way if you make a change to the underlying structure, as long as the text is the same, it will be easy to find the new FlowDocument position.

Mean answered 16/3, 2023 at 21:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.