Moo Can (Turn Over)

Do you remember those cans that moo when you turn them upside down? The Moo Can app brings this classic children’s toy back to life in digital form! Moo Can makes a moo sound when you turn your phone upside down. Gravity also affects the cow on the screen, which rotates and falls toward whatever edge of the screen is currently on the bottom.

Just like the real cans, you can shake the phone to make a harsh sound that is different from the desired “moo” caused by gently turning the phone upside down. You can also change the cow to a sheep or cat, each of which makes its own unique sounds.

The Main Page

Moo Can has a main page, a calibration page, and an instructions page (not shown in this chapter). Listing 47.1 contains the XAML for the main page, and Listing 47.2 contains its code-behind.

LISTING 47.1 MainPage.xaml—The User Interface for Moo Can’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=”Portrait”>
<!– The application bar, with five menu items –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar BackgroundColor=”#9CB366” ForegroundColor=”White”>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”cow” Click=”AnimalMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”sheep” Click=”AnimalMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”cat” Click=”AnimalMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”instructions”
Click=”InstructionsMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”calibrate”
Click=”CalibrateMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<Canvas>
<Canvas.Background>
<ImageBrush ImageSource=”Images/background.png”/>
</Canvas.Background>
<!– The cow, sheep, or cat –>
<Image x:Name=”AnimalImage” RenderTransformOrigin=”.5,.5”
Canvas.Left=”32” Canvas.Top=”50” Width=”434” Height=”507”>
<Image.RenderTransform>
<CompositeTransform x:Name=”AnimalTransform”/>
</Image.RenderTransform>
</Image>
</Canvas>
</phone:PhoneApplicationPage>

[/code]

The user interface contains an animal image (whose source is set in code-behind) placed over the background image. It also uses an application bar menu, shown in Figure 47.1, for switching the animal or navigating to either of the other two pages. The application bar is given hard-coded colors, so it blends in with the grass in the background image when the menu is closed.

The application bar menu on the main page.
FIGURE 47.1 The application bar menu on the main page.

LISTING 47.2 MainPage.xaml.cs—The Code-Behind for Moo Can’s Main Page

[code]

using System;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using Microsoft.Phone.Applications.Common; // For AccelerometerHelper
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
Setting<string> chosenAnimal = new Setting<string>(“ChosenAnimal”, “cow”);
bool upsideDown = false;
// The start, middle, end, and length of the
// vertical path that the animal moves along
const double MAX_Y = 170;
const double MIN_Y = -75;
const double MID_Y = 47.5;
const double LENGTH_Y = 245;
// The start, middle and end of the clockwise rotation of the animal
const double MAX_ANGLE_CW = 180;
const double MIN_ANGLE_CW = 0;
const double MID_ANGLE_CW = 90;
// The start, middle and end of the counter-clockwise rotation of the animal
const double MIN_ANGLE_CCW = -180;
const double MAX_ANGLE_CCW = 0;
const double MID_ANGLE_CCW = -90;
// The length of the rotation, regardless of which direction
const double LENGTH_ANGLE = 180;
public MainPage()
{
InitializeComponent();
// Use the accelerometer via Microsoft’s helper
AccelerometerHelper.Instance.ReadingChanged += Accelerometer_ReadingChanged;
SoundEffects.Initialize();
// Allow the app to run (producing sounds) even when the phone is locked.
// Once disabled, you cannot re-enable the default behavior!
PhoneApplicationService.Current.ApplicationIdleDetectionMode =
IdleDetectionMode.Disabled;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Restore the chosen animal
ChangeAnimal();
// Start the accelerometer with Microsoft’s helper
AccelerometerHelper.Instance.Active = true;
// While on this page, don’t allow the screen to auto-lock
PhoneApplicationService.Current.UserIdleDetectionMode =
IdleDetectionMode.Disabled;
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Restore the ability for the screen to auto-lock when on other pages
PhoneApplicationService.Current.UserIdleDetectionMode =
IdleDetectionMode.Enabled;
}
void ChangeAnimal()
{
switch (this.chosenAnimal.Value)
{
case “cow”:
this.AnimalImage.Source = new BitmapImage(
new Uri(“Images/cow.png”, UriKind.Relative));
break;
case “sheep”:
this.AnimalImage.Source = new BitmapImage(
new Uri(“Images/sheep.png”, UriKind.Relative));
break;
case “cat”:
this.AnimalImage.Source = new BitmapImage(
new Uri(“Images/cat.png”, UriKind.Relative));
break;
}
}
// Process data coming from the accelerometer
void Accelerometer_ReadingChanged(object sender,
AccelerometerHelperReadingEventArgs e)
{
// Transition to the UI thread
this.Dispatcher.BeginInvoke(delegate()
{
// Move the animal vertically based on the vertical force
this.AnimalTransform.TranslateY =
Clamp(MID_Y – LENGTH_Y * e.AverageAcceleration.Y, MIN_Y, MAX_Y);
// Clear the upside-down flag, only when completely upright
if (this.AnimalTransform.TranslateY == MAX_Y)
this.upsideDown = false;
// Rotate the animal to always be upright
if (e.AverageAcceleration.X <= 0)
this.AnimalTransform.Rotation = Clamp(MID_ANGLE_CW +
LENGTH_ANGLE * e.AverageAcceleration.Y, MIN_ANGLE_CW, MAX_ANGLE_CW);
else
this.AnimalTransform.Rotation = Clamp(MID_ANGLE_CCW –
LENGTH_ANGLE * e.AverageAcceleration.Y, MIN_ANGLE_CCW, MAX_ANGLE_CCW);
// Play the appropriate shake sound when shaken
if (ShakeDetection.JustShook(e.OriginalEventArgs))
{
switch (this.chosenAnimal.Value)
{
case “cow”: SoundEffects.MooShake.Play(); break;
case “sheep”: SoundEffects.BaaShake.Play(); break;
case “cat”: SoundEffects.MeowShake.Play(); break;
}
}
// Play the normal sound when first turned upside-down
if (!this.upsideDown && this.AnimalTransform.TranslateY == MIN_Y)
{
this.upsideDown = true;
switch (this.chosenAnimal.Value)
{
case “cow”: SoundEffects.Moo.Play(); break;
case “sheep”: SoundEffects.Baa.Play(); break;
case “cat”: SoundEffects.Meow.Play(); break;
}
}
});
}
// “Clamp” the incoming value so it’s no lower than min & no larger than max
static double Clamp(double value, double min, double max)
{
return Math.Max(min, Math.Min(max, value));
}
// Application bar handlers
void AnimalMenuItem_Click(object sender, EventArgs e)
{
this.chosenAnimal.Value = (sender as IApplicationBarMenuItem).Text;
ChangeAnimal();
}
void InstructionsMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/InstructionsPage.xaml”,
UriKind.Relative));
}
void CalibrateMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(
“/Shared/Calibrate/CalibratePage.xaml?appName=Moo Can”, UriKind.Relative));
}
}
}

[/code]

  • This app moves and rotates the animal based on the accelerometer’s data, as shown in Figure 47.2. However, the raw data has a lot of noise, so using it directly would produce a jerky result. (This noise didn’t matter for the previous three apps because the punch, throw, and shake detection are coarse.) Therefore, this listing makes use of an AccelerometerHelper class published by Microsoft that performs data smoothing for you. It also simplifies the starting/stopping interaction with the accelerometer.
The chosen animal rotates and falls as you turn the phone upside down.
FIGURE 47.2 The chosen animal rotates and falls as you turn the phone upside down.
  • AccelerometerHelper exposes its functionality via a static Instance property, so the constructor uses this to attach a handler to its ReadingChanged event. This event is just like the ReadingChanged event from the preceding chapters, but with richer data passed to handlers.
  • This project uses a SoundEffects class just like the one from previous apps but with six properties for six possible sounds: Moo, MooShake, Baa, BaaShake, Meow, and MeowShake.
  • This app runs while locked and this page prevents the screen from auto-locking. This way, you can make the noises without keeping the phone screen on. At the same time, you can continue to watch the cow fall up and down without having to periodically tap the screen to keep it on.
  • To start the accelerometer, OnNavigatedTo simply sets AccelerometerHelper’s Instance’s Active property to true. Internally, this calls Start (if not already started) with the same sort of exception handling done in preceding chapters. This app doesn’t bother stopping the accelerometer, but this could be done by setting Active to false inside OnNavigatedFrom.
  • Inside Accelerometer_ReadingChanged, the AccelerometerHelper-specific AccelerometerHelperReadingEventArgs instance is leveraged to get the average acceleration in the X and Y directions. This class is different from the AccelerometerReadingEventArgs used by the preceding apps. It exposes several properties for getting the data with various types of smoothing applied.

AccelerometerHelperReadingEventArgs exposes four properties that enable you to choose how raw or smooth you want your data to be:

  • RawAcceleration—The same noisy data you would get from the Accelerometer class’s ReadingChanged event (but with calibration potentially applied, as described later in this chapter).
  • LowPassFilteredAcceleration—Applies a first-order low-pass filter over the raw data. The result is smoother data with a little bit of latency.
  • OptimalyFilteredAcceleration [sic]—This uses the same low-pass filter, but only when the current value is close enough to a rolling average value. If the value is sufficiently greater, the raw value is reported instead. (This algorithm is done independently for all three axes.) This gives a nice balance between having smooth results and low latency. Small changes are handled smoothly and large changes are reported quickly.
  • AverageAcceleration—Reports the mean of the most recent 25 data points collected from the “optimally filtered” algorithm.This gives the smoothest result of any of the choices, but it also has the highest latency.

Each property exposes X, Y, Z, and Magnitude properties (but unlike AccelerometerReadingEventArgs, no Timestamp property). Magnitude reports the length of the 3D vector formed by the other three values, which is the square root of X2 + Y2 + Z2. For more details about the algorithms behind these properties, see http://bit.ly/accelerometerhelper.

  • This app uses the same ShakeDetection class shown in the preceding chapter. Because the JustShook method is defined to accept an instance of AccelerometerReadingEventArgs—not AccelerometerHelperReadingEventArgs—this listing uses an OriginalEventArgs property to retrieve it. This property actually isn’t exposed by the AccelerometerHelperReadingEventArgs class; I added it directly to my copy of the source code.
  • A turn upside down is detected by noticing the first moment that the Y acceleration value is large enough after a point in time when it has been small enough. In other words, after the upside-down orientation has been detected, the user must turn the phone right side up to clear the value of upsideDown before another upside-down turn will be detected. Because the animal transform’s TranslateY value is proportional to the Y-axis acceleration (and restricted to a range of MIN_Y to MAX_Y), the phone is completely upside down when TranslateY is MIN_Y and the phone is completely upright when TranslateY is MAX_Y.
  • This app purposely does not make any noise when turned right side up, because a real can does not either. (I didn’t realize this until I bought a few of the cans and tried for myself.)

The Calibration Page

Moo Can enables users to calibrate the accelerometer in case its notion of right side up or upside down are slightly askew from reality. The process of calibration simply involves asking the user when they believe the phone is level, remembering the accelerometer’s reading at that moment, and then using those values as an offset to the accelerometer data from that point onward.

One benefit of using AccelerometerHelper rather than the raw accelerometer APIs is that calibration functionality is built in. It collects the data, stores it as isolated storage application settings (named “AccelerometerCalibrationX” and “AccelerometerCalibrationY”), and automatically offsets the data it returns—even the supposedly “raw” data.

The calibration page, designed to be shared among multiple apps, shows you how to take advantage of it. Listing 47.3 contains the XAML and Listing 47.4 contains the codebehind. It produces the page shown in Figure 47.3.

The calibration page enables calibrating just the X dimension, just the Y dimension, or both, but only when the phone is held fairly still and level.
FIGURE 47.3 The calibration page enables calibrating just the X dimension, just the Y dimension, or both, but only when the phone is held fairly still and level.

LISTING 47.3 CalibratePage.xaml—The User Interface for The Accelerometer Calibration Page

[code]

<phone:PhoneApplicationPage x:Class=”WindowsPhoneApp.CalibratePage”
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:local=”clr-namespace:WindowsPhoneApp”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”PortraitOrLandscape”>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”*”/>
</Grid.RowDefinitions>
<!– The standard header –>
<StackPanel Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock x:Name=”ApplicationName”
Style=”{StaticResource PhoneTextTitle0Style}”/>
<TextBlock Text=”calibrate” Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<ScrollViewer Grid.Row=”1”>
<StackPanel>
<TextBlock Margin=”24” TextWrapping=”Wrap” Text=”Tap the button …”/>
<Button x:Name=”CalibrateButton” Content=”calibrate” IsEnabled=”False”
Height=”150” Click=”CalibrateButton_Click”
local:Tilt.IsEnabled=”True”/>
<TextBlock x:Name=”WarningText” Visibility=”Collapsed” Margin=”24,0”
TextWrapping=”Wrap” FontWeight=”Bold”
Text=”Your phone is not still or level enough!”/>
</StackPanel>
</ScrollViewer>
</Grid>
</phone:PhoneApplicationPage>

[/code]

LISTING 47.4 CalibratePage.xaml.cs—The Code-Behind for the Accelerometer Calibration Page

[code]

using System.Windows;
using System.Windows.Navigation;
using Microsoft.Phone.Applications.Common; // For AccelerometerHelper
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class CalibratePage : PhoneApplicationPage
{
bool calibrateX = true, calibrateY = true;
public CalibratePage()
{
InitializeComponent();
// Use the accelerometer via Microsoft’s helper
AccelerometerHelper.Instance.ReadingChanged += Accelerometer_ReadingChanged;
// Ensure it is active
AccelerometerHelper.Instance.Active = true;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Set the application name in the header
if (this.NavigationContext.QueryString.ContainsKey(“appName”))
{
this.ApplicationName.Text =
this.NavigationContext.QueryString[“appName”].ToUpperInvariant();
}
// Check for calibration parameters
if (this.NavigationContext.QueryString.ContainsKey(“calibrateX”))
{
this.calibrateX =
bool.Parse(this.NavigationContext.QueryString[“calibrateX”]);
}
if (this.NavigationContext.QueryString.ContainsKey(“calibrateY”))
{
this.calibrateY =
bool.Parse(this.NavigationContext.QueryString[“calibrateY”]);
}
}
// Process data coming from the accelerometer
void Accelerometer_ReadingChanged(object sender,
AccelerometerHelperReadingEventArgs e)
{
this.Dispatcher.BeginInvoke(delegate()
{
bool canCalibrateX = this.calibrateX &&
AccelerometerHelper.Instance.CanCalibrate(this.calibrateX, false);
bool canCalibrateY = this.calibrateY &&
AccelerometerHelper.Instance.CanCalibrate(false, this.calibrateY);
// Update the enabled state and text of the calibration button
this.CalibrateButton.IsEnabled = canCalibrateX || canCalibrateY;
if (canCalibrateX && canCalibrateY)
this.CalibrateButton.Content = “calibrate (flat)”;
else if (canCalibrateX)
this.CalibrateButton.Content = “calibrate (portrait)”;
else if (canCalibrateY)
this.CalibrateButton.Content = “calibrate (landscape)”;
else
this.CalibrateButton.Content = “calibrate”;
this.WarningText.Visibility = this.CalibrateButton.IsEnabled ?
Visibility.Collapsed : Visibility.Visible;
});
}
void CalibrateButton_Click(object sender, RoutedEventArgs e)
{
if (AccelerometerHelper.Instance.Calibrate(this.calibrateX,
this.calibrateY) ||
AccelerometerHelper.Instance.Calibrate(this.calibrateX, false) ||
AccelerometerHelper.Instance.Calibrate(false, this.calibrateY))
{
// Consider it a success if we were able to
// calibrate in either direction (or both)
if (this.NavigationService.CanGoBack)
this.NavigationService.GoBack();
}
else
{
MessageBox.Show(“Unable to calibrate. Make sure you’re holding your “ +
“phone still, even when tapping the button!”, “Calibration Error”,
MessageBoxButton.OK);
}
}
}
}

