×
Namespaces

Variants
Actions

在Windows Phone上使用WriteableBitmap

From Nokia Developer Wiki
Jump to: navigation, search
WP Metro Icon UI.png
SignpostIcon WP7 70px.png
Article Metadata

代码示例
兼容于
文章
WS_YiLunLuo 在 31 May 2012 创建
最后由 hamishwillee 在 16 Jul 2013 编辑

Contents

简介

WriteableBitmap是XAML系列框架的一个功能,可以用于包括Windows Phone在内的所有使用XAML框架的平台上,但是不同的平台的代码可能稍有不同。它的作用是操作位图的具体像素。在Windows Phone中,出于安全考虑,你不能使用unsafe code,因此无法使用指针,而必须通过其它方式访问像素。

本文演示了两个常见的使用WriteableBitmap的场景:

  • 填充颜色:演示如何在Windows Phone中使用通用的算法,以及如何通过模拟CLR调用方法的过程来避免stackoverflow。

Fill Color.png

  • 开枪效果展示:演示如何将原始图片缩放至指定大小,以及如何操作像素。

Gun Shoot.png

你可以File:WriteableBitmap.WindowsPhone.zip下载本文附带的源代码。

什么时候使用WriteableBitmap,什么时候不要用

直到几年前,无论什么时候需要修改位图的像素,人们往往都会选择类似于WriteableBitmap的基于CPU运行的方案。但是,如今GPU越来越好了,人们渐渐开始使用shader effect。请记住,使用WriteableBitmap编写的代码运行在CPU上,假如你需要针对每个像素分别进行操作,就不得不按照顺序遍历所有像素。大多数Windows Phone目前只有一个单核的CPU,所以同一时刻只有一个像素可以被处理。与此相对的,GPU可以同时操作很多很多像素。此外,很多场合下使用HLSL相比起使用WriteableBitmap,算法都可以得到简化,因为你只需要考虑一个特定的像素,而不需要考虑当前在第几个像素,从像素列表中获得当前像素的颜色,通过位移运算转换成ARGB格式,等等。

所以,假如Windows Phone支持HLSL,我们强烈推荐大家使用HLSL而不是WriteableBitmap。很遗憾,目前Windows Phone并不支持HLSL......因此,我们推荐大家在Windows Phone上使用WriteableBitmap来实现一些运算不是很复杂,不需要太多CPU资源的操作,并且只要有可能,就将代码逻辑放在后台线程上运行。此外,如果你需要同时支持Windows Phone和Windows 8 Metro,你需要针对位图操作部分写两套代码。在Windows Phone中使用用WriteableBitmap,在Windows 8中,你可以通过DirectX使用所有shader effect的功能,从而不借助CPU,而是通过GPU完成所需操作。出于篇幅考虑,本文不会介绍Windows 8。

除此之外,即使在支持shader effect的平台上,偶尔WriteableBitmap还是有用的。有些功能很难使用shader effect来完成。例如,如果一个针对像素的操作依赖于另外一个像素操作后的结果,从而必须一个个像素按顺序进行操作,就不太适合使用shader effect了。Shader effect适用于对所有像素使用相似的操作(注意并不是相同),你可以传入一些参数,例如使用shader effect可以方便地制作一个旋涡效果,你可以使用同样的逻辑配合不同的参数让GPU同时处理所有像素。但是用某一颜色(或者渐变刷)填充某一区域,就不是那么简单了。

在以下场合下请考虑使用WriteableBitmap:

  • 平台不支持shader effect,例如Windows Phone。
  • 平台支持shader effect,但是你必须支持很老很老的显卡,例如六七年前购买的电脑。
  • 你的算法要求一个个按顺序处理像素。

在Windows Phone上使用WriteableBitmap的方法

现在大家知道了什么场合下使用WriteableBitmap是合适的,那么我们就来看看具体怎样使用吧。正如本文开始所说的那样,我们来看看两个不同的场景。

颜色填充

在这个场景中,我们实现了一个画图板中常见的功能:用某一颜色填充一块封闭区域。这主要是为了演示如何用WriteableBitmap实现标准的算法。

让我们使用一个简单的边缘跟踪算法:生长法。从手指点击的点开始,如果这一点的颜色和要填充的颜色相同,就停止操作。否则就将手指点击的点填充上目标颜色,并且向左右上下四个方向伸展,若是某一点的颜色和需要填充的颜色不同,就为那点填充颜色并且以那点为中心继续向四周扩展。

这个算法最简单的实现是使用递归。但是如果要填充的区域较大,很快你就会遇上stackoverflow了。为了避免这一问题,我们必须破除递归。尾递归通常都是可以用循环代替的。让我们来模拟一下CLR调用方法的过程。首先创建一个stack(栈)。这个栈中间的元素的数量代表要调用的方法的数量,每个元素存放调用方法需要的参数。针对每次递归,我们都往栈中存入一个元素。当我们需要调用递归方法时,我们pop这个栈从而获得最上面的那个元素,它代表着本次递归所需要的参数,然后调用一个非递归版本的方法。当栈中不再拥有任何元素时,就意味着所有的递归都结束了,于是我们可以跳出循环。

以下代码代表的是stack中的数据:

    /// <summary>
