C1RichTextBox具有自定义复制/粘贴行为

在使用IE 10的Silverlight 5中使用C1RichTextBox时,我面临两个主要问题:

  1. 在剪贴板粘贴操作期间,如何检测内容是从Silverlight应用程序中的另一个C1RichTextBox还是从外部应用程序复制的? 从外部应用程序,只应粘贴文本而不进行格式化。
  2. 将大型内嵌图像从一个C1RichTextBox复制/粘贴到另一个不起作用。 元素将图像内容存储在其数据URL中。 如果图像变得太大(大约1MB),则在复制到剪贴板时会删除src属性。

解决方案应该:

  • 没有全局剪贴板或C1RichTextBox的编辑行为的C1RichTextBox
  • C1RichTextBox实现的更改具有C1RichTextBox
  • 无需修改/解析/分析剪贴板中的HTML文档。

我花了一段时间来计算所有这些(更多……),我很高兴与任何必须处理这些问题的人分享。

我正在使用派生类来解决问题

 public class C1RichTextBoxExt : C1RichTextBox { 

1.使用纯文本从外部应用程序粘贴

解决方案理论上很简单:在RichTextBox中的文本被复制/剪切到剪贴板后,获取HTML。 粘贴时,将剪贴板中的当前HTML与上次复制的内容进行比较。 因为ComponentOne中的剪贴板是全局的,所以如果在另一个应用程序中完成复制/剪切,则内容会更改,因此HTML将不同。

要记住最后复制的HTML,我们在C1RichTextBoxExt使用静态成员:

 private static string _clipboardHtml; 

坏消息是: C1RichTextBox.ClipboardCopy()等方法不是虚拟的。 好消息是:可以禁用调用这些方法的Copy / Cut / Paste键盘快捷键,例如在构造函数中:

 RemoveShortcut(ModifierKeys.Control, Key.C); RemoveShortcut(ModifierKeys.Control, Key.Insert); RemoveShortcut(ModifierKeys.Control, Key.V); RemoveShortcut(ModifierKeys.Shift , Key.Insert); RemoveShortcut(ModifierKeys.Control, Key.X); RemoveShortcut(ModifierKeys.Shift , Key.Delete); 

既然不再调用方法C1RichTextBox.ClipboardCopy()等,我们可以通过重写OnKeyDown来连接我们自己的版本:

 protected override void OnKeyDown(KeyEventArgs e) { if ((Keyboard.Modifiers == ModifierKeys.Control) && (e.Key == Key.C)) { ClipboardCopy(); } else if ((Keyboard.Modifiers == ModifierKeys.Control) && (e.Key == Key.Insert)) { ClipboardCopy(); } else if ((Keyboard.Modifiers == ModifierKeys.Control) && (e.Key == Key.V)) { ClipboardPaste(); } else if ((Keyboard.Modifiers == ModifierKeys.Control) && (e.Key == Key.X)) { ClipboardCut(); } else if ((Keyboard.Modifiers == ModifierKeys.Shift) && (e.Key == Key.Insert)) { ClipboardPaste(); } else if ((Keyboard.Modifiers == ModifierKeys.Shift) && (e.Key == Key.Delete)) { ClipboardCut(); } else { // default behaviour base.OnKeyDown(e); return; } e.Handled = true; // base class should not fire KeyDown event } 

为了不小心调用基类方法,我覆盖它们(见下文,使用new修饰符)。 ClipboardCopy()方法只调用基类,然后存储剪贴板HTML。 这里的一个小缺陷是使用Dispatcher.BeginInvoke()因为C1RichTextBox.ClipboardCopy()将所选文本存储在Dispatcher.BeginInvoke()调用内的剪贴板中。 因此,只有在调度员有机会运行C1RichTextBox提供的操作后,才能使用该内容。

 new public void ClipboardCopy() { base.ClipboardCopy(); Dispatcher.BeginInvoke(() => { _clipboardHtml = C1.Silverlight.Clipboard.GetHtmlData(); }); } 

ClipboardCut方法非常相似:

 new public void ClipboardCut() { base.ClipboardCut(); Dispatcher.BeginInvoke(() => { _clipboardHtml = C1.Silverlight.Clipboard.GetHtmlData(); }); } 

ClipboardPaste方法现在可以检测是否粘贴外部数据。 仅粘贴文本并非如此简单。 我想出了用剪贴板的纯文本表示替换当前剪贴板内容的想法。 粘贴完成后,应该恢复剪贴板,以便可以在其他应用程序中再次粘贴内容。 这也必须在Dispatcher.BeginInvoke()因为基类方法C1RichTextBox.ClipboardPaste()也在延迟操作中执行粘贴操作。

 new public void ClipboardPaste() { // If the text in the global clipboard matches the text stored in _clipboardText it is // assumed that the HTML in the C1 clipboard is still valid // (no other Copy was made by the user). string current = C1.Silverlight.Clipboard.GetHtmlData(); if(current == _clipboardHtml) { // text is the same -> Let base class paste HTML base.ClipboardPaste(); } else { // let base class paste text only string text = C1.Silverlight.Clipboard.GetTextData(); C1.Silverlight.Clipboard.SetData(text); base.ClipboardPaste(); Dispatcher.BeginInvoke(() => { // restore clipboard C1.Silverlight.Clipboard.SetData(current); }); } } 

2.复制/粘贴大型内嵌图像

这里的想法是类似的:复制时记住图像,在粘贴期间将它们放回原处。

首先,我们需要存储文档中图像的位置:

 private static List _clipboardImages; private static int _imageCounter; 

(_imageCounter的使用将在后面解释……)

然后,在执行复制/剪切之前,我们搜索所有图像:

 new public void ClipboardCopy() { _clipboardImages = FindImages(Selection); base.ClipboardCopy(); // ... as posted above } 

和类似的:

 new public void ClipboardCut() { _clipboardImages = FindImages(Selection); base.ClipboardCut(); // ... as posted above } 

查找图像的方法是:

 private List FindImages(C1TextRange selection = null) { var result = new List(); if (selection == null) { // Document Contains all elements at the document level. foreach (C1TextElement elem in Document) { FindImagesRecursive(elem, result); } } else { // Selection contains all (selected) elements -> no need to search recursively foreach (C1TextElement elem in selection.ContainedElements) { if (elem is C1InlineUIContainer) { FindImage(elem as C1InlineUIContainer, result); } } } return result; } private void FindImagesRecursive(C1TextElement elem, List list) { if (elem is C1Paragraph) { var para = (C1Paragraph)elem; foreach (C1Inline inl in para.Inlines) { FindImagesRecursive(inl, list); } } else if (elem is C1Span) { var span = (C1Span)elem; foreach (C1Inline child in span.Inlines) { FindImagesRecursive(child, list); } } else if (elem is C1InlineUIContainer) { FindImage(elem as C1InlineUIContainer, list); } } private void FindImage(C1InlineUIContainer container, List list) { if (container.Content is BitmapImage) { list.Add(container.Content as BitmapImage); } } 

我不会详细介绍上述方法,如果分析C1RichTextBox.Document的结构,它们非常简单。

现在,我们如何恢复图像? 我发现最好的是使用C1RichTextBox.HtmlFilterConvertingHtmlNode事件。 每次将HTML节点转换为C1TextElement时都会触发此事件。 我们在构造函数中订阅它:

 HtmlFilter.ConvertingHtmlNode += new EventHandler(HtmlFilter_ConvertingHtmlNode); 

并像这样实现它:

 void HtmlFilter_ConvertingHtmlNode(object sender, ConvertingHtmlNodeEventArgs e) { if (e.HtmlNode is C1HtmlElement) { var elem = e.HtmlNode as C1HtmlElement; if (elem.Name.ToLower() == "img" && _clipboardImages != null && _clipboardImages.Count > _imageCounter) { if (!elem.Attributes.ContainsKey("src")) // key comparison is not case sensitive { e.Parent.Children.Add(_clipboardImages[_imageCounter].Clone()); e.Handled = true; } _imageCounter++; } } } 

因此,对于名称为“img”的每个HTML元素节点,我们检查是否缺少“src”属性。 如果是这样,我们将添加下一个存储的图像,并通过设置e.Handled = true;告诉事件源现在处理该事件(对于此HTML节点) e.Handled = true; 哪个图像是“下一个”图像由_imageCounter字段确定,该字段针对每个访问的“img”元素递增。

调用ClipboardPaste()时必须重置_imageCounter字段,所以我们这样做:

 new public void ClipboardPaste() { _imageCounter = 0; string current = C1.Silverlight.Clipboard.GetHtmlData(); // ... as posted above } 

结论

如果你复制/粘贴(没有双关语…)所有代码块一起发布在上面,你应该得到一个没有副作用的解决方案(至少今天作者都不知道),对变化是强有力的几乎没有HTML处理。