1
votes

As shown at the end below, using Microsoft WORD VBA, we can search all highlighted texts in MS WORD document. Same can be achieved using C# in a legacy MS Office VSTO add-in. Question: How can we programmatically get TexRanges of all highlighted texts in a WPF RichTextBox?

WPF RichTextBox Display:

enter image description here

Code to get 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 output:

In the following output, We can see the rtf for the highlighted text test is {\lang9\highlight2\ltrch test}. How can we programmatically get the highlighted text (i.e. test) here. This is just an example., I mean to get all highlighted texts programmatically?

{\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}
}
}

Can we achieve the same programmatically in a WPF RichTextBox like we do (for example) in a WORD Document through a VBA macro shown below or through c# in a legacy VSTO add-in:

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
1
Do you mean searching for all words that is highlighted only with the specified color or any highlighted parts of text in the current document?Jackdaw
@Jackdaw I should have specified it. I meant all words (texts) highlighted with yellow color.nam
If a part of a text formatted like on the screenshot what you do expect to get? Does the TextRange containing the "lly highlighted te" fragment?Jackdaw
@Jackdaw Good example. That is correct. Whatever is highlighted (not necessarily a whole word). Same as we do an advanced search in MS WORD searching for whatever is highlighted.nam

1 Answers

1
votes

To understand what is algorithm can be used to analyze a content of the FlowDocument the best way to read the Flexible Content Display With Flow Documents post or, for example, read the chapter “Documents” in book MacDonald M. - Pro WPF 4.5 in C#. Windows Presentation Foundation in .NET 4.5 (The Experts Voice in .NET), 2012.

So, the FlowDocument contains BlockCollection Blocks property. This is the top-level blocks of the whole the FlowDocument content.

Code below uses this property to parse and analyze all elements in the FlowDocument recursively, including to search for text fragments with a specified background color.

For the test purpose the application window contains the Color ComboBox which allows to choose an some color and paint selected fragments of text using the Set Color button. The Search color text button starts scanning the document, finds the text colored with the specified color and uses the created TextRange list to repainting the text in pink.

enter image description here

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
    }
}