Paint (Ink Presenter)

0
226

Paint is a classic finger-painting app, but with several powerful options:

  • You can paint on top of any color canvas or a photo from your pictures library and paint with multiple fingers simultaneously (naturally).
  • In addition to using the rich color picker shared by many apps in this book, Paint provides many options for customizing the brush strokes.
  • You can undo and redo your strokes to get them just right.
  • A “stroke straightening” feature can help you create more precise artwork, either by straightening your diagonal lines or by snapping your lines to be completely vertical/horizontal.
  • Save your masterpieces to your phone’s pictures library.

Paint uses multi-touch the same way as the preceding chapter’s Musical Robot app, but it applies the data to an interesting element worth knowing about—ink presenter.

An ink presenter holds a collection of objects known as strokes that are meant to represent handwriting. Each stroke contains a collection of points that are connected to form each one. Each stroke also exposes a DrawingAttributes object with four properties: Color, OutlineColor, Width, and Height. Therefore, this app’s main drawing surface is simply an ink presenter whose strokes are added based on the touch data and settings chosen for the DrawingAttributes object.

Paint has two pages (in addition to standard instructions and about pages not shown in this chapter)—the main page containing the drawing surface and a palette page for adjusting the brush settings. This chapter begins by examining the palette page first, as it uses an ink presenter in a simpler fashion.

The Palette Page

 The palette page exposes a way to change each of the four properties on DrawingAttributes.
FIGURE 39.1 The palette page exposes a way to change each of the four properties on DrawingAttributes.

Paint’s palette page, pictured in Figure 39.1, enables changing each of the properties on the DrawingAttributes object. It links to this book’s shared color picker for the main color as well as the optional outline color, and exposes two sliders for independently controlling a stroke’s width and height.

The page has a hard-coded stroke that demonstrates how the different settings affect the resulting strokes as the user changes them. This is especially helpful for visualizing width and height changes, as shown in Figure 39.2.

Demonstrating every combination of the minimum and maximum brush sizes.
FIGURE 39.2 Demonstrating every combination of the minimum and maximum brush sizes.

The User Interface

Listing 39.1 contains the XAML for the palette page.

LISTING 39.1 PalettePage.xaml—The User Interface for Paint’s Palette Page

[code]

<phone:PhoneApplicationPage x:Class=”WindowsPhoneApp.PalettePage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
xmlns:local=”clr-namespace:WindowsPhoneApp”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”Portrait” shell:SystemTray.IsVisible=”True”>
<Canvas>
<!– The standard header –>
<StackPanel Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock Text=”PAINT” Style=”{StaticResource PhoneTextTitle0Style}”/>
<TextBlock Text=”palette” Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<!– The translucent foreground-colored palette image –>
<Rectangle Canvas.Left=”6” Width=”474” Height=”632” Opacity=”.6”
Fill=”{StaticResource PhoneForegroundBrush}”>
<Rectangle.OpacityMask>
<ImageBrush ImageSource=”Images/paletteBackground.png”/>
</Rectangle.OpacityMask>
</Rectangle>
<!– The InkPresenter with a single 5-point stroke –>
<InkPresenter x:Name=”PreviewInkPresenter” Canvas.Left=”236” Canvas.Top=”220”>
<InkPresenter.Strokes>
<Stroke>
<Stroke.StylusPoints>
<StylusPoint X=”100” Y=”0”/>
<StylusPoint X=”0” Y=”0”/>
<StylusPoint X=”80” Y=”80”/>
<StylusPoint X=”0” Y=”120”/>
<StylusPoint X=”0” Y=”170”/>
</Stroke.StylusPoints>
</Stroke>
</InkPresenter.Strokes>
</InkPresenter>
<!– Paint color –>
<TextBlock Text=”Paint color” Canvas.Left=”84” Canvas.Top=”431” FontSize=”23”
Foreground=”{StaticResource PhoneBackgroundBrush}”/>
<Ellipse x:Name=”PaintColorEllipse” Canvas.Left=”78” Canvas.Top=”305”
Width=”120” Height=”120” Stroke=”{StaticResource PhoneBackgroundBrush}”
StrokeThickness=”10” local:Tilt.IsEnabled=”True”
MouseLeftButtonUp=”PaintColorEllipse_MouseLeftButtonUp”/>
<!– Outline color –>
<CheckBox x:Name=”OutlineCheckBox” Canvas.Left=”210” Canvas.Top=”521”
Foreground=”{StaticResource PhoneBackgroundBrush}”
local:Tilt.IsEnabled=”True” Checked=”OutlineCheckBox_IsCheckedChanged”
Unchecked=”OutlineCheckBox_IsCheckedChanged”>
<TextBlock FontSize=”23” Foreground=”{StaticResource PhoneBackgroundBrush}”>
Outline<LineBreak/>color
</TextBlock>
</CheckBox>
<Ellipse x:Name=”OutlineColorEllipse” Canvas.Left=”213” Canvas.Top=”414”
Width=”120” Height=”120” Stroke=”{StaticResource PhoneBackgroundBrush}”
StrokeThickness=”10” local:Tilt.IsEnabled=”True”
MouseLeftButtonUp=”OutlineColorEllipse_MouseLeftButtonUp”/>
<!– Brush width –>
<TextBlock Text=”Brush width” Canvas.Left=”35” Canvas.Top=”660”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
<Slider x:Name=”BrushWidthSlider” Canvas.Left=”24” Canvas.Top=”680”
Minimum=”2” Maximum=”55” Width=”203”
ValueChanged=”BrushWidthSlider_ValueChanged”/>
<!– Brush height –>
<TextBlock Text=”Brush height” Canvas.Left=”263” Canvas.Top=”660”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
<Slider x:Name=”BrushHeightSlider” Canvas.Left=”252” Canvas.Top=”680”
Minimum=”2” Maximum=”55” Width=”203”
ValueChanged=”BrushHeightSlider_ValueChanged”/>
</Canvas>
</phone:PhoneApplicationPage>