/// Use this class to simulate the paramater of a recursive method on the CLR call stack.
/// </summary>
public class ParameterData
{
//The current (center) point.
public Point centerPoint;
//The current point's position in the pixel array.
public int centerColorPosition;
public WriteableBitmap bmp;
}

以下代码展示了如何判断左边的像素是否和当前像素颜色相同。若相同,就往栈中加入一个新的元素。

                //Left
if (pd.centerPoint.X >= 1)
{
Point leftPoint = new Point(pd.centerPoint.X - 1, pd.centerPoint.Y);
int leftColorPosition = pd.centerColorPosition - 1;
//Is the left point's color the same as the clicked point's color? If so, paint the left point with the new color. Otherwise, we don't need to continue left any more.
if (bmp.Pixels[leftColorPosition] == clickedColor)
{
bmp.Pixels[leftColorPosition] = this._newColor;
//Create a new parametar, push it to the call stack, so another method will be "invoked".
ParameterData pdLeft = new ParameterData()
{
bmp = bmp,
centerColorPosition = leftColorPosition,
centerPoint = leftPoint
};
stack.Push(pdLeft);
}
}

在现实生活中,你可能还需要进一步优化算法,例如0xfffffffe and 0xffffffff确实是两个不同的颜色,但是它们很接近。我们简单的算法认为它们不同,可是你的场景可能需要认为它们是相同的颜色。

开枪效果展示

这个场景中,我们使用两张不同大小的图片,将它们缩放至同样的大小,并且让其中一张覆盖另外一张。

为了让程序简单,我们没有做复杂的选择图片的效果。当你打开这个页面时,请点一下屏幕,这会弹出Windows Phone自带的PhotoChooserTask。在这边你可以选择第一幅画。随后再次点击屏幕,选择第二幅画,之后就可以看到上方的那张图画了。

当你用手指点击上方的图片时,程序认为你开了一枪,将上方的图片打出一个洞,于是就可以看到下面那张被隐藏的图片了。 这个示例主要演示如何缩放图片,以及如何修改每一个像素。

它的工作原理是:我们根据两幅画创建两个WriteableBitmap,将所有数据都存入内存(这可能会占用好几兆内存,这也是WriteableBitmap不如shader effect的地方之一)。因为这两幅画的大小不同,我们需要将它们转化成相同大小(我们选择800*480,也就是Windows Phone的标准分辨率)。首先计算两幅画的像素差的比例。例如要把一个1600*1200像素的位图转化为800*480,我们计算(1600*1200)/(800*480)=5,而1/5*0.2,于是将每个像素的位置乘以0.2再取整就可以了。在优化的算法中,你可能会考虑如何让缩小后的图片更显得平滑,不过我们的示例为了简单,只是单纯地在每5个像素中扔掉1个像素。

现在WriteableBitmap创建好了,我们将Image控件的Source属性设置成上方的位图。原始图片不再需要了。

当你用手指点击图片从而开枪时,程序会计算出一个半径为50像素的圆,以及这个圆的外接正方形。有了外接正方形,我们就只需要遍历该正方形区域内的像素,而不需要遍历全部800*480个像素了。在这个正方形区域中遍历像素,然后判断该像素是否位于圆内。如果是,就用下方图片中对应的像素取代上方图片的像素。这样一来,你就可以看穿上方的图片了。为了模拟枪弹痕迹,我们在该圆形区域边缘画一个半透明的灰色圆圈,这一点展示了如何获取并修改ARGB值。

以下代码用于计算矩形的四个顶点:

            Point clickedPoint = e.GetPosition(this.topImage);
//Calculate the rectangle in which the gun shoots, so we don't need to iterate through all pixels.
int topLeftX = (int)clickedPoint.X - _radius;
if (topLeftX < 0)
{
topLeftX = 0;
}
int topLeftY = (int)clickedPoint.Y - _radius;
if (topLeftY < 0)
{
topLeftY = 0;
}
int bottomRightX = (int)clickedPoint.X + _radius;
if (bottomRightX > _resultPixelWidth)
{
bottomRightX = _resultPixelWidth;
}
int bottomRightY = (int)clickedPoint.Y + _radius;
if (bottomRightY > _resultPixelHeight)
{
bottomRightY = _resultPixelHeight;
}

WriteableBitmap提供了一个Pixels属性。这是一个int数组类型,每个int代表一个像素。当然,我们知道,像素是由ARGB四个分量组成的。所以为了获取ARGB的值,需要使用位移运算。

以下代码展示了如何通过位移运算获取RGB的值。Pixels属性的每一个int都是32为,最高的8位代表B,其次是G,再次是R,最后是A。

                            uint blendPixelR = blendPixel << 8 >> 24;
uint blendPixelG = blendPixel << 16 >> 24;
uint blendPixelB = blendPixel << 24 >> 24;

最后,不要忘记重新设置Image控件的Source属性,否则你看不到更新。

总结

如果你要在更高级的场合中使用WriteableBitmap,必须会图形学算法才行啊。至少也要了解位图是如何在内存中存储的,关于像素和颜色的各种知识,位移运算的使用,也都是必须的。

我们希望今后版本的Windows Phone能够支持shader effect,这样一来Windows Phone就更能和Windows 8统一起来了,而且还可以大大提高图形相关的程序的效率。不过目前我们不知道微软的计划,请大家耐心等待。

This page was last modified on 16 July 2013, at 10:11.
81 page views in the last 30 days.