I'm trying to get my head around the TextPointer class in a WPF RichTextBox.
I would like to be able to keep track of them so that I can associate information with areas in the text.
I am currently working with a very simple example to try and figure out what is going on. In the PreviewKeyDown event I am storing the caret position and then in the PreviewKeyUp event I am creating a TextRange based on the before and after caret positions. Here is a code sample that illustrates what I am trying to do:
// The caret position before typing
private TextPointer caretBefore = null;
private void rtbTest_PreviewKeyDown(object sender, KeyEventArgs e)
{
// Store caret position
caretBefore = rtbTest.CaretPosition;
}
private void rtbTest_PreviewKeyUp(object sender, KeyEventArgs e)
{
// Get text between before and after caret positions
TextRange tr = new TextRange(caretBefore, rtbTest.CaretPosition);
MessageBox.Show(tr.Text);
}
The problem is that the text that I get is blank. For example, if I type the character 'a' then I would expect to find the text "a" in the TextRange.
Does anyone know what is going wrong? It could be something very simple but I've spent an afternoon getting nowhere.
I am trying to embrace the new WPF technology but find that the RichTextBox in particular is so complicated that it makes even doing simple things like this difficult. If anyone has any links that do a good job of explaining the TextPointer, I would appreciate it if you can let me know.
When you add and remove text from a FlowDocument, all the TextPointers adjust their position based on a number of heuristics designed to make them stay in as close to the same "place" as possible.
For deletions this is simple: If the TextPointer is in the deleted text, it ends up betweem the characters that had been surrounding the deleted text. But for insertions it is not so simple: When text or other elements are inserted into a FlowDocument exactly at an existing TextPointer, should the TextPointer end up before or after the inserted text? TextPointer has a property called "LogicalDirection" that controls this.
What is happening in your case is that the "caretBefore" position you are capturing is exactly the TextPosition where the typed character is inserted, and in your test cases your LogicalDirection is LogicalDirection.Forward. Thus when the character is inserted, your "caretBefore" is ending up after the inserted character, which coincides with TextPosition giving you an empty TextRange.
How does a TextPointer get a LogicalDirection assigned to it? If you click on a the RichTextBox to set the caret position, the click is interpreted as being between two characters. If the actual point you clicked was on the second character, LogicalDirection is set to Forward, but if the actual point you clicked on was the first character, LogicalDirection is set to Backward.
Try this experiment:
- Set your FontSize="40" and prepopulate the RichTextBox with the text "ABCD" in the constructor
- Click on the right side of the B and type an "X" between the B and the C. LogicalDirection is Backward, so your "beforeCaret" ends up before the "X" and your MessageBox shows the "X".
- Click on the left side of the C and type an "X" between the B and the C. LogicalDirection is Forward, so your "beforeCaret" ends up after the "X" and your MessageBox is empty.
This behaviour is counterintuitive: When you don't know that LogicalDirection exists, you would think that clicking on the right-hand side of the B or the left-hand side of the C would give you exactly the same caret position.
Note: An easy way to visualize what is going on is to command out your MessageBox.Show and instead do a caretBefore.InsertTextInRun("^");
How do you achieve the result you need? LogicalDirection is read-only. One way is to use TextRange to force the construction of a TextPointer with a LogicalDirection of Backward:
caretBefore = new TextRange(caretBefore, caretBefore.DocumentEnd).Start;
Do this in PreviewKeyDown. If you wait until PreviewKeyUp it is already too late: caretBefore has moved. This works because as far as I can tell, the Start of a non-empty TextRange always has a LogicalDirection of Backward.
Another option is to save the symbol offset from the beginning of the document (note that this is not a character offset!). In this case you could store the offset in PreviewKeyDown:
caretBeforeOffset = caretBefore.DocumentStart.OffsetToPosition(caretBefore);
and reset caretBefore to the same symbol offset in PreviewKeyUp:
caretBefore = caretBefore.DocumentStart.GetPositionAtOffset(caretBeforeOffset,
LogicalDirection.Forward);
Although this works it is not as general as forcing your TextPointer to have a LogicalDirection of Backward: Any text changes earlier in the document between the PreviewKeyDown and PreviewKeyUp will cause the symbol offset calculation to find the wrong location, which is what TextPointers were designed to fix in the first place.
I don't know of any good resources for learning about TextPointers except for reading the documentation and playing with them, which is exactly what you have already been doing.
For me TextPointer before = yourRichTextBox.CaretPosition.GetPositionAtOffset(-1, LogicalDirection.Backward);
works in order to obtain the position of the character that is just before the Caret. You can then obtain the TextRange
for the inserted character with TextRange range = new TextRange(before, yourRichTextBox.CaretPosition);
(You should be checking for null
when using before
because if there's nothing before the caret it'll be null
)