[/code]

  • This page enables calibration in just one dimension—or both—based on the query parameters passed when navigating to it. Listing 47.2 simply passes “appName=Moo Can” as the query string, so this app will enable any kind of calibration. Most likely, the user will be holding the phone in the portrait orientation when attempting to calibrate.
  • The calibration method— AccelerometerHelper.Instance.Calibrate—only succeeds if the phone is sufficiently still and at least somewhat-level in the relevant dimension(s). The AccelerometerHelper.Instance.CanCalibrate method tells you whether calibration will succeed, although the answer can certainly change between the time you call CanCalibrate and the time you call Calibrate, so you should always be prepared for Calibrate to fail.
  • The ReadingChanged event handler continually checks CanCalibrate so it can enable/disable the calibration button appropriately. CanCalibrate has two Boolean parameters that enable you to specify whether you care about just the X dimension, just the Y dimension, or both. (Calibrating the Z axis is not meaningful.) Listing 47.4 checks each dimension individually (if the app cares about the dimension) so it can display a helpful message to the user. The only way you can calibrate both X and Y dimensions simultaneously is by placing the phone flat on a surface parallel to the ground.
  • CalibrateButton_Click tries to calibrate whatever will succeed. Calibrate has the same two parameters as CanCalibrate, so this listing first attempts to calibrate both dimensions, but if that fails (by returning false), it tries to calibrate each dimension individually.

Moo Can (Turn Over)

Boxing Glove (Accelerometer Basics)

Boxing Glove is an app for people who feel like being immature (or people who simply are immature). With it, you can throw punches into the air and hear a variety of punching/groaning sound effects. The punching sound effects occur right when you hit your imaginary target.

Boxing Glove supports right- or left-handed punching, and has a few entertaining features, such as a button that makes a “ding ding ding” bell sound as if to signify the start of a fight.

To provide the effect of making a punching sound at the appropriate time, this app uses the phone’s accelerometer.

The Accelerometer

Several times a second, the phone’s accelerometer reports the direction and magnitude of the total force being applied to the phone. This force is expressed with three values—X, Y, and Z—where X is horizontal, Y is vertical, and Z is perpendicular to the screen. This is illustrated in Figure 44.1.

The three accelerometer dimensions, relative to the phone screen.
FIGURE 44.1 The three accelerometer dimensions, relative to the phone screen.

The magnitude of each value is a multiplier of g (the gravitational force on the surface of Earth). Each value is restricted to a range from -2 to 2. If the phone is resting flat on a table with the screen up, the values reported for X and Y are roughly zero, and the value of Z is roughly -1 (1 g into the screen toward the ground). That’s because the only force being applied to the phone in this situation is gravity. By shifting the phone’s angle and orientation and then keeping it roughly still, the values of X, Y, and Z reveal which way is down in the real world thanks to the ever-present force of gravity. When you abruptly move or shake the phone, the X, Y, and Z values are able to reveal this activity as well.

Regardless of how you contort your phone, the X,Y, and Z axes used for the accelerometer data remain fixed to the phone. For example, the Y axis always points toward the top edge of the phone.

To get the accelerometer data, you create an instance of the Accelerometer class from the Microsoft.Devices.Sensors namespace in the Microsoft.Devices.Sensors assembly.

This assembly is not referenced by Windows Phone projects by default, so you must add it via the Add Reference dialog in Visual Studio.

The Accelerometer class exposes Start and Stop methods and—most importantly— a ReadingChanged event. This event gets raised many times a second (after Start is called) and reports the data via properties on the event-args parameter passed to handlers. These properties are X, Y, Z, and Timestamp. The physical accelerometer is always running; Start and Stop simply start/stop the data reporting to your app.

Sounds pretty simple, right? The class is indeed simple although, as you’ll see in the remaining chapters, interpreting the data in a satisfactory way can be complicated.

To get the best performance and battery life, it’s good to stop the accelerometer data reporting when you don’t need the data and then restart it when you do.

The main page, with its application bar menu expanded.
FIGURE 44.2 The main page, with its application bar menu expanded.

The User Interface

Boxing Glove has a main page, a settings page, an instructions page, and an about page. The latter two pages aren’t interesting and therefore aren’t shown in this chapter, but Listing 44.1 contains the XAML for the main page. The page, with its application bar menu expanded, is shown in Figure 44.2.

LISTING 44.1 MainPage.xaml—The User Interface for Boxing Glove’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=”Portrait”>
<!– The application bar, with two buttons and three menu items –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar Opacity=”.5”>
<shell:ApplicationBarIconButton Text=”ring bell”
IconUri=”/Images/appbar.bell.png” Click=”RingBellButton_Click” />
<shell:ApplicationBarIconButton Text=”switch hand”
IconUri=”/Images/appbar.leftHand.png”
Click=”SwitchHandButton_Click” />
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”instructions”
Click=”InstructionsMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”settings”
Click=”SettingsMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”about”
Click=”AboutMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<Border Background=”{StaticResource PhoneAccentBrush}”>
<Image Source=”Images/hand.png” RenderTransformOrigin=”.5,.5”>
<Image.RenderTransform>
<!– ScaleX is 1 for right-handed or -1 for left-handed –>
<CompositeTransform x:Name=”ImageTransform” ScaleX=”1”/>
</Image.RenderTransform>
</Image>
</Border>
</phone:PhoneApplicationPage>

[/code]

Besides the application bar, this page basically contains an image that instructs the user how to hold the phone. The background takes on the theme accent color, so the screen in Figure 44.2 is from a phone whose accent color is set to red.

The application bar contains a button for performing a “ding ding ding” bell sound on demand (to mimic the start of a fight in a boxing ring), and a button for swapping between right-handed mode and left-handed mode. The composite transform flips the image horizontally (in code-behind) when left-handed mode is in use.

The Code-Behind

Listing 44.2 contains the code-behind for the main page. It makes use of two persisted settings defined in a separate Settings.cs file as follows:

[code]

public static class Settings
{
public static readonly Setting<bool> IsRightHanded =
new Setting<bool>(“IsRightHanded”, true);
public static readonly Setting<double> Threshold =
new Setting<double>(“Threshold”, 1.5);
}

[/code]

LISTING 44.2 MainPage.xaml.cs—The Code-Behind for Boxing Glove’s Main Page

[code]

using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Navigation;
using Microsoft.Devices.Sensors;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
Accelerometer accelerometer;
IApplicationBarIconButton switchHandButton;
DateTimeOffset acceleratingQuicklyForwardTime = DateTimeOffset.MinValue;
Random random = new Random();
double currentThreshold;
public MainPage()
{
InitializeComponent();
this.switchHandButton = this.ApplicationBar.Buttons[1]
as IApplicationBarIconButton;
// Initialize the accelerometer
this.accelerometer = new Accelerometer();
this.accelerometer.ReadingChanged += Accelerometer_ReadingChanged;
SoundEffects.Initialize();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Start the accelerometer
try
{
this.accelerometer.Start();
}
catch
{
MessageBox.Show(
“Unable to start your accelerometer. Please try running this app again.”,
“Accelerometer Error”, MessageBoxButton.OK);
}
// Also ensures the threshold is updated on return from settings page
UpdateForCurrentHandedness();
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
// Stop the accelerometer
try
{
this.accelerometer.Stop();
}
catch { /* Nothing to do */ }
}
// Process data coming from the accelerometer
void Accelerometer_ReadingChanged(object sender,
AccelerometerReadingEventArgs e)
{
// Only pay attention to large-enough magnitudes in the X dimension
if (Math.Abs(e.X) < Math.Abs(this.currentThreshold))
return;
// See if the force is in the same direction as the threshold
// (forward punching motion)
if (e.X * this.currentThreshold > 0)
{
// Forward acceleration
this.acceleratingQuicklyForwardTime = e.Timestamp;
}
else if (e.Timestamp – this.acceleratingQuicklyForwardTime
< TimeSpan.FromSeconds(.2))
{
// This is large backward force shortly after the forward force.
// Time to make the punching noise!
this.acceleratingQuicklyForwardTime = DateTimeOffset.MinValue;
// We’re on a different thread, so transition to the UI thread.
// This is a requirement for playing the sound effect.
this.Dispatcher.BeginInvoke(delegate()
{
switch (this.random.Next(0, 4))
{
case 0: SoundEffects.Punch1.Play(); break;
case 1: SoundEffects.Punch2.Play(); break;
case 2: SoundEffects.Punch3.Play(); break;
case 3: SoundEffects.Punch4.Play(); break;
}
switch (this.random.Next(0, 10)) // Only grunt some of the time
{
case 0: SoundEffects.Grunt1.Play(); break;
case 1: SoundEffects.Grunt2.Play(); break;
case 2: SoundEffects.Grunt3.Play(); break;
}
});
}
}
void UpdateForCurrentHandedness()
{
this.currentThreshold = (Settings.IsRightHanded.Value ?
Settings.Threshold.Value :
-Settings.Threshold.Value);
this.ImageTransform.ScaleX = (Settings.IsRightHanded.Value ? 1 : -1);
// Show the opposite hand on the application bar button
if (Settings.IsRightHanded.Value)
this.switchHandButton.IconUri = new Uri(“/Images/appbar.leftHand.png”,
UriKind.Relative);
else
this.switchHandButton.IconUri = new Uri(“/Images/appbar.rightHand.png”,
UriKind.Relative);
}
// Application bar handlers
void RingBellButton_Click(object sender, EventArgs e)
{
SoundEffects.DingDingDing.Play();
}
void SwitchHandButton_Click(object sender, EventArgs e)
{
Settings.IsRightHanded.Value = !Settings.IsRightHanded.Value;
UpdateForCurrentHandedness();
}
void InstructionsMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/InstructionsPage.xaml”,
UriKind.Relative));
}
void SettingsMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/SettingsPage.xaml”,
UriKind.Relative));
}
void AboutMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/AboutPage.xaml”,
UriKind.Relative));
}
}
}

[/code]

  • The constructor contains the code for initializing the accelerometer. It constructs an instance of Accelerometer and attaches a handler to its ReadingChanged event. The SoundEffects class, defined in Listing 44.3, is also initialized.
  • OnNavigatedTo contains the code for starting the accelerometer, and OnNavigatedTo stops it. If it weren’t stopped, the handler would still be called (and the sound effects would still be made) while the main page is on the back stack. This would happen when the user visits the settings, instructions, or about pages. Leaving the accelerometer running would not be a problem, and may even be desirable for some apps. However, Listing 44.1 stops it to avoid two threads potentially reading/writing the threshold variable at the same time, as discussed in a later sidebar.

The calls to Accelerometer.Start and Accelerometer.Stop can throw an exception!

This can happen at development-time if you omit the ID_CAP_SENSORS capability from your app manifest. For a published app in the marketplace (which automatically gets the appropriate capability), this should not happen unless you have previously called the Accelerometer instance’s Dispose method.Of course, there’s not much that many accelerometer-based apps can do when the accelerometer fails to start, so Boxing Glove simply instructs to user to try closing and reopening the app and hope for the best.

  • Inside Accelerometer_ReadingChanged, the handler for the ReadingChanged event, only two properties of the AccelerometerReadingEventArgs instance are examined: X and Timestamp. The algorithm is as follows: If the app detects a strong forward horizontal force followed quickly by a strong backward horizontal force, it’s time to make a punching sound. Making the sound when the forward force is detected isn’t good enough, because the sound would be made too early. The sound should occur when the punching motion stops (i.e. hits the imaginary target of the punch). The detected backward force does not result from the phone being moved backward, but rather from the fast deceleration that occurs when the user stops their flying fist in mid-air.
  • The definition of “strong” used by Accelerometer_ReadingChanged’s algorithm is determined by a threshold that is configurable by the user on the settings page. The absolute value of the threshold ranges from almost 0 (.1) to almost 2 (1.9). The sign of the threshold depends on whether the app is in right-handed mode or lefthanded mode. In right-handed mode, forward motion means pushing to phone toward its left, so the threshold of forward force is negative. In left-handed mode, forward motion means pushing the phone toward its right, so the threshold of forward force is positive. This adjustment is made inside UpdateForCurrentHandedness.
  • When it’s time to make a punching noise, the sound is randomly chosen, potentially along with a randomly chosen grunting sound.

The accelerometer’s ReadingChanged event is raised on a non-UI thread!

This is great for processing the data without creating a bottleneck on the UI thread, but it does mean that you must explicitly transition to the UI thread before performing any work that requires it.This includes updating any UI elements or, as in this app, playing a sound effect. Listing 44.2 uses the page dispatcher’s BeginInvoke method to play the sound effect on the UI thread.

Although you can start the accelerometer from a page’s constructor, it’s normally better to wait until an event such as Loaded.That’s because starting it within the constructor could cause the ReadingChanged event to be raised earlier than when you’re prepared to handle it. For example, it can be raised before (or during) the deserialization of persisted settings, causing a failure if a setting is accessed in the event handler. It can be raised before the CompositionTarget.Rendering event is raised,which would be a problem if the implementation of SoundEffects (shown in the next listing) waited for the first Rendering event to call XNA’s FrameworkDispatcher.Update method.

Boxing Glove chooses to start the accelerometer in OnNavigatedTo and stop it in OnNavigatedFrom so the ReadingChanged event-handler thread doesn’t try to access the threshold member while the call to UpdateForCurrentHandedness inside OnNavigatedTo potentially updates it on the UI thread.

The SoundEffects class used by Listing 44.2 is shown in Listing 44.3. It encapsulates the work of setting up the sound effects, with code similar to the apps from Part V, “Audio & Video.”

LISTING 44.3 SoundEffects.cs—Initializes and Exposes Boxing Glove’s Eight Sound Effects

[code]

using System;
using System.Windows.Resources;
using Microsoft.Xna.Framework.Audio; // For SoundEffect
namespace WindowsPhoneApp
{
public static class SoundEffects
{
public static void Initialize()
{
StreamResourceInfo info;
info = App.GetResourceStream(new Uri(“Audio/punch1.wav”, UriKind.Relative));
Punch1 = SoundEffect.FromStream(info.Stream);
info = App.GetResourceStream(new Uri(“Audio/punch2.wav”, UriKind.Relative));
Punch2 = SoundEffect.FromStream(info.Stream);
info = App.GetResourceStream(new Uri(“Audio/punch3.wav”, UriKind.Relative));
Punch3 = SoundEffect.FromStream(info.Stream);
info = App.GetResourceStream(new Uri(“Audio/punch4.wav”, UriKind.Relative));
Punch4 = SoundEffect.FromStream(info.Stream);
info = App.GetResourceStream(new Uri(“Audio/grunt1.wav”, UriKind.Relative));
Grunt1 = SoundEffect.FromStream(info.Stream);
info = App.GetResourceStream(new Uri(“Audio/grunt2.wav”, UriKind.Relative));
Grunt2 = SoundEffect.FromStream(info.Stream);
info = App.GetResourceStream(new Uri(“Audio/grunt3.wav”, UriKind.Relative));
Grunt3 = SoundEffect.FromStream(info.Stream);
info = App.GetResourceStream(new Uri(“Audio/dingDingDing.wav”,
UriKind.Relative));
DingDingDing = SoundEffect.FromStream(info.Stream);
CompositionTarget.Rendering += delegate(object sender, EventArgs e)
{
// Required for XNA Sound Effect API to work
Microsoft.Xna.Framework.FrameworkDispatcher.Update();
};
// Call also once at the beginning
Microsoft.Xna.Framework.FrameworkDispatcher.Update();
}
public static SoundEffect Punch1 { get; private set; }
public static SoundEffect Punch2 { get; private set; }
public static SoundEffect Punch3 { get; private set; }
public static SoundEffect Punch4 { get; private set; }
public static SoundEffect Grunt1 { get; private set; }
public static SoundEffect Grunt2 { get; private set; }
public static SoundEffect Grunt3 { get; private set; }
public static SoundEffect DingDingDing { get; private set; }
}
}

