Reader Level:
ARTICLE

Working with Vector Graphics on a Bitmap, Images and Tombstoning for Windows Phone 7

Posted by Charles Petzold Articles | Windows Phone December 28, 2010
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.
  • 0
  • 0
  • 9421
Download Files:
 

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:

ch1.gif

Tap once, and the whole page becomes the bitmap displayed by the Image element:

ch2.gif

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:

ch3.gif

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.

ch4.gif

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:

ch5.gif

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.

ch6.gif

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.

COMMENT USING

Trending up