在图形用户界面(GUI)编程中,经常需要将图像绘制到屏幕上。然而,如果图像的像素格式与屏幕的像素格式不一致,就需要进行格式转换。GDI+ 提供了 CachedBitmap 类来方便地缓存转换后的图像版本。但在 .NET 中,这种功能并没有直接暴露出来,因此,通常在每次绘图调用时,都需要转换与屏幕格式不匹配的位图。
乍一看,似乎对于大型图像来说,即使只绘制暴露或失效的图像区域,使用 Graphics.DrawImage() 也不会太影响性能。但是,进一步的性能测试显示,Graphics.DrawImage() 的速度始终与整个位图的大小有关,而与正在绘制的区域大小无关。如果告诉GDI+绘制一个与屏幕像素格式不同的位图的 1x1 像素区域,它会在完成绘图调用之前将整个位图转换为屏幕像素格式!
这显然是不可接受的,尤其是因为数码相机图像通常是 24 位每像素(bpp)。因此,编写了一个例程,它会创建一个新的位图,其大小与需要绘制的区域相同。然后,它将源位图的正确部分复制到该区域,并将其绘制到屏幕上。这将大大提高性能。
然而,也想知道是否可以让GDI+直接转换位图的相关区域,而不是有额外复制例程的开销。意识到,通过 EditableBitmap 类,可以创建一个 EditableBitmap 作为另一个 EditableBitmap 的位的视图。然后,可以告诉 GDI+ "步长" 是源位图步长的大小。因此,将有一个明确的 GDI+ 位图信息类,GDI+ 可以与之工作,但不会有任何与额外位图的位相关的内存开销。每次绘图调用时,也不会有与复制位图部分相关的性能降低,无论这种降低有多小。添加了一个名为 CreateView() 的方法,它接受一个矩形来指定视图的边界:
public EditableBitmap CreateView(Rectangle viewArea)
{
if (disposed)
throw new ObjectDisposedException("this");
return new EditableBitmap(this, viewArea);
}
它将 "魔法" 委托给一个受保护的构造函数:
protected EditableBitmap(EditableBitmap source, Rectangle viewArea)
{
owner = source;
pixelFormatSize = source.pixelFormatSize;
byteArray = source.byteArray;
byteArray.AddReference();
stride = source.stride;
try
{
startOffset = source.startOffset + (stride * viewArea.Y) +
(viewArea.X * pixelFormatSize);
bitmap = new Bitmap(viewArea.Width, viewArea.Height,
stride, source.Bitmap.PixelFormat,
(IntPtr)(((int)byteArray.bitPtr) + startOffset));
}
finally
{
if (bitmap == null)
byteArray.ReleaseReference();
}
}
构造函数复制了所有将与源位图相同的属性。它在 Owner 属性中存储了它是一个视图的位图的引用。然后,它计算从所有者字节数组的开始到视图位图的第一个像素的字节偏移量。然后,它创建了一个GDI+Bitmap 对象,传入偏移字节指针和视图的宽度和高度。到目前为止,一切顺利!有一个位图对象,它让 GDI+ 认为它是一个独立的位图,而实际上它指向一个更大的位图的一部分。
然而,仍然需要担心那些烦人的资源管理问题。EditableBitmap 类依赖于一个固定字节数组来存储像素数据。对于位图视图,该字节数组在多个位图之间共享。希望在根位图被处置时,视图仍然可以操作,反之亦然。因此,必须确保只有在没有更多的 EditableBitmap 实例使用它时,位数组才会被销毁。为此,添加了一个名为 SharedPinnedByteArray 的新类,它管理字节数组并保持引用计数。当引用计数达到零或它被最终确定时,它将取消固定字节数组。
最终结果是,对于大型、非屏幕格式的位图,渲染时间大大加快。包括的演示展示了速度差异。当运行它时,它会要求使用文件对话框选择一个位图。选择一个大位图。尝试滚动,特别是拖动滚动条滑块。很可能会看到 "撕裂:" 区域,这些区域绘制得足够慢,以至于可以很容易地看到缺失的区域。现在点击位图显示区域,以便表单的标题栏显示 "新方法"。再次尝试滚动,它应该非常平滑。