我将 FlowDocument 与 BlockUIContainer 和 InlineUIContainer 元素一起使用,这些元素包含(或作为基类)一些自定义块 - SVG、数学公式等。因此,使用 Selection.Load(stream, DataFormats.XamlPackage) 将不起作用,因为序列化将删除 * UIContainers,除非 Child 属性是 Microsoft 参考源中可用的图像:
private static void WriteStartXamlElement(...)
{
...
if ((inlineUIContainer == null || !(inlineUIContainer.Child is Image)) &&
(blockUIContainer == null || !(blockUIContainer.Child is Image)))
{
...
elementTypeStandardized = TextSchema.GetStandardElementType(elementType, /*reduceElement:*/true);
}
...
}
在这种情况下,唯一的选择是使用完美工作的 XamlWriter.Save 和 XamlReader.Load,序列化和反序列化 FlowDocument 的所有必需属性和对象,但必须手动实现 Copy+Paste 作为 Copy+ 的默认实现粘贴使用 Selection.Load/Save。
复制/粘贴非常重要,因为它还用于处理在 RichTextBox 控件中或之间拖动元素 - 这是无需自定义拖动代码即可操作对象的唯一方法。
这就是为什么我希望使用 FlowDocument 序列化来实现复制/粘贴,但不幸的是它存在一些问题:
- 在当前解决方案中,需要对整个 FlowDocument 对象进行序列化/反序列化。就性能而言,这应该不是问题,但我需要存储需要从中粘贴哪些选择范围的信息(CustomRichTextBoxTag 类)。
显然,对象不能从一个文档中删除并添加到另一个文档(我最近发现的一个死胡同):“InlineCollection”元素不能插入到树中,因为它已经是树的子节点。
[TextElementCollection.cs] public void InsertAfter(TextElementType previousSibling, TextElementType newItem) { ... if (previousSibling.Parent != this.Parent) throw new InvalidOperationException(System.Windows.SR.Get("TextElementCollection_PreviousSiblingDoesNotBelongToThisCollection", new object[1] { (object) previousSibling.GetType().Name })); ... }
我考虑在所有需要移动到另一个文档的元素中使用反射来设置 FrameworkContentElement._parent,但这是最后的手段 hackish 和肮脏的解决方案:
从理论上讲,我只能复制所需的对象:(可选)在选择开始时使用文本部分运行,中间的所有段落和内联以及(可能)在最后部分运行,将这些封装在自定义类中并使用序列化/反序列化XamlReader/XamlWriter。
- 另一个我没有想到的解决方案。
这是自定义的 RichTextBox 控件实现,其中包含部分工作的自定义复制/粘贴代码:
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Markup;
namespace FlowMathTest
{
public class CustomRichTextBoxTag: DependencyObject
{
public static readonly DependencyProperty SelectionStartProperty = DependencyProperty.Register(
"SelectionStart",
typeof(int),
typeof(CustomRichTextBoxTag));
public int SelectionStart
{
get { return (int)GetValue(SelectionStartProperty); }
set { SetValue(SelectionStartProperty, value); }
}
public static readonly DependencyProperty SelectionEndProperty = DependencyProperty.Register(
"SelectionEnd",
typeof(int),
typeof(CustomRichTextBoxTag));
public int SelectionEnd
{
get { return (int)GetValue(SelectionEndProperty); }
set { SetValue(SelectionEndProperty, value); }
}
}
public class CustomRichTextBox: RichTextBox
{
public CustomRichTextBox()
{
DataObject.AddCopyingHandler(this, OnCopy);
DataObject.AddPastingHandler(this, OnPaste);
}
protected override void OnSelectionChanged(RoutedEventArgs e)
{
base.OnSelectionChanged(e);
var tag = Document.Tag as CustomRichTextBoxTag;
if(tag == null)
{
tag = new CustomRichTextBoxTag();
Document.Tag = tag;
}
tag.SelectionStart = Document.ContentStart.GetOffsetToPosition(Selection.Start);
tag.SelectionEnd = Document.ContentStart.GetOffsetToPosition(Selection.End);
}
private void OnCopy(object sender, DataObjectCopyingEventArgs e)
{
if(e.DataObject != null)
{
e.Handled = true;
var ms = new MemoryStream();
XamlWriter.Save(Document, ms);
e.DataObject.SetData(DataFormats.Xaml, ms);
}
}
private void OnPaste(object sender, DataObjectPastingEventArgs e)
{
var xamlData = e.DataObject.GetData(DataFormats.Xaml) as MemoryStream;
if(xamlData != null)
{
xamlData.Position = 0;
var fd = XamlReader.Load(xamlData) as FlowDocument;
if(fd != null)
{
var tag = fd.Tag as CustomRichTextBoxTag;
if(tag != null)
{
InsertAt(Document, Selection.Start, Selection.End, fd, fd.ContentStart.GetPositionAtOffset(tag.SelectionStart), fd.ContentStart.GetPositionAtOffset(tag.SelectionEnd));
e.Handled = true;
}
}
}
}
public static void InsertAt(FlowDocument destDocument, TextPointer destStart, TextPointer destEnd, FlowDocument sourceDocument, TextPointer sourceStart, TextPointer sourceEnd)
{
var destRange = new TextRange(destStart, destEnd);
destRange.Text = string.Empty;
// insert partial text of the first run in the selection
if(sourceStart.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.Text)
{
var sourceRange = new TextRange(sourceStart, sourceStart.GetNextContextPosition(LogicalDirection.Forward));
destStart.InsertTextInRun(sourceRange.Text);
sourceStart = sourceStart.GetNextContextPosition(LogicalDirection.Forward);
destStart = destStart.GetNextContextPosition(LogicalDirection.Forward);
}
var field = typeof(FrameworkContentElement).GetField("_parent", BindingFlags.NonPublic | BindingFlags.Instance);
while(sourceStart != null && sourceStart.CompareTo(sourceEnd) <= 0 && sourceStart.Paragraph != null)
{
var sourceInline = sourceStart.Parent as Inline;
if(sourceInline != null)
{
sourceStart.Paragraph.Inlines.Remove(sourceInline);
if(destStart.Parent is Inline)
{
field.SetValue(sourceInline, null);
destStart.Paragraph.Inlines.InsertAfter(destStart.Parent as Inline, sourceInline);
}
else
{
var p = new Paragraph();
destDocument.Blocks.InsertAfter(destStart.Paragraph, p);
p.Inlines.Add(sourceInline);
}
sourceStart = sourceStart.GetNextContextPosition(LogicalDirection.Forward);
}
else
{
var sourceBlock = sourceStart.Parent as Block;
field.SetValue(sourceBlock, null);
destDocument.Blocks.InsertAfter(destStart.Paragraph, sourceBlock);
sourceStart = sourceStart.GetNextContextPosition(LogicalDirection.Forward);
}
}
}
}
}
还有一个问题 - 是否存在使用 XamlReader 和 XamlWriter 为 FlowDocument 自定义复制+粘贴代码的现有解决方案?如何修复上面的代码,使其不会抱怨不同的 FlowDocument 对象或解决此限制?
编辑:作为一个实验,我实现了 2),以便可以将对象从一个 FlowDocument 移动到另一个。上面的代码已更新 - 所有对“字段”变量的引用。