[/code]

  • The ink presenter’s collection of strokes contains just one 5-point stroke. The stroke doesn’t specify any explicit drawing attributes because those are set in code-behind. By default, a stroke is given a width and height of 3, a color of black, and an outline color of transparent.
  • The StylusPoint objects that must be used to define a stroke are just like Point objects, but they have one additional property—PressureFactor—that unfortunately has no effect on Windows Phone.
  • The slider-enforced minimum and maximum width/height values of 2 and 55, respectively, are arbitrary. The corresponding properties on DrawingAttributes can be set to any nonnegative double.

Can I change the thickness of a stroke’s outline?

No, it is always a thin, approximately 1-pixel border.However, you could mimic a thicker border by rendering a duplicate stroke with a larger width and height behind each “real” stroke. It turns out that this is exactly how the outline color is rendered anyway, which you can see by giving the stroke a translucent or transparent color.This is demonstrated in Figure 39.3, which uses a translucent white stroke color and a green outline color.

A stroke outline is really just a slightly larger stroke underneath, which can be seen when a translucent paint color is used.
FIGURE 39.3 A stroke outline is really just a slightly larger stroke underneath, which can be seen when a translucent paint color is used.

What’s the difference between an ink presenter with strokes and a path with polylines?

Either of these two elements can be used for an app like Paint, as the differences between them are subtle.The main reason to prefer an ink presenter is that it performs better for the large number of points that are generated by finger movement. It’s also slightly easier to serialize its strokes so they can be saved and then later restored.

A path is more powerful because it can express mathematical curves between any two points, whereas each stroke’s stylus points are connected with straight lines. (There are just so many points when plotting finger movement that you don’t normally notice the connections.) It also supports arbitrary brushes (like gradient or image brushes instead of a solid color), and you can leverage its fill and stroke properties to either fill in a closed shape or to provide a true border of any thickness.

An ink presenter is more powerful because it can contain arbitrary UI elements in addition to strokes. (That’s because InkPresenter derives from Canvas.) It also holds the promise of easily enabling pressure-sensitive painting, as each stylus point exposes a PressureFactor property that can be set to a value from 0 to 1. However, given that setting this property currently has no effect on Windows Phone, and touch points never report how hard a finger is pressing the screen, this advantage is only a theoretical one for the future.

The Code-Behind

Listing 39.2 contains the code-behind for the palette page.

LISTING 39.2 PalettePage.xaml.cs—The Code-Behind for Paint’s Palette Page

[code]

