4

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?

Polygons
  • 104
  • 1
  • 8
  • 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? – Cee McSharpface May 28 '17 at 21:26
  • @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: https://stackoverflow.com/questions/15772602/how-to-undo-and-redo-in-c-sharp-rich-text-box). 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. – Polygons May 28 '17 at 21:43

4 Answers4

4

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

(I hate RichTextBox by the way.)

Maciek Świszczowski
  • 1,155
  • 11
  • 28
  • 1
    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. – Polygons May 28 '17 at 22:05
  • This is short, obvious, and to the point. Just wish CaretPosition wasn't set twice, but I'm willing to live with that. – Joel Rondeau Oct 19 '21 at 18:20
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.

Dave
  • 83
  • 1
  • 4
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;
    }
}
user487779
  • 550
  • 5
  • 12
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.