我们可以在 WPF 的 RichTextBox 中以编程方式搜索突出显示的文本吗

Can we programmatically search highlighted text in RichTextBox of WPF

如下文末尾所示,使用Microsoft WORD VBA,我们可以搜索MS WORD文档中的所有高亮文本。在遗留 MS Office VSTO add-in 中使用 C# 也可以实现同样的效果。 问题:我们如何以编程方式获取 WPF RichTextBox 中所有 highlighted 文本的 TexRanges?

WPF RichTextBox 显示:

获取rtf的代码:

string sRTF = "";
TextRange tr = new TextRange(rtbTest.Document.ContentStart, rtbTest.Document.ContentEnd);

using (MemoryStream ms = new MemoryStream())
{
    tr.Save(ms, DataFormats.Rtf);
    sRTF = ASCIIEncoding.Default.GetString(ms.ToArray());
}

Debug.Write(sRTF);

RTF 输出:

在下面的输出中,我们可以看到突出显示的文本 test 的 rtf 是 {\lang9\highlight2\ltrch test}。我们如何以编程方式在此处获取突出显示的文本(即 test)。这只是一个示例。,我的意思是以编程方式获取所有突出显示的文本?

{\rtf1\ansi\ansicpg1252\uc1\htmautsp\deff2{\fonttbl{\f0\fcharset0 Times New Roman;}{\f2\fcharset0 Calibri;}}{\colortbl\red0\green0\blue0;\red255\green255\blue255;\red255\green255\blue0;}\loch\hich\dbch\pard\plain\ltrpar\itap0{\lang1033\fs18\f2\cf0 \cf0\ql{\fs22\f2 {\lang9\ltrch This is a }{\lang9\highlight2\ltrch test}{\lang9\ltrch  for a WPF RichTextBox}\li0\ri0\sa200\sb0\fi0\ql\par}
{\f2 {\ltrch }\li0\ri0\sa0\sb0\fi0\ql\par}
}
}

我们能否像我们(例如)在 WORD 文档中通过如下所示的 VBA 宏或通过遗留 VSTO 加载项中的 c# 以编程方式在 WPF RichTextBox 中实现相同的功能:

Selection.Find.ClearFormatting
Selection.Find.Highlight = True
With Selection.Find
    .Text = ""
    .Replacement.Text = ""
    .Forward = True
    .Wrap = wdFindContinue
    .Format = True
    .MatchCase = False
    .MatchWholeWord = False
    .MatchWildcards = False
    .MatchSoundsLike = False
    .MatchAllWordForms = False
End With
Selection.Find.Execute
Selection.Find.Execute

要理解什么是算法可以用来分析FlowDocument的内容最好的方法是阅读Flexible Content Display With Flow Documentspost或者,例如阅读“文档”一章”在书中 MacDonald M. - Pro WPF 4.5 in C#。 Windows Presentation Foundation in .NET 4.5(.NET 专家之声),2012.

因此,FlowDocument 包含 BlockCollection Blocks 属性。这是整个 FlowDocument 内容的顶级块。

下面的代码使用此 属性 递归解析和分析 FlowDocument 中的所有元素,包括搜索具有指定背景颜色的文本片段。

出于测试目的,应用程序 window 包含 Color ComboBox 允许选择一些颜色并使用 设置颜色按钮。 搜索彩色文本 按钮开始扫描文档,找到带有指定颜色的文本并使用创建的 TextRange 列表将文本重绘为粉红色。

MainWindow.xaml

<Window ...
        Title="MainWindow" Height="350" Width="500" >
    <Grid>

        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>


        <RichTextBox Name="rtb" BorderBrush="LightGreen" 
                     Padding="5" Margin="10" VerticalScrollBarVisibility="Auto">
            <FlowDocument>
                <Paragraph>
                    <Run>?</Run> 
                </Paragraph>
            </FlowDocument>            
        </RichTextBox>
        <Grid Grid.Row="1" Margin="5">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="auto"/>
                <ColumnDefinition Width="auto" MinWidth="60"/>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>

            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>

            <Label Content="Color: " />
            <ComboBox x:Name="ComboColor" Grid.Column="1" Width="150" Margin="3" SelectedValuePath="Name" >
                <ComboBox.ItemTemplate>
                    <DataTemplate>
                        <StackPanel Orientation="Horizontal">
                            <Rectangle Fill="{Binding Name}" Width="16" Height="16" Margin="0,0,5,0" />
                            <TextBlock Text="{Binding Name}" />
                        </StackPanel>
                    </DataTemplate>
                </ComboBox.ItemTemplate>
            </ComboBox>

            <Button Content="Set color" Grid.Row="1" Grid.Column="1" Padding="3" Margin="3" Click="SetColor_Click"/>
            <Button Content="Search colored text" Grid.Row="2" Grid.Column="1" Padding="3" Margin="3" Click="Search_Click"/>
        </Grid>

    </Grid>
</Window>

MainWindow.xaml.cs

