Search code examples
c#.netwpfrichtextboxarabic

Highlighting an arabic word using its index in a RichTextBox?


I wrote a small WPF program that is supposed to highlight the X-th word of any Arabic sentence I put in a RichTextBox.

For example, I enter this text and specify word 2 (index: 1) :

enter image description here

This is what I'm supposed to see :

enter image description here

This is what I see :

enter image description here

My code
XAML:

<RichTextBox x:Name="ArabicRTB" FlowDirection="RightToLeft">
    <FlowDocument>
        <Paragraph>
            <Run x:Name="ArabicRTB_Run" Text="كتبه البحرين هنا"/>
        </Paragraph>
    </FlowDocument>
</RichTextBox>

C# :

private (int, int) WordIndexToCharIndex(int index)
{
    int charIndex = 0;
    int wordIndex = 0;

    foreach (string word in ArabicRTB_Run.Text.Split(' '))
    {
        if (wordIndex == index)
        {
            return (charIndex, word.Length);
        }

        wordIndex++;
        charIndex += word.Length + 2;
    }

    throw new IndexOutOfRangeException();
}
 
private void Button_Click(object sender, RoutedEventArgs e)
{
    // Here I specify the index of the word I need to highlight
    var wordPos = WordIndexToCharIndex(1);

    TextPointer pointer = ArabicRTB.Document.ContentStart;    
    TextPointer start = pointer.GetPositionAtOffset(wordPos.Item1);
    TextPointer end = start.GetPositionAtOffset(wordPos.Item2);

    var selection = new TextRange(start, end);
    selection.ApplyPropertyValue(TextElement.ForegroundProperty, Brushes.Goldenrod);
}    

Solution

  • The code below shows how to calculate a word position calculation based on the context related methods.

    The MainWindow.xaml:

    <Window ...>
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="*"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>
            <RichTextBox Name="rtb" BorderBrush="{x:Null}" Padding="5" Margin="10" FlowDirection="RightToLeft" VerticalScrollBarVisibility="Auto" FontSize="18">
                <FlowDocument>
                    <Paragraph>
                        <Run Text="كت" Background="Aqua"/><Run Text="به" Background="Beige"/>
                        <Run Text="البحرين" Background="ForestGreen" />
                        <Run Text="هنا"/>
                    </Paragraph>              
                </FlowDocument>            
            </RichTextBox>
            <Button Grid.Row="1" Click="Button_SearchAsync" Margin="2" Padding="3">Press to test</Button>
        </Grid>
    </Window>
    

    Part of the MainWindow.xaml.cs:

    private async void Button_SearchAsync(object sender, RoutedEventArgs e)
    {
        var index = 1;
        await FindWordAsync(rtb, index);
        rtb.Focus();
    }
    
    public async Task FindWordAsync(RichTextBox rtb, int index)
    {
        await Task<object>.Factory.StartNew(() =>
        {
            this.Dispatcher.Invoke(() =>
            {                   
                var range = new TextRange(rtb.Document.ContentStart, rtb.Document.ContentEnd).CalculateTextRange(index);
                if (range is TextRange)
                {
                    // If it found color in red
                    this.Dispatcher.Invoke(() => { range.ApplyPropertyValue(TextElement.ForegroundProperty, Brushes.Red); });
                }
            });
            return Task.FromResult<object>(null);
        });
    }
    

    The method below CalculateTextRange() is actually makes the calculation of the requested word by it index. It is looking a little complicated because of including support for tables and lists.

    public static class TextRangeExt
    {
        public static TextRange CalculateTextRange(this TextRange range, int index)
        {
            string pattern = @"\b\w+\b";
            int correction = 0;           
            TextPointer start = range.Start;
    
            foreach (Match match in Regex.Matches(range.Text, pattern))
            {
                System.Diagnostics.Debug.WriteLine("match.Index= " + match.Index + ", match.Length= " + match.Length + " |" + match.Value + "|");                
                if (CalculateTextRange(start, match.Index - correction, match.Length) is TextRange tr)
                {                  
                    correction = match.Index + match.Length;
                    start = tr.End;
                    if (index-- <= 0) return tr;
                }
            }        
            return null; 
        }
    
        // Return calculated a `TextRange` of the string started from `iStart` index and having `length` size or `null`.
        private static TextRange CalculateTextRange(TextPointer startSearch, int iStart, int length)
        {
            return (startSearch.GetTextPositionAtOffset(iStart) is TextPointer start)
                ? new TextRange(start, start.GetTextPositionAtOffset(length))
                : null;
        }
    
        private static TextPointer GetTextPositionAtOffset(this TextPointer position, int offset)
        {
            for (TextPointer current = position; current != null; current = position.GetNextContextPosition(LogicalDirection.Forward))
            {
                position = current;
                var adjacent = position.GetAdjacentElement(LogicalDirection.Forward);
                var navigator = position.GetPointerContext(LogicalDirection.Forward);
                switch (navigator)
                {
                    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--;
                        }
                        else if (adjacent is ListItem lsItem)
                        {
                            var index = new TextRange(lsItem.ElementStart, lsItem.ElementEnd).Text.IndexOf('\t');
                            if (index >= 0) offset -= index + 1;
                        }                       
                        break;
    
                    case TextPointerContext.ElementEnd:
                        if (adjacent is Paragraph para)
                        {                          
                            var correction = 0;
                            if (para.Parent is TableCell tcell)
                            {
                                var bCount = tcell.Blocks.Count;
                                var cellText = new TextRange(tcell.Blocks.FirstBlock.ContentStart, tcell.Blocks.LastBlock.ContentEnd).Text;
    
                                if ((bCount == 1 && cellText.EndsWith(Environment.NewLine)) || bCount > 1) 
                                {
                                    correction = 2;
                                }
                                else if (tcell.Parent is TableRow trow)
                                {
                                    var cells = trow.Cells.Count;
                                    correction = (cells <= 0 || trow.Cells.IndexOf(tcell) != cells - 1) ? 1 : 2;
                                } 
                            }
                            else
                            {
                                correction = 2;
                            }
                            offset -= correction;
                        }                                        
                        break;
                }
            }
            return position;
        }
    }