WPF WriteableBitmap

一、什么是WriteableBitmap

概念性的东西挺多的,这里就不一一介绍了,感兴趣的可以直接去查看官方文档,这里主要以用例的形势,介绍WriteableBitmap能给我们带来什么?怎么实现?官方文档:https://docs.microsoft.com/zh-cn/dotnet/api/system.windows.media.imaging.writeablebitmap?redirectedfrom=MSDN&view=windowsdesktop-6.0
简单来说,提供一个可写入的bitmapsource

二、传统的bitmapsource

图像是一次性加载,在界面中使用之后,是一张固定的图片,不可以编辑的,这在绝大多数的程序中足够我们使用,一般我们使用图片作为控件的背景图、待机图等。

三、什么情况下需要使用WriteableBitmap

需要编辑图片的某些区域
图片需要频繁编辑
图片需要精确到像素级别的编辑

以上三种无论哪种情况,都需要使用WriteableBitmap,下面介绍wpf使用WriteableBitmap的具体实例。

编辑图像中指定像素点的颜色

  • xaml文件

新建一个页面,其中定义Image控件,Name=img

<Grid x:Name="grid">
        <Image Height="450" Width="800" Name="img" Stretch="Uniform"></Image>
        <StackPanel>
            <Button Content="加载位图" Name="btnLoadBitmap" Click="btnLoadBitmap_Click"/>
            <Button Content="全部反色" Name="btnUpdateBitmap" Click="btnUpdateBitmap_Click"/>
            <Button Content="部分反色" Name="btnUpdatePartBitmap" Click="btnUpdatePartBitmap_Click"/>
        </StackPanel>
    </Grid>
  • cs类

在load方法中,给img赋予图像源WriteableBitmap
其中bitmapWidth和bitmapHeight变量是图片的宽、高

int bitmapWidth = 50, bitmapHeight = 20;
private void MyWriteableBitmap_Loaded(object sender, RoutedEventArgs e)
        {
            bitmap = new WriteableBitmap(bitmapWidth, bitmapHeight, 96, 96, PixelFormats.Bgra32, null);
            img.Source = bitmap;
        }

WriteableBitmap的构造函数,分别为图像宽、图像高,其中96是dpi(可以根据实际情况填写),PixelFormats枚举有很多值,Bgra32,代表的是32位彩色图,即每个像素的颜色由4个字节表示,依次为B、G、R、A

  • 生成图片——WritePixels
    此时肯定是什么东西都没有,因为这里我们还没有生成图片,这里的图片可以直接用本地图片资源,也可以自己生成图片,这里为了方便后边编辑图片,所以采用手动生成图片的形势
    在加载位图按钮的处理事件中,加入以下代码
private void btnLoadBitmap_Click(object sender, RoutedEventArgs e)
        {
            int length = bitmap.BackBufferStride * bitmapHeight;
            byte[] buffer = new byte[length];
            for (int i = 0; i < length; i += 4)
            {
                if (i / 4 % 2 == 0)
                {
                    buffer[i] = 0;//B
                    buffer[i + 1] = 0;//G
                    buffer[i + 2] = 255;//R
                    buffer[i + 3] = 255;//A
                }
                else
                {
                    buffer[i] = 0;//B
                    buffer[i + 1] = 255;//G
                    buffer[i + 2] = 0;//R
                    buffer[i + 3] = 255;//A
                }
            }
            bitmap.WritePixels(new Int32Rect(0, 0, (int)bitmap.Width, (int)bitmap.Height), buffer, bitmap.BackBufferStride, 0);
        }

代码解释:length=bitmap.BackBufferStride * bitmapHeight;通过调用bitmap的属性BackBufferStride,可以直接得到Stride,其中Stride就是每一行必须需要的字节数,Stride=bitmapWidth4,本例中就是504=200,因为每一个像素需要4个字节来表示其颜色,乘上bitmapHeight之后,就是整张图片所需要的字节数200*20=4000;
生成颜色:根据%2是否为0,分别赋予不同的颜色(条件无所谓,这里是随便加了个条件,目的是让图片有不同颜色可区分),其中每4个字节表示一个像素,依次为B/G/R/A,
写入图片:最后调用WritePixels方法,将字节流写入,其中第一个参数为Int32Rect(0,0,bitmapWidth,bitmapHeight,buffer,bitmap.BackBufferStride,0);意思是,从(0,0)点开始填充整张图片,如果想填充某块区域就可以通过修改改Int32Rect参数,这个后边会说。

  • 此时运行程序
    点击加载位图,如图所示:

    我们通过手动编写代码,生成了一张红绿相间的图片,接下来我们就针对这个图片进行编辑
  • 将整张图反色处理——Lock、Unlock和AddDirtyRect
    在全部反色按钮的事件处理方法中,加入以下代码:
