TRichTextBox –一个通用的 RichTextBox,可以显示动画图像等
本文介绍了如何将Windows控件对象插入RichTextBox中并用它们来承载图像。
摘要
在RichTextBox
中显示图像是一个常见的需求,但可用的解决方案有限。通过剪贴板粘贴或嵌入RTF内容仅支持静态图像。本文介绍了如何将Windows控件对象插入RichTextBox
并用它们来承载图像。这是一个直接但灵活且可用的解决方案。随附的演示项目展示了一个简单的结果示例。
引言
在EE(Experts Exchange)上,我经常遇到关于如何将图像插入RichTextBox
的问题。特别是对于开发即时通讯(IM)项目的开发者来说,除了处理聊天文本消息外,表情图标是不可避免的元素。一张哭泣的图片比“我哭了”、“我想哭”这样的苍白文字更能传达情感。然而,RichTextBox
基本上是程序员唯一的选择,除非你编写自己的阅读器或浏览器。使用Web浏览器控件实际上也是一个不错的选择。我们可能会在后续文章中讨论它(但不是这篇)。
有两种“标准”的方法可以在RichTextBox
中显示静态图像。CodeProject文章《在运行时向RichTextBox插入纯文本和图像》详细描述了两种实现方法:
- 将图像复制到剪贴板,然后将数据粘贴到目标
RichTextBox
。 - 读取RTF规范,并通过RTF标签包装的图元文件插入图像数据。
操作剪贴板内容确实令人厌烦,而且不是良好的编程实践。例如,我习惯在打字时将文字复制作为备份,并在发布到论坛之前一定会再次复制整篇文章。万一发生什么问题,剪贴板可以作为应急备份工具。
实际上,在粘贴图像后恢复原始剪贴板数据是很容易的,负责任的开发者应该这样做。
public void InsertImage()
{
...
string lstrFile = fileDialog.FileName;
Bitmap myBitmap = new Bitmap(lstrFile);
//keep the clipboard data before set image in it.
object orgData = Clipboard.GetDataObject
Clipboard.SetDataObject(myBitmap);
DataFormats.Format myFormat = DataFormats.GetFormat (DataFormats.Bitmap);
if(NoteBox.CanPaste(myFormat)) {
NoteBox.Paste(myFormat);
}
else {
MessageBox.Show("The data format that you attempted site" +
" is not supportedby this control.");
}
//Restore the original data.
Clipboard.SetDataObject(orgData )
...
}
上述两种解决方案的根本问题是它们禁用了GIF图像的动画。动画GIF文件包含多个图像帧。“复制和粘贴”只读取图像的第一帧,所以动画就停止了。而“RTF标签”将图像数据转换为一个巨大的字符串,并且RTF规范不足以支持从字符串中获取下一个活动帧。
插入Windows控件
Picture box(图片框)可以显示动画图像。而用户控件可以包含其他控件。所以将控件插入RichTextBox
以达到任何可能目的,这并不是什么不寻常的事情。在它可以工作之前,有两个问题需要解决:
- 首先,内部控件在父控件中保持静止,而且……
- 其次,输入文本会跑到控件后面,而不是围绕它换行。
我们需要使内部控件在RichTextBox
滚动时与文本一起移动,并且我们需要为内部控件留出足够的空间以防止重叠——并覆盖附近的文本,如下图所示。
为了解决上述问题,文本的起始点可以用作参考点来决定控件相对于文本的位置。当内容从PosA移动到PosB时,PictureBox
也必须移动到新的位置,这很容易计算。
thePictureBox.newPosition = PosB - PosA + thePictureBox.oldPosition = PosB - Delta
RichTextBox.GetPositionFromCharIndex
方法允许我们检索文本起始位置(PosA),其字符索引为零。我们需要存储此值,并在VScroll
事件中使用相同的方法检索文本的新起始位置(PosB)。然后我们可以设置内部控件的新位置。
调用GetPositionFromCharIndex
的开销有点大。考虑到在一个聊天会话中,可能会向RichTextBox
插入几十甚至上百张图片,因此频繁调用该函数会导致滚动非常缓慢。
获取新位置的替代调用是Win32 API GetScrollPos()
。在大多数情况下,从GetScrollPos()
获得的值几乎与从GetPositionFromCharIndex
获得的值的负值完全相同……只有几点差异,肉眼看不出来。然而,进一步的测试表明,如果用户单击滚动条并拖动,GetScrollPos()
返回的值不会改变。只有在用户释放鼠标按钮后才能检索到新的位置值。因此,图片在滚动时不会移动,只有在滚动停止后它们才会飞到新的位置。我们必须发送消息来通知位置变化。所以我们必须添加更多的编码逻辑。最后,在VScroll
中获取位置的调用是:
private void TRichTextBox_VScroll(object sender, EventArgs e)
{
Point pt = new Point();
SendMessage(this.Handle, EM_GETSCROLLPOS, 0, ref pt);
foreach (MetaInfo one in ControlList)
{
one.TheControl.Location =
new Point(one.TheControl.Location.X, -pt.Y - one.DeltaY);
}
}
使用代码
所有推导过程都是为了让该技术易于使用。首先,在Form1.Designer.cs中将你的RichTextBox
派生自TRichTextBox
。
private Trestan.TRichTextBox richTextBox1; this.richTextBox1 = new Trestan.TRichTextBox();
其次,无论何时你需要使用PictureBox
来显示图像,只需调用richTextBox1.AddControl
将其添加到列表中。
PictureBox thePic = new PictureBox();
thePic.Image = theImage;
thePic.Size = theImage.Size;
this.richTextBox1.AddControl(thePic);
就是这样!TRichTextBox
会为你处理所有剩余的事情。
附加/注释
- GDI+似乎比GDI函数更精细。某些GDI函数可以加载的图像,在GDI+中可能会导致崩溃。因此,在将图像设置为
PictureBox
之前,会进行一个简单的健全性检查,其中图像的多个帧会逐一读取,如果在任何过程中捕获到异常,该图像将被丢弃。 - 一个自我管理函数
RemoveSome()
,它将使用文本长度和接收到的图像数量作为阈值,自动截断早期接收到的消息。 - 最有趣的是,你不限于只将
PictureBox
控件添加到TRichTextBox
中;你可以将几乎任何类型的控件插入其中。它们都会随着文本一起滚动,成为内容的一个整体组成部分。如下图所示,每个消息行都附带一个按钮。因此,TRichTextBox
更有潜力成为P2P论坛的客户端读取器,而无需中心化网站。
最终结果