[/code]

The Settings Page

The settings page, shown in Figure 44.3, contains a slider and a reset button for adjusting the threshold value from .1 to 1.9. The XAML is shown in Listing 44.4 and its codebehind is in Listing 44.5.

The settings page enables the user to adjust the accelerometer threshold, described as “required punching strength.”
FIGURE 44.3 The settings page enables the user to adjust the accelerometer threshold, described as “required punching strength.”

LISTING 44.4 SettingsPage.xaml—The User Interface for Boxing Glove’s Settings Page

[code]

<phone:PhoneApplicationPage
x:Class=”WindowsPhoneApp.SettingsPage” x:Name=”Page”
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=”PortraitOrLandscape”
shell:SystemTray.IsVisible=”True”>
<Grid Background=”{StaticResource PhoneBackgroundBrush}”>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”*”/>
</Grid.RowDefinitions>
<!– The standard settings header –>
<StackPanel Grid.Row=”0” Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock Text=”SETTINGS” Style=”{StaticResource PhoneTextTitle0Style}”/>
<TextBlock Text=”boxing glove”
Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<ScrollViewer Grid.Row=”1”>
<StackPanel Margin=”{StaticResource PhoneMargin}”>
<TextBlock Text=”Required punching strength”
Foreground=”{StaticResource PhoneSubtleBrush}”
Margin=”{StaticResource PhoneMargin}”/>
<Slider x:Name=”StrengthSlider” Minimum=”.1” Maximum=”1.9”
LargeChange=”.1”
Value=”{Binding Threshold, Mode=TwoWay, ElementName=Page}”/>
<Button Content=”reset” Click=”ResetButton_Click”
local:Tilt.IsEnabled=”True”/>
<TextBlock TextWrapping=”Wrap” Margin=”{StaticResource PhoneMargin}”
Text=”…”/>
</StackPanel>
</ScrollViewer>
</Grid>
</phone:PhoneApplicationPage>

[/code]

LISTING 44.5 SettingsPage.xaml.cs—The Code-Behind for Boxing Glove’s Settings Page

[code]

using System.Windows;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class SettingsPage : PhoneApplicationPage
{
public SettingsPage()
{
InitializeComponent();
}
// Simple property bound to the slider
public double Threshold
{
get { return Settings.Threshold.Value; }
set { Settings.Threshold.Value = value; }
}
void ResetButton_Click(object sender, RoutedEventArgs e)
{
this.StrengthSlider.Value = Settings.Threshold.DefaultValue;
}
}
}

[/code]

The Finished Product

Boxing Glove (Accelerometer Basics)

Jigsaw Puzzle (Drag Gesture & WriteableBitmap)

Jigsaw Puzzle enables you to turn any picture into a challenging 30-piece jigsaw puzzle. You can use one of the included pictures, or choose a photo from your camera or pictures library. You can even zoom and crop the photo to get it just right. Drag pieces up from a scrollable tray at the bottom of the screen and place them where you think they belong. As you drag a piece, it snaps to each of the 30 possible correct positions to reduce your frustration when arranging pieces. Jigsaw Puzzle also can solve the puzzle for you, or reshuffle the pieces, both with fun animations.

Other than the instructions page, Jigsaw Puzzle contains a main page and a page for cropping an imported picture. This app leverages gesture listener’s drag events for a few different reasons. On the main page, dragging is used for moving puzzle pieces and for scrolling the tray of unused pieces at the bottom of the screen. On the page for cropping imported pictures, dragging is used to pan the picture.

Are you thinking of ways to increase the difficulty of the puzzles in this app? Although the puzzle pieces would become too difficult to drag if you make them much smaller (without also enabling zooming), you could enable pieces to be rotated.The next chapter demonstrates how to implement a rotation gesture.

The Main Page

Jigsaw Puzzle’s main page contains the 30 pieces arranged in 6 rows of 5. Each piece is a 96×96 canvas that contains a vector drawing represented as a Path element. 14 distinct shapes are used (4 if you consider rotated/flipped versions as equivalent), shown in Figure 42.1.

The 14 shapes consist of 4 corner pieces, 8 edge pieces, and 2 middle pieces.
FIGURE 42.1 The 14 shapes consist of 4 corner pieces, 8 edge pieces, and 2 middle pieces.

Each piece is actually larger than 96 pixels in at least one dimension, which is fine because each Path can render outside the bounds of its parent 96×96 canvas. Each Path is given an appropriate offset inside its parent canvas to produce the appropriate interlocking pattern, as illustrated in Figure 42.2. Every puzzle presented by this app uses these exact 30 pieces in the exact same spots; only the image on the pieces changes.

The choice of a vector-based path to represent each piece is important because it enables the nonrectangular shapes to interlock and retain precise hit-testing. If puzzle-pieceshaped images were instead used as an opacity mask on rectangular elements, the bounding box of each piece would respond to gestures on the entire area that overlaps the bounding box of any pieces underneath. This would cause the wrong piece to move in many areas of the puzzle. The use of paths also enables us to apply a custom stroke to each piece to highlight its edges.

The User Interface

Listing 42.1 contains the XAML for the main page.

The 30 vector-based shapes, each shown with its parent canvas represented as a yellow square outline.
FIGURE 42.2 The 30 vector-based shapes, each shown with its parent canvas represented as a yellow square outline.

LISTING 42.1 MainPage.xaml—The User Interface for Jigsaw Puzzle’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”
xmlns:toolkit=”clr-namespace:Microsoft.Phone.Controls;
➥assembly=Microsoft.Phone.Controls.Toolkit”
SupportedOrientations=”Portrait”>
<!– Listen for drag events anywhere on the page –>
<toolkit:GestureService.GestureListener>
<toolkit:GestureListener DragStarted=”GestureListener_DragStarted”
DragDelta=”GestureListener_DragDelta”
DragCompleted=”GestureListener_DragCompleted”/>
</toolkit:GestureService.GestureListener>
<!– The application bar, with 4 buttons and 5 menu items –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar Opacity=”.5” ForegroundColor=”White”
BackgroundColor=”#443225”>
<shell:ApplicationBarIconButton Text=”picture”
IconUri=”/Shared/Images/appbar.picture.png” Click=”PictureButton_Click”/>
<shell:ApplicationBarIconButton Text=”start over”
IconUri=”/Shared/Images/appbar.delete.png” Click=”StartOverButton_Click”/>
<shell:ApplicationBarIconButton Text=”solve” IsEnabled=”False”
IconUri=”/Images/appbar.solve.png” Click=”SolveButton_Click”/>
<shell:ApplicationBarIconButton Text=”instructions”
IconUri=”/Shared/Images/appbar.instructions.png”
Click=”InstructionsButton_Click”/>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”cat and fish”
Click=”ApplicationBarMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”city”
Click=”ApplicationBarMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”statue of liberty”
Click=”ApplicationBarMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”traffic”
Click=”ApplicationBarMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”under water”
Click=”ApplicationBarMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<!– Prevent off-screen pieces from appearing during a page transition –>
<phone:PhoneApplicationPage.Clip>
<RectangleGeometry Rect=”0,0,480,800”/>
</phone:PhoneApplicationPage.Clip>
<Canvas Background=”#655”>
<!– The tray at the bottom –>
<Rectangle x:Name=”Tray” Fill=”#443225” Width=”480” Height=”224”
Canvas.Top=”576”/>
<!– All 30 pieces placed where they belong –>
<Canvas x:Name=”PiecesCanvas”>
<!– Row 1 –>
<Canvas Width=”96” Height=”96”>
<Path Data=”F1M312.63,0L385.7,0C385.48,…” Height=”129” Stretch=”Fill”
Width=”96” Stroke=”#2000”>
<Path.Fill>
<ImageBrush Stretch=”None” AlignmentX=”Left” AlignmentY=”Top”/>
</Path.Fill>
</Path>
<Canvas.RenderTransform><CompositeTransform/></Canvas.RenderTransform>
</Canvas>
<Canvas Canvas.Left=”96” Width=”96” Height=”96”>
<Path Data=”F1M25.12,909.28C49.47,909.27,…” Height=”96”
Canvas.Left=”-33” Stretch=”Fill” Width=”162” Stroke=”#2000”>
<Path.Fill>
<ImageBrush Stretch=”None” AlignmentX=”Left” AlignmentY=”Top”>
<ImageBrush.Transform>
<TranslateTransform X=”-63”/>
</ImageBrush.Transform>
</ImageBrush>
</Path.Fill>
</Path>
<Canvas.RenderTransform><CompositeTransform/></Canvas.RenderTransform>
</Canvas>
… 27 pieces omitted …
<Canvas Canvas.Left=”384” Canvas.Top=”480” Width=”96” Height=”96”>
<Path Data=”F1M777.45,0L800.25,0C802.63,…” Canvas.Left=”-33”
Height=”96” Stretch=”Fill” Width=”129” Stroke=”#2000”>
<Path.Fill>
<ImageBrush Stretch=”None” AlignmentX=”Left” AlignmentY=”Top”>
<ImageBrush.Transform>
<TranslateTransform X=”-351” Y=”-480”/>
</ImageBrush.Transform>
</ImageBrush>
</Path.Fill>
</Path>
<Canvas.RenderTransform><CompositeTransform/></Canvas.RenderTransform>
</Canvas>
</Canvas>
<!– The image without visible piece boundaries, shown when solved –>
<Image x:Name=”CompleteImage” Visibility=”Collapsed” IsHitTestVisible=”False”
Stretch=”None”/>
</Canvas>
</phone:PhoneApplicationPage>

[/code]

  • A gesture listener is attached to the entire page to listen for the three drag events: DragStarted, DragDelta, and DragCompleted.

