在 WPF 中将一个现成的 Bitmap 位图转换成 ImageSource 用于显示一个麻烦的事儿,因为 WPF 并没有提供多少可以转过来的方法。不过产生 Bitmap 来源却非常多,比如屏幕截图、GDI 图、数组或其它非托管框架生成的图片。


WPF 官方提供了一种方法,使用 System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap() 方法。官方解释称这是托管和非托管位图相互转换所用的方法。然而此方法有一个很严重的弊端——每次都会生成全新的位图,即便每次 DeleteObject 之后,内存依然不会即时释放。

DeleteObject:

[DllImport("gdi32")]
static extern int DeleteObject(IntPtr o);

DeleteObject 的指针源于 Bitmap.GetHbitmap() 方法,且得到的指针会作为 System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap() 的参数之一。


在持续输出图像的时候(例如播放 Gif 图、持续显示屏幕截图等)不及时释放内存非常致命!为了防止重复创建图片,WriteableBitmap 似乎成了比较好的选择。

但是 WriteableBitmap 没有提供与位图 Bitmap 的互操作。然而它们都提供了像素操作。

于是,我们考虑内存拷贝来完成转换,代码如下:

public static class WriteableBitmapExtensions
{
    public static void CopyFrom(this WriteableBitmap wb, Bitmap bitmap)
    {
        if (wb == null) throw new ArgumentNullException(nameof(wb));
        if (bitmap == null) throw new ArgumentNullException(nameof(bitmap));

        var ws = wb.PixelWidth;
        var hs = wb.PixelHeight;
        var wt = bitmap.Width;
        var ht = bitmap.Height;
        if (ws != wt || hs != ht) throw new ArgumentException("暂时只支持相同尺寸图片的复制。");

        var width = ws;
        var height = hs;
        var bytes = ws * hs * wb.Format.BitsPerPixel / 8;

        var rBitmapData = bitmap.LockBits(new Rectangle(0, 0, width, height),
            ImageLockMode.ReadOnly, bitmap.PixelFormat);

        wb.Lock();
        unsafe
        {
            Buffer.MemoryCopy(rBitmapData.Scan0.ToPointer(), wb.BackBuffer.ToPointer(), bytes, bytes);
        }
        wb.AddDirtyRect(new Int32Rect(0, 0, width, height));
        wb.Unlock();

        bitmap.UnlockBits(rBitmapData);
    }
}

我写了一个持续不断截取屏幕并输出显示的控件,在我的 The New Surface Pro 2736*1826 分辨率下内存一直保持 168M 从不变化。

内存占用

这个方法的简化空间还非常大,比如,如果数据源是一个一次申请不断修改的数组,那么连 Bitmap 都可以不需要了,直接拷贝数组空间即可。我的朋友林德熙为此将这段代码简化得只剩下几行代码了:WPF 使用不安全代码快速从数组转 WriteableBitmap - 林德熙


本文会经常更新,请阅读原文: https://walterlv.github.io/post/convert-bitmap-to-imagesource-using-unsafe-method.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

知识共享许可协议 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名 吕毅 (包含链接: https://walterlv.github.io ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系