Blograby

Paint (Ink Presenter)

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

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

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.

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]

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.

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]

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.

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]

FIGURE 39.5 The sequence of straightening two strokes.

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

 

 

 

Exit mobile version