如何以编程方式替换 WPF RichTextBox 的某些内容而不丢失格式?

How to programmatically replace some content of WFP RichTextBox without loosing the formatting?

问题:我们如何以编程方式替换 WPF RichTextBox 中的某些文本而不丢失其格式?在下面的代码中,我显然没有做正确的事情。我的在线搜索提供了一些相关建议,但他们似乎在使用 Winform,其中 RichTextBox 具有 rtf 属性 - 例如 this 一个。

以下代码在 WPF RichTexBox 中正确地将文本 abcd 替换为 rstu,但它丢失了 RichTextBox 的格式,如下面两张图片所示:

//rtbTest is the name of the RichTextBox
TextRange textRange = new TextRange(rtbTest.Document.ContentStart, rtbTest.Document.ContentEnd);
string oldText = textRange.Text;
string newText = oldText.Replace("abcd", "rstu");
textRange.Text = newText;

用 rstu 替换 abcd 之前的 RichTextBox 屏幕截图:

用 rstu 替换 abcd 后的 RichTextBox 屏幕截图:

如我们所见,格式丢失了。下面显示的列表并不是真正的格式化编号列表,它可能只是未格式化的文本(如 1. Item 1 等)

它正在丢失格式,因为您将 RTF 存储到一个字符串中,而该字符串不保留 RTF 格式。

您可以保存如下,

TextRange textRange = new TextRange(rtbTest.Document.ContentStart, rtbTest.Document.ContentEnd);
string rtf;
using (var memoryStream = new MemoryStream())
{
    textRange.Save(memoryStream, DataFormats.Rtf);
    rtf = ASCIIEncoding.Default.GetString(memoryStream.ToArray());
}

rtf = rtf.Replace("abcd", "rstu");


MemoryStream stream = new MemoryStream(ASCIIEncoding.Default.GetBytes(rtf));
rtbTest.SelectAll();
rtbTest.Selection.Load(stream, DataFormats.Rtf);

修改 RichTextBox 内容时,分析 TextPointer 上下文非常重要。如何实施请参见以下示例:

MainWindow.xaml

<Window ...
        Title="MainWindow" Height="350" Width="500">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>            
        </Grid.RowDefinitions>
        <RichTextBox x:Name="rtb" AllowDrop="True" VerticalScrollBarVisibility="Auto" Padding="2">
            <FlowDocument>
                <Paragraph>
                    <Run Text="Paste a content to the document..."/>                                      
                </Paragraph>
            </FlowDocument>
        </RichTextBox>
        <Button Grid.Row="1" Click="FindAndReplace">Find &amp; Replace </Button>
    </Grid>
</Window>

部分MainWindow.xaml.cs

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;

private void FindAndReplace(object sender, RoutedEventArgs e)
{
    var textToFind = "ABC";
    var textReplaceBy = "<A-B-C>";

    TextPointer start = rtb.Document.ContentStart;
    while (true)
    {
        var searchRange = new TextRange(start, rtb.Document.ContentEnd);
        TextRange foundRange = searchRange.FindText(textToFind);
        if (foundRange == null)
            break;

        foundRange.Text = textReplaceBy;             
        start = foundRange.End; // Continue the searching 
    }
    rtb.Focus();
}

TextRangeExt.cs

using System;
using System.Windows.Documents;

namespace WpfApp7
{
    public static class TextRangeExt
    {
        public static TextRange FindText(this TextRange searchRange, string searchText)
        {
            TextRange result = null;
            int offset = searchRange.Text.IndexOf(searchText, StringComparison.OrdinalIgnoreCase);
            if (offset >= 0)
            {
                var start = searchRange.Start.GetTextPositionAtOffset(offset);
                result = new TextRange(start, start.GetTextPositionAtOffset(searchText.Length));
            }
            return result;
        }

        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 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--;
                        }
                        else if (adjacent is ListItem lsItem)
                        {
                            var trange = new TextRange(lsItem.ElementStart, lsItem.ElementEnd);
                            var index = trange.Text.IndexOf('\t');
                            if (index >= 0)
                            {
                                offset -= index + 1;
                            }                            
                        }
                        break;
                    case TextPointerContext.ElementEnd:
                        if (adjacent is Paragraph)
                            offset -= 2;                        
                        break;
                }
            }
            return position;
        }

    }
}