Search code examples
c#.netwpfxamlrichtextbox

How to copy and paste in wpf richtextbox containing runs and inlineUICOntainer?


I have created a RichTextBox in wpf containing the below content

<RichTextBox x:Name="RichTextBox"
             IsDocumentEnabled="True"
             AutoWordSelection="True"
             IsInactiveSelectionHighlightEnabled="True">
    <FlowDocument>
        <Paragraph>
            <InlineUIContainer>
                <TextBlock Text="Sample"/>
            </InlineUIContainer>
        </Paragraph>
    </FlowDocument>
</RichTextBox>

What I want to achieve here is
1)Copy selected text from the RichTextBox and paste inside the same RichTextBox
2)Copy selected text from one RichTextBox and paste into another RichTextBox in the same application
3)Copy selected text from the RichTextBox and into the clipboard as plaintext which can then be pasted into apps like notepad etc

Issue 1:
when i type any normal text and try copying the whole content in richtextbox, InlineUIContainer is not copied,

Issue 2:
When I want to select only few words in a whole sentence, containing a mix of Runs and InlineUIContainer , its the same.

What i have seen from other stack overflow answers on almost similar queries, that you iterate through the blocks, but it seems to not work, How do i even copy and paste

I understand, there is no direct approach and needs some looping through or working with the text pointers.

This is something i tried to format myself on CTRL+C

StringBuilder sb = new StringBuilder();
foreach (Block block in RichTextBox.Document.Blocks)
{
    if (block is Paragraph p)
    {
        foreach (Inline inline in p.Inlines)
        {
            if (inline is Run run)
            {
                if (RichTextBox.Selection.Contains(run.ContentStart))
                {
                    sb.Append(run.Text);
                }
            }
            else if (inline is InlineUIContainer iuic)
            {
                if (RichTextBox.Selection.Contains(iuic.ContentStart))
                {
                    var tb = iuic.Child as TextBlock;
                    sb.Append(tb.Text);
                }
            }
        }
    }
}

This above code detects the InlineUIContainer, but if i select some portion of text in Run, ex :

<Run>This is example</Run>

If i select only "example", it will give me the whole Run "This is example" instead of just "example"


Solution

  • This answer is modified version of @BionicCode answer, and this answer exactly what was needed, beware, below code is not optimized

    what we are doing here is

    1. We are checking all the elements within the selected range
    2. InlinUIContainer is checked if its Text Pointer is within the selection range
    3. For normal Text, we iterate 1 character at a time until we reach end of selection or end of content or if next element is a InlinUIContainer.
    if (richTextBox.Selection.IsEmpty) return;
        
    var parsingInProgress   = true;
    var selectedTextBuilder = new StringBuilder();
        
    TextPointer currentStartPosition = richTextBox.Selection.Start;
    TextPointerContext context       = currentStartPosition.GetPointerContext(LogicalDirection.Forward);
    Paragraph paragraph              = currentStartPosition.Paragraph;
        
    while (context != TextPointerContext.None)
    {
        // Append a line break if we are in the scope of a new Paragraph
        if (currentStartPosition.Paragraph != paragraph)
        {
            _ = selectedTextBuilder.AppendLine();
        }
        
        var text = string.Empty;
        switch (context)
        {
            case TextPointerContext.EmbeddedElement:
            {
                if (richTextBox.Selection.Contains(currentStartPosition))
                {
                    DependencyObject element = currentStartPosition.GetAdjacentElement(LogicalDirection.Forward);
                    if(element is TextBlock textBlock)
                    {
                        text = textBlock.Text;
                    }
                }
                else
                {
                    parsingInProgress = false;
                }
            }
            break;
    
            case TextPointerContext.Text:
            {
                //we will parse 1 character at a time until we reach the end of the selection or an embedded element
                while (true)
                {
                    //get the next character position
                    var currentEndPosition = currentStartPosition.GetPositionAtOffset(1, LogicalDirection.Forward);
                    if (richTextBox.Selection.Contains(currentEndPosition))
                    {
                        var currentCharacterTextRange = new TextRange(currentStartPosition, currentEndPosition);
        
                        text                += currentCharacterTextRange.Text;
                        currentStartPosition = currentEndPosition;
        
                        //check if the next char exists or we reached content end
                        var nextToCurrentEndPosition = currentEndPosition.GetPositionAtOffset(1, LogicalDirection.Forward);
                        if(nextToCurrentEndPosition == null)
                        {
                            parsingInProgress = false;
                            break;
                        }
                        else if(!richTextBox.Selection.Contains(nextToCurrentEndPosition))
                        {
                            parsingInProgress = false;
                            break;
                        }
        
        
                        //check if next context is an embedded element
                        var tempContextStartPosition = currentEndPosition.GetNextContextPosition(LogicalDirection.Forward);
                        if ((tempContextStartPosition == null))
                        {
                            parsingInProgress = false;
                            break;
                        }
        
                        //check if the next context is an embedded element
                        var tempContext = tempContextStartPosition.GetPointerContext(LogicalDirection.Forward);
                        if (tempContext == TextPointerContext.EmbeddedElement)
                        {
                            //stop parsing
                            break;
                        }
                    }
                    else
                    {
                        parsingInProgress   = false;
                        break;
                    }
                }
            }
            break;
        }
        
        if (text != null)
        {
            _ = selectedTextBuilder.Append(text);
        }
        
        paragraph = currentStartPosition.Paragraph;
        if (!parsingInProgress)
            break;
        
        do
        {
            currentStartPosition = currentStartPosition.GetNextContextPosition(LogicalDirection.Forward);
            context              = currentStartPosition.GetPointerContext(LogicalDirection.Forward);
        } while ((context != TextPointerContext.Text) &&
                  (context != TextPointerContext.EmbeddedElement) &&
                  (context != TextPointerContext.None));
    }
        
    var plainText = selectedTextBuilder.ToString().Replace("\r\n", string.Empty);
    

    How to copy the runs and InlinUIContainer to be pasted again in the richtextBox

    1. You can serialize the data into any data structure of your choice, in my case, I created a struct with Text and Alternate as members, and add this on every match to a list.
    2. On paste, Deserialize this data and insert at caret Position as run and InlinUIContainer

    How to copy and paste into other apps like notepad
    in the above code, copy the plaintext to clipboard which will do the job