My favorite way to create vector artwork based on an illustration is with Vector Magic (http://vectormagic.com). It’s not free, but it does a fantastic job of converting image files to a variety of vector formats. If you download the result as a PDF file and then rename the file extension to .ai, you can import it into Expression Blend, which converts it to XAML.

  • Although each piece is placed in its final “solved” position, the code-behind adjusts each position by modifying TranslateX and TranslateY properties on the CompositeTransform assigned to each piece. This gives us the nice property that no matter where a piece is moved, it can be returned to its solved position by setting both of these properties to zero.
  • The magic behind making each puzzle piece contain a portion of a photo is enabled by the image brush that fills each path. (The actual image is set in code-behind.) To make each piece contain the correct portion of the photo, each image brush (except the one used on the piece in the top left corner) is given a TranslateTransform. This shifts its rendering by the distance that the piece is from the top-left corner. (To make this work, each image brush is marked with top-left alignment, rather than its default center alignment.)

Fortunately for apps such as Jigsaw Puzzle, using many image brushes that point to the same image is efficient. Silverlight shares the underlying image rather than creating a separate copy for each brush.

  • The scrolling tray at the bottom isn’t an actual scroll viewer; it’s just a simple rectangle. The code-behind manually scrolls puzzle pieces when they sufficiently overlap this rectangle. This is done for two reasons: It’s convenient to keep the pieces on the same canvas at all times, and the gesture listener currently interferes with Silverlight elements such as scroll viewers.
  • The CompleteImage element at the bottom of the listing is used to show the complete image once the puzzle is solved, without the puzzle piece borders and tiny gaps between pieces obscuring it. Because it is aligned with the puzzle, showing this image simply makes it seem like the puzzle edges have faded away. Figure 42.3 shows what this looks like for the cat-and-fish puzzle shown at the beginning of this chapter. Because CompleteImage is not hit-testable, the user can still drag a piece while it is showing. As soon as any piece moves out of its correct position, the codebehind hides the image once again.
Once the puzzle is solved, the puzzle piece borders are no longer visible.
FIGURE 42.3 Once the puzzle is solved, the puzzle piece borders are no longer visible.

The Code-Behind

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

LISTING 42.2 MainPage.xaml.cs—The Code-Behind for Jigsaw Puzzle’s Main Page

[code]

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
bool isDraggingTray;
bool isDraggingPiece;
double cumulativeDeltaX;
double cumulativeDeltaY;
int topmostZIndex;
List<FrameworkElement> piecesOnTray = new List<FrameworkElement>();
Random random = new Random();
IApplicationBarIconButton solveButton;
public MainPage()
{
InitializeComponent();
this.solveButton = this.ApplicationBar.Buttons[2]
as IApplicationBarIconButton;
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
// Persist the offset currently being applied to each piece, so
// they can appear in the same locations next time
Settings.PieceOffsets.Value.Clear();
foreach (FrameworkElement piece in this.PiecesCanvas.Children)
{
Settings.PieceOffsets.Value.Add(new Point(
(piece.RenderTransform as CompositeTransform).TranslateX,
(piece.RenderTransform as CompositeTransform).TranslateY));
}
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
RefreshPuzzleImage();
bool arePiecesCorrect = false;
if (Settings.PieceOffsets.Value.Count == this.PiecesCanvas.Children.Count)
{
// Restore the persisted position of each piece
for (int i = 0; i < this.PiecesCanvas.Children.Count; i++)
{
UIElement piece = this.PiecesCanvas.Children[i];
CompositeTransform t = piece.RenderTransform as CompositeTransform;
t.TranslateX = Settings.PieceOffsets.Value[i].X;
t.TranslateY = Settings.PieceOffsets.Value[i].Y;
}
arePiecesCorrect = AreAllPiecesCorrect();
}
else
{
// This is the first run. After a 1-second delay, animate the pieces
// from their solved positions to random positions on the tray.
DispatcherTimer timer = new DispatcherTimer {
Interval = TimeSpan.FromSeconds(1) };
timer.Tick += delegate(object sender, EventArgs args)
{
StartOver();
timer.Stop();
};
timer.Start();
}
if (arePiecesCorrect)
ShowAsSolved();
else
ShowAsUnsolved();
}
// The three drag event handlers
void GestureListener_DragStarted(object sender, DragStartedGestureEventArgs e)
{
// Determine if we’re dragging the tray, a piece, or neither
FrameworkElement source = e.OriginalSource as FrameworkElement;
if (source == this.Tray)
{
// An empty spot on the tray is being dragged
if (e.Direction == System.Windows.Controls.Orientation.Horizontal)
this.isDraggingTray = true;
return;
}
FrameworkElement piece = GetPieceFromDraggedSource(source);
if (piece == null)
return;
if (e.Direction == System.Windows.Controls.Orientation.Horizontal &&
GetPieceTop(piece) > Constants.ON_TRAY_Y)
{
// Although a piece is being dragged, the piece is on the tray and the
// drag is horizontal, so consider this to be a tray drag instead
this.isDraggingTray = true;
}
else
{
this.isDraggingPiece = true;
// A piece is being dragged, so record its pre-drag position
CompositeTransform t = piece.RenderTransform as CompositeTransform;
this.cumulativeDeltaX = t.TranslateX;
this.cumulativeDeltaY = t.TranslateY;
}
}
void GestureListener_DragDelta(object sender, DragDeltaGestureEventArgs e)
{
if (this.isDraggingTray)
{
// Scroll the tray
ScrollTray(e.HorizontalChange);
}
else if (this.isDraggingPiece)
{
FrameworkElement piece = GetPieceFromDraggedSource(
e.OriginalSource as FrameworkElement);
if (piece == null)
return;
CompositeTransform t = piece.RenderTransform as CompositeTransform;
// Apply the position change caused by dragging.
// We’re keeping track of the total change from DragStarted so the piece
// remains in the right spot after repeated snapping and unsnapping.
this.cumulativeDeltaX += e.HorizontalChange;
this.cumulativeDeltaY += e.VerticalChange;
t.TranslateX = this.cumulativeDeltaX;
t.TranslateY = this.cumulativeDeltaY;
// Ensure that this piece is on top of all others
this.topmostZIndex++;
Canvas.SetZIndex(piece, this.topmostZIndex);
// Ensure that the puzzle is no longer in the solved state
ShowAsUnsolved();
// If the piece is not on the tray, snap it to a solved horizontal
// and/or vertical boundary if it’s close enough
double left = GetPieceLeft(piece);
double top = GetPieceTop(piece);
if (top > Constants.ON_TRAY_Y)
return; // The piece is on the tray, so never mind
// Snapping to a horizontal boundary
if (left % Constants.PIECE_WIDTH < Constants.SNAPPING_MARGIN)
t.TranslateX -= left % Constants.PIECE_WIDTH;
else if (left % Constants.PIECE_WIDTH >
Constants.PIECE_WIDTH – Constants.SNAPPING_MARGIN)
t.TranslateX += Constants.PIECE_WIDTH – left % Constants.PIECE_WIDTH;
// Snapping to a vertical boundary
if (top % Constants.PIECE_HEIGHT < Constants.SNAPPING_MARGIN)
t.TranslateY -= top % Constants.PIECE_HEIGHT;
else if (top % Constants.PIECE_HEIGHT >
Constants.PIECE_HEIGHT – Constants.SNAPPING_MARGIN)
t.TranslateY += Constants.PIECE_HEIGHT – top % Constants.PIECE_HEIGHT;
}
}
void GestureListener_DragCompleted(object sender,
DragCompletedGestureEventArgs e)
{
// Give the tray an extra push (simulating inertia) based on
// the final dragging horizontal velocity
if (this.isDraggingTray && e.HorizontalVelocity != 0)
ScrollTray(e.HorizontalVelocity / 10);
this.isDraggingTray = this.isDraggingPiece = false;
if (AreAllPiecesCorrect())
ShowAsSolved();
}
FrameworkElement GetPieceFromDraggedSource(FrameworkElement source)
{
// When a piece is dragged, the source is the path,
// but we want to return its parent canvas
if (source == null || source.Parent == null ||
(source.Parent as FrameworkElement).Parent == null ||
(source.Parent as FrameworkElement).Parent != this.PiecesCanvas)
return null;
else
return source.Parent as FrameworkElement;
}
double GetPieceTop(FrameworkElement piece)
{
return Canvas.GetTop(piece) +
(piece.RenderTransform as CompositeTransform).TranslateY;
}
double GetPieceLeft(FrameworkElement piece)
{
return Canvas.GetLeft(piece) +
(piece.RenderTransform as CompositeTransform).TranslateX;
}
void ScrollTray(double amount)
{
// Retrieve the minimum and maximum horizontal positions among all
// pieces in the tray, to provide bounds on how far it can scroll
double minX = double.MaxValue;
double maxX = double.MinValue;
this.piecesOnTray.Clear();
foreach (FrameworkElement piece in this.PiecesCanvas.Children)
{
if (GetPieceTop(piece) > Constants.ON_TRAY_Y)
{
this.piecesOnTray.Add(piece);
double left = GetPieceLeft(piece);
if (left < minX) minX = left;
if (left > maxX) maxX = left;
}
}
if (this.piecesOnTray.Count == 0)
return;
// Change the amount if it would make the tray scroll too far
if (amount < 0 && (maxX + amount < this.ActualWidth –
Constants.MAX_PIECE_WIDTH || minX < Constants.NEGATIVE_SCROLL_BOUNDARY))
amount = Math.Max(-maxX + this.ActualWidth – Constants.MAX_PIECE_WIDTH,
Constants.NEGATIVE_SCROLL_BOUNDARY – minX);
if (amount > 0 && minX + amount > Constants.TRAY_LEFT_MARGIN)
amount = Constants.TRAY_LEFT_MARGIN – minX;
// “Scroll” the tray by moving each piece on the tray the same amount
foreach (FrameworkElement piece in this.piecesOnTray)
(piece.RenderTransform as CompositeTransform).TranslateX += amount;
}
// Move each piece to the tray in a random order
void StartOver()
{
// Copy the children to an array so their order
// in the collection is preserved
UIElement[] pieces = this.PiecesCanvas.Children.ToArray();
// Shuffle the children in place
for (int i = pieces.Length – 1; i > 0; i–)
{
int r = this.random.Next(0, i);
// Swap the current child with the randomly-chosen one
UIElement temp = pieces[i]; pieces[i] = pieces[r]; pieces[r] = temp;
}
// Now move the pieces to the bottom in their random order
for (int i = 0; i < pieces.Length; i++)
{
UIElement piece = pieces[i];
// Alternate the pieces between two rows
CreatePieceMovingStoryboard(piece, TimeSpan.Zero, TimeSpan.FromSeconds(1),
(i % 2 * Constants.TRAY_2ND_ROW_HORIZONTAL_OFFSET) +
(i / 2) * Constants.TRAY_HORIZONTAL_SPACING – Canvas.GetLeft(piece),
(i % 2 * Constants.TRAY_VERTICAL_SPACING) + Constants.TRAY_TOP_MARGIN
– Canvas.GetTop(piece)).Begin();
// Reset the z-index of each piece
Canvas.SetZIndex(piece, 0);
}
this.topmostZIndex = 0;
ShowAsUnsolved();
The Main Page 925
}
// Create a storyboard that animates the piece to the specified position
Storyboard CreatePieceMovingStoryboard(UIElement piece, TimeSpan beginTime,
TimeSpan duration, double finalX, double finalY)
{
DoubleAnimation xAnimation = new DoubleAnimation { To = finalX,
Duration = duration, EasingFunction = new QuinticEase() };
DoubleAnimation yAnimation = new DoubleAnimation { To = finalY,
Duration = duration, EasingFunction = new QuinticEase() };
Storyboard.SetTargetProperty(xAnimation, new PropertyPath(“TranslateX”));
Storyboard.SetTargetProperty(yAnimation, new PropertyPath(“TranslateY”));
Storyboard storyboard = new Storyboard { BeginTime = beginTime };
Storyboard.SetTarget(storyboard, piece.RenderTransform);
storyboard.Children.Add(xAnimation);
storyboard.Children.Add(yAnimation);
return storyboard;
}
bool AreAllPiecesCorrect()
{
for (int i = 0; i < this.PiecesCanvas.Children.Count; i++)
{
UIElement piece = this.PiecesCanvas.Children[i];
CompositeTransform t = piece.RenderTransform as CompositeTransform;
if (t.TranslateX != 0 || t.TranslateY != 0)
return false; // This piece is in the wrong place
}
// All pieces are in the right place
return true;
}
void ShowAsSolved()
{
this.solveButton.IsEnabled = false;
int piecesToMove = 0;
Storyboard storyboard = null;
// For any piece that’s out of place, animate it to the solved position
for (int i = 0; i < this.PiecesCanvas.Children.Count; i++)
{
UIElement piece = this.PiecesCanvas.Children[i];
CompositeTransform t = piece.RenderTransform as CompositeTransform;
if (t.TranslateX == 0 && t.TranslateY == 0)
continue; // This piece is already in the right place
// Animate it to a (0,0) offset, which is its natural position
storyboard = CreatePieceMovingStoryboard(piece,
TimeSpan.FromSeconds(.3 * piecesToMove), // Spread out the animations
TimeSpan.FromSeconds(1), 0, 0);
storyboard.Begin();
// Ensure each piece moves on top of pieces already in the right place
this.topmostZIndex++;
Canvas.SetZIndex(piece, this.topmostZIndex);
piecesToMove++;
}
if (storyboard == null)
{
// Everything is in the right place
this.CompleteImage.Visibility = Visibility.Visible;
}
else
{
// Delay the showing of CompleteImage until the last storyboard
// has completed
storyboard.Completed += delegate(object sender, EventArgs e)
{
// Ensure that the user didn’t unsolve the puzzle during the animation
if (!this.solveButton.IsEnabled)
this.CompleteImage.Visibility = Visibility.Visible;
};
}
}
void ShowAsUnsolved()
{
this.solveButton.IsEnabled = true;
this.CompleteImage.Visibility = Visibility.Collapsed;
}
void RefreshPuzzleImage()
{
ImageSource imageSource = null;
// Choose the right image based on the setting
switch (Settings.PhotoIndex.Value)
{
// The first case is for a custom photo saved
// from CroppedPictureChooserPage
case -1:
try { imageSource = IsolatedStorageHelper.LoadFile(“custom.jpg”); }
catch { imageSource = new BitmapImage(new Uri(“Images/catAndFish.jpg”,
UriKind.Relative)); }
break;
// The remaining cases match the indices in the application bar menu
case 0:
imageSource = new BitmapImage(new Uri(“Images/catAndFish.jpg”,
UriKind.Relative));
break;
case 1:
imageSource = new BitmapImage(new Uri(“Images/city.jpg”,
UriKind.Relative));
break;
case 2:
imageSource = new BitmapImage(new Uri(“Images/statueOfLiberty.jpg”,
UriKind.Relative));
break;
case 3:
imageSource = new BitmapImage(new Uri(“Images/traffic.jpg”,
UriKind.Relative));
break;
case 4:
imageSource = new BitmapImage(new Uri(“Images/underWater.jpg”,
UriKind.Relative));
break;
}
if (imageSource != null)
{
this.CompleteImage.Source = imageSource;
// Each of the 30 pieces needs to be filled with the right image
foreach (Canvas piece in this.PiecesCanvas.Children)
((piece.Children[0] as Shape).Fill as ImageBrush).ImageSource =
imageSource;
}
}
// Application bar handlers
void PictureButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/CroppedPictureChooserPage.xaml”,
UriKind.Relative));
}
void StartOverButton_Click(object sender, EventArgs e)
{
if (MessageBox.Show(“Are you sure you want to dismantle the puzzle and “ +
“start from scratch?”, “Start over”, MessageBoxButton.OKCancel)
== MessageBoxResult.OK)
StartOver();
}
void SolveButton_Click(object sender, EventArgs e)
{
if (MessageBox.Show(“Do you give up? Are you sure you want the puzzle to “
+ “be solved for you?”, “Solve”, MessageBoxButton.OKCancel)
!= MessageBoxResult.OK)
return;
ShowAsSolved();
}
void InstructionsButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/InstructionsPage.xaml”,
UriKind.Relative));
}
void ApplicationBarMenuItem_Click(object sender, EventArgs e)
{
for (int i = 0; i < this.ApplicationBar.MenuItems.Count; i++)
{
// Set the persisted photo index to match the menu item index
if (sender == this.ApplicationBar.MenuItems[i])
Settings.PhotoIndex.Value = i;
}
RefreshPuzzleImage();
}
}
}

[/code]

  • This app uses two settings defined in a separate Settings.cs file for remembering the user’s chosen photo and for remembering the position of every puzzle piece:

    [code]
    public static class Settings
    {
    public static Setting<int> PhotoIndex = new Setting<int>(“PhotoIndex”, 2);
    public static Setting<List<Point>> PieceOffsets =
    new Setting<List<Point>>(“PieceOffsets”, new List<Point>());
    }
    [/code]

  • This app also uses many constants, defined as follows in a Constants.cs file:

    [code]
    public static class Constants
    {
    public const int PUZZLE_WIDTH = 480;
    public const int PUZZLE_HEIGHT = 576;
    public const int PIECE_WIDTH = 96;
    public const int PIECE_HEIGHT = 96;
    public const int MAX_PIECE_WIDTH = 162;
    public const int SNAPPING_MARGIN = 15;
    public const int NEGATIVE_SCROLL_BOUNDARY = -1550;
    public const int TRAY_HORIZONTAL_SPACING = 110;
    public const int TRAY_VERTICAL_SPACING = 80;
    public const int TRAY_LEFT_MARGIN = 24;
    public const int TRAY_TOP_MARGIN = 590;
    public const int TRAY_2ND_ROW_HORIZONTAL_OFFSET = 50;
    public const int ON_TRAY_Y = 528;
    }
    [/code]

  • The first time Jigsaw Puzzle is run, the pieces animate from their solved positions to a random ordering on the tray. (This condition is detected in OnNavigatedTo because the PieceOffsets list does not initially contain the same number of elements as pieces in PiecesCanvas.) This ordering of pieces on the tray is shown in Figure 42.4. Every other time, the pieces are placed exactly where they were previously left by reapplying their persisted TranslateX and TranslateY values.
The puzzle pieces are arranged on the tray once they animate away from their solved positions.
FIGURE 42.4 The puzzle pieces are arranged on the tray once they animate away from their solved positions.
  • The three drag event handlers act differently depending on whether a puzzle piece is being dragged or the tray is being dragged. When the tray is dragged horizontally, we want it to scroll and reveal off-screen pieces. When a piece is dragged, it should move wherever the user’s finger takes it.
  • The DragStarted event handler (GestureListener_DragStarted) determines which type of dragging is occurring and sets either isDraggingTray or isDraggingPiece. (This handler can be called in cases where neither is true, such as dragging on an empty upper part of the screen, because these handlers are attached to the whole page.)

    DragStarted isn’t raised as soon as a finger touches the screen and starts moving; the gesture listener waits for the finger to move more than 12 pixels away to ensure that the gesture is a drag rather than a tap, and to determine the primary direction of the drag. DragStartedGestureEventArgs exposes this primary direction as a Direction property that is either Horizontal or Vertical.

    GestureListener_DragStarted leverages the Direction property to determine which kind of drag is happening. If the element reporting the event is the tray and the direction is horizontal, then it considers the gesture to be a tray drag. If a piece is being dragged horizontally and the vertical position of the piece visually makes it look like it’s on the tray, it also considers the gesture to be a tray drag. This is important to avoid the requirement that the tray can only be dragged on an empty spot. If a piece is being dragged vertically, or if it’s dragged in any direction far enough from the tray, then it’s considered to be a piece drag.

    Although this scheme is easy to implement, users might find the requirement to drag pieces off the tray in a mostly vertical fashion to be confusing and/or inconvenient. A more flexible approach would be to perform your own math and use a wider angle range.

The Direction property passed to drag events never changes until a new drag is initiated!

Although the Direction property exposed to DragStarted handlers is also exposed to DragDelta and DragCompleted handlers, its value never changes until the drag has completed and a new drag has started.This is true even if the actual direction of the finger motion changes to be completely vertical instead of horizontal, or vice versa.This makes it easy to implement panning or other motion that is locked to one axis, although it also means that detecting more flexible motion requires you to interpret the finger motion manually.

