【发布时间】:2016-07-09 05:32:50
【问题描述】:
我的任务是创建一个部分可编辑的RichTextBox。我在 Xaml 中看到了为 ReadOnly 部分添加 TextBlock 元素的建议,但是这会产生不理想的视觉效果,即不能很好地包装。 (它应该显示为一个连续的文本块。)
我已经使用一些reverse string formatting 将一个工作原型拼凑在一起以限制/允许编辑,并将其与inline Run elements 的动态创建相结合以用于显示目的。使用字典来存储文本的可编辑部分的当前值,我在任何 TextChanged 事件触发时相应地更新 Run 元素 - 其想法是,如果可编辑部分的文本被完全删除,它将被替换回它的默认值。
在字符串中:“Hi NAME,欢迎来到 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元素。但屏幕显示:- 预期:“嗨,NAME,欢迎来到网球训练营。”
- 实际:“Hi NAME网球训练营。”
旁注:粘贴时也可以复制这种丢失运行元素的行为。突出显示 "SPORT" 并粘贴 "Tennis",包含 "camp." 的 Run 将丢失。
问题:
如何让每个 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;
}
}
}
}
【问题讨论】:
-
RichTextBox通过FlowDocument类提供的不仅仅是文本编辑器。这使它成为显示格式化文本、图像、表格甚至UIElements的便捷控件。然而,所有这些都伴随着复杂性的代价。因此,处理RichTextBox可能会相当混乱。您是否有机会切换到更方便的控件,例如更适合文本处理的AvalonEdit? -
据我所知,问题是
TryParseExact(richText失败了。开始修复它是否合理? -
@qqww2 我不熟悉
AvalonEdit或者它的许可要求,但我会考虑。 -
@OhBeWise 对不起,也许我错了,或者我误解了您的要求。我正在尝试运行您的代码,我看到了 - 如果我删除“SPORT”(例如) - 那么
bool extracted是错误的......如果有意义的话,我会继续解决这个问题...... -
@MachineLearning 啊,您可能是 dbl-click 突出显示,所以当您尝试删除“SPORT”时,它会检测到您尝试使用空格删除“SPORT” - 并且该空间不可编辑。由于这个错误,我实际上已经覆盖了突出显示的行为。我只是没有在这个例子中包含它 - 很抱歉。
标签: c# wpf richtextbox