using System;
using System.Windows;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class PalettePage : PhoneApplicationPage
{
public PalettePage()
{
InitializeComponent();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Respect the saved settings
this.PaintColorEllipse.Fill =
new SolidColorBrush(Settings.PaintColor.Value);
this.OutlineColorEllipse.Fill =
new SolidColorBrush(Settings.OutlineColor.Value);
this.OutlineCheckBox.IsChecked = Settings.HasOutline.Value;
this.BrushWidthSlider.Value = Settings.BrushWidth.Value;
this.BrushHeightSlider.Value = Settings.BrushHeight.Value;
// Update the ink presenter with the current settings
DrawingAttributes attributes =
this.PreviewInkPresenter.Strokes[0].DrawingAttributes;
attributes.Color = Settings.PaintColor.Value;
attributes.Width = Settings.BrushWidth.Value;
attributes.Height = Settings.BrushHeight.Value;
if (Settings.HasOutline.Value)
attributes.OutlineColor = Settings.OutlineColor.Value;
else
attributes.OutlineColor = Colors.Transparent; // Hide the outline
}
void PaintColorEllipse_MouseLeftButtonUp(object sender,
MouseButtonEventArgs e)
{
// Get a string representation of the colors we need to pass to the color
// picker, without the leading #
string currentColorString =
Settings.PaintColor.Value.ToString().Substring(1);
string defaultColorString =
Settings.PaintColor.DefaultValue.ToString().Substring(1);
// The color picker works with the same isolated storage value that the
// Setting works with, but we have to clear its cached value to pick up
// the value chosen in the color picker
Settings.PaintColor.ForceRefresh();
// Navigate to the color picker
this.NavigationService.Navigate(new Uri(
“/Shared/Color Picker/ColorPickerPage.xaml?”
+ “&currentColor=” + currentColorString
+ “&defaultColor=” + defaultColorString
+ “&settingName=PaintColor”, UriKind.Relative));
}
void OutlineColorEllipse_MouseLeftButtonUp(object sender,
MouseButtonEventArgs e)
{
// Get a string representation of the colors, without the leading #
string currentColorString =
Settings.OutlineColor.Value.ToString().Substring(1);
string defaultColorString =
Settings.OutlineColor.DefaultValue.ToString().Substring(1);
// The color picker works with the same isolated storage value that the
// Setting works with, but we have to clear its cached value to pick up
// the value chosen in the color picker
Settings.OutlineColor.ForceRefresh();
// Navigate to the color picker
this.NavigationService.Navigate(new Uri(
“/Shared/Color Picker/ColorPickerPage.xaml?”
+ “showOpacity=true”
+ “&currentColor=” + currentColorString
+ “&defaultColor=” + defaultColorString
+ “&settingName=OutlineColor”, UriKind.Relative));
}
void OutlineCheckBox_IsCheckedChanged(object sender, RoutedEventArgs e)
{
// Toggle the outline
Settings.HasOutline.Value = this.OutlineCheckBox.IsChecked.Value;
if (Settings.HasOutline.Value)
this.PreviewInkPresenter.Strokes[0].DrawingAttributes.OutlineColor =
Settings.OutlineColor.Value;
else
this.PreviewInkPresenter.Strokes[0].DrawingAttributes.OutlineColor =
Colors.Transparent;
}
void BrushWidthSlider_ValueChanged(object sender,
RoutedPropertyChangedEventArgs<double> e)
{
if (this.BrushWidthSlider != null) // Ignore during XAML parsing
{
Settings.BrushWidth.Value = (int)this.BrushWidthSlider.Value;
this.PreviewInkPresenter.Strokes[0].DrawingAttributes.Width =
Settings.BrushWidth.Value;
}
}
void BrushHeightSlider_ValueChanged(object sender,
RoutedPropertyChangedEventArgs<double> e)
{
if (this.BrushHeightSlider != null) // Ignore during XAML parsing
{
Settings.BrushHeight.Value = (int)this.BrushHeightSlider.Value;
this.PreviewInkPresenter.Strokes[0].DrawingAttributes.Height =
Settings.BrushHeight.Value;
}
}
}
}

[/code]

  • This app uses the following settings defined in a separate Settings.cs file:
    [code]
    public static class Settings
    {
    // Drawing attributes for strokes
    public static readonly Setting<Color> PaintColor = new Setting<Color>(
    “PaintColor”, (Color)Application.Current.Resources[“PhoneAccentColor”]);
    public static readonly Setting<Color> OutlineColor =
    new Setting<Color>(“OutlineColor”, Colors.Black);
    public static readonly Setting<bool> HasOutline =
    new Setting<bool>(“HasOutline”, false);
    public static readonly Setting<int> BrushWidth =
    new Setting<int>(“BrushWidth”, 10);
    public static readonly Setting<int> BrushHeight =
    new Setting<int>(“BrushHeight”, 10);
    // Background color
    public static readonly Setting<Color> PageColor =
    new Setting<Color>(“PageColor”, Colors.White);
    }
    [/code]
    All but the last one are modified by this page.
  • To update the stroke with all the current values, this code simply retrieves the 0th element of the ink presenter’s Strokes collection.
  • When the user turns off the outline color (by unchecking the check box), the outline color is set to transparent. This is the only way to prevent the outline color from interfering with the size of the stroke and even the color of the stroke if the main color is translucent.

The Main Page

Paint’s main page is nothing more than a drawing surface and an application bar with several available actions. As demonstrated in Figure 39.4, although the application bar adjusts for the current orientation, the artwork remains fixed relative to the screen. Having the artwork rotate would be problematic, as the page size would effectively change. Having the application bar rotate, however, is a nice touch when doing landscape- oriented artwork.

The application bar rotates according to the current orientation, but the artwork does not (relative to the physical screen).
FIGURE 39.4 The application bar rotates according to the current orientation, but the artwork does not (relative to the physical screen).

When designing this app, I wanted the palette button on the application bar to be colored with the current paint color as a helpful visual aid. However, it’s not currently possible to emit dynamic images to be used by application bar buttons. Therefore, I decided to update the application bar’s background color with the current paint color as the next best thing. In Figure 39.4, the current paint color is a light, translucent blue.

The User Interface

Listing 39.3 contains the XAML for the main page.

LISTING 39.3 MainPage.xaml—The User Interface for Paint’s Main Page

[code]