In Jigsaw Puzzle, this fact can cause frustration if a user tries to drag a piece from the tray directly to its final position, yet the straight-line path is more horizontal than it is vertical.To help combat this, the instructions page explains that pieces must be dragged upward to leave the tray.

  • The DragDelta event exposes two more properties than DragStarted: HorizontalChange and VerticalChange. For tray dragging, the HorizontalChange value is passed to a ScrollTray helper method. This method provides the illusion of scrolling by manually updating the horizontal position of every piece whose vertical position makes it appear to be on the tray. Keeping all the pieces in the same canvas at all times (instead of moving pieces on the tray to a separate panel inside an actual scroll viewer) makes the logic throughout this page easier.

    For piece dragging, both HorizontalChange and VerticalChange are applied to the current piece’s transform, and then the piece is snapped to one or two solved-piece boundary locations if it’s close enough to a horizontal and/or vertical boundary. Ordinarily, the values of HorizontalChange and VerticalChange would be directly added to TranslateX and TranslateY, respectively, but this doesn’t work well when snapping is done. Because each snap moves the piece by as much as 14 pixels away from its natural position, continued snapping would cause the piece to drift further away from the user’s finger if we continued to add the HorizontalChange and VerticalChange values. Instead, by manually tracking the total cumulative distance from the beginning of the drag, the piece is returned to its natural position after it breaks free of a snapping boundary.

The HorizontalChange and VerticalChange properties exposed by DragDelta are relative to the previous raising of DragDelta!

Unlike PinchGestureEventArgs, whose DistanceRatio and TotalAngleDelta properties are relative to the values when pinching or stretching started, the HorizontalChange and VerticalChange properties exposed by DragDeltaGestureEventArgs and DragCompletedGestureEventArgs do not accumulate as dragging proceeds.

  • The DragCompleted event exposes all the properties from DragDelta plus two more: HorizontalVelocity and VerticalVelocity. These values are the same ones exposed to the Flick event, and enable inertial flicking motion at the end of a drag. Just like in Chapter 40, “Darts,” the velocity is scaled down and then used to continue the dragging motion a bit. This is done for tray dragging only, to make it better mimic a real scroll viewer. Therefore, only the horizontal component of the velocity is used. At the end of every drag action, the location of each piece is checked to see whether the puzzle has been solved. We know that the pieces are all in the correct spots if their transforms all have TranslateX and TranslateY values of zero.
  • The “picture” and “instructions” button click handlers navigate to other pages, and the “start over” and “solve” button click handlers trigger animations that either move the pieces to random spots on the tray or to their solved positions. The solve animation is performed by the ShowAsSolved method, which animates each outof- place piece to its correct position over the course of one second, spaced .3 seconds apart. The resulting effect smoothly fills in the pieces in row major order, as pictured in Figure 42.5.
The automatic solving animation makes the pieces float into place according to their order in the canvas.
FIGURE 42.5 The automatic solving animation makes the pieces float into place according to their order in the canvas.

The Cropped Picture Chooser Page

Because the photo chooser can sometimes be slow to launch, and because decoding the chosen picture can be slow, this page shows a “loading” message during these actions. Figure 42.6 demonstrates the user flow through this page.

 The sequence of events when navigating to the cropped photo chooser page.
FIGURE 42.6 The sequence of events when navigating to the cropped photo chooser page.

The User Interface

Listing 42.3 contains the XAML for this page.

LISTING 42.3 CroppedPictureChooserPage.xaml—The User Interface for Jigsaw Puzzle’s Cropped Picture Chooser Page

[code]

<phone:PhoneApplicationPage x:Class=”WindowsPhoneApp.CroppedPictureChooserPage”
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:toolkit=”clr-namespace:Microsoft.Phone.Controls;
➥assembly=Microsoft.Phone.Controls.Toolkit”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”Portrait”>
<!– The 2-button application bar, shown on return from the PhotoChooserTask –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar Opacity=”0” IsVisible=”False” ForegroundColor=”White”>
<shell:ApplicationBarIconButton Text=”done”
IconUri=”/Shared/Images/appbar.done.png” Click=”DoneButton_Click”/>
<shell:ApplicationBarIconButton Text=”cancel”
IconUri=”/Shared/Images/appbar.cancel.png” Click=”CancelButton_Click”/>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<!– Listen for drag events anywhere on the page –>
<toolkit:GestureService.GestureListener>
<toolkit:GestureListener DragDelta=”GestureListener_DragDelta”
PinchStarted=”GestureListener_PinchStarted”
PinchDelta=”GestureListener_PinchDelta”/>
</toolkit:GestureService.GestureListener>
<!– Prevent a zoomed-in photo from making the screen go blank –>
<phone:PhoneApplicationPage.Clip>
<RectangleGeometry Rect=”0,0,480,800”/>
</phone:PhoneApplicationPage.Clip>
<Canvas Background=”#443225”>
<!– Shown on return from the PhotoChooserTask –>
<Canvas x:Name=”CropPanel” Visibility=”Collapsed”>
<!– Designate the puzzle boundary –>
<Rectangle Fill=”White” Canvas.Top=”112” Width=”480” Height=”576”/>
<!– The canvas provides screen-centered zooming –>
<Canvas Canvas.Top=”112” Width=”480” Height=”576”
RenderTransformOrigin=”.5,.5”>
<Canvas.RenderTransform>
<!– For zooming –>
<CompositeTransform x:Name=”CanvasTransform”/>
</Canvas.RenderTransform>
<Image x:Name=”Image” Stretch=”None” CacheMode=”BitmapCache”>
<Image.RenderTransform>
<!– For panning –>
<CompositeTransform x:Name=”ImageTransform”/>
</Image.RenderTransform>
</Image>
</Canvas>
<!– Top and bottom borders that let the image show through slightly –>
<Rectangle Opacity=”.8” Fill=”#443225” Width=”480” Height=”112”/>
<Rectangle Canvas.Top=”688” Opacity=”.8” Fill=”#443225” Width=”480”
Height=”112”/>
<!– The title and instructions –>
<StackPanel Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock Text=”CROP PICTURE” Foreground=”White”
Style=”{StaticResource PhoneTextTitle0Style}”/>
<TextBlock Foreground=”White” TextWrapping=”Wrap” Width=”432”>
Pinch &amp; stretch your fingers to zoom.<LineBreak/>
Drag to move the picture.
</TextBlock>
</StackPanel>
</Canvas>
<!– Shown while launching the PhotoChooserTask –>
<Canvas x:Name=”LoadingPanel”>
<StackPanel Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock x:Name=”LoadingTextBlock” Text=”LOADING…” Foreground=”White”
Style=”{StaticResource PhoneTextTitle0Style}” Width=”432”
TextWrapping=”Wrap”/>
</StackPanel>
</Canvas>
</Canvas>
</phone:PhoneApplicationPage>

[/code]

  • Drag gestures (and pinch/stretch gestures) are detected with a page-level gesture listener, just like on the main page.
  • The white rectangle not only reveals the puzzle’s dimensions if the picture doesn’t completely cover the area, but it also ends up giving the puzzle a white background when this happens. This is demonstrated in Figure 42.7.
  • Two different transforms are used to make the user’s gestures feel natural. Whereas drag gestures adjust ImageTransform much like the dragging of puzzle pieces on the main page, pinch and stretch gestures are applied to CanvasTransform. Because the canvas represents the puzzle area and it’s marked with a centered render transform origin of (.5,.5), the user’s gesture always zooms the image centered around the middle of the puzzle. If this were applied to the image instead, zooming would occur around the image’s middle, which might be far off-screen as the image gets zoomed and panned.
  • Two subtle things on this page prevent its performance from being disastrous. The bitmap caching on the image makes the panning and zooming much smoother than it would be otherwise, as does the clip applied to the page. (The page’s clip also prevents the entire screen from going blank if the photo is zoomed in to an extreme amount, caused by Silverlight failing to render a surface that is too big.)
A white rectangle serves as the puzzle’s background if the picture doesn’t completely cover it.
FIGURE 42.7 A white rectangle serves as the puzzle’s background if the picture doesn’t completely cover it.

If an image has transparent regions, those regions are not hit-testable when used to fill a shape!

This app ensures that the picture used to fill the puzzle pieces never contains any transparency. Those transparent regions would not respond to any touch events,making it difficult or impossible to drag the affected pieces.Note that this is different behavior compared to giving an otherwise- rectangular element transparent regions with an opacity mask. With an opacity mask, the transparent regions are still hit-testable (just like an element marked with an opacity of 0).

The Code-Behind

Listing 42.4 contains the code-behind for this page.

LISTING 42.4 CroppedPictureChooserPage.xaml.cs—The Code-Behind for Jigsaw Puzzle’s Cropped Picture Chooser Page

[code]

using System;
using System.IO;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Microsoft.Phone;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Tasks;
namespace WindowsPhoneApp
{
public partial class CroppedPictureChooserPage : PhoneApplicationPage
{
bool loaded;
double scaleWhenPinchStarted;
public CroppedPictureChooserPage()
{
InitializeComponent();
this.Loaded += CroppedPictureChooserPage_Loaded;
}
void CroppedPictureChooserPage_Loaded(object sender, RoutedEventArgs e)
{
if (this.loaded)
return;
this.loaded = true;
// When navigating to this page in the forward direction only (from the
// main page or when reactiving the app), launch the photo chooser task
Microsoft.Phone.Tasks.PhotoChooserTask task = new PhotoChooserTask();
task.ShowCamera = true;
task.Completed += delegate(object s, PhotoResult args)
{
if (args.TaskResult == TaskResult.OK)
{
WriteableBitmap imageSource = PictureDecoder.DecodeJpeg(
args.ChosenPhoto);
// Perform manual “uniform to fill” scaling by choosing the larger
// of the two scales that make the image just fit in one dimension
double scale = Math.Max(
(double)Constants.PUZZLE_WIDTH / imageSource.PixelWidth,
(double)Constants.PUZZLE_HEIGHT / imageSource.PixelHeight);
this.CanvasTransform.ScaleX = this.CanvasTransform.ScaleY = scale;
// Center the image in the puzzle
this.ImageTransform.TranslateY =
-(imageSource.PixelHeight – Constants.PUZZLE_HEIGHT) / 2;
this.ImageTransform.TranslateX =
-(imageSource.PixelWidth – Constants.PUZZLE_WIDTH) / 2;
// Show the cropping user interface
this.Image.Source = imageSource;
this.LoadingPanel.Visibility = Visibility.Collapsed;
this.CropPanel.Visibility = Visibility.Visible;
this.ApplicationBar.IsVisible = true;
}
else
{
// The user cancelled from the photo chooser, but we can’t automatically
// navigate back right here, so update “LOADING…” with instructions
this.LoadingTextBlock.Text =
“Press the Back button again to return to the puzzle.”;
}
};
task.Show();
}
// Raised for single-finger dragging
void GestureListener_DragDelta(object sender, DragDeltaGestureEventArgs e)
{
// Pan the image based on the drag
this.ImageTransform.TranslateX +=
e.HorizontalChange / this.CanvasTransform.ScaleX;
this.ImageTransform.TranslateY +=
e.VerticalChange / this.CanvasTransform.ScaleY;
}
// Raised when two fingers touch the screen (likely to begin a pinch/stretch)
void GestureListener_PinchStarted(object sender,
PinchStartedGestureEventArgs e)
{
this.scaleWhenPinchStarted = this.CanvasTransform.ScaleX;
}
// Raised continually as either or both fingers move
void GestureListener_PinchDelta(object sender, PinchGestureEventArgs e)
{
// The distance ratio is always relative to when the pinch/stretch started,
// so be sure to apply it to the ORIGINAL zoom level, not the CURRENT
double scale = this.scaleWhenPinchStarted * e.DistanceRatio;
this.CanvasTransform.ScaleX = this.CanvasTransform.ScaleY = scale;
}
// Application bar handlers
void DoneButton_Click(object sender, EventArgs e)
{
// Create a new bitmap with the puzzle’s dimensions
WriteableBitmap wb = new WriteableBitmap(Constants.PUZZLE_WIDTH,
Constants.PUZZLE_HEIGHT);
// Render the page’s contents to the puzzle, but shift it upward
// so only the region intended to be for the puzzle is used
wb.Render(this, new TranslateTransform { Y = -112 });
// We must explicitly tell the bitmap to draw its new contents
wb.Invalidate();
using (MemoryStream stream = new MemoryStream())
{
// Fill the stream with a JPEG representation of this bitmap
wb.SaveJpeg(stream, Constants.PUZZLE_WIDTH, Constants.PUZZLE_HEIGHT,
0 /* orientation */, 100 /* quality */);
// Seek back to the beginning of the stream
stream.Seek(0, SeekOrigin.Begin);
// Save the file to isolated storage.
// This overwrites the file if it already exists.
IsolatedStorageHelper.SaveFile(“custom.jpg”, stream);
}
// Indicate that the user has chosen to use a custom image
Settings.PhotoIndex.Value = -1;
// Return to the puzzle
if (this.NavigationService.CanGoBack)
this.NavigationService.GoBack();
}
void CancelButton_Click(object sender, EventArgs e)
{
// Don’t do anything, just return to the puzzle
if (this.NavigationService.CanGoBack)
this.NavigationService.GoBack();
}
}
}

[/code]

  • Inside DragDelta, the HorizontalChange and VerticalChange values are directly added to the image transform each time; although they must be divided by any scale applied to the parent canvas so the distance that the image travels remains consistent. This direct application works well because the TranslateX and TranslateY are not changed through any other means, such as the snapping logic used on the main page.

    The PinchStarted handler records the scale when two fingers touch the screen, arbitrarily choosing ScaleX because both ScaleX and ScaleY are always set to the same value. The PinchDelta handler multiplies the initial scale by the finger distance ratio and then applies it to the canvas transform.

  • The handler for the done button’s click event, DoneButton_Click, leverages WriteableBitmap’s killer feature—the ability to capture the contents of any element and write it to a JPEG file. A new WriteableBitmap is created with the puzzle’s dimensions, and then this (the entire page) is rendered into it with a transform that shifts it 112 pixels upward. This is necessary to avoid rendering the page’s header into the captured image. Figure 42.8 demonstrates what happens if null is passed for the second parameter of Render.

    After refreshing the bitmap with a call to Invalidate, SaveJpeg writes the contents in JPEG format to a memory stream which can then be written to isolated storage.

If the page is rendered to the puzzle image from its top-left corner, the page header becomes part of the puzzle!
FIGURE 42.8 If the page is rendered to the puzzle image from its top-left corner, the page header becomes part of the puzzle!

If you want to take a screenshot of your app for your marketplace submission on a real phone rather than the emulator, you can temporarily put code in your page that captures the screen and saves it to your pictures library as follows:

[code]

void CaptureScreen()
{
// Create a new bitmap with the page’s dimensions
WriteableBitmap wb = new WriteableBitmap((int)this.ActualWidth,
(int)this.ActualHeight);
// Render the page’s contents with no transform applied
wb.Render(this, null);
// We must explicitly tell the bitmap to draw its new contents
wb.Invalidate();
using (MemoryStream stream = new MemoryStream())
{
// Fill the stream with a JPEG representation of this bitmap
wb.SaveJpeg(stream, (int)this.ActualWidth, (int)this.ActualHeight,
0 /* orientation */, 100 /* quality */);
// Seek back to the beginning of the stream
stream.Seek(0, SeekOrigin.Begin);
// Requires referencing Microsoft.Xna.Framework.dll
// and the ID_CAP_MEDIALIB capability, and only works
// when the phone is not connected to Zune
new Microsoft.Xna.Framework.Media.MediaLibrary().SavePicture(
“screenshot.jpg”, stream);
}
}

