如何阻止 RichTextBox Inline 在 TextChanged 中被删除?

How to stop RichTextBox Inline from being deleted in TextChanged?

我的任务是创建一个部分可编辑的 RichTextBox。我在 Xaml 中看到了为 ReadOnly 部分添加 TextBlock 元素的建议,但是这具有不理想的环绕效果。 (它应该显示为一个连续的文本块。

我使用一些 reverse string formatting to restrict/allow edits and coupled that with dynamic creation of inline Run elements 拼凑了一个工作原型用于显示目的。使用字典来存储文本可编辑部分的当前值,我根据任何 TextChanged 事件触发器相应地更新 Run 元素 - 我的想法是,如果可编辑部分的文本被完全删除,它将被替换回其默认值。

字符串中:"Hi NAME, welcome to SPORT camp.",只有NAMESPORT可编辑。

                ╔═══════╦════════╗                    ╔═══════╦════════╗
Default values: ║ Key   ║ Value  ║    Edited values:  ║ Key   ║ Value  ║
                ╠═══════╬════════╣                    ╠═══════╬════════╣
                ║ NAME  ║ NAME   ║                    ║ NAME  ║ John   ║
                ║ SPORT ║ SPORT  ║                    ║ SPORT ║ Tennis ║
                ╚═══════╩════════╝                    ╚═══════╩════════╝

 "Hi NAME, welcome to SPORT camp."    "Hi John, welcome to Tennis camp."

问题:

删除特定 运行 中的整个文本值会从 RichTextBox Document 中删除 运行(以及后面的 运行)。即使我将它们全部添加回来,它们也不再正确显示在屏幕上。例如,使用上述设置中的编辑字符串:

旁注:这种丢失-运行-元素的行为也可以在粘贴时复制。突出显示 "SPORT" 并粘贴 "Tennis" 和包含 “阵营”的 运行。 丢了。

问题:

如何在每个 Run 元素被替换后即使通过破坏性操作也能保持可见?

代码:

我试图将代码精简到最少的示例,所以我删除了:

要进行测试,请将 class 放入您的 WPF 项目资源文件夹,修复命名空间,然后将控件添加到视图。

using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;

namespace WPFTest.Resources
{
  public class MyRichTextBox : RichTextBox
  {
    public MyRichTextBox()
    {
      this.TextChanged += MyRichTextBox_TextChanged;
      this.Background = Brushes.LightGray;

      this.Parameters = new Dictionary<string, string>();
      this.Parameters.Add("NAME", "NAME");
      this.Parameters.Add("SPORT", "SPORT");

      this.Format = "Hi {0}, welcome to {1} camp.";
      this.Text = string.Format(this.Format, this.Parameters.Values.ToArray<string>());

      this.Runs = new List<Run>()
      {
        new Run() { Background = Brushes.LightGray, Tag = "Hi " },
        new Run() { Background = Brushes.Black, Foreground = Brushes.White, Tag = "NAME" },
        new Run() { Background = Brushes.LightGray, Tag = ", welcome to " },
        new Run() { Background = Brushes.Black, Foreground = Brushes.White, Tag = "SPORT" },
        new Run() { Background = Brushes.LightGray, Tag = " camp." },
      };

      this.UpdateRuns();
    }

    public Dictionary<string, string> Parameters { get; set; }
    public List<Run> Runs { get; set; }
    public string Text { get; set; }
    public string Format { get; set; }

    private void MyRichTextBox_TextChanged(object sender, TextChangedEventArgs e)
    {
      string richText = new TextRange(this.Document.Blocks.FirstBlock.ContentStart, this.Document.Blocks.FirstBlock.ContentEnd).Text;
      string[] oldValues = this.Parameters.Values.ToArray<string>();
      string[] newValues = null;

      bool extracted = this.TryParseExact(richText, this.Format, out newValues);

      if (extracted)
      {
        var changed = newValues.Select((x, i) => new { NewVal = x, Index = i }).Where(x => x.NewVal != oldValues[x.Index]).FirstOrDefault();
        string key = this.Parameters.Keys.ElementAt(changed.Index);
        this.Parameters[key] = string.IsNullOrWhiteSpace(newValues[changed.Index]) ? key : newValues[changed.Index];

        this.Text = richText;
      }
      else
      {
        e.Handled = true;
      }

      this.UpdateRuns();
    }

    private void UpdateRuns()
    {
      this.TextChanged -= this.MyRichTextBox_TextChanged;

      foreach (Run run in this.Runs)
      {
        string value = run.Tag.ToString();

        if (this.Parameters.ContainsKey(value))
        {
          run.Text = this.Parameters[value];
        }
        else
        {
          run.Text = value;
        }
      }

      Paragraph p = this.Document.Blocks.FirstBlock as Paragraph;
      p.Inlines.Clear();
      p.Inlines.AddRange(this.Runs);

      this.TextChanged += this.MyRichTextBox_TextChanged;
    }

    public bool TryParseExact(string data, string format, out string[] values)
    {
      int tokenCount = 0;
      format = Regex.Escape(format).Replace("\{", "{");
      format = string.Format("^{0}$", format);

      while (true)
      {
        string token = string.Format("{{{0}}}", tokenCount);

        if (!format.Contains(token))
        {
          break;
        }

        format = format.Replace(token, string.Format("(?'group{0}'.*)", tokenCount++));
      }

      RegexOptions options = RegexOptions.None;

      Match match = new Regex(format, options).Match(data);

      if (tokenCount != (match.Groups.Count - 1))
      {
        values = new string[] { };
        return false;
      }
      else
      {
        values = new string[tokenCount];

        for (int index = 0; index < tokenCount; index++)
        {
          values[index] = match.Groups[string.Format("group{0}", index)].Value;
        }

        return true;
      }
    }
  }
}

我建议移动代码以在 UpdateRuns 中创建运行

    private void UpdateRuns()
    {
        this.TextChanged -= this.MyRichTextBox_TextChanged;

        this.Runs = new List<Run>()
  {
    new Run() { Background = Brushes.LightGray, Tag = "Hi " },
    new Run() { Background = Brushes.Black, Foreground = Brushes.White, Tag = "NAME" },
    new Run() { Background = Brushes.LightGray, Tag = ", welcome to " },
    new Run() { Background = Brushes.Black, Foreground = Brushes.White, Tag = "SPORT" },
    new Run() { Background = Brushes.LightGray, Tag = " camp." },
  };

        foreach (Run run in this.Runs)

您的代码的问题是,当您通过用户界面更改文本时,内部 Run 对象被修改、创建、删除,所有疯狂的事情都发生在幕后。内部结构非常复杂。例如,这里有一个在无辜单行 p.Inlines.Clear();:

内部调用的方法
private int DeleteContentFromSiblingTree(SplayTreeNode containingNode, TextPointer startPosition, TextPointer endPosition, bool newFirstIMEVisibleNode, out int charCount)
{
    SplayTreeNode leftSubTree;
    SplayTreeNode middleSubTree;
    SplayTreeNode rightSubTree;
    SplayTreeNode rootNode;
    TextTreeNode previousNode;
    ElementEdge previousEdge;
    TextTreeNode nextNode;
    ElementEdge nextEdge;
    int symbolCount;
    int symbolOffset;

    // Early out in the no-op case. CutContent can't handle an empty content span.
    if (startPosition.CompareTo(endPosition) == 0)
    {
        if (newFirstIMEVisibleNode)
        {
            UpdateContainerSymbolCount(containingNode, /* symbolCount */ 0, /* charCount */ -1);
        }
        charCount = 0;
        return 0;
    }

    // Get the symbol offset now before the CutContent call invalidates startPosition.
    symbolOffset = startPosition.GetSymbolOffset();

    // Do the cut.  middleSubTree is what we want to remove.
    symbolCount = CutContent(startPosition, endPosition, out charCount, out leftSubTree, out middleSubTree, out rightSubTree);

    // We need to remember the original previous/next node for the span
    // we're about to drop, so any orphaned positions can find their way
    // back.
    if (middleSubTree != null)
    {
        if (leftSubTree != null)
        {
            previousNode = (TextTreeNode)leftSubTree.GetMaxSibling();
            previousEdge = ElementEdge.AfterEnd;
        }
        else
        {
            previousNode = (TextTreeNode)containingNode;
            previousEdge = ElementEdge.AfterStart;
        }
        if (rightSubTree != null)
        {
            nextNode = (TextTreeNode)rightSubTree.GetMinSibling();
            nextEdge = ElementEdge.BeforeStart;
        }
        else
        {
            nextNode = (TextTreeNode)containingNode;
            nextEdge = ElementEdge.BeforeEnd;
        }

        // Increment previous/nextNode reference counts. This may involve
        // splitting a text node, so we use refs.
        AdjustRefCountsForContentDelete(ref previousNode, previousEdge, ref nextNode, nextEdge, (TextTreeNode)middleSubTree);

        // Make sure left/rightSubTree stay local roots, we might
        // have inserted new elements in the AdjustRefCountsForContentDelete call.
        if (leftSubTree != null)
        {
            leftSubTree.Splay();
        }
        if (rightSubTree != null)
        {
            rightSubTree.Splay();
        }
        // Similarly, middleSubtree might not be a local root any more,
        // so splay it too.
        middleSubTree.Splay();

        // Note TextContainer now has no references to middleSubTree, if there are
        // no orphaned positions this allocation won't be kept around.
        Invariant.Assert(middleSubTree.ParentNode == null, "Assigning fixup node to parented child!");
        middleSubTree.ParentNode = new TextTreeFixupNode(previousNode, previousEdge, nextNode, nextEdge);
    }

    // Put left/right sub trees back into the TextContainer.
    rootNode = TextTreeNode.Join(leftSubTree, rightSubTree);
    containingNode.ContainedNode = rootNode;
    if (rootNode != null)
    {
        rootNode.ParentNode = containingNode;
    }

    if (symbolCount > 0)
    {
        int nextNodeCharDelta = 0;
        if (newFirstIMEVisibleNode)
        {
            // The following node is the new first ime visible sibling.
            // It just moved, and loses an edge character.
            nextNodeCharDelta = -1;
        }

        UpdateContainerSymbolCount(containingNode, -symbolCount, -charCount + nextNodeCharDelta);
        TextTreeText.RemoveText(_rootNode.RootTextBlock, symbolOffset, symbolCount);
        NextGeneration(true /* deletedContent */);

        // Notify the TextElement of a content change. Note that any full TextElements
        // between startPosition and endPosition will be handled by CutTopLevelLogicalNodes,
        // which will move them from this tree to their own private trees without changing
        // their contents.
        Invariant.Assert(startPosition.Parent == endPosition.Parent);
        TextElement textElement = startPosition.Parent as TextElement;
        if (textElement != null)
        {               
            textElement.OnTextUpdated();                    
        }
    }

    return symbolCount;
}

有兴趣的可以看看here的源码

解决方法是不要直接在 FlowDocument 中使用您创建的 Run 对象进行比较。在添加它们之前总是复制它:

private void UpdateRuns()
{
    TextChanged -= MyRichTextBox_TextChanged;

    List<Run> runs = new List<Run>();
    foreach (Run run in Runs)
    {
        Run newRun;
        string value = run.Tag.ToString();

        if (Parameters.ContainsKey(value))
        {
            newRun = new Run(Parameters[value]);
        }
        else
        {
            newRun = new Run(value);
        }

        newRun.Background = run.Background;
        newRun.Foreground = run.Foreground;

        runs.Add(newRun);
    }

    Paragraph p = Document.Blocks.FirstBlock as Paragraph;
    p.Inlines.Clear();
    p.Inlines.AddRange(runs);

    TextChanged += MyRichTextBox_TextChanged;
}