Question

I am trying to place the caret in the RichTextBox inside a Run in a way that enables the user to continue typing with the same formatting. Directly after this Run, there is another Run with different formatting.

Here is some code to show you what I mean:

private void Button_Click(object sender, RoutedEventArgs e)
{
    Paragraph p = new Paragraph();

    // Add the new Paragraph to the RichTextBox flowdocument.
    rtb1.Document.Blocks.Add(p);

    // Create the first Run.
    Run r1 = new Run("This is a ");

    // Create the second Run.
    Run r2 = new Run("test");
    r2.TextDecorations = TextDecorations.Underline;

    // Add the new Runs to the Paragraph.
    p.Inlines.Add(r1);
    p.Inlines.Add(r2);

    // Place the caret at the end of the first Run, and give focus to the RichTextBox.
    rtb1.CaretPosition = r1.ContentEnd;
    rtb1.Focus();
}

If I continue to type, the new input will get the same formatting as the second Run.

I also tried to set the CaretPosition to

new TextRange(r1.ElementEnd, r1.ElementEnd).Start

hoping that since it had the logical direction set to backward it would work, but to no avail.

Is there a way to set the CaretPosition inside the first Run?

Was it helpful?

Solution

I observed what happened when I selected the space between "a" and "test" and started typing, and saw that a new Run was created. I tried to create a new Run and place it between r1 and r2, and set the CaretPosition to newRun.ContentStart, but that did not work.

I found out that if I ...

  1. created a new Run containing a space,
  2. placed the CaretPosition at the start of the new Run (newRun.ContentStart),
  3. removed the space from the new Run

... I would be able to continue typing with the same formatting as the new Run.

I do not like this approach because I need to create a new Run instead of just using the one in front of it. I will leave it here just in case no one knows a better approach, and will accept it as the best answer in a week if no one can provide a better solution.

Here is the complete code:

private void Button_Click(object sender, RoutedEventArgs e)
{
    Paragraph p = new Paragraph();

    // Add the new Paragraph to the RichTextBox flowdocument.
    rtb1.Document.Blocks.Add(p);

    // Create the first Run.
    Run r1 = new Run("This is a ");

    // Create the second Run.
    Run r2 = new Run("test");
    r2.TextDecorations = TextDecorations.Underline;

    // Add the new Runs to the Paragraph.
    p.Inlines.Add(r1);
    p.Inlines.Add(r2);

    // Create a new Run. This will be placed between r1 and r2.
    Run newRun = new Run();

    // Copy the formatting from r1 to our new Run.
    CopyInlineFormattingToRun(r1, ref newRun);

    // Set the text of the new Run to be a space.
    newRun.Text = " ";

    // Place our new Run after r1.
    CreateNewRunAfterExistingRun(r1, newRun);

    // Put the caret at the start of newRun.
    rtb1.CaretPosition = newRun.ContentStart;

    // Remove the space at the end of newRun.
    newRun.ContentEnd.DeleteTextInRun(-1);

    // Give focus to the RichTextBox.
    rtb1.Focus();
}

/// <summary>
/// Copies some of the properties in <paramref name="mainInline"/> to <paramref name="receiverRun"/>.
/// </summary>
/// <param name="mainInline">The <see cref="Inline"/> containing the desired formatting.</param>
/// <param name="receiverRun">The <see cref="Run"/> which should receive the formattinh from <paramref name="mainInline"/>.</param>
internal static void CopyInlineFormattingToRun(Inline mainInline, ref Run receiverRun)
{
    receiverRun.FontFamily = mainInline.FontFamily;
    receiverRun.FontSize = mainInline.FontSize;
    receiverRun.FontStretch = mainInline.FontStretch;
    receiverRun.FontStyle = mainInline.FontStyle;
    receiverRun.FontWeight = mainInline.FontWeight;
    receiverRun.Foreground = mainInline.Foreground;
    receiverRun.Background = mainInline.Background;
    receiverRun.TextDecorations = mainInline.TextDecorations;
}

/// <summary>
/// Places a <see cref="Run"/> after an existing <see cref="Run"/>.
/// </summary>
/// <param name="existingRun">The existing <see cref="Run"/> already present in the <see cref="FlowDocument"/>.</param>
/// <param name="newRun">The new <see cref="Run"/> which will be placed after the <paramref name="existingRun"/> in the <see cref="FlowDocument"/>.</param>
private void CreateNewRunAfterExistingRun(Run existingRun, Run newRun)
{
    Paragraph p = (Paragraph)existingRun.Parent;
    p.Inlines.InsertAfter(existingRun, newRun);
}

If you instead want to do this on the other side of the Run, you will need to change CreateNewRunAfterExistingRun() to use InsertAfter and instead of ContentStart you will have to use

// Put the caret at the start of newRun.
rtb1.CaretPosition = newRun.ContentEnd;

Edit: You could probably subscribe to the LostFocus or LostKeyboardFocus event, and merge the two Runs if you want to.

OTHER TIPS

The trick is that there are two "end" points for the run.

Rather than rtb1.CaretPosition = newRun.ContentEnd; (which seems to lie just "outside" the end of the run), use rtb1.CaretPosition = newRun.ElementEnd; (which seems to lie just "inside" it) and then whatever style is active at the end of the run will be applied to all text typed subsequently.

With this tweak to the accepted answer, there is no need to add a space and then DeleteTextInRun(-1) to trick the control into working properly.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top