[/code]

Once there, you can sync it to your desktop with Zune to retrieve the photo in its full resolution. This has a few important limitations, however:

  • It doesn’t capture parts of the user interface outside of the Silverlight visual tree—the application bar, status bar, and message boxes.
  • It doesn’t capture any popups, even ones that are attached to an element on the page.
  • It doesn’t capture any WebBrowser instances.
  • It doesn’t capture any MediaElement instances.

Also, you need to determine a way to invoke this code without impacting what you’re capturing.

Listing 42.4 doesn’t make any attempt to preserve the page’s state in the face of deactivation and reactivation; it simply relaunches the photo chooser.There are a few strategies for preserving the state of this page.The most natural would be to persist the image to a separate temporary file in isolated storage, along with values in page state that remember the current zoom and panning values.

What’s the difference between detecting dragging with gesture listener drag events versus using mouse down, mouse move, and mouse up events?

One major difference is that the drag events are only raised when one finger is in contact with the screen. With the mouse events (or with the multi-touch FrameReported event), you can base dragging on the primary finger and simply ignore additional touch points.This may or may not be a good thing, depending on your app. Because the preceding chapter uses the mouse events for panning, it gives the user the ability to do zooming and panning as a combined gesture, which mimics the behavior of the built-in Maps app. In Jigsaw Puzzle’s cropped photo chooser page, on the other hand, the user must lift their second finger if they wish to pan right after zooming the picture.

Another difference is that the drag events are not raised until the gesture listener is sure that a drag is occurring, e.g. one finger has made contact with the screen and has already moved a little bit. In contrast, the mouse move event is raised as soon as the finger moves at all. For Jigsaw Puzzle, the delayed behavior of the drag events is beneficial for helping to avoid accidental dragging.

A clear benefit of the drag events, if applicable to your app, is that the finger velocity at the end of the gesture is exposed to your code.However, you could still get this information when using the mouse approach if you also attach a handler to gesture listener’s Flick event.The Direction property exposed to the drag events, discussed earlier, also enables interesting behavior that is tedious to replicate on your own.

The Finished Product

Windows Phone Jigsaw Puzzle (Drag Gesture & WriteableBitmap)
Jigsaw Puzzle (Drag Gesture & WriteableBitmap)

Darts (Gesture Listener & Flick Gesture)

Darts is an addictive and slick game that provides a great reason to demonstrate support for the flick gesture (a single-finger, rapid swipe). To make detecting flicks easy, this app leverages a feature in the Silverlight for Windows Phone Toolkit called the gesture listener.

In this one-player game, you throw a dart by flicking the screen. The direction and strength of your flick determines the angle and distance of the throw. Depending on where the dart lands, you could earn 0–60 points with each throw. Try to get the highest score possible with 20 darts!

This game has a lot of potential for easy-to-add enhancements. For some examples, check out my “Hooked on Darts” version of this app in the Windows Phone Marketplace.

Detecting Gestures

The three preceding apps in this part of the book leverage touch points in a straightforward fashion. Often, however, you want to support standard gestures such as flicks, pinch-and-stretch zooming, and rotation. Detecting such gestures based on raw touch point data from the FrameReported event would be a major undertaking.

Fortunately, two options exist that make detecting gestures much easier: Silverlight’s manipulation events and the Silverlight for Windows Phone Toolkit’s gesture listener.

Manipulation Events

Silverlight defines three manipulation events on every UI element:

  • ManipulationStarted
  • ManipulationDelta
  • ManipulationCompleted

These events combine the information from all active touch points and package the data in an easy-to-consume form. ManipulationStarted gets raised when the TouchDown action happens for the first finger. ManipulationDelta gets raised for each TouchMove. ManipulationCompleted gets raised after TouchUp is reported for all fingers.

The ManipulationDelta event gives you information about how the relevant element is expected to be translated or scaled based on a one-finger panning gesture or a two-finger pinch or stretch. The ManipulationDelta and ManipulationCompleted events also provide translation and scale velocities in both X and Y dimensions.

Gesture Listener

Although the manipulation events are helpful for detecting gestures, the Silverlight for Windows Phone Toolkit’s gesture listener raises the bar for simplicity and ease-of-use. In fact, all the remaining apps in this part of the book use the gesture listener instead of the manipulation events.

The GestureListener class exposes nothing but 12 events for detecting 6 types of gestures:

  • Tap (with the Tap event)
  • Double Tap (with the DoubleTap event)
  • Touch & Hold (with the Hold event)
  • Flick (with the Flick event)
  • Pinch & Stretch (with the PinchStarted, PinchDelta, and PinchCompleted events)
  • Drag (with the DragStarted, DragDelta, and DragCompleted events)

The remaining two events— GestureBegin and GestureCompleted— are generic events raised for any gestures, analogous to the ManipulationStarted and ManipulationCompleted events.

The Tap event is similar to MouseLeftButtonUp, except that it only gets raised if there’s no movement between the finger press and release.

A gesture listener can be attached to any element with the GestureService class, which exposes a GestureListener attachable property. For example, the following XAML detects a touch & hold gesture (pressing an element for one second) on a text block:

[code]

<TextBlock …>
<toolkit:GestureService.GestureListener>
<toolkit:GestureListener Hold=”GestureListener_Hold”/>
</toolkit:GestureService.GestureListener>
</TextBlock>

[/code]

The gesture listener can cause performance problems!

As of the February 2011 version of the Silverlight for Windows Phone Toolkit, the gesture listener internally uses XNA’s TouchPanel class to detect gestures.However, this can cause performance problems and can even interfere with the input received by other Silverlight controls.Therefore, until these issues are resolved in a future version of the toolkit, you should avoid using the gesture listener (or the XNA TouchPanel) in traditional apps with typical controls.Darts and the other apps in this book that use the gesture listener do not have such problems.

The User Interface

Darts has a main page, an instructions page, and an about page. The latter two pages aren’t interesting and therefore aren’t shown in this chapter, but Listing 40.1 contains the XAML for the main page. The main page has an “intro panel” canvas that does triple duty as a welcome screen, a game-over screen, as well as a paused screen. Underneath the intro panel is a canvas with all the game elements. Figure 40.1 shows the intro panel in its initial state and then the appearance of the page once the user flicks the screen to start the game.

The main page overlays an intro panel on top of the game canvas.
FIGURE 40.1 The main page overlays an intro panel on top of the game canvas.

LISTING 40.1 MainPage.xaml—The User Interface for Darts’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”
xmlns:toolkit=”clr-namespace:Microsoft.Phone.Controls;
➥assembly=Microsoft.Phone.Controls.Toolkit”
FontFamily=”Segoe WP Black” FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”White” SupportedOrientations=”Portrait”>
<!– Allow the gesture anywhere on the page –>
<toolkit:GestureService.GestureListener>
<toolkit:GestureListener Flick=”GestureListener_Flick”/>
</toolkit:GestureService.GestureListener>
<!– Add several animations to the page’s resource dictionary –>
<phone:PhoneApplicationPage.Resources>
<!– The dart-throwing animation, adjusted from code-behind based on
the angle and velocity of the flick –>
<Storyboard x:Name=”DartThrowStoryboard”
Completed=”DartThrowStoryboard_Completed”>
<!– Animate the horizontal position –>
<DoubleAnimation x:Name=”DartXAnimation”
Storyboard.TargetName=”DartTransform”
Storyboard.TargetProperty=”TranslateX” Duration=”0:0:.5”>
<DoubleAnimation.EasingFunction>
<CircleEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<!– Animate the vertical position –>
<DoubleAnimation x:Name=”DartYAnimation”
Storyboard.TargetName=”DartTransform”
Storyboard.TargetProperty=”TranslateY” Duration=”0:0:.5”>
<DoubleAnimation.EasingFunction>
<CircleEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<!– Animate the horizontal scale –>
<DoubleAnimation x:Name=”DartScaleXAnimation”
Storyboard.TargetName=”DartTransform”
Storyboard.TargetProperty=”ScaleX” By=”1.2”
Duration=”0:0:.25” AutoReverse=”True”>
<DoubleAnimation.EasingFunction>
<CircleEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<!– Animate the vertical scale –>
<DoubleAnimation x:Name=”DartScaleYAnimation”
Storyboard.TargetName=”DartTransform”
Storyboard.TargetProperty=”ScaleY” By=”4”
Duration=”0:0:.25” AutoReverse=”True”>
<DoubleAnimation.EasingFunction>
<CircleEase/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
<!– Leave the dart in its ending position for half a second before returning
it to the bottom of the page for another throw –>
<Storyboard x:Name=”DartDelayStoryboard” Duration=”0:0:.5”
Completed=”DartDelayStoryboard_Completed”/>
<!– Move the dart back to the starting position –>
<Storyboard x:Name=”DartReturnStoryboard”>
<DoubleAnimation Storyboard.TargetName=”DartTransform”
Storyboard.TargetProperty=”TranslateX” To=”0”
Duration=”0:0:.2”>
<DoubleAnimation.EasingFunction>
<QuinticEase EasingMode=”EaseInOut”/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
<DoubleAnimation Storyboard.TargetName=”DartTransform”
Storyboard.TargetProperty=”TranslateY” To=”0”
Duration=”0:0:.2”>
<DoubleAnimation.EasingFunction>
<QuinticEase EasingMode=”EaseInOut”/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
<!– Slide the dart completely off the bottom of the screen –>
<Storyboard x:Name=”DartOffScreenStoryboard”>
<DoubleAnimation Storyboard.TargetName=”DartTransform”
Storyboard.TargetProperty=”TranslateY” To=”130”
Duration=”0:0:.6”>
<DoubleAnimation.EasingFunction>
<QuinticEase EasingMode=”EaseInOut”/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
<!– Slide the intro panel off the top of the screen –>
<Storyboard x:Name=”IntroOffStoryboard”
Completed=”IntroOffStoryboard_Completed”>
<DoubleAnimation Storyboard.TargetName=”IntroPanelTransform”
Storyboard.TargetProperty=”TranslateY” To=”-800”
Duration=”0:0:1”>
<DoubleAnimation.EasingFunction>
<QuinticEase EasingMode=”EaseInOut”/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
<!– Slide the intro panel back onto the screen from up above –>
<Storyboard x:Name=”IntroOnStoryboard”>
<DoubleAnimation Storyboard.TargetName=”IntroPanelTransform”
Storyboard.TargetProperty=”TranslateY” To=”0”
Duration=”0:0:1”>
<DoubleAnimation.EasingFunction>
<QuinticEase EasingMode=”EaseInOut”/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
… more storyboards …
<!– Animate in (then out) a message, such as the # of points just earned –>
<Storyboard x:Name=”ShowMessageStoryboard”
Storyboard.TargetName=”MessageTransform”>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty=”TranslateY”>
<DiscreteDoubleKeyFrame KeyTime=”0:0:0” Value=”800”/>
<EasingDoubleKeyFrame KeyTime=”0:0:.5” Value=”430”>
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
<DiscreteDoubleKeyFrame KeyTime=”0:0:2.5” Value=”430”/>
<EasingDoubleKeyFrame KeyTime=”0:0:3” Value=”-800”>
<EasingDoubleKeyFrame.EasingFunction>
<QuadraticEase/>
</EasingDoubleKeyFrame.EasingFunction>
</EasingDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</phone:PhoneApplicationPage.Resources>
<!– The application bar, with two buttons and one menu item –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar BackgroundColor=”#5F3000” ForegroundColor=”White”
Opacity=”.8”>
<shell:ApplicationBarIconButton Text=”instructions”
IconUri=”/Shared/Images/appbar.instructions.png”
Click=”InstructionsButton_Click”/>
<shell:ApplicationBarIconButton Text=”delete”
IconUri=”/Shared/Images/appbar.delete.png” Click=”DeleteButton_Click”/>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”about” Click=”AboutMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<Grid Background=”#5F3000”>
<Grid.Clip>
<RectangleGeometry Rect=”0,0,480,800”/>
</Grid.Clip>
<!– The game canvas –>
<Canvas>
<!– The dartboard background –>
<Ellipse Width=”480” Height=”480” Fill=”#8000”/>
<!– The dartboard segments –>
<Canvas x:Name=”DartboardSegments”>
<!– Vector art created in Expression Blend. Each region with a different
point value is a distinct Path element (82 total) –>
<Path x:Name=”D1” Data=”M268.269483095,61.50827908C287.000166325,
64.476988515,305.148564115,70.37132529,322.042831735,
78.980320525L240.000027675,240.000437465z” Fill=”Green”
Canvas.Left=”239.417” Canvas.Top=”60.925” Stretch=”Fill”
Width=”83.209” Height=”179.658”/>
… 81 more paths …
</Canvas>
<!– The dartboard numbers –>
<Canvas Opacity=”.5”>
<TextBlock Text=”20” Canvas.Left=”218” Canvas.Top=”1”
FontFamily=”Segoe WP” FontSize=”40”/>
… 19 more text blocks …
</Canvas>
<!– Each time a dart lands, a little black “hole” is placed in this
canvas to mark the position –>
<Canvas x:Name=”HolesCanvas”/>
<!– The dart –>
<Image x:Name=”DartImage” Canvas.Top=”675” Canvas.Left=”202”
Width=”100”>
<Image.RenderTransform>
<CompositeTransform x:Name=”DartTransform” TranslateY=”130”/>
</Image.RenderTransform>
</Image>
</Canvas>
<!– A display for the current score and # of darts remaining –>
<StackPanel x:Name=”ScorePanel” Visibility=”Collapsed”
VerticalAlignment=”Bottom” HorizontalAlignment=”Right”
Margin=”18,64”>
<TextBlock Text=”SCORE” Foreground=”{StaticResource PhoneSubtleBrush}”
HorizontalAlignment=”Right”/>
<TextBlock x:Name=”ScoreTextBlock” HorizontalAlignment=”Right”
FontSize=”{StaticResource PhoneFontSizeExtraExtraLarge}”
Margin=”0,-20,0,0”>
<TextBlock.RenderTransform>
<CompositeTransform x:Name=”ScoreTransform”/>
</TextBlock.RenderTransform>
</TextBlock>
<TextBlock Text=”DARTS REMAINING”
Foreground=”{StaticResource PhoneSubtleBrush}”
HorizontalAlignment=”Right”/>
<TextBlock x:Name=”DartsRemainingTextBlock” HorizontalAlignment=”Right”
FontSize=”{StaticResource PhoneFontSizeExtraExtraLarge}”
Margin=”0,-20,0,0”>
<TextBlock.RenderTransform>
<CompositeTransform x:Name=”DartsRemainingTransform”/>
</TextBlock.RenderTransform>
</TextBlock>
</StackPanel>
<!– The welcome/paused/game-over screen –>
<Grid x:Name=”IntroPanel”>
<Grid.RenderTransform>
<CompositeTransform x:Name=”IntroPanelTransform”/>
</Grid.RenderTransform>
<Rectangle Fill=”#5F3000” Opacity=”.6”/>
<!– The title and subtitle –>
<TextBlock Text=”DARTS” Margin=”-19,200,0,0” FontSize=”140”>
<TextBlock.RenderTransform>
<RotateTransform Angle=”-20”/>
</TextBlock.RenderTransform>
</TextBlock>
<TextBlock x:Name=”Subtitle” Text=”DO A PRACTICE FLICK TO BEGIN”
Margin=”40,340,-40,0” FontSize=”27”>
<TextBlock.RenderTransform>
<RotateTransform Angle=”-20”/>
</TextBlock.RenderTransform>
</TextBlock>
<!– A display for the best score, average score, and # of games –>
<StackPanel x:Name=”StatsPanel” VerticalAlignment=”Bottom”
HorizontalAlignment=”Right” Margin=”18,64”>
<TextBlock Text=”BEST SCORE” Foreground=”{StaticResource PhoneSubtleBrush}”
HorizontalAlignment=”Right”/>
<TextBlock x:Name=”BestScoreTextBlock” HorizontalAlignment=”Right”
FontSize=”{StaticResource PhoneFontSizeExtraExtraLarge}”
Margin=”0,-20,0,0”>
<TextBlock.RenderTransform>
<CompositeTransform x:Name=”BestScoreTransform”/>
</TextBlock.RenderTransform>
</TextBlock>
<TextBlock x:Name=”AvgScoreHeaderTextBlock” Text=”AVG SCORE”
Foreground=”{StaticResource PhoneSubtleBrush}”
HorizontalAlignment=”Right”/>
<TextBlock x:Name=”AvgScoreTextBlock” HorizontalAlignment=”Right”
FontSize=”{StaticResource PhoneFontSizeExtraExtraLarge}”
Margin=”0,-20,0,0”>
<TextBlock.RenderTransform>
<CompositeTransform x:Name=”AvgScoreTransform”/>
</TextBlock.RenderTransform>
</TextBlock>
</StackPanel>
</Grid>
<!– An animated message –>
<TextBlock x:Name=”MessageTextBlock” RenderTransformOrigin=”.5,.5”
FontWeight=”Bold” HorizontalAlignment=”Center” FontSize=”120”>
<TextBlock.RenderTransform>
<CompositeTransform x:Name=”MessageTransform” TranslateY=”800”/>
</TextBlock.RenderTransform>
</TextBlock>
</Grid>
</phone:PhoneApplicationPage>