<phone:PhoneApplicationPage x:Class=”WindowsPhoneApp.MainPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
SupportedOrientations=”PortraitOrLandscape”>
<!– The application bar, and that’s it! –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar>
<shell:ApplicationBarIconButton Text=”palette”
IconUri=”/Images/appbar.palette.png” Click=”PaletteButton_Click”/>
<shell:ApplicationBarIconButton Text=”undo”
IconUri=”/Shared/Images/appbar.undo.png” Click=”UndoButton_Click”/>
<shell:ApplicationBarIconButton Text=”redo”
IconUri=”/Shared/Images/appbar.redo.png” Click=”RedoButton_Click”/>
<shell:ApplicationBarIconButton Text=”straighten”
IconUri=”/Images/appbar.straighten1.png” Click=”StraightenButton_Click”/>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”set background color”
Click=”SetBackgroundColorMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”set background picture”
Click=”SetBackgroundPictureMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”erase all strokes”
Click=”EraseMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”save to pictures library”
Click=”SaveToPicturesLibraryMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”instructions”
Click=”InstructionsMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”about”
Click=”AboutMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
</phone:PhoneApplicationPage>

[/code]

This XAML file has the distinction of being the only one in this book where the page has no content! It only sets the values of its SupportedOrientations and ApplicationBar properties. That’s because the content shown on main page is created from code-behind and placed in a frame-rooted popup. This is what enables the behavior demonstrated 39.4, in which the application bar rotates but the content does not.

The Code-Behind

Listing 39.4 contains the code-behind for the main page.

LISTING 39.4 MainPage.xaml.cs—The Code-Behind for Paint’s Main Page

