WPF允许使用Image元素显示位图。然而,按这种方法显示图片的方法完全是单向的。应用程序使用现成的位图,读取问题,并在窗口中显示位图。就其本身而言,Image元素没有提供创建和编辑位图信息的方法。
这正是WriteableBitmap类的用武之地。该类继承自BitmapSource,BitmapSource类是当设置Image.Source属性时使用的类(不管是在代码中直接设置图像,还是在XAML中隐式地设置图像)。但BitmapSource是只读的位图数据映射,而WriteableBitmap类是可修改的像素数组,为实现许多有趣得效果提供了可能。
一、生成位图
为使用WriteableBitmap类生成一幅位图,必须提供提供几部分重要信息:以像素为单位的宽度和高度、两个方向上的DPI分辨率以及图像格式。
下面是创建一幅与当前图像元素尺寸相同的位图的示例:
// Create the bitmap, with the dimensions of the image placeholder. WriteableBitmap wb = new WriteableBitmap((int)img.Width, (int)img.Height, 96, 96, PixelFormats.Bgra32, null);
PixelFormats枚举提供了许多像素格式,但只有一半格式呗认为是可写入的并且得到了WriteableBitmap类的支持。下面是可供使用的像素格式:
- Bgra32。这种格式使用32位sRGB颜色。这意味每个像素由32位(或4个字节)表示。第1个字节表示蓝色通道的贡献(作为从0到255之间的数字)。第2个字节用于绿色通道,第3个字节用于红色通道,第4个字节用于alpha值(0表示完全透明,255表示完全不透明)。正如可能看到的,颜色的顺序(蓝绿、红和alpha)与名称Bgra32中字母的顺序是匹配的。
- Bgr32。.这种格式为每个像素使用4个字节,就像Bgra32格式一样。区别是忽略了alpha通道。当不需要透明度时可使用这种格式。
- Pbgra32。就像Bgra32格式一样,该格式为每个像素使用4个字节。区别在于处理半透明像素的方式。为了提高透明度计算的性能,每个颜色字节是预先相乘的(因此在Pbgra32中有字母P)。这意味着每个颜色字节被乘上了alpha值并除以255.在Bgra32格式中具有B、G、R、A值(255,100,0,200)的半透明像素,在Pbgra32格式中变成了(200,78,0,200)。
- BlackWhite、Gray2、Gray4、Gray8。这些格式是黑白和灰度格式。单词Gray后面的数字和每像素的位置相对应。因此,这些格式是压缩的,但它们不支持颜色。
- Indexed1、Indexed2、Indexed4、Indexed8。这些是索引格式,这意味着每个像素指向颜色调色板中的一个值。当使用这些格式中的某种格式时,必须做出WriteableBitmap构造函数的最后一个参数传递相应的ColorPalette对象。单词Indexed后面的数字和每像素的位数相对应。索引格式是压缩的,使用这些格式稍微复杂一些,并且分别支持更少的颜色——2、4、16以及256种颜色。
前三种格式——Bgra32、Bgr32以及Pbgra32——是最常见的选择。
二、写入WriteableBitmap对象
开始时,WriteableBitmap对象中所有字节的值都是0。本质上,就是一个大的黑色的矩形。为使用内容填充WriteableBitmap对象,需要使用WritePixels()方法。WritePixels()方法将字节数组复制到指定位置的位图中,可调用WritePixels()方法设置单个像素、整幅位图或选择的某块矩形区域。为从WriteableBitmap对象中获取像素,需要使用CopyPixels()方法,该方法将希望获取的多个字节转换成字节数组。总之,WritePixels()和CopyPixels()方法没有提供可供使用的最方便编程模型,但这是低级像素访问需要付出的代价。
为成功地使用WritePixels()方法,需要理解图像格式并需要理解如何将像素编码到字节。例如,在Bgra32类型的32位位图中,每个像素需要4个字节,每个字节分别用于蓝、绿、红以及alpha成分。下面的代码显示了如何手动设置这些数值,然后将它们转换成数组:
int alpha = 0; int red = 0; int green = 0; int blue = 0; byte[] colorData={blue,gree,red,alpha};
需要注意,在此顺序是很关键的。字节数组必须遵循在Bgra32标准中设置的蓝、绿、红、alpha顺序。
当调用WritePixels()方法时,提供Int32Rect对象以指定位图中希望更新的矩形区域。Int32Rect封装了4部分信息:更新区域左上角的X和Y坐标,以及更新区域的宽度和高度。
下面的代码采用在前面代码中显示的colorData数组,并使用该数组设置WriteableBitmap对象中的第一个像素:
// Define the update square (which is as big as the entire image). Int32Rect rect = new Int32Rect(0, 0, (int)img.Width, (int)img.Height); wb.WritePixels(rect, colorData, 0, 0);
使用这种方法,可创建生产WriteableBitmap对象的代码例程。只需要循环处理图像中的所有列和所有行,并在每次迭代中更新单个像素。
Random rand = new Random(); for (int x = 0; x < wb.PixelWidth; x++) { for (int y = 0; y < wb.PixelHeight; y++) { int alpha = 0; int red = 0; int green = 0; int blue = 0; // Determine the pixel's color. if ((x % 5 == 0) || (y % 7 == 0)) { red = (int)((double)y / wb.PixelHeight * 255); green = rand.Next(100, 255); blue = (int)((double)x / wb.PixelWidth * 255); alpha = 255; } else { red = (int)((double)x / wb.PixelWidth * 255); green = rand.Next(100, 255); blue = (int)((double)y / wb.PixelHeight * 255); alpha = 50; } // Set the pixel value. byte[] colorData = { (byte)blue, (byte)green, (byte)red, (byte)alpha }; // B G R Int32Rect rect = new Int32Rect(x, y, 1, 1); int stride = (wb.PixelWidth * wb.Format.BitsPerPixel) / 8; wb.WritePixels(rect, colorData, stride, 0); //wb.WritePixels(.[y * wb.PixelWidth + x] = pixelColorValue; } }
上述代码包含一个额外细节:针对跨距(stride)的计算,WritePixels()方法需要跨距。从技术角度看,跨距是每行像素数据需要的字节数量。可通过将每行中像素的数量乘上所使用格式的每像素位数(通常为4,如本例使用的Bgra32格式),然后将所得结果除以8,进而将其从位数转换成字节数。
完成每个像素的生产过程后,需要显示最终位图。通常将使用Image元素完成该工作:
img.Source = wb;
即使是在写入和显示位图后,也仍可自由地读取和修改WriteableBitmap对象中的像素,从而可以构建更特殊的用于位图编辑以及位图命中测试的例程。
三、更高效的像素写入
尽管上一节中显示的代码可以工作,但并非最佳方法。如果需要一次性写入大量像素数据——甚至是整幅图像——最好使用更大的块,因为调用WritePixels()方法需要一定的开销,并且调用该方法越频繁,应用程序的运行速度就越慢。
下图显示了一个测试应用程序。该测试程序通过使用沿着规则网格线散步的基本随机模式填充像素来创建一幅动态位图。本章示例采用两种方法执行该任务:使用上一节中国解释的逐像素方法和稍后介绍看到的一次写入策略。如果测试该应用程序,将发现一次写入技术快很多。
为一次更新多个像素,需要理解像素被打包进字节数组的方式。无论使用哪种格式,更新缓冲区都将包括一维字节数组。这个数组提供了用于图像矩形区域中像素的数值,从左向右延伸填充每行,然后自上而下延伸。
为找到某个特定像素,需要使用以下公式,下移数行,然后一道该行中恰当的位置:
(x + y * wb.PixelWidth) * BitsPerPixel
例如,为设置一幅Bgra32格式(每个像素具有4个字节)的位图中额像素(40,100),需要使用下面的代码:
int pixelOffset = (x + y * wb.PixelWidth) * wb.Format.BitsPerPixel / 8; pixels[pixelOffset] = (byte)blue; pixels[pixelOffset + 1] = (byte)green; pixels[pixelOffset + 2] = (byte)red; pixels[pixelOffset + 3] = (byte)alpha;
根据上面的方法,下面是创建前面示例的完整代码,首先在一个数组中填充所有数据,然后值通过一次WritePixels()方法调用将其复制到WriteableBitmap对象中:
// Create the bitmap, with the dimensions of the image placeholder. WriteableBitmap wb = new WriteableBitmap((int)img.Width, (int)img.Height, 96, 96, PixelFormats.Bgra32, null); // Define the update square (which is as big as the entire image). Int32Rect rect = new Int32Rect(0, 0, (int)img.Width, (int)img.Height); byte[] pixels = new byte[(int)img.Width * (int)img.Height * wb.Format.BitsPerPixel / 8]; Random rand = new Random(); for (int y = 0; y < wb.PixelHeight; y++) { for (int x = 0; x < wb.PixelWidth; x++) { int alpha = 0; int red = 0; int green = 0; int blue = 0; // Determine the pixel's color. if ((x % 5 == 0) || (y % 7 == 0)) { red = (int)((double)y / wb.PixelHeight * 255); green = rand.Next(100, 255); blue = (int)((double)x / wb.PixelWidth * 255); alpha = 255; } else { red = (int)((double)x / wb.PixelWidth * 255); green = rand.Next(100, 255); blue = (int)((double)y / wb.PixelHeight * 255); alpha = 50; } int pixelOffset = (x + y * wb.PixelWidth) * wb.Format.BitsPerPixel / 8; pixels[pixelOffset] = (byte)blue; pixels[pixelOffset + 1] = (byte)green; pixels[pixelOffset + 2] = (byte)red; pixels[pixelOffset + 3] = (byte)alpha; } int stride = (wb.PixelWidth * wb.Format.BitsPerPixel) / 8; wb.WritePixels(rect, pixels, stride, 0); } // Show the bitmap in an Image element. img.Source = wb;
在实际应用程序中,可选择折中方法。如果需要更新位图中一块较大的区域,不会一次写入一个像素,因为这种方法的运行速度太慢。但也不会在内存中同时保存全部图像数据,因为图像数据可能会很大(毕竟,一幅每像素需要4个字节的1000X1000像素的图像需要将近4MB的内存,这一要求不会很过分,但是耶比较高)。相反,应当写入一大块图像数据而不是单个像素,当一次生成一整幅位图时尤其如此。
本章示例完整标记:
<Window x:Class="Drawing.GenerateBitmap" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="GenerateBitmap" Height="460" Width="472" SizeToContent="WidthAndHeight"> <Grid x:Name="LayoutRoot" Background="White"> <Grid.RowDefinitions> <RowDefinition Height="Auto">RowDefinition> <RowDefinition>RowDefinition> Grid.RowDefinitions> <Button Content="Button" Grid.Row="1" Height="81" HorizontalAlignment="Left" Margin="106,90,0,0" Name="button1" VerticalAlignment="Top" Width="193" /> <Button Content="Generate Bitmap" Width="120" Margin="5" Padding="10" Click="cmdGenerate2_Click" HorizontalAlignment="Center">Button> <Image Grid.Row="1" x:Name="img" Margin="5" Width="400" Height="300" IsHitTestVisible="False">Image> Grid> Window>
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Shapes; namespace Drawing { ////// GenerateBitmap.xaml 的交互逻辑 /// public partial class GenerateBitmap : Window { public GenerateBitmap() { InitializeComponent(); } private void cmdGenerate_Click(object sender, RoutedEventArgs e) { // Create the bitmap, with the dimensions of the image placeholder. WriteableBitmap wb = new WriteableBitmap((int)img.Width, (int)img.Height, 96, 96, PixelFormats.Bgra32, null); // Define the update square (which is as big as the entire image). Int32Rect rect = new Int32Rect(0, 0, (int)img.Width, (int)img.Height); byte[] pixels = new byte[(int)img.Width * (int)img.Height * wb.Format.BitsPerPixel / 8]; Random rand = new Random(); for (int y = 0; y < wb.PixelHeight; y++) { for (int x = 0; x < wb.PixelWidth; x++) { int alpha = 0; int red = 0; int green = 0; int blue = 0; // Determine the pixel's color. if ((x % 5 == 0) || (y % 7 == 0)) { red = (int)((double)y / wb.PixelHeight * 255); green = rand.Next(100, 255); blue = (int)((double)x / wb.PixelWidth * 255); alpha = 255; } else { red = (int)((double)x / wb.PixelWidth * 255); green = rand.Next(100, 255); blue = (int)((double)y / wb.PixelHeight * 255); alpha = 50; } int pixelOffset = (x + y * wb.PixelWidth) * wb.Format.BitsPerPixel / 8; pixels[pixelOffset] = (byte)blue; pixels[pixelOffset + 1] = (byte)green; pixels[pixelOffset + 2] = (byte)red; pixels[pixelOffset + 3] = (byte)alpha; } int stride = (wb.PixelWidth * wb.Format.BitsPerPixel) / 8; wb.WritePixels(rect, pixels, stride, 0); } // Show the bitmap in an Image element. img.Source = wb; } private void cmdGenerate2_Click(object sender, RoutedEventArgs e) { // Create the bitmap, with the dimensions of the image placeholder. WriteableBitmap wb = new WriteableBitmap((int)img.Width, (int)img.Height, 96, 96, PixelFormats.Bgra32, null); Random rand = new Random(); for (int x = 0; x < wb.PixelWidth; x++) { for (int y = 0; y < wb.PixelHeight; y++) { int alpha = 0; int red = 0; int green = 0; int blue = 0; // Determine the pixel's color. if ((x % 5 == 0) || (y % 7 == 0)) { red = (int)((double)y / wb.PixelHeight * 255); green = rand.Next(100, 255); blue = (int)((double)x / wb.PixelWidth * 255); alpha = 255; } else { red = (int)((double)x / wb.PixelWidth * 255); green = rand.Next(100, 255); blue = (int)((double)y / wb.PixelHeight * 255); alpha = 50; } // Set the pixel value. byte[] colorData = { (byte)blue, (byte)green, (byte)red, (byte)alpha }; // B G R Int32Rect rect = new Int32Rect(x, y, 1, 1); int stride = (wb.PixelWidth * wb.Format.BitsPerPixel) / 8; wb.WritePixels(rect, colorData, stride, 0); //wb.WritePixels(.[y * wb.PixelWidth + x] = pixelColorValue; } } // Show the bitmap in an Image element. img.Source = wb; } } }