使用 RichTextBox 中的索引突出显示阿拉伯语单词?

Highlighting an arabic word using its index in a RichTextBox?

我写了一个小的 WPF 程序,它应该突出显示我在 RichTextBox.

中输入的任何阿拉伯语句子的第 X 个单词

例如,我输入这个文本并指定单词 2(索引:1):

这是我应该看到的:

这是我看到的:

我的代码
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);
}    

下面的代码展示了如何根据上下文相关的方法计算单词位置。

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>

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

下面CalculateTextRange()的方法实际上是通过index计算请求的单词。由于包括对表格和列表的支持,它看起来有点复杂。

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