[code]

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using Microsoft.Phone;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
using Microsoft.Phone.Tasks;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
// Undo and redo stacks
Stack<HistoryEntry> undoStack = new Stack<HistoryEntry>();
Stack<HistoryEntry> redoStack = new Stack<HistoryEntry>();
// The in-progress strokes, tracked separately for each unique finger
Dictionary<int, Stroke> fingerStrokes = new Dictionary<int, Stroke>();
// The popup and its contents
Popup popup = new Popup { IsOpen = true };
Grid grid = new Grid { Width = 480, Height = 800 };
InkPresenter inkPresenter = new InkPresenter();
Image backgroundImage = new Image {
Stretch = Stretch.Uniform, RenderTransformOrigin = new Point(.5, .5),
RenderTransform = new CompositeTransform()
};
// Application bar buttons and a menu item that are changed by code-behind
IApplicationBarIconButton undoButton;
IApplicationBarIconButton redoButton;
IApplicationBarIconButton straightenButton;
IApplicationBarMenuItem backgroundPictureMenuItem;
public MainPage()
{
InitializeComponent();
// Assign the application bar items
this.undoButton = this.ApplicationBar.Buttons[1]
as IApplicationBarIconButton;
this.redoButton = this.ApplicationBar.Buttons[2]
as IApplicationBarIconButton;
this.straightenButton = this.ApplicationBar.Buttons[3]
as IApplicationBarIconButton;
this.backgroundPictureMenuItem = this.ApplicationBar.MenuItems[1]
as IApplicationBarMenuItem;
// Restore the background image, if persisted previously
if (IsolatedStorageHelper.FileExists(“background.jpg”))
SetBackgroundImage(IsolatedStorageHelper.LoadImageFile(“background.jpg”));
// Restore the strokes, if persisted previously.
// These are stored in a file rather than isolated storage settings due to
// a problem with the default serializer.
StrokeCollection strokes =
IsolatedStorageHelper.LoadSerializedObjectFromFile(“strokes.xml”,
typeof(StrokeCollection)) as StrokeCollection;
if (strokes != null)
this.inkPresenter.Strokes = strokes;
// Refresh the app bar based on the presence of a background image & strokes
RefreshAppBarMenu();
RefreshAppBarButtons();
// Attach the UI to the popup, which is already showing (IsOpen=true)
this.grid.Children.Add(this.backgroundImage);
this.grid.Children.Add(this.inkPresenter);
this.popup.Child = this.grid;
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
// Need to hide the popup so the other page can be shown!
this.popup.IsOpen = false;
// Unsubscribe from this application-wide event
Touch.FrameReported -= Touch_FrameReported;
// Persist the current strokes.
// These are stored in a file rather than isolated storage settings due to
// a problem with the default serializer.
IsolatedStorageHelper.SaveFile(“strokes.xml”, this.inkPresenter.Strokes);
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Ensure the popup is shown, as it gets hidden when navigating away
this.popup.IsOpen = true;
// Reapply the background color, in case we just returned
// from the color picker page
this.grid.Background = new SolidColorBrush(Settings.PageColor.Value);
// Apply the current paint color as the app bar background color
Color paintColor = Settings.PaintColor.Value;
// Prevent the background from getting too transparent,
// potentialy making the buttons and menu items unreadable
if (paintColor.A < 60)
paintColor.A = 60;
this.ApplicationBar.BackgroundColor = paintColor;
// Choose a foreground color that will be visible over the background color
if (IsLight(Settings.PaintColor.Value))
this.ApplicationBar.ForegroundColor = Colors.Black;
else
this.ApplicationBar.ForegroundColor = Colors.White;
// Subscribe to the touch/multi-touch event.
// This is application-wide, so only do this when on this page.
Touch.FrameReported += Touch_FrameReported;
}
void Touch_FrameReported(object sender, TouchFrameEventArgs e)
{
// Get all touch points
TouchPointCollection points = e.GetTouchPoints(this.inkPresenter);
// Process each touch point individually
foreach (TouchPoint point in points)
{
// The “touch device” is each finger, and it has a unique ID
int fingerId = point.TouchDevice.Id;
if (point.Action == TouchAction.Down)
{
// Start a new stroke
Stroke stroke = new Stroke();
// Apply all the current settings
stroke.DrawingAttributes.Color = Settings.PaintColor.Value;
stroke.DrawingAttributes.Width = Settings.BrushWidth.Value;
stroke.DrawingAttributes.Height = Settings.BrushHeight.Value;
if (Settings.HasOutline.Value)
stroke.DrawingAttributes.OutlineColor = Settings.OutlineColor.Value;
else
stroke.DrawingAttributes.OutlineColor = Colors.Transparent;
// The first point of this stroke is the current finger position
stroke.StylusPoints.Add(
new StylusPoint(point.Position.X, point.Position.Y));
// Track which finger this stroke belongs to
this.fingerStrokes[fingerId] = stroke;
// Add it to the ink presenter’s collection of strokes
this.inkPresenter.Strokes.Add(stroke);
}
else if (point.Action == TouchAction.Move)
{
// Keep adding new points to the stroke
if (this.fingerStrokes.ContainsKey(fingerId))
this.fingerStrokes[fingerId].StylusPoints.Add(
new StylusPoint(point.Position.X, point.Position.Y));
}
else // TouchAction.Up
{
// The stroke is finished
if (this.fingerStrokes.ContainsKey(fingerId))
{
// Enable this action to be undone
this.undoStack.Push(
new HistoryEntry { StrokeAdded = this.fingerStrokes[fingerId] });
this.redoStack.Clear();
// Stop tracking this stroke
this.fingerStrokes.Remove(fingerId);
// Refresh the state of the undo/redo/straighten buttons
RefreshAppBarButtons();
}
}
}
}
bool IsLight(Color color)
{
return ((color.R + color.G + color.B) / 3 > 127.5);
}
void SetBackgroundImage(ImageSource source)
{
this.backgroundImage.Source = source;
// The ImageOpened event doesn’t get raised after this, but the values for
// ActualWidth and ActualHeight aren’t correct yet. The BeginInvoke enables
// us to retrieve the values.
this.Dispatcher.BeginInvoke(delegate()
{
// Rotate the image based on whether it’s landscape or portrait
if (this.backgroundImage.ActualWidth > this.backgroundImage.ActualHeight)
{
this.backgroundImage.Width = 800;
this.backgroundImage.Margin = new Thickness((480 – 800) / 2, 0, 0, 0);
(this.backgroundImage.RenderTransform as CompositeTransform).Rotation =
90;
}
else
{
this.backgroundImage.Width = 480;
this.backgroundImage.Margin = new Thickness(0, 0, 0, 0);
(this.backgroundImage.RenderTransform as CompositeTransform).Rotation =
0;
}
});
}
// Update the state of the application bar menu
void RefreshAppBarMenu()
{
if (IsolatedStorageHelper.FileExists(“background.jpg”))
this.backgroundPictureMenuItem.Text = “remove background picture”;
else
this.backgroundPictureMenuItem.Text = “set background picture”;
}
// Update the state of the application bar buttons
void RefreshAppBarButtons()
{
this.undoButton.IsEnabled = (this.undoStack.Count > 0);
this.redoButton.IsEnabled = (this.redoStack.Count > 0);
this.straightenButton.IsEnabled = (this.inkPresenter.Strokes.Count > 0);
// Customize the straighten button icon based on the last stroke’s shape
if (this.inkPresenter.Strokes.Count > 0)
{
Stroke lastStroke =
this.inkPresenter.Strokes[this.inkPresenter.Strokes.Count – 1];
if (lastStroke.StylusPoints.Count > 2)
this.straightenButton.IconUri =
new Uri(“/Images/appbar.straighten1.png”, UriKind.Relative);
else
this.straightenButton.IconUri =
new Uri(“/Images/appbar.straighten2.png”, UriKind.Relative);
}
}
// Application bar button handlers
void PaletteButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(
new Uri(“/PalettePage.xaml”, UriKind.Relative));
}
void UndoButton_Click(object sender, EventArgs e)
{
if (this.undoStack.Count == 0)
return;
// Get the previous action
HistoryEntry entry = this.undoStack.Pop();
// If a stroke was added, remove it
if (entry.StrokeAdded != null)
this.inkPresenter.Strokes.Remove(entry.StrokeAdded);
// If strokes were removed, add them back
if (entry.StrokesRemoved != null)
foreach (Stroke s in entry.StrokesRemoved)
this.inkPresenter.Strokes.Add(s);
// Enable the undo to be undone
this.redoStack.Push(entry);
// Update the state of the undo/redo/straighten buttons
RefreshAppBarButtons();
}
void RedoButton_Click(object sender, EventArgs e)
{
if (this.redoStack.Count == 0)
return;
// Get the action that was just undone
HistoryEntry entry = this.redoStack.Pop();
// If a stroke was added, add it back
if (entry.StrokeAdded != null)
this.inkPresenter.Strokes.Add(entry.StrokeAdded);
// If strokes were removed, remove them again
if (entry.StrokesRemoved != null)
foreach (Stroke s in entry.StrokesRemoved)
this.inkPresenter.Strokes.Remove(s);
// Enable this action to be undone
this.undoStack.Push(entry);
// Update the state of the undo/redo/straighten buttons
RefreshAppBarButtons();
}
void StraightenButton_Click(object sender, EventArgs e)
{
if (this.inkPresenter.Strokes.Count == 0)
return;
bool straightened = false;
Stroke lastStroke =
this.inkPresenter.Strokes[this.inkPresenter.Strokes.Count – 1];
// Clone the stroke before changing it, simply so the original stroke
// can be placed in the undo stack.
// The DrawingAttributes instance is shared by both, but we don’t change it.
Stroke newStroke = new Stroke { DrawingAttributes =
lastStroke.DrawingAttributes };
foreach (StylusPoint point in lastStroke.StylusPoints)
newStroke.StylusPoints.Add(point);
if (newStroke.StylusPoints.Count > 2)
{
// This is a raw stroke, so do the first round of straightening simply
// by removing every point except its two endpoints
while (newStroke.StylusPoints.Count > 2)
newStroke.StylusPoints.RemoveAt(1);
straightened = true;
}
else if (newStroke.StylusPoints.Count == 2)
{
// This is already a straight line, so make it completely horizontal or
// completely vertical depending on which is closer
double deltaX = newStroke.StylusPoints[0].X – newStroke.StylusPoints[1].X;
double deltaY = newStroke.StylusPoints[0].Y – newStroke.StylusPoints[1].Y;
if (Math.Abs(deltaX) > Math.Abs(deltaY))
{
// The line is more horizontal than vertical
if (newStroke.StylusPoints[0].Y != newStroke.StylusPoints[1].Y)
{
// Give the horizontal line the average Y value
double newY = (newStroke.StylusPoints[0].Y +
newStroke.StylusPoints[1].Y) / 2;
newStroke.StylusPoints[0] =
new StylusPoint(newStroke.StylusPoints[0].X, newY);
newStroke.StylusPoints[1] =
new StylusPoint(newStroke.StylusPoints[1].X, newY);
straightened = true;
}
}
else
{
// The line is more vertical than horizontal
if (newStroke.StylusPoints[0].X != newStroke.StylusPoints[1].X)
{
// Give the vertical line the average X value
double newX = (newStroke.StylusPoints[0].X +
newStroke.StylusPoints[1].X) / 2;
newStroke.StylusPoints[0] =
new StylusPoint(newX, newStroke.StylusPoints[0].Y);
newStroke.StylusPoints[1] =
new StylusPoint(newX, newStroke.StylusPoints[1].Y);
straightened = true;
}
}
}
if (straightened)
{
// Remove the old stroke and swap in the cloned and modified stroke
this.inkPresenter.Strokes.Remove(lastStroke);
this.inkPresenter.Strokes.Add(newStroke);
// Update the undo/redo stacks
HistoryEntry entry = new HistoryEntry { StrokeAdded = newStroke };
entry.StrokesRemoved = new Stroke[] { lastStroke };
this.undoStack.Push(entry);
this.redoStack.Clear();
// Update the state of the undo/redo/straighten buttons
RefreshAppBarButtons();
}
}
// Application bar menu handlers
void SetBackgroundColorMenuItem_Click(object sender, EventArgs e)
{
// Get a string representation of the colors, without the leading #
string currentColorString = Settings.PageColor.Value.ToString().Substring(1);
string defaultColorString =
Settings.PageColor.DefaultValue.ToString().Substring(1);
// The color picker works with the same isolated storage value that the
// Setting works with, but we have to clear its cached value to pick up
// the value chosen in the color picker
Settings.PageColor.ForceRefresh();
// Navigate to the color picker
this.NavigationService.Navigate(new Uri(
“/Shared/Color Picker/ColorPickerPage.xaml?”
+ “showOpacity=false”
+ “&currentColor=” + currentColorString
+ “&defaultColor=” + defaultColorString
+ “&settingName=PageColor”, UriKind.Relative));
}
void SetBackgroundPictureMenuItem_Click(object sender, EventArgs e)
{
if (IsolatedStorageHelper.FileExists(“background.jpg”))
{
// “remove background picture” was tapped
IsolatedStorageHelper.DeleteFile(“background.jpg”);
this.backgroundImage.Source = null;
RefreshAppBarMenu();
return;
}
// “set background picture” was tapped
PhotoChooserTask task = new PhotoChooserTask();
task.ShowCamera = true;
task.Completed += delegate(object s, PhotoResult args)
{
if (args.TaskResult == TaskResult.OK)
{
// Apply the image to the background
SetBackgroundImage(PictureDecoder.DecodeJpeg(args.ChosenPhoto));
// Seek back to the beginning of the stream again
args.ChosenPhoto.Seek(0, SeekOrigin.Begin);
// Save the file to isolated storage.
// This overwrites the file if it already exists.
IsolatedStorageHelper.SaveFile(“background.jpg”, args.ChosenPhoto);
RefreshAppBarMenu();
}
};
task.Show();
}
void EraseMenuItem_Click(object sender, EventArgs e)
{
// Allow this to be undone by storing all the current strokes
HistoryEntry entry = new HistoryEntry();
entry.StrokesRemoved = this.inkPresenter.Strokes.ToArray();
this.undoStack.Push(entry);
this.redoStack.Clear();
// Erase them all
this.inkPresenter.Strokes.Clear();
// Update the state of the undo/redo buttons
RefreshAppBarButtons();
}
void SaveToPicturesLibraryMenuItem_Click(object sender, EventArgs e)
{
// Create a new bitmap with the page’s dimensions
WriteableBitmap bitmap = new WriteableBitmap((int)this.grid.ActualWidth,
(int)this.grid.ActualHeight);
// Render the contents to the bitmap
bitmap.Render(grid, null);
// We must explicitly tell the bitmap to draw its new contents
bitmap.Invalidate();
using (MemoryStream stream = new MemoryStream())
{
// Fill the stream with a JPEG representation of this bitmap
bitmap.SaveJpeg(stream, (int)this.grid.ActualWidth,
(int)this.grid.ActualHeight,
0 /* orientation */, 100 /* quality */);
// Seek back to the beginning of the stream
stream.Seek(0, SeekOrigin.Begin);
// Save the image
try
{
new Microsoft.Xna.Framework.Media.MediaLibrary().SavePicture(
“paint.jpg”, stream);
}
catch
{
MessageBox.Show(“To do this, please disconnect your phone from Zune.”,
“Please Disconnect”, MessageBoxButton.OK);
return;
}
}
MessageBox.Show(
“Your artwork has been saved. Go to your phone’s Pictures hub to view it.”,
“Success”, MessageBoxButton.OK);
}
void InstructionsMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/InstructionsPage.xaml”,
UriKind.Relative));
}
void AboutMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(
new Uri(“/Shared/About/AboutPage.xaml?appName=Paint”, UriKind.Relative));
}
}
}