[/code]

  • To detect the flick gesture, this app handles the Flick event of a gesture listener attached to the whole page.
  • This page contains lots of storyboards for smoothly transitioning elements on and off the screen. The most important one is DartThrowStoryboard, which makes the dart travel based on the flick data. Its first two animations, which alter the dart transform’s TranslateX and TranslateY values, have their By values set in code-behind. The second two animations, which alter ScaleX and ScaleY, make the dart appear to jump out of and then back into the screen in a pseudo-3D arc motion. ScaleY is increased much more than ScaleX to make the image appear to flatten out, because the image already has Y-rotation perspective applied to it. The next section shows how the in-flight dart appears.
  • This app’s dartboard is one of the more interesting chunks of XAML in the whole book. It consists of 82 path elements and 19 text blocks on top of an ellipse. (Although I usually type all my XAML by hand, I created this part in Expression Blend!) Representing each region with a unique score value as a distinct element makes it easy to figure out how many points to award the user when the dart lands. The code-behind performs hit testing to find the topmost element hit by the dart and then awards points accordingly.

    Why are there 82 paths? Because there are 4 for each of the 20 wedges (the outer ring that awards double points, the outer “normal” wedge, the inner ring that awards triple points, and then the inner “normal” wedge), plus the outer and inner bull’s-eyes. Figure 40.2 illustrates how these 82 elements stack to form the final dartboard, as if the elements were separated and viewed at a 3D perspective.

  • The DartImage element doesn’t have its Source assigned. This is done in codebehind, because the source is continually changed between three different images.
  • The root grid has a clip applied to avoid a far-flung dart from causing the screen to go blank. This happens whenever Silverlight tries to render an element that is too far away.
A visualization of the 82 distinct dartboard segments.
FIGURE 40.2 A visualization of the 82 distinct dartboard segments.

The Code-Behind

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

LISTING 40.2 MainPage.xaml.cs—The Code-Behind for Darts’Main Page

[code]

using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
The Code-Behind 885
{
// Persistent settings
Setting<int> bestScore = new Setting<int>(“BestScore”, 0);
Setting<double> avgScore = new Setting<double>(“AvgScore”, 0);
Setting<int> numGames = new Setting<int>(“NumGames”, 0);
// Current game state
int dartsRemaining;
int score;
// Three different dart images
BitmapImage dartStartImageSource =
new BitmapImage(new Uri(“Images/dartStart.png”, UriKind.Relative));
BitmapImage dartBigImageSource =
new BitmapImage(new Uri(“Images/dartBig.png”, UriKind.Relative));
BitmapImage dartSmallImageSource =
new BitmapImage(new Uri(“Images/dartSmall.png”, UriKind.Relative));
public MainPage()
{
InitializeComponent();
// Use the starting dart image
this.DartImage.Source = this.dartStartImageSource;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Respect the persisted values
UpdateStatsLabels();
}
protected override void OnBackKeyPress(CancelEventArgs e)
{
base.OnBackKeyPress(e);
// If the game is in progress, pause it rather than exiting the app
if (this.IntroPanel.Visibility == Visibility.Collapsed)
{
this.Subtitle.Text =
“PAUSED.nFLICK TO RESUME THE GAME,nOR PRESS BACK TO EXIT.”;
ShowIntroPanel();
e.Cancel = true;
}
}
// The event handler for flicks
void GestureListener_Flick(object sender, FlickGestureEventArgs e)
{
if (this.IntroPanel.Visibility == Visibility.Visible)
{
// Start a new game if we’re out of darts
if (this.dartsRemaining == 0)
{
// Remove all holes and clear the tracking variables
this.HolesCanvas.Children.Clear();
this.dartsRemaining = 20;
this.score = 0;
// Animate the off-screen dart to the starting position
this.DartReturnStoryboard.Begin();
// Animate a message
this.MessageTextBlock.Text = “GO!”;
this.ShowMessageStoryboard.Begin();
}
// Remove the intro panel (for both new-game and unpause cases)
HideIntroPanel();
}
else
{
// First ensure that a dart throw is not in progress
if (this.DartThrowStoryboard.GetCurrentState() == ClockState.Active ||
this.DartDelayStoryboard.GetCurrentState() == ClockState.Active)
return;
// Throw the dart!
this.DartTransform.TranslateX = 0;
this.DartTransform.TranslateY = 0;
// Here is where the flick data is used
this.DartXAnimation.By = e.HorizontalVelocity / 10;
this.DartYAnimation.By = e.VerticalVelocity / 10;
// The animation scales the image up by as much as 4 vertically, so
// replace the image with a higher-resolution one
this.DartImage.Source = this.dartBigImageSource;
this.DartThrowStoryboard.Begin();
}
}
void IntroOffStoryboard_Completed(object sender, EventArgs e)
{
// The intro panel is now off-screen, so collapse it for better performance
this.IntroPanel.Visibility = Visibility.Collapsed;
}
void DartThrowStoryboard_Completed(object sender, EventArgs e)
{
// The dart has landed, so change the image to a small one
// that looks better that this scale (and is angled like it’s sticking into
// the wall)
this.DartImage.Source = this.dartSmallImageSource;
// Determine the exact point where the tip of the dart has landed
Point point = new Point(
// X: The tip isn’t quite centered in the image, hence the slight offset
Canvas.GetLeft(this.DartImage) + this.DartTransform.TranslateX
+ this.DartImage.Width / 2 – 11.5,
// Y: The tip is at the top edge of the image
Canvas.GetTop(this.DartImage) + this.DartTransform.TranslateY
);
// Place a “hole” where the tip of the dart landed
Ellipse hole = new Ellipse { Fill = new SolidColorBrush(Colors.Black),
Width = 5, Height = 5 };
Canvas.SetLeft(hole, point.X – hole.Width / 2);
Canvas.SetTop(hole, point.Y – hole.Height / 2);
this.HolesCanvas.Children.Add(hole);
// Calculate the score for this throw
int pointsEarned = DetermineScoreAtPoint(point);
// Update the game state and display
this.score += pointsEarned;
this.dartsRemaining–;
UpdateScoreLabels(pointsEarned > 0);
// Let the dart sit for a while before it animates back to the start
this.DartDelayStoryboard.Begin();
if (this.dartsRemaining > 0)
{
// Animate in the score message
this.MessageTextBlock.Text = pointsEarned.ToString();
this.ShowMessageStoryboard.Begin();
}
else
{
// Game over!
// Update the stats
double oldTotal = this.avgScore.Value * this.numGames.Value;
// New average
this.avgScore.Value = (oldTotal + score) / (this.numGames.Value + 1);
// New total number of games
this.numGames.Value++;
this.Subtitle.Text = “FINAL SCORE: “ + this.score + “. FLICK AGAIN!”;
if (this.score > this.bestScore.Value)
{
// New best score
this.bestScore.Value = this.score;
// Animate a best-score message
this.MessageTextBlock.Text = “BEST!”;
this.ShowMessageStoryboard.Begin();
}
ShowIntroPanel();
}
}
void DartDelayStoryboard_Completed(object sender, EventArgs e)
{
// Restore the image
this.DartImage.Source = this.dartStartImageSource;
// Move the dart to the starting position for the next throw,
// or off-screen if the game is over
if (this.dartsRemaining > 0)
this.DartReturnStoryboard.Begin();
else
this.DartOffScreenStoryboard.Begin();
}
int DetermineScoreAtPoint(Point p)
{
// Retrieve all elements inside DartboardSegments that intersect with p
foreach (UIElement element in
VisualTreeHelper.FindElementsInHostCoordinates(p,
this.DartboardSegments))
{
// Fortunately, the list of elements is ordered from top-to-bottom.
// We only care about the top-most element, so we can directly return a
// value based on the first item in this collection.
if (element == this.InnerBull)
return 50;
else if (element == this.OuterBull)
return 25;
else if (element is Path)
{
// The elements are named with a letter and a number.
// The letter is D for double score, T for triple score, or something
// else (A or B) for a normal region.
// The number is the score.
string name = (element as FrameworkElement).Name;
// Retrieve the score from the name
int score = int.Parse(name.Substring(1));
// Apply a multiplier, if applicable
if (name[0] == ‘D’)
score *= 2;
else if (name[0] == ‘T’)
score *= 3;
return score;
}
}
// No relevant element was hit
return 0;
}
void ShowIntroPanel()
{
UpdateStatsLabels();
this.IntroOnStoryboard.Begin();
this.IntroPanel.Visibility = Visibility.Visible;
this.ScorePanel.Visibility = Visibility.Collapsed;
this.ApplicationBar.IsVisible = true;
}
void HideIntroPanel()
{
UpdateScoreLabels(true);
this.IntroOffStoryboard.Begin();
this.ScorePanel.Visibility = Visibility.Visible;
this.ApplicationBar.IsVisible = false;
}
void UpdateStatsLabels()
{
if (this.numGames.Value > 0)
{
this.BestScoreTextBlock.Text = this.bestScore.Value.ToString();
this.AvgScoreTextBlock.Text = this.avgScore.Value.ToString(“0.#”);
if (this.numGames.Value == 1)
this.AvgScoreHeaderTextBlock.Text = “AVG SCORE (1 GAME)”;
else
this.AvgScoreHeaderTextBlock.Text = “AVG SCORE (“ + this.numGames.Value
+ “ GAMES)”;
}
else
{
this.BestScoreTextBlock.Text = “0”;
this.AvgScoreTextBlock.Text = “0”;
this.AvgScoreHeaderTextBlock.Text = “AVG SCORE”;
}
// Animate the textblocks out then in
this.SlideAvgScoreStoryboard.Begin();
this.SlideBestScoreStoryboard.Begin();
}
void UpdateScoreLabels(bool animateScore)
{
this.ScoreTextBlock.Text = this.score.ToString();
this.DartsRemainingTextBlock.Text = this.dartsRemaining.ToString();
// Animate the textblocks out then in
this.SlideDartsRemainingStoryboard.Begin();
if (animateScore)
this.SlideScoreStoryboard.Begin();
}
// Application bar handlers
void InstructionsButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/InstructionsPage.xaml”,
UriKind.Relative));
}
void DeleteButton_Click(object sender, EventArgs e)
{
if (MessageBox.Show(“Are you sure you want to clear your scores?”,
“Delete history”, MessageBoxButton.OKCancel) == MessageBoxResult.OK)
{
this.numGames.Value = 0;
this.bestScore.Value = 0;
UpdateStatsLabels();
}
}
void AboutMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(
“/Shared/About/AboutPage.xaml?appName=Darts”, UriKind.Relative));
}
}
}

[/code]

  • Three dart image sources are created (once) and then assigned as DartImage’s Source value at various stages of the dart-throwing procedure. Figure 40.3 shows all three. The start image looks more vertical, whereas the big and small images have a lot more perspective. The small image is used while the dart is sticking into the dartboard (or wall). The big image is used only while the dart is in flight, because otherwise the up-scaling of the small image would produce a pixelated result. The combination of DartThrowStoryboard’s motion and the image-swapping produces a pretty realistic pseudo-3D dart motion demonstrated in Figure 40.4.
The three dart images are swapped in and out to provide the best-looking result.
FIGURE 40.3 The three dart images are swapped in and out to provide the best-looking result.
The dart’s flight path when it hits a double 3 (6 points).
FIGURE 40.4 The dart’s flight path when it hits a double 3 (6 points).
  • OnBackKeyPress is overridden to show a paused screen when the back key is pressed during a game, canceling the exiting of the app. When the intro panel is shown as a paused screen or game-over screen, the text under the “DARTS” title is updated to explain what is going on, as shown in Figure 40.5.
The intro screen subtitle updates to show that the game is paused or to show the final score.
FIGURE 40.5 The intro screen subtitle updates to show that the game is paused or to show the final score.

 

  • GestureListener_Flick is the flick handler that processes the data from FlickGestureEventArgs and adjusts the two relevant dartthrowing animations appropriately. The Flick event is described in the next section.
  • DetermineScoreAtPoint uses a static method called FindElementsInHostCoordinates on System.Windows.Media. VisualTreeHelper to find the topmost element underneath the passed-in point. (FindElementsInHostCoordinates also exposes an overload that accepts a rectangular region rather than a point.) Because of the naming scheme used by the dartboard elements, the hit element’s name can be converted into the correct point value.

Your game might fail marketplace certification if pressing the Back hardware button doesn’t pause the game!

The Windows Phone 7 Certification Requirements (http://go.microsoft.com/ ?linkid=9730558) state:

For games, when the Back button is pressed during gameplay, the game can choose to present a pause context menu or dialog or navigate the user to the prior menu screen. Pressing the Back button again while in a paused context menu or dialog closes the menu or dialog.

Although this makes the pausing behavior seem optional, I’ve had a game fail certification because it chose not to provide a pausing experience. (And this was a pool game in which pausing is meaningless!) Even if you can get away without doing this, it’s probably a good idea to have your app include this pausing behavior for consistency with user expectations for Windows Phone games.

VisualTreeHelper.FindElementsInHostCoordinates is the best way to determine what element or elements are underneath a specific point or region. Unlike approaches from previous chapters that involve mapping a point into an element’s coordinate space one element at a time, FindElementsInHostCoordinates examines all elements that are children of the passed-in element. (It also examines the passed-in element itself.)

The Flick Event

The FlickGestureEventArgs instance passed to the Flick event exposes the following properties:

  • Angle—A double value expressed in degrees
  • Direction—Either Horizontal or Vertical, revealing the primary direction of the flick
  • HorizontalVelocity—A double value expressing the horizontal magnitude of the 2D velocity vector in pixels per second
  • VerticalVelocity—A double value expressing the vertical magnitude of the 2D velocity vector in pixels per second

This app has no use for the Direction property, which is meant for elements whose motion should be locked to a single direction when the flick occurs. Instead, this app’s dart freely moves in whatever angle the flick reports, even if it’s completely horizontal or downward instead of upward.

Because Listing 40.2 applies the horizontal and vertical velocities as offsets to the dart’s position, it doesn’t even need to check the Angle property. The ratio between the two velocities effectively gives the appropriate angle. When set as offsets to the dart’s position, the velocity values are divided by 10 to give a distance range that works well for this app. This factor was arrived at by trial and error.

A flick event is not raised until the finger has broken contact with the screen. Before then, the finger motion raises drag (and other) events.

If you want to detect flicks without using the gesture listener (perhaps to avoid the problems discussed at the beginning of this chapter), you can use the ManipulationCompleted event defined on all UI elements.The data passed to its handlers includes a FinalVelocities property that is equivalent to the pair of HorizontalVelocity and VerticalVelocity properties exposed to Flick event handlers.

The Finished Product

Darts (Gesture Listener & Flick Gesture)

Paint (Ink Presenter)

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)

 

 

 

