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

时间:2016-07-08 17:59:37

标签: c# wpf richtextbox

我的任务是创建部分可编辑的RichTextBox。我已经在Xaml中看到了为TextBlock部分添加ReadOnly元素的建议,但这会产生不良包装效果的不良视觉效果。 (它应该显示为一个连续文本块。

我使用一些reverse string formatting修补了一个正在运行的原型来限制/允许编辑,并将其与动态创建inline Run elements结合起来用于显示目的。使用字典存储文本的可编辑部分的当前值,我会根据任何Run事件触发器相应地更新TextChanged元素,并认为如果可编辑部分的文本被完全删除,它将会被替换回其默认值。

在字符串中:“Hi NAME,欢迎来到SPORT阵营。”,只有 NAME SPORT 可以编辑。

                ╔═══════╦════════╗                    ╔═══════╦════════╗
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中删除该运行(以及以下运行)。即使我将它们全部添加回来,它们也不再在屏幕上正确显示。例如,使用上面设置中编辑过的字符串:

  • 用户突出显示文本“John”并单击删除,而不是保存空值,应将其替换为默认文本 “NAME”。在内部发生这种情况。字典获取正确的值,Run.Text具有值,Document包含所有正确的Run元素。但屏幕显示:

    • 预计:“嗨,NAME,欢迎来到网球营。”
    • 实际:“嗨NAMETennis阵营。”

Actual vs Expected screenshot

Sidenote :粘贴时也可以复制此运行元素丢失行为。突出显示“SPORT”并粘贴“网球”,并且包含“阵营的运行。”将丢失。

问题

如果替换后,即使通过破坏性操作,如何保持每个Run元素都可见?

代码

我试图将代码删除到最小的例子,所以我删除了:

  • xaml中的每个DependencyProperty和关联的绑定
  • 逻辑重新计算插入位置(抱歉
  • 将链接字符串格式扩展方法从第一个链接重构为类中的单个方法。 (注意:此方法适用于简单的示例字符串格式。我的代码用于更强大的格式化已被排除。因此请坚持为这些测试目的提供的示例。)
  • 使可编辑的部分清晰可见,无需调整配色方案。

要进行测试,请将该类放入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;
      }
    }
  }
}

2 个答案:

答案 0 :(得分:2)

代码的问题在于,当您通过用户界面更改文本时,内部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; } 内深处调用的方法:

Run

如果您有兴趣,可以查看here的源代码。

解决方案是不要直接在FlowDocument中使用您为比较目的创建的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; } 个对象。在添加之前,请务必复制它:

class Base:
    hello = 'A'

    def greet(self):
        print(self.hello)

    def greet2(self):
        print(Base.hello)


class Derived(Base):
    hello = 'B'

d = Derived()
d.greet()   # prints B
d.greet2()  # prints A

答案 1 :(得分:1)

我建议移动代码以在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)