[/code]

  • The undo/redo feature is implemented as two stacks of the following simple data type:
    [code]
    public struct HistoryEntry
    {
    public Stroke StrokeAdded { get; set; }
    public IList<Stroke> StrokesRemoved { get; set; }
    }
    [/code]
    The undo feature supports not just the removal of newly added strokes, but undoingthe straightening of a stroke and undoing the erasure of all strokes simultaneously. If it weren’t for these two extra undo cases, this app wouldn’t need an undo stack at all—it could just treat the ink presenter’s Strokes collection as the stack and remove each stroke from the end of the list.
  • The fingerStrokes dictionary is used just like the fingerSounds dictionary from the preceding chapter, tracking each in-progress stroke while associating it with the correct finger. The hack to work around missing Up actions is not done here, however, because the only bad effect caused by this is extra entries left behind in the dictionary.
  • Although ink presenters can contain arbitrary elements with canvas-style layout, this page uses an image behind the ink presenter—placing both in a one-cell grid—to take advantage of grid’s automatic layout.
  • Because the application bar’s background is set to whatever the user has chosen as the paint color (inside OnNavigatedTo), we must ensure that the buttons and text are visible on top of this color no matter what. A simple IsLight method is used, defined toward the end of the listing, to make the foreground white if the background is dark or to make the foreground black if the background is light. The code in OnNavigatedTo also prevents the application bar background from becoming too transparent, as that could cause the buttons and text to become unreadable based on whatever the artwork happened to contain underneath.
  • Touch_FrameReported contains the code at the heart of this app. When a finger touches down, a new stroke is created and given drawing attributes that match all of the current settings. A stylus point is then added to its StylusPoints collection that matches the finger’s current location, and then it is added to the ink presenter’s Strokes collection. When a finger moves, the correct stroke is retrieved based on the finger ID, and then a new stylus point is added to it based on the current location. When a finger breaks contact with the screen, no further changes need to be made to the stroke, but the undo/redo stacks are adjusted appropriately and the application bar is refreshed.
  • The stroke-straightening feature works in two phases. The first time it is tapped, all stylus points on the most recent stroke are removed except for the starting and ending points. This causes it to form a straight but likely diagonal line. The second time it is tapped, the location of both points is adjusted to make the line horizontal or vertical, whichever is a closer match. The stroke is cloned and the copy is modified, but that’s only done so that the original stroke can be placed in the undo stack. If straightening did not need to be undone, the changes to the stroke’s points could be done directly to the instance already in the ink presenter.
  • The straightening process is demonstrated in Figure 39.5 for two different strokes. Notice that the straighten button’s icon changes to indicate which phase the most recent stroke is currently in.
