如何阻止 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.",只有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
元素。但是屏幕显示:
- 预计:"Hi NAME, welcome to Tennis camp."
- 实际:"Hi NAMETennis camp."
旁注:这种丢失-运行-元素的行为也可以在粘贴时复制。突出显示 "SPORT" 并粘贴 "Tennis" 和包含 “阵营”的 运行。 丢了。
问题:
如何在每个 Run
元素被替换后即使通过破坏性操作也能保持可见?
代码:
我试图将代码精简到最少的示例,所以我删除了:
- 每个
DependencyProperty
和 xaml 中的关联绑定
- 逻辑重新计算插入符位置(抱歉)
- 将 linked 字符串格式化扩展方法从第一个 link 重构为 class 中包含的单个方法。 (注意:此方法适用于简单的示例字符串格式。我的更强大格式的代码已被排除。因此请坚持为这些测试目的提供的示例。)
- 使可编辑部分清晰可见,不用管配色方案。
要进行测试,请将 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;
}
我的任务是创建一个部分可编辑的 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.",只有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
元素。但是屏幕显示:- 预计:"Hi NAME, welcome to Tennis camp."
- 实际:"Hi NAMETennis camp."
旁注:这种丢失-运行-元素的行为也可以在粘贴时复制。突出显示 "SPORT" 并粘贴 "Tennis" 和包含 “阵营”的 运行。 丢了。
问题:
如何在每个 Run
元素被替换后即使通过破坏性操作也能保持可见?
代码:
我试图将代码精简到最少的示例,所以我删除了:
- 每个
DependencyProperty
和 xaml 中的关联绑定
- 逻辑重新计算插入符位置(抱歉)
- 将 linked 字符串格式化扩展方法从第一个 link 重构为 class 中包含的单个方法。 (注意:此方法适用于简单的示例字符串格式。我的更强大格式的代码已被排除。因此请坚持为这些测试目的提供的示例。)
- 使可编辑部分清晰可见,不用管配色方案。
要进行测试,请将 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;
}