The Code-Behind

Listing 2.2 contains the code-behind, which must handle all the special features of this flashlight—strobe mode, SOS mode, and various colors.

LISTING 2.2 MainPage.xaml.cs—The Code-Behind for Flashlight

[code]

using System;
using System.Reflection;
using System.Windows;
using System.Windows.Media;
using System.Windows.Threading;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
// Members for the two application bar buttons:
IApplicationBarIconButton sosButton;
IApplicationBarIconButton strobeButton;
// For the two special modes:
SolidColorBrush onBrush;
SolidColorBrush offBrush = new SolidColorBrush(Colors.Black);
DispatcherTimer strobeTimer = new DispatcherTimer();
DispatcherTimer sosTimer = new DispatcherTimer();
int sosStep;
// Remember the chosen color, for future app activations or launches:
Setting<Color> savedColor = new Setting<Color>(“SavedColor”, Colors.White);
// The current mode (Solid, Sos, or Strobe)
FlashlightMode mode = FlashlightMode.Solid;
public MainPage()
{
InitializeComponent();
// Assign application bar buttons to member fields, because this cannot be
// done by InitializeComponent:
this.sosButton = this.ApplicationBar.Buttons[0]
as IApplicationBarIconButton;
this.strobeButton = this.ApplicationBar.Buttons[1]
as IApplicationBarIconButton;
// Initialize the timer for strobe mode
this.strobeTimer.Interval = TimeSpan.FromSeconds(.1); // Not too fast!
this.strobeTimer.Tick += StrobeTimer_Tick;
// Initialize the timer for SOS mode
this.sosTimer.Interval = TimeSpan.Zero;
this.sosTimer.Tick += SosTimer_Tick;
// Attach the same Click handler to all menu items in the application bar
foreach (IApplicationBarMenuItem menuItem in this.ApplicationBar.MenuItems)
menuItem.Click += MenuItem_Click;
// Restore persisted color
this.onBrush = new SolidColorBrush(this.savedColor.Value);
this.BackgroundGrid.Background = onBrush;
}
// The menu item Click handler that changes the flashlight color
void MenuItem_Click(object sender, EventArgs e)
{
// Grab the text from the menu item to determine the desired color
string chosenColor = (sender as IApplicationBarMenuItem).Text;
// Use reflection to turn the color name (e.g. “red”) into an actual Color
Color c = (Color)typeof(Colors).GetProperty(chosenColor,
BindingFlags.Public | BindingFlags.Static | BindingFlags.IgnoreCase).
GetValue(null, null);
// Persist this choice and set the background color
this.savedColor.Value = c;
this.onBrush = new SolidColorBrush(this.savedColor.Value);
this.BackgroundGrid.Background = onBrush;
}
// The Click handler for the strobe button
void StrobeButton_Click(object sender, EventArgs e)
{
// First, reset the current state to solid mode
FlashlightMode mode = this.mode;
RestoreSolidMode();
// If we were already in strobe mode, then this click
// cancels it and we are done
if (mode == FlashlightMode.Strobe)
return;
// Show a warning
MessageBoxResult result = MessageBox.Show(“Strobe lights can trigger “ +
“seizures for people with photosensitive epilepsy. “ +
“Are you sure you want to start the strobe light?”,
“Warning!”, MessageBoxButton.OKCancel);
// If the user agreed, change to strobe mode
if (result == MessageBoxResult.OK)
{
// Change the button icon, the mode, and start the timer
(sender as IApplicationBarIconButton).IconUri =
new Uri(“Images/cancel.png”, UriKind.Relative);
this.mode = FlashlightMode.Strobe;
this.strobeTimer.Start();
}
}
void StrobeTimer_Tick(object sender, EventArgs e)
{
// Toggle the background on every tick
if (this.BackgroundGrid.Background == this.onBrush)
this.BackgroundGrid.Background = this.offBrush;
else
this.BackgroundGrid.Background = this.onBrush;
}
// The Click handler for the SOS button
void SosButton_Click(object sender, EventArgs e)
{
// First, reset the current state to solid mode
FlashlightMode mode = this.mode;
RestoreSolidMode();
// If we were already in SOS mode, then this click
// cancels it and we are done
if (mode == FlashlightMode.Sos)
return;
// Change to SOS mode
// Change the button icon, the mode, a counter, and start the timer
(sender as IApplicationBarIconButton).IconUri =
new Uri(“Images/cancel.png”, UriKind.Relative);
this.mode = FlashlightMode.Sos;
this.sosStep = 0;
this.sosTimer.Start();
}
void SosTimer_Tick(object sender, EventArgs e)
{
// Toggle the background, but also adjust the time between each tick in
// order to make the dot-dot-dot-dash-dash-dash-dot-dot-dot pattern
switch (this.sosStep)
{
case 1: case 3: case 5: // Each dot in the first S
case 13: case 15: case 17: // Each dot in the second S
this.BackgroundGrid.Background = this.onBrush;
this.sosTimer.Interval = TimeSpan.FromSeconds(.2); // A short value
break;
case 7: case 9: case 11: // Each dash in the O
this.BackgroundGrid.Background = this.onBrush;
this.sosTimer.Interval = TimeSpan.FromSeconds(1); // A long value
break;
case 18: // The space between the end of one SOS
// and the beginning of the next one
this.BackgroundGrid.Background = this.offBrush;
this.sosTimer.Interval = TimeSpan.FromSeconds(1);
break;
default: // The space between each dot/dash
this.BackgroundGrid.Background = this.offBrush;
this.sosTimer.Interval = TimeSpan.FromSeconds(.2);
break;
}
// Cycle from 0 – 18
this.sosStep = (this.sosStep + 1) % 19;
}
// Reset the state associated with mode switches
void RestoreSolidMode()
{
this.strobeTimer.Stop();
this.sosTimer.Stop();
this.BackgroundGrid.Background = onBrush;
this.sosButton.IconUri = new Uri(“Images/sos.png”, UriKind.Relative);
this.strobeButton.IconUri = new Uri(“Images/strobe.png”, UriKind.Relative);
this.mode = FlashlightMode.Solid;
}
// All three modes
enum FlashlightMode
{
Solid,
Sos,
Strobe
}
}
}

[/code]

Notes:

  • The sosButton and strobeButton fields are explicitly assigned by referencing items in the application bar’s Buttons collection because the XAML naming approach is not supported for items in the application bar, as described in an earlier warning. Assigning such member variables right after InitializeComponent is a nice practice to avoid hardcoded indices scattered throughout your code.
  • Two DispatcherTimers are used to perform the SOS and strobe on/off patterns. This technique is described in an upcoming sidebar.
  • Rather than setting all eight menu item Click event handlers in XAML, the constructor loops through the MenuItems collection and assigns MenuItem_Click to each item. This shortcut works nicely in this case because the same handler is able to work for all menu items.
  • Once again, an instance of the Setting class is used to remember the user’s color preference for subsequent usage of the app. Because BackgroundGrid’s background is set in the code-behind, the “White” setting in XAML in Listing 2.1 is actually unnecessary.
  • To work for every menu item, the MenuItem_Click handler must figure out which color has just been selected. To do this, it retrieves the label from the menu item that was just tapped (passed as the sender) then uses a .NET reflection trick to turn the string into an instance of the Color class. This works thanks to the static Colors class that
    contains named properties for several color instances. Note that this trick only works for the small set of colors represented by this class. You could define your own Colors class with a different set of properties, however, if you wanted to do this with different colors.
  • In this app, the chosen color setting is persisted as soon as a new one is selected (inside MenuItem_Click). This is unlike the previous app, in which this action was only done when the page was about to be departed (inside OnNavigatedFrom).
  • The sender in MenuItem_Click is cast to IApplicationBarMenuItem (an interface implemented by ApplicationBarMenuItem) rather than directly to ApplicationBarMenuItem. A similar thing is done in later event handlers with IApplicationBarIconButton when the sender is an ApplicationBarIconButton. The
    Windows Phone team prefers that code references the interfaces rather than the concrete classes to allow for future flexibility, although this is only important for class libraries that don’t also instantiate the concrete button and menu item instances (as this app does in its XAML).
  • The strobe button Click handler, StrobeButton_Click, shows a standard message box to guard against accidental activation of strobe mode and to educate the user about the danger of strobe lights.
  • The Tick event handler for the strobe timer simply toggles the background between the “on” brush (white, unless the user changed the color) and the “off” brush (black). Once the strobe timer has been started, this is called every .1 seconds, as configured in this page’s constructor. Although you could certainly make this toggle more frequently, I would strongly caution against it. When I tried a value of .05 seconds instead, I got a bad headache after a quick test!
  • Both button Click handlers temporarily change the button’s icon to a cancel image because a subsequent click to the button returns the flashlight to solid mode. They use the sender to access the tapped button just for demonstration, as they could have easily used the strobeButton and sosButton fields instead.
  • The Tick event handler for the SOS timer is more complicated than the handler for the strobe timer, as it has to
    repeatedly produce the Morse code pattern for the SOS distress signal (dot-dot-dot-dash-dash-dash-dotdot-
    dot). It dynamically adjusts the timer’s interval to achieve this effect.
  • The capitalization of Sos in the code follows a .NET Framework coding guideline of avoiding more than two consecutive capital letters, even for a well-known abbreviation. You can see this practice throughout the .NET
    Framework APIs, with terms such as Uri, Xml, Xaml, and so on. Note that this guideline does not apply to labels inside user interfaces!

DispatcherTimer and Other Time-Based Approaches for Executing Code

DispatcherTimer, used by Flashlight, is the most natural timer to use in a Silverlight app.You can start and stop it at any time, customize its frequency with its Interval property, and handle its Tick event to perform work at the chosen interval. Event handlers for Tick are guaranteed to be called on the UI thread, so code inside these handlers can manipulate elements on the page the same way this is done everywhere else. DispatcherTimer is not the only timer available, however.

The System.Threading namespace has a Timer class that provides similar functionality, but the callback you provide does not get called on the UI thread.With this mechanism, you need to partition any logic that updates the UI into a different method and use the page’s dispatcher to invoke it on the UI thread.Here’s an example:

[code]

void TimerCallback(object state)
{
// Call the DoTheRealWork method on the UI thread:
this.Dispatcher.BeginInvoke(DoTheRealWork);
}

[/code]

Unless your timer-based code has no need to update the UI, you should stick to using DispatcherTimer instead of Timer.

The Reactive Extensions for .NET also includes a mechanism for creating a sequence that produces each value at a timed interval (Microsoft.Phone.Reactive.Observable.Timer) but the apps in this book series avoid using Reactive Extensions for the sake of having easilyunderstood code.

Any of these timers can work great for the needs of Flashlight and apps like it, but they should not be used for animations.These timers are not in sync with the screen’s refresh rate, nor are they in sync with the Silverlight rendering engine. Instead,many animations should use the animations classes covered throughout Part II,“Transforms & Animations.”These classes could even be used in Flashlight instead of a timer.

Complex animations (such as physics-based animations) can use a static CompositionTarget.Rendering event that gets raised on every frame, regardless of the exact timing

Message Boxes

Flashlight uses a message box to show a standard warning that enables the user to cancel the action. On most platforms, using a message box to communicate information is indicative of a lazy programmer who doesn’t want to create a nicer-looking user interface. On Windows Phone, however, a message box is not only appropriate for many situations, but it has a lot of niceties that are hard to create on your own! As with the phone’s builtin apps, it animates in and out (with a flip), it dims and disables the rest of the screen (including the application bar), its buttons tilt when pressed, it automatically shows the status bar with a background that matches the message box background (regardless of the
app’s SystemTray.IsVisible setting), it makes a pleasant sound, and it vibrates. Naturally, it also respects the user’s theme and the phone’s orientation.

MessageBox contains two overloads of its static Show method. With one, you simply pass a single piece of text:

[code]MessageBox.Show(“This is the message.”);[/code]

The standard message box with no caption and a single OK button.
FIGURE 2.7 The standard message box with no
caption and a single OK button.

As shown in Figure 2.7, the resultant message box shows the message with a single OK button. It looks odd because it
has no caption, so apps should not use this overload of Show.

The message box used by Flashlight, shown in the context of the entire page.
FIGURE 2.8 The message box used by
Flashlight, shown in the context of the entire page.

The more functional overload of Show, used by Flashlight, enables you to set the text and caption, plus choose what
buttons you want with a value from the MessageBoxButton enumeration: OK (a single OK button) or OKCancel (two
buttons—OK and cancel). Figure 2.8 shows the message box created back in Listing 2.2.

Both overloads of Show return a MessageBoxResult enumeration value that indicates which button, if any, was tapped. The only supported values are OK and Cancel. The latter is returned if the user taps the cancel button or if the user simply dismisses the message box with the hardware Back button.

Unfortunately, MessageBox.Show does not support custom labels for the two buttons. The “ok” and “cancel” labels are
all you get. Built-in phone apps, on the other hand, often customize the “ok” label to be more specific to the task at
hand, such as “call” versus “don’t call” or “delete” versus “cancel.”

You can actually customize the text on the two message box buttons, but not with the MessageBox class. Instead, this functionality is hidden in an odd place—the Microsoft.Xna.Framework.GamerServices assembly! The Guide class in the
Microsoft.Xna.Framework.GamerServices namespace provides a pair of static methods that any app (XNA or Silverlight) can use without any special capabilities—BeginShowMessageBox and EndShowMessageBox. BeginShowMessageBox can be used as follows:

[code]

Guide.BeginShowMessageBox(“Title”,
“This is the message.”,
new string[] { “button 1”, “button 2” }, // 2 buttons with custom labels
0, // Button index that has focus
// (irrelevant for the phone)
MessageBoxIcon.None, // This is ignored
new AsyncCallback(OnMessageBoxClosed), // Callback to process result
null // Custom state given to callback
);

[/code]

The OnMessageBoxClosed callback, which uses EndShowMessageBox, can look as follows:

[code]

void OnMessageBoxClosed(IAsyncResult result)
{
// See which button was tapped (if any)
int? buttonIndex = Guide.EndShowMessageBox(result);
if (buttonIndex == 1)
// Perform action #1
else if (buttonIndex == 2)
// Perform action #2
else
// Message box was dismissed with the hardware back button
}

[/code]

Despite the fact that you pass an arbitrary list of button labels to BeginShowMessageBox, only one or two labels are supported because you can only have one or two buttons. When using your own labels, be sure to follow design guidelines by putting the positive OKstyle button on the left and the negative cancel-style button on the right.

special features of this flashlight
special features of this flashlight