The sequence of straightening two strokes.
FIGURE 39.5 The sequence of straightening two strokes.
  • XNA’s MediaLibrary.SavePicture method is called to save the artwork to the pictures library. Similar to the Local FM Radio and Subservient Cat apps, it checks for a failure case caused when the phone is connected to Zune on a PC. There’s one more failure case caused by the Zune connection: Calling Show on PhotoChooserTask causes the Completed event to be raised with the event-args TaskResult property set to TaskResult.Cancel. Because this isn’t easily distinguishable from the user cancelling the task, this case is left alone. It’s more likely to cause confusion for developers than users.

Manual Serialization and Deserialization

Although an ink presenter’s Strokes collection is serializable, attempting to assign such a collection to an isolated storage application setting (or a page state item) does not work. The automatic serialization process throws an exception. Therefore, rather than using a Setting object to persist and retrieve the ink presenter’s strokes, Listing 39.4 uses two methods in the project’s IsolatedStorageHelper class implemented as follows:

[code]

public static void SaveFile(string filename, object serializableObject)
{
using (IsolatedStorageFile userStore =
IsolatedStorageFile.GetUserStoreForApplication())
using (IsolatedStorageFileStream stream = userStore.CreateFile(filename))
using (StreamWriter writer = new StreamWriter(stream))
{
// Serialize the object to XML and write it to the file
XmlSerializer serializer = new XmlSerializer(serializableObject.GetType());
serializer.Serialize(writer, serializableObject);
}
}
public static object LoadSerializedObjectFromFile(string filename, Type type)
{
using (IsolatedStorageFile userStore =
IsolatedStorageFile.GetUserStoreForApplication())
{
if (userStore.FileExists(filename))
{
using (IsolatedStorageFileStream stream =
userStore.OpenFile(filename, FileMode.Open))
using (StreamReader reader = new StreamReader(stream))
{
// Deserialize the object from the XML in the file
XmlSerializer serializer = new XmlSerializer(type);
return serializer.Deserialize(reader);
}
}
}
return null;
}

