简体   繁体   中英

WPF RichTextBox - Multiple Block Selection

Hi I'm currently working on a WPF project (with/ C#) that involves the use of RichTextBox . I currently have completed most of my work, but need assistance obtaining more than one block via mouse selection, for instance to set TextAlignment or Margin. For your reference, the following will select one block by setting a TextPointer for the RichTextBox caret, and then iterating through Document.Blocks to obtain an individual block. But I want an approach that will allow me to obtain more than just one block. Could somebody provide more insight on how to select multiple blocks (each block on a different line)?

// XAML

<RichTextBox Name="rtb" HorizontalAlignment="Left">
   <FlowDocument>
      <Paragraph TextAlignment="Left" Margin="0.0">
         <Run Text="Hello"/>
      </Paragraph>
      <Paragraph TextAlignment="Right" Margin="0.0">
         <Run Text="World"/>
      </Paragraph>
      <Paragraph TextAlignment="Left" Margin="1.0">
         <Run Text="Hello"/>
      </Paragraph>
      <Paragraph TextAlignment="Right" Margin="1.0">
         <Run Text="World"/>
      </Paragraph>
   </FlowDocument>
</RichTextBox>


// Code Behind

var curCaret = rtb.CaretPosition;
Block curBlock = rtb.Document.Blocks.Where(x => x.ContentStart.CompareTo(curCaret) ==    -1 && x.ContentEnd.CompareTo(curCaret) == 1).FirstOrDefault();

If you just want to find the top-level Blocks that overlap the current selection range , you can do this:

public static class FlowDocumentHelper
{
    public static bool Overlaps(this TextElement element, TextPointer start, TextPointer end)
    {
        return element.ContentEnd.CompareTo(start) > 0 && element.ContentStart.CompareTo(end) < 0;
    }
}

and then

var blocks = richTextBox.Document.Blocks.Where(block => block.Overlaps(richTextBox.Selection.Start, richTextBox.Selection.End));

However, if you are looking for the paragraphs that overlap the current selection range this will not work because they could be buried deep inside the text element hierarchy, for instance inside a figure inside a list inside a table inside a section . To actually discover the paragraphs overlapping the current selection range, you have to recursively walk the hierarchy of blocks, and WPF provides no straightforward way to do this (though they certainly have this information internally!).

Thus one could either handcraft recursive iterators for all possible TextElement classes that might contain children, or iterate through all the TextPointer objects in the document and use them to discover the hierarchy. The following uses the latter strategy:

public static class FlowDocumentHelper
{
    public static IEnumerable<TTextElement> WalkTextRange<TTextElement>(this FlowDocument doc, TextPointer start, TextPointer end) where TTextElement : TextElement
    {
        var lastVisited = new Dictionary<int, TTextElement>();
        foreach (var stack in doc.WalkTextHierarchy())
        {
            var element = stack.Peek() as TTextElement;
            if (element != null)
            {
                TTextElement previous;
                if (!lastVisited.TryGetValue(stack.Count - 1, out previous) || previous != element)
                {
                    if (element.Overlaps(start, end))
                        yield return element;
                    lastVisited[stack.Count - 1] = element;
                }
            }
        }
    }

    public static bool Overlaps(this TextElement element, TextPointer start, TextPointer end)
    {
        return element.ContentEnd.CompareTo(start) > 0 && element.ContentStart.CompareTo(end) < 0;
    }

    public static IEnumerable<Stack<DependencyObject>> WalkTextHierarchy(this FlowDocument doc)
    {
        if (doc == null)
            throw new ArgumentNullException();

        var stack = new Stack<DependencyObject>();

        // Keep a TextPointer for FlowDocument.ContentEnd handy, so we know when we're done.
        TextPointer docEnd = doc.ContentEnd;

        // Keep going until the TextPointer is equal to or greater than ContentEnd.
        for (var iterator = doc.ContentStart; 
            (iterator != null) && (iterator.CompareTo(docEnd) < 0);
            iterator = iterator.GetNextContextPosition(LogicalDirection.Forward))
        {
            var parent = iterator.Parent;

            // Identify the type of content immediately adjacent to the text pointer.
            TextPointerContext context = iterator.GetPointerContext(LogicalDirection.Forward);

            switch (context)
            {
                case TextPointerContext.ElementStart:
                case TextPointerContext.EmbeddedElement:
                case TextPointerContext.Text:
                    PushElement(stack, parent);
                    yield return stack;
                    break;

                case TextPointerContext.ElementEnd:
                    break;

                default:
                    throw new System.Exception("Unhandled TextPointerContext " + context.ToString());
            }

            switch (context)
            {
                case TextPointerContext.ElementEnd:
                case TextPointerContext.EmbeddedElement:
                case TextPointerContext.Text:
                    PopToElement(stack, parent);
                    break;

                case TextPointerContext.ElementStart:
                    break;

                default:
                    throw new System.Exception("Unhandled TextPointerContext " + context.ToString());
            }
        }
    }

    static int IndexOf<T>(Stack<T> source, T value)
    {
        int index = 0;
        var comparer = EqualityComparer<T>.Default;
        foreach (T item in source)
        {
            if (comparer.Equals(item, value))
                return index;
            index++;
        }
        return -1;
    }

    static void PopToElement<T>(Stack<T> stack, T item)
    {
        for (int index = IndexOf(stack, item); index >= 0; index--)
            stack.Pop();
    }

    static void PushElement<T>(Stack<T> stack, T item)
    {
        PopToElement(stack, item);
        stack.Push(item);
    }
}

And then

var paragraphs = richTextBox.Document.WalkTextRange<Paragraph>(richTextBox.Selection.Start, richTextBox.Selection.End);

(Note - Moderately tested non-production code.)

Finally, if you want to allow your users to Ctrl-Select multiple ranges of text that are not next to each other, as can be done in Word as described here: Select items that aren't next to each other , then I think you are out of luck. RichTextBox does not appear to support this user interaction.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM