Highlight parts of text in FlowDocument

2019-04-29 09:36发布

问题:

I want to highlight some parts of text in a FlowDocument based on the search results. What I am doing is getting the indexes where the searched word occures in the text of the FlowDocument and then apply background color on the text range starting at the found index, ending at the found index + search word length.

TextRange content = new TextRange(myFlowDocument.ContentStart, 
                                  myFlowDocument.ContentEnd);
List<int> highlights = GetHighlights(content.Text, search);

foreach (int index in highlights)
{
    var start = myFlowDocument.ContentStart;
    var startPos = start.GetPositionAtOffset(index);
    var endPos = start.GetPositionAtOffset(index + search.Length);
    var textRange = new TextRange(startPos, endPos);
    textRange.ApplyPropertyValue(TextElement.BackgroundProperty, 
               new SolidColorBrush(Colors.Yellow));
}

TextRange newRange = new TextRange(myFlowDocument.ContentStart, 
                                   newDocument.ContentEnd);
FlowDocument fd = (FlowDocument)XamlReader.Parse(newRange.Text);

The problem is, that I am searching indexes in the text of the document but when I'm returning the FlowDocument the xaml tags are added and I see the highlights moved. How can I fix it?

回答1:

You need to iterate using GetNextContextPosition(LogicalDirection.Forward) and get TextPointer, use this one with previous TextPointer to construct TextRange. On this TextRange you can apply your logic.

What you can't do is use single TextRange from FlowDocument for searching text. FlowDocument is not only text:

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        String search = this.content.Text;

        TextPointer text = doc.ContentStart;
        while (true)
        {
            TextPointer next = text.GetNextContextPosition(LogicalDirection.Forward);
            if (next == null)
            {
                break;
            }
            TextRange txt = new TextRange(text, next);

            int indx = txt.Text.IndexOf(search);
            if (indx > 0)
            {
                TextPointer sta = text.GetPositionAtOffset(indx);
                TextPointer end = text.GetPositionAtOffset(indx + search.Length);
                TextRange textR = new TextRange(sta, end);
                textR.ApplyPropertyValue(TextElement.BackgroundProperty, new SolidColorBrush(Colors.Yellow));
            }
            text = next;
        }
    }

UPDATE: it does not work allways, for example if you have list, special characters (\t) are counted by IndexOf, but GetPositionAtOffset doesn't count them in:

•   ListItem 1
•   ListItem 2
•   ListItem 3

this line:

int indx = txt.Text.IndexOf(search);

could be replaced with:

int indx = Regex.Replace(txt.Text, 
     "[^a-zA-Z0-9_.]+", "", RegexOptions.Compiled).IndexOf(search);


回答2:

Finaly, inspired by the answer of user007, after making some modifications I managed to highlight all the occurences of a string in a FlowDocument. Here is my code:

for (TextPointer position = newDocument.ContentStart;
        position != null && position.CompareTo(newDocument.ContentEnd) <= 0;
        position = position.GetNextContextPosition(LogicalDirection.Forward))
{
    if (position.CompareTo(newDocument.ContentEnd) == 0)
    {
        return newDocument;
    }

    String textRun = position.GetTextInRun(LogicalDirection.Forward);
    StringComparison stringComparison = StringComparison.CurrentCulture;
    Int32 indexInRun = textRun.IndexOf(search, stringComparison);
    if (indexInRun >= 0)
    {
        position = position.GetPositionAtOffset(indexInRun);
        if (position != null)
        {
            TextPointer nextPointer = position.GetPositionAtOffset(search.Length);
            TextRange textRange = new TextRange(position, nextPointer);
            textRange.ApplyPropertyValue(TextElement.BackgroundProperty, 
                          new SolidColorBrush(Colors.Yellow));
        }
    }
}


回答3:

kzub's answer did not seem to work for me so I also created a solution expanding on user007 to highlight all instances of a substring in TextPointer text. It also ignores case and will highlight all matches:

    public static void HighlightWords(TextPointer text, string searchWord, string stringText)
    {
        int instancesOfSearchKey = Regex.Matches(stringText.ToLower(), searchWord.ToLower()).Count;

        for (int i = 0; i < instancesOfSearchKey; i++)
        {
            int lastInstanceIndex = HighlightNextInstance(text, searchWord);
            if (lastInstanceIndex == -1)
            {
                break;
            }
            text = text.GetPositionAtOffset(lastInstanceIndex);
        }
    }

    private static int HighlightNextInstance(TextPointer text, string searchWord)
    {
        int indexOfLastInstance = -1;

        while (true)
        {
            TextPointer next = text.GetNextContextPosition(LogicalDirection.Forward);
            if (next == null)
            {
                break;
            }
            TextRange newText = new TextRange(text, next);

            int index = newText.Text.ToLower().IndexOf(searchWord.ToLower());
            if (index != -1)
            {
                indexOfLastInstance = index;
            }

            if (index > 0)
            {
                TextPointer start = text.GetPositionAtOffset(index);
                TextPointer end = text.GetPositionAtOffset(index + searchWord.Length);
                TextRange textRange = new TextRange(start, end);
                textRange.ApplyPropertyValue(TextElement.BackgroundProperty, new SolidColorBrush(Colors.Yellow));
            }
            text = next;
        }

        return indexOfLastInstance;
    }