[/code]

Manual serialization and deserialization is done with System.Runtime.Serialization.XmlSerializer from the System.Xml.Serialization assembly. The serialized XML it produces looks like the following for a one-point, onestroke collection:

[code]

<?xml version=”1.0” encoding=”utf-16”?>
<ArrayOfStroke xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance”
xmlns:xsd=”http://www.w3.org/2001/XMLSchema”>
<Stroke>
<StylusPoints>
<StylusPoint>
<X>100</X>
<Y>0</Y>
<PressureFactor>0.5</PressureFactor>
</StylusPoint>
</StylusPoints>
<DrawingAttributes>
<Color>
<A>255</A>
<R>27</R>
<G>161</G>
<B>226</B>
</Color>
<OutlineColor>
<A>0</A>
<R>255</R>
<G>255</G>
<B>255</B>
</OutlineColor>
<Width>10</Width>
<Height>10</Height>
</DrawingAttributes>
</Stroke>
</ArrayOfStroke>

[/code]

XmlSerializer isn’t the only available option, however. Silverlight for Windows Phone also ships with System.Runtime.Serialization.DataContractSerializer (in the System.Runtime.Serialization assembly) and System.Runtime.Serialization .Json.DataContractJsonSerializer (in the System.Servicemodel.Web assembly).

DataContractSerializer serializes objects to XML, but in a different way than XmlSerializer. It also happens to support the serialization of a broader set of types and properties. The serialized XML it produces looks like the following for the same stroke collection:

[code]

<ArrayOfStroke xmlns:i=”http://www.w3.org/2001/XMLSchema-instance” xmlns=”http://s
chemas.datacontract.org/2004/07/System.Windows.Ink”><Stroke><DrawingAttributes><Co
lor xmlns:d4p1=”http://schemas.datacontract.org/2004/07/System.Windows.Media”><d4p
1:A>255</d4p1:A><d4p1:B>226</d4p1:B><d4p1:G>161</d4p1:G><d4p1:R>27</d4p1:R></Color
><Height>10</Height><OutlineColor xmlns:d4p1=”http://schemas.datacontract.org/2004
/07/System.Windows.Media”><d4p1:A>0</d4p1:A><d4p1:B>255</d4p1:B><d4p1:G>255</d4p1:
G><d4p1:R>255</d4p1:R></OutlineColor><Width>10</Width></DrawingAttributes><StylusP
oints xmlns:d3p1=”http://schemas.datacontract.org/2004/07/System.Windows.Input”><d
3p1:StylusPoint><d3p1:PressureFactor>0.5</d3p1:PressureFactor><d3p1:X>100</d3p1:X>
<d3p1:Y>0</d3p1:Y></d3p1:StylusPoint></StylusPoints></Stroke></ArrayOfStroke>

[/code]

Rather than pretty-printing the XML, which is not needed in this case, it produces one big line.

DataContractJsonSerializer serializes objects to JavaScript Object Notation (JSON), the popular format that is usually much more compact than XML. Here is the serialized JSON for the same stroke collection, which again is produced as one big line:

[code]

[{“DrawingAttributes”:{“Color”:{“A”:255,”B”:226,”G”:161,”R”:27},”Height”:10,”Outli neColor”:{“A”:0,”B”:255,”G”:255,”R”:255},”Width”:10},”StylusPoints”:[{“PressureFac tor”:0.5,”X”:100,”Y”:0}]}]

[/code]

The Finished Product

Paint (Ink Presenter)