using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;

namespace WpfApp17
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            ComboColor.ItemsSource = typeof(Colors).GetProperties();
            ComboColor.SelectedValue = "Brown";
        }

        private void Search_Click(object sender, RoutedEventArgs e)
        {
            Parsing(rtb);
        }

        public void Parsing(RichTextBox rtb)
        {
            // Get selected color 
            var c = System.Drawing.Color.FromName(ComboColor.SelectedValue.ToString());
          
            var parser = new RtfDocumentParser();

            // Initialization with selected color
            parser.Init(Color.FromArgb(c.A, c.R, c.G, c.B));

            // Processing 
            parser.Analyze(rtb);

            // Color found TextRanges to pink
            foreach (var tr in parser.TextRanges)
            {
                tr.ApplyPropertyValue(TextElement.BackgroundProperty, new SolidColorBrush(Colors.Pink));
            }
        }

        private void SetColor_Click(object sender, RoutedEventArgs e)
        {
            var selection = rtb.Selection;
            if (!selection.IsEmpty)
            {
                var c = System.Drawing.Color.FromName(ComboColor.SelectedValue.ToString());
                selection.ApplyPropertyValue(TextElement.BackgroundProperty, new SolidColorBrush(Color.FromArgb(c.A, c.R, c.G, c.B)));
            }
        }
    }
}

RtfDocumentParser.cs

using System;
using System.Collections.Generic;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;

namespace WpfApp17
{
    public class RtfDocumentParser
    {
        #region Public properties

        public Color SearchColor { get; private set; }
        public IList<TextRange> TextRanges { get; private set; }
        #endregion

        #region Private properties

        private TextPointer Start { get; set; }
        private TextPointer End { get; set; }
        #endregion

        #region ctor
        public RtfDocumentParser()
        {
            Init(Colors.Yellow);
        }
        #endregion

        #region Public methods
        public void Init(Color color)
        {
            SearchColor = color;
            TextRanges = new List<TextRange>();
        }

        public void Analyze(RichTextBox rtb)
        {
            ParseBlockCollection(rtb.Document.Blocks);
            CloseRange();
        }
        #endregion

        #region Private methods 

        private void ParseBlockCollection(BlockCollection blocks)
        {
            foreach (var block in blocks)
            {
                CloseRange();

                if (block is Paragraph para) { ParseInlineCollection(para.Inlines); }
                else if (block is List list)
                {
                    foreach (var litem in list.ListItems)
                    {
                        CloseRange();
                        TextRange range = new TextRange(litem.ElementStart, litem.ElementEnd);
                        ParseBlockCollection(litem.Blocks);
                    }
                }
                else if (block is Table table)
                {
                    foreach (TableRowGroup rowGroup in table.RowGroups)
                    {
                        foreach (TableRow row in rowGroup.Rows)
                        {
                            foreach (var cell in row.Cells)
                            {
                                ParseBlockCollection(cell.Blocks);
                            }
                        }
                    }
                }
                else if (block is BlockUIContainer blockui) { /* blockui.Child */ }
                else if (block is Section section) { ParseBlockCollection(section.Blocks); }
                else { throw new NotImplementedException(); }
            }
        }

        public void ParseInlineCollection(InlineCollection inlines)
        {
            foreach (var inline in inlines)
            {
                if (inline is Run r)
                {
                    Analyze(r);
                }
                else if (inline is InlineUIContainer || inline is LineBreak lbreak)
                {
                    CloseRange();
                }
                else if (inline is Span span)
                {
                    ParseInlineCollection(span.Inlines);
                }
            }
        }

        private void Analyze(Run run)
        {
            if (run.Background is SolidColorBrush rBrush)
            {
                CheckPositions(rBrush.Color, run.ElementStart, run.ElementEnd);
            }
            else if (run.Parent is Span span && span.Background is SolidColorBrush sBrush)
            {
                CheckPositions(sBrush.Color, run.ElementStart, run.ElementEnd);
            }
            else if (End != null)
            {
                CloseRange();
            }
        }

        private void CheckPositions(Color color, TextPointer start, TextPointer end)
        {
            if (color == SearchColor)
            {
                if (Start == null)
                {
                    Start = start;
                }
                else if (!IsMatch(start, End))
                {
                    TextRanges.Add(new TextRange(Start, End));
                    Start = start;
                }
                End = end;
            }
            else if (End != null)
            {
                CloseRange();
            }
        }
     
        private bool IsMatch(TextPointer start, TextPointer position)
        {
            for (; position != null; position = position.GetNextContextPosition(LogicalDirection.Forward))
            {
                //var context = position.GetPointerContext(LogicalDirection.Forward);
                if (start.CompareTo(position) == 0)
                    return true; // Match
            }
            return false;
        }

        private void CloseRange()
        {
            if (End is TextPointer) 
            {
                TextRanges.Add(new TextRange(Start, End));
            }
            Start = End = null;
        }

        #endregion
    }
}