简体   繁体   中英

RichTextBox in C# WPF changes position during execution

I am trying to create a syntax colorizer in WPF C#. To obtain that, I need to get location of every word and color it depending of matching pattern. RichTextBox has quite complicated system of selecting text, so I guessed that it would be easier to check position of given word in pure string and connect it to position in text. Well, nope. Depending of how many I already changed the color of text, it behaves differently, sometimes even matching wrong number of letters.

Example:

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

namespace Example
{
    /// <summary>
    ///     Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            var message = "Hello World and everyone here!";

            richTextBox.Document.Blocks.Clear();
            richTextBox.Document.Blocks.Add(new Paragraph(new Run(message)));

            var position = richTextBox.Document.ContentStart;

            var text = position.GetTextInRun(LogicalDirection.Forward);

            ShowPos(position,2,5); // shows message "Hello"
            ShowPos(position,8,5); // shows message "Wor" if upper line is not
                                   // commented - otherwise, shows "World"

        }

        private void ShowPos(TextPointer position, int from, int length)
        {
            var posA = position.GetPositionAtOffset(from);
            var posB = posA.GetPositionAtOffset(length);
            var range = new TextRange(posA, posB);
            var textA = range.Text;

            //if removed, everything works fine
            range.ApplyPropertyValue(ForegroundProperty, Brushes.DarkRed); 

            MessageBox.Show(textA);
        }
    }
}

It's pretty frustrating. What should i do to match and colorize every single word?

This is a very old post, but maybe someone will find this solution useful.

Unfortunately, searching for text and determining its position in the RichTextBox is a little complicated process. Since the text can contain different formatting elements like background/foreground color, different fonts, images and so on, the context must be used to determine position of required text.

One of the possible solution is shown below.

MainWindow.xaml

<Window ...
        Title="MainWindow" Height="250" Width="400"
        KeyDown="Window_KeyDown">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <RichTextBox Grid.Row="0" x:Name="rtb" AllowDrop="True" 
                     VerticalScrollBarVisibility="Auto" Padding="2" FontSize="18" 
                     HorizontalAlignment="Left" >
            <FlowDocument>
                <Paragraph>
                    <Run FontSize="10"  Text="In this example, you will learn to print Hello World." />
                    <Run Text="In this example, you will learn to print Hello World." />
                    <Run FontSize="20" Text="In this example, you will learn to print Hello World." />                   
                </Paragraph>
            </FlowDocument>
        </RichTextBox>

        <Button Grid.Row="1" Click="Button_Click">Color Words</Button>
    </Grid>
</Window> 

MainWindow.xaml.cs


private void Button_Click(object sender, RoutedEventArgs e)
{
    var pairs = new Dictionary<string, SolidColorBrush>(StringComparer.InvariantCultureIgnoreCase)
    {
        { "hello", Brushes.Red },
        { "world", Brushes.Blue }
    };

    // Define the range to be used to analyze for the specified words
    var textRange = new TextRange(rtb.Document.ContentStart, rtb.Document.ContentEnd);

    // Build list of Word/TextRange pairs
    var list = CalculateTextRange(textRange, pairs.Select(d => d.Key).ToList());

    // Color words by using calculated `TextRange`.
    for (int i = 0; i < list.Count; i++)
    {
        list[i].Value.ApplyPropertyValue(TextElement.ForegroundProperty, pairs[list[i].Key]);
    }

    rtb.Focus();
}

public static IList<KeyValuePair<string, TextRange>> CalculateTextRange(TextRange range, IList<string> words)
{
    // Building the regular expression pattern 
    var pattern = new StringBuilder(@"\b");
    for (int i = 0; i < words.Count; i++)
    {
        if (i > 0) pattern.Append('|');
        pattern.Append(words[i]);
    }
    pattern.Append(@"\b");
    Regex regExp = new Regex(pattern.ToString(), RegexOptions.IgnoreCase);

    TextRange search = range;
    int correction = 0;
    var result = new List<KeyValuePair<string, TextRange>>();

    // Enumerate all found mathes and creating list of Word/TextRange pairs
    foreach (Match match in regExp.Matches(range.Text))
    {
        if (CalculateTextRange(search, match.Index - correction, match.Length) is TextRange tr)
        {
            result.Add(new KeyValuePair<string, TextRange>(match.Value, tr));
            correction = match.Index + match.Length;
            search = new TextRange(tr.End, search.End);
        }
    }
    return result;
}
        
// Calculates a `TextRange` of the string started from `iStart` index
// and having `length` size or `null`.
public static TextRange CalculateTextRange(TextRange search, int iStart, int length)
{
    return (GetTextPositionAtOffset(search.Start, iStart) is TextPointer start)
        ? new TextRange(start, GetTextPositionAtOffset(start, length))
        : null;
}

// Calculate `TextPointer` from defined text position by specified offset.
public static TextPointer GetTextPositionAtOffset(TextPointer position, int offset)
{
    for (TextPointer current = position; current != null; current = position.GetNextContextPosition(LogicalDirection.Forward))
    {
        position = current;
        var adjacent = position.GetAdjacentElement(LogicalDirection.Forward);
        var context = position.GetPointerContext(LogicalDirection.Forward);
        switch (context)
        {
            case TextPointerContext.Text:
                int count = position.GetTextRunLength(LogicalDirection.Forward);
                if (offset <= count)
                {
                    return position.GetPositionAtOffset(offset);
                }
                offset -= count;
                break;
            case TextPointerContext.ElementStart:
                if (adjacent is InlineUIContainer)
                    offset--;
                break;
            case TextPointerContext.ElementEnd:
                if (adjacent is Paragraph)
                    offset -= 2;
                break;
        }
    }
    return position;
}

The code above contains the Button_Click() handler that performs the following steps when user is clicked on the Color Words button.

  1. Defines list of words to be found in the RichTextBox and color for each word in the list (actually a brush in this case):
var pairs = new Dictionary<string, SolidColorBrush>(StringComparer.InvariantCultureIgnoreCase)
    {
        { "hello", Brushes.Red },
        { "world", Brushes.Blue }
    }; 
  1. Defining the range to be used to analyze for the specified words. In this example whole RichBoxText document is used:
var textRange = new TextRange(rtb.Document.ContentStart, rtb.Document.ContentEnd);
  1. Calculating list of TextRange for all words specified in the list and found in the document:
// Build list of Word/TextRange pairs
var list = CalculateTextRange(textRange, pairs.Select(d => d.Key).ToList());
  1. Coloring all found words.

在此处输入图像描述

The picture is added to check that the position is calculated correctly even in this case.

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