This chapter
is taken from book "Programming Windows Phone 7" by Charles Petzold published by
Microsoft press.
http://www.charlespetzold.com/phone/index.html
WriteableBitmap and
UIElement
WriteableBitmap does
not including any facility to save
bitmaps. However, the WriteableBitmap class does give you access to all the
pixels that define the bitmap. Only one pixel format is supported, where each
pixel is a 32-bit value. You can obtain the pixel bits from an existing bitmap,
or set new pixel bits on a WriteableBitmap to define the image. Access to these
pixel bits allows you a great deal of flexibility in how you save or load
bitmaps. You can provide your own bitmap "encoder" to save pixel bits in a
particular bitmap format, or your own "decoder" to access a file of a particular
format and convert to the uncompressed pixel bits.
WriteableBitmap
has two ways to get the visuals of a UIElement onto a bitmap. The first uses one of the constructors:
WriteableBitmap writeableBitmap = new WriteableBitmap(element, transform);
The element argument is of type
UIElement and the transform argument
is of type Transform. This constructor creates a
bitmap based on the size of the UIElement argument
as possibly modified by the Transform argument
(which you can set to null).
Here's a simple sample program. The content grid is given a background based
on the current accent color. It contains a TextBlock and an Image
element:
<Grid
x:Name="ContentPanel"
Grid.Row="1"
Margin="12,0,12,0"
Background="{StaticResource
PhoneAccentBrush}">
<TextBlock
Text="Tap anywhere to
capture page"
HorizontalAlignment="Center"
VerticalAlignment="Center"
/>
<Image
Name="img"
Stretch="Fill" />
</Grid>
The Image
element has no bitmap to display but when it does, it will ignore the bitmap's
aspect ratio to fill the content grid and obscure the TextBlock.
When the screen is tapped, the code-behind file simply
sets the Image element source to a
new WriteableBitmap based on the page
itself:
namespace
RecursivePageCaptures
{
public partial
class MainPage
: PhoneApplicationPage
{
public MainPage()
{
InitializeComponent();
}
protected
override void OnManipulationStarted(ManipulationStartedEventArgs
args)
{
img.Source = new
WriteableBitmap(this,
null);
args.Complete();
args.Handled = true;
base.OnManipulationStarted(args);
}
}
}
When you first run the program, the screen looks like
this:
Tap once, and the whole page becomes the bitmap displayed by the Image
element:
Keep in mind that the PhoneApplicationPage object being captured has its
Background property set to the default value of null, so that's why you see the
original background of the content panel behind the captured titles. You can
continue tapping the screen to recapture the page content, now including the
previous Image element:
The Pixel Bits
The Pixels property of WritableBitmap is an array of int, which means that
each pixel is 32 bits wide. The Pixels
property itself is get-only so you can't replace the actual
array, but you can set and get elements of that array.
When you first create a
WriteableBitmap,
all the pixels are zero, which you can think of as "transparent black" or
"transparent white" or "transparent chartreuse."
By directly writing into the
Pixels array of a WriteableBitmap you can create
any type of image you can conceive. Comparatively simple algorithms let you
create styles of brushes that are not supported by the standard Brush
derivatives. The content area of the CircularGradient project consists solely of
an Image element waiting for a bitmap:
<Grid
x:Name="ContentPanel"
Grid.Row="1"
Margin="12,0,12,0">
<Image
Name="img"
HorizontalAlignment="Center"
VerticalAlignment="Center"
/>
</Grid>
The code-behind file for MainPage defines a rather arbitrary radius value and
makes a square WriteableBitmap twice that value. The two for loops for x and y
touch every pixel in that bitmap:
namespace
CircularGradient
{
public partial
class MainPage
: PhoneApplicationPage
{
const int
RADIUS = 200;
public MainPage()
{
InitializeComponent();
WriteableBitmap writeableBitmap =
new WriteableBitmap(2
* RADIUS, 2 * RADIUS);
for (int
y = 0; y < writeableBitmap.PixelWidth; y++)
{
if (Math.Sqrt(Math.Pow(x
- RADIUS, 2) + Math.Pow(y - RADIUS, 2)) <
RADIUS)
{
double angle = Math.Atan2(y -
RADIUS, x - RADIUS);
byte R = (byte)(255 *
Math.Abs(angle) /
Math.PI);
byte B = (byte)(255 - R);
int color = 255 << 24 | R << 16 | B;
writeableBitmap.Pixels[y * writeableBitmap.PixelWidth +
x] = color;
}
}
writeableBitmap.Invalidate();
img.Source = writeableBitmap;
}
}
}
The center of the
WriteableBitmap is the point (200, 200). The code
within the nested for
loops begins by skipping every pixel that is more than 200
pixels in length from that center. Within the square bitmap, only a circle will
have non-transparent pixels.
Vector Graphics on a Bitmap
You can combine the two approaches of drawing on a WriteableBitmap. The next
sample displays a Path on a WriteableBitmap
against a gradient that uses transparency so that you can
see how the premultiplied alphas work.
The Path Markup Syntax for the cat is defined in a
Path element in
the Resources
section of the MainPage.xaml file:
<phone:PhoneApplicationPage.Resources>
<Path
x:Key="catPath"
Data="M 160 140 L 150 50
220 103
M 320 140 L 330 50 260
103
M 215 230 L 40 200
M 215 240 L 40 240
M 215 250 L 40 280
M 265 230 L 440 200
M 265 240 L 440 240
M 265 250 L 440 280
M 240 100 A 100 100 0 0 1
240 300
A 100 100 0 0 1 240 100
M 180 170 A 40 40 0 0 1
220 170
A 40 40 0 0 1 180 170
M 300 170 A 40 40 0 0 1
260 170
A 40 40 0 0 1 300 170" />
</phone:PhoneApplicationPage.Resources>
I'm using this
Path element solely to force the XAML parser to
acknowledge this string as Path Markup Syntax; the Path
element won't be used for any other purpose in the program.
The content area consists of just an
Image element awaiting a bitmap:
<Grid
x:Name="ContentPanel"
Grid.Row="1"
Margin="12,0,12,0">
<Image
Name="img"
HorizontalAlignment="Center"
VerticalAlignment="Center"
/>
</Grid>
Everything else happens in the constructor of the
MainPage class. It's a
little lengthy but well commented and I'll also walk you through the logic:
namespace
VectorToRaster
{
public partial
class MainPage
: PhoneApplicationPage
{
public MainPage()
{
InitializeComponent();
// Get PathGeometry from resource
Path catPath =
this.Resources["catPath"]
as Path;
PathGeometry pathGeometry =
catPath.Data as
PathGeometry;
catPath.Data = null;
// Get geometry bounds
Rect bounds =
pathGeometry.Bounds;
// Create new path for rendering on bitmap
Path newPath =
new Path
{
Stroke = this.Resources["PhoneForegroundBrush"]
as Brush,
StrokeThickness = 5,
Data = pathGeometry,
};
// Create the WriteableBitmap
WriteableBitmap
writeableBitmap =
new
WriteableBitmap((int)(bounds.Width +
newPath.StrokeThickness),
(int)(bounds.Height
+ newPath.StrokeThickness));
// Color the background of the bitmap
Color baseColor = (Color)this.Resources["PhoneAccentColor"];
// Treat the bitmap as an ellipse:
// radiusX and radiusY are also
the centers!
double radiusX =
writeableBitmap.PixelWidth / 2.0;
double radiusY =
writeableBitmap.PixelHeight / 2.0;
for (int
y = 0; y < writeableBitmap.PixelHeight; y++)
for (int
x = 0; x < writeableBitmap.PixelWidth; x++)
{
double angle =
Math.Atan2(y - radiusY, x - radiusX);
double ellipseX = radiusX *
(1 + Math.Cos(angle));
double ellipseY = radiusY *
(1 + Math.Sin(angle));
double ellipseToCenter =
Math.Sqrt(Math.Pow(ellipseX -
radiusX, 2) +
Math.Pow(ellipseY
- radiusY, 2));
double pointToCenter =
Math.Sqrt(Math.Pow(x -
radiusX, 2) + Math.Pow(y - radiusY, 2));
double opacity =
Math.Min(1, pointToCenter / ellipseToCenter);
byte A = (byte)(opacity
* 255);
byte R = (byte)(opacity
* baseColor.R);
byte G = (byte)(opacity
* baseColor.G);
byte B = (byte)(opacity
* baseColor.B);
int color = A << 24 | R <<
16 | G << 8 | B;
writeableBitmap.Pixels[y * writeableBitmap.PixelWidth + x] =
color;
}
writeableBitmap.Invalidate();
// Find transform to move Path to edges
TranslateTransform
translate = new
TranslateTransform
{
X = -bounds.X + newPath.StrokeThickness / 2,
Y = -bounds.Y + newPath.StrokeThickness / 2
};
writeableBitmap.Render(newPath, translate);
writeableBitmap.Invalidate();
// Set bitmap to Image element
img.Source = writeableBitmap;
}
}
}
Here's the result:
Images and Tombstoning
In the 1890s, American puzzle-make Sam Loyd popularized a puzzle that was
invented a couple decades earlier and has since come to be known as the 15
Puzzle, or the 14-15 Puzzle, or (in France) Jeu de Taquin,
the "teasing game." In its classic form, the puzzle consists of 15 tiles labeled
1 through 15 arranged randomly in a 4×4 grid, leaving one blank tile. The goal
is to shift the tiles around so the numbers are sequential.
The version I'm going to show you does not use numbered tiles. Instead it lets you access a photo from the phone's picture library and chops that up into tiles. (The game becomes rather more difficult as a result.) As a bonus, the program shows you how to save images when an application is tombstoned.
The program's content area consists of a
Grid
named playGrid (used for holding the tiles) and two buttons:
<Grid
x:Name="ContentPanel"
Grid.Row="1"
Margin="12,0,12,0">
<Grid.RowDefinitions>
<RowDefinition
Height="*" />
<RowDefinition
Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition
Width="*" />
<ColumnDefinition
Width="*" />
</Grid.ColumnDefinitions>
<Grid
Name="playGrid"
Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="2"
HorizontalAlignment="Center"
VerticalAlignment="Center"
/>
<Button
Content="load"
Grid.Row="1"
Grid.Column="0"
Click="OnLoadClick" />
<Button
Name="scrambleButton"
Content="scramble"
Grid.Row="2"
Grid.Column="1"
IsEnabled="False"
Click="OnScrambleClick" />
</Grid>
</Grid>
Seemingly redundantly, the XAML file also includes two buttons in the
ApplicationBar also labeled "load" and "scramble":
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar
IsVisible="False">
<shell:ApplicationBarIconButton
x:Name="appbarLoadButton"
IconUri="/Images/appbar.folder.rest.png"
Text="load"
Click="OnLoadClick" />
<shell:ApplicationBarIconButton
x:Name="appbarScrambleButton"
IconUri="/Images/appbar.refresh.rest.png"
Text="scramble"
IsEnabled="False"
Click="OnScrambleClick" />
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
The MainPage class in the code-behind file begins with some constants. The
program is set up for 4 tiles horizontally and vertically but you can change
those. (Obviously in Portrait mode, the program works best if VERT_TILES is
greater than HORZ_TILES.) Other fields involve storing state information in the PhoneApplicationService object for tombstoning, and using the PhotoChooserTask
for picking a photo.
Here's the entire manipulation logic:
protected
override
void
OnManipulationStarted(ManipulationStartedEventArgs
args)
{
if
(args.OriginalSource is
Image)
{
Image img = args.OriginalSource
as
Image;
MoveTile(img);
args.Complete();
args.Handled =
true;
}
base.OnManipulationStarted(args);
}
void
MoveTile(Image
img)
{
int
touchedRow = -1, touchedCol = -1;
for
(int
y = 0; y < VERT_TILES; y++)
for
(int
x = 0; x < HORZ_TILES; x++)
if
(tileImages[y, x] == img)
{
touchedRow = y;
touchedCol = x;
}
if
(touchedRow == emptyRow)
{
int sign =
Math.Sign(touchedCol
- emptyCol);
for (int
x = emptyCol; x != touchedCol; x += sign)
{
tileImages[touchedRow, x] = tileImages[touchedRow, x + sign];
Grid.SetColumn(tileImages[touchedRow, x],
x);
}
tileImages[touchedRow, touchedCol] =
null;
emptyCol = touchedCol;
}
else
if
(touchedCol == emptyCol)
{
int sign =
Math.Sign(touchedRow
- emptyRow);
for (int
y = emptyRow; y != touchedRow; y += sign)
{
tileImages[y,
touchedCol] = tileImages[y + sign, touchedCol];
Grid.SetRow(tileImages[y, touchedCol], y);
}
tileImages[touchedRow, touchedCol] =
null;
emptyRow = touchedRow;
}
}
The randomizing logic piggy-backs on this manipulation
logic. When the "scramble" button is clicked, the program attaches a handler for
the CompositionTarget.Rendering
event:
void OnScrambleClick(object
sender, EventArgs
args)
{
scrambleCountdown = 10 * VERT_TILES * HORZ_TILES;
scrambleButton.IsEnabled =
false;
appbarScrambleButton.IsEnabled =
false;
CompositionTarget.Rendering +=
OnCompositionTargetRendering;
}
void
OnCompositionTargetRendering(object
sender, EventArgs
args)
{
MoveTile(tileImages[emptyRow, rand.Next(HORZ_TILES)]);
MoveTile(tileImages[rand.Next(VERT_TILES), emptyCol]);
if
(--scrambleCountdown == 0)
{
CompositionTarget.Rendering -=
OnCompositionTargetRendering;
scrambleButton.IsEnabled =
true;
appbarScrambleButton.IsEnabled =
true;
}
}
The event handler calls MoveTile twice, once to move a tile from the same row
as the empty square, and secondly to move a tile from the same column as the
empty square.
This program also handles tombstoning, which means that
it saves the entire game state when the user navigates away from the page, and
restores that game state when the game is re-activated.
When the program returns from its tombstoned state, the
process goes in reverse:
protected
override
void
OnNavigatedTo(NavigationEventArgs
args)
{
object
objHaveValidTileImages;
if
(appService.State.TryGetValue("haveValidTileImages",
out
objHaveValidTileImages) &&
(bool)objHaveValidTileImages)
{
emptyRow = (int)appService.State["emptyRow"];
emptyCol = (int)appService.State["emptyCol"];
for (int
row = 0; row < VERT_TILES; row++)
for (int
col = 0; col < HORZ_TILES; col++)
if (col != emptyCol || row != emptyRow)
{
byte[] buffer = (byte[])appService.State[TileKey(row,
col)];
MemoryStream stream =
new
MemoryStream(buffer);
BitmapImage bitmapImage =
new
BitmapImage();
bitmapImage.SetSource(stream);
WriteableBitmap tile =
new
WriteableBitmap(bitmapImage);
GenerateImageTile(tile, row, col);
}
haveValidTileImages =
true;
appbarScrambleButton.IsEnabled =
true;
}
base.OnNavigatedTo(args);
}
The method reads the
byte buffer and
converts into a MemoryStream,
from which a BitmapImage
and then a
WriteableBitmap is created. The method then uses the
earlier GenerateTileImage
method to create each
Image element and add it to the
Grid.