private void btnUpdateBitmap_Click(object sender, RoutedEventArgs e)
        {
            Int32Rect rect1 = new Int32Rect(0, 0, bitmapWidth, bitmapHeight);
            unsafe
            {
                var bytes = (byte*)bitmap.BackBuffer.ToPointer();
                bitmap.Lock();

                //全部反色
                for (int i = 0; i < bitmap.BackBufferStride * bitmap.PixelHeight; i += 4)
                {
                    bytes[i] = (byte)(255 - bytes[i]);//B
                    bytes[i + 1] = (byte)(255 - bytes[i + 1]);//G
                    bytes[i + 2] = (byte)(255 - bytes[i + 2]);//R
                    bytes[i + 3] = 255;//A
                }
                bitmap.AddDirtyRect(rect1);
                bitmap.Unlock();
            }
        }

代码解释:
rect1:为和图片本身相同的矩形
unsafe:由于C#不允许使用指针,所以这里使用unsafe标识不安全代码,如果编译不通过,需要在vs开启允许不安全代码选项即可;
bytes: = (byte*)bitmap.BackBuffer.ToPointer();获取指针
Lock:在进行操作前,先使用Lock方法,锁定后台缓冲区,这时,呈现系统得不到后台缓冲区的数据,因此不发送更新,屏幕不发生变化
修改bytes:通过循环将所有RGB信息取反,
AddDirtyRect:调用bitmap.AddDirtyRect方法,指示代码对后台缓冲区所做的更改,通知呈现系统,缓冲区哪部分发生了改变。
Unlock:解除对后台缓冲区的锁定

运行程序,点击加载位图后,点击全部取反按钮,可以看见,图片发生了变化,如下图所示:

至此,我们完成了对这个图片的编辑,但是我们是编辑了一整幅图像,如果我们只需要编辑某些点的颜色,该怎么处理呢?接下来我们演示

  • 编辑指定点集合的颜色
    首先我们生成一些需要编辑的点Points,一共是169个点,是从点(8,8)到点(20,20)之间的所有的点。
            for (int r = 8; r <= 20; r++)
            {
                for (int c = 8; c <= 20; c++)
                {
                    Points.Add(new Point(r, c));
                }
            }

然后我们再部分反色按钮的事件处理方法中,加入以下代码:

private void btnUpdatePartBitmap_Click(object sender, RoutedEventArgs e)
        {
            unsafe
            {
                var bytes = (byte*)bitmap.BackBuffer.ToPointer();
                bitmap.Lock();
                foreach (var p in Points)
                {
                    int index = (int)(p.Y * bitmap.BackBufferStride + p.X * 4);
                    bytes[index] = (byte)(255 - bytes[index]);
                    bytes[index + 1] = (byte)(255 - bytes[index + 1]);
                    bytes[index + 2] = (byte)(255 - bytes[index + 2]);
                    bytes[index + 3] = 255;
                }


                bitmap.AddDirtyRect(new Int32Rect(8, 8, 12, 12));
                bitmap.Unlock();
            }
        }

代码解释:
不同于上边编辑整张图的是,这里我们根据实际的点坐标,计算得到其对应的BackBuffer中的一组(4个)字节,并对这四个字节进行颜色编辑,
也就是说,编辑某1个点(比如(8,8)),对应的要先找到BackBuffer中的4个字节,方法就是p.Y * bitmap.BackBufferStride + p.X 4,其中bitmap.BackBufferStride代表的是一行所需的字节数,所以p.Y就是得到Y行之前所有的字节数;
然后加上p.X*4,就得到了该点在BackBuffer中对应的地址index
AddDirtyRect:这里我们指定了一个矩形(8,8,12,12)就是对应于我们修改的点集合(8,8)到(20,20)
运行程序,点击加载位图,再点击部分反色,就会出现如下图所示:这样我们就完成了编辑指定点集合的颜色。

总结

WriteableBitmap在需要编辑图片的时候,是非常有效的手段,通过它我们可以做到在内存中直接修改图片的字节流,并自动通知到界面自动刷新,且效率较高,因为不需要每次重新加载图像
关于编辑指定点颜色需要注意的是:
1、确定点集合
2、确定正确的DirtyRect,也就是bitmap.AddDirtyRect的参数rect,意思就是,rect一定要完全包含Points的点,才会生效,所以这个rect一定不可以错,最简单的方式就是点集合中x最小的就是rect的x,y最小的就是rect的y,其中MaxX-MinX=Width,MaxY-MinY=Height,即Int32Rect rect = new Int32Rect(MinX,MinY,MaxX-Minx,MaxY-MinY);
3、lock、Unlock

posted @ 2022-06-27 14:16  大苹果coding  阅读(2623)  评论(0编辑  收藏  举报