The remaining two programs in this article create images
that you might want to save for posterity (if not prosperity). For
example, a program might want to save a bitmap in isolated storage so
the user can work on a particular image from session to session.
However, it is most valuable to the user to save a bitmap into the picture library on the phone. There is a special folder (or “album” as it’s termed) called “Saved Pictures” specifically for this purpose. From the picture
library, the user can view the resultant bitmap, or email it, or send
it with a text message. The bitmap is also moved to the user’s PC during
normal synchronization, at which point it might be printed.
Access to the picture library is provided with the XNA libraries, but you can use those libraries from a Silverlight program. You’ll need a reference to the Microsoft.Xna.Framework library, and a using directive for the Microsoft.Xna.Framework.Media namespace.
In your program, you create an instance of the MediaLibrary class. The SavedPictures property returns a PictureCollection with a Picture object for each item currently in the Saved Pictures album. These can be presented to the user with names.
The MediaLibrary class also contains a method named SavePicture that requires two arguments: a filename and a Stream referencing a bitmap in JPEG format. This Stream object is commonly a MemoryStream whose contents have been created by a call to the SaveJpeg extension method of WriteableBitmap.
The Monochromize program lets the user select a picture from the picture library. As soon as the program obtains the photo in the form of a WriteableBitmap, it accesses the Pixels
property and converts it to monochrome. A Save button navigates to a
screen that lets the user enter a filename and press OK; on navigation
back to the program, the monochrome bitmap is saved to the picture
library under that name.
The page in Monochromize
that lets the user enter a filename is the Windows Phone 7 equivalent of
a traditional save-file dialog box, and so I called it SaveFileDialog. It derives from PhoneApplicationPage and resides in the Petzold.Phone.Silverlight library.
I took a little different strategy to return filename information to the particular program that makes use of the SaveFileDialog page: When the user presses the “save” or “cancel” button, SaveFileDialog calls the GoBack method of the NavigationService object as usual, but during the subsequent OnNavigagedFrom override, it attempts to call a method in the program’s main page called SaveFileDialogCompleted. For this reason, any page that navigates to SaveFileDialog should also implement the following interface:
Example 1. Silverlight Project: Petzold.Phone.Silverlight File: ISaveFileDialogCompleted.cs
namespace Petzold.Phone.Silverlight { public interface ISaveFileDialogCompleted { void SaveFileDialogCompleted(bool okPressed, string filename); } }
|
The content area of SaveFileDialog has the traditional TextBox with two buttons labeled “save” and “cancel”:
Example 2. Silverlight Project: Petzold.Phone.Silverlight File: SaveFileDialog.xaml (excerpt)
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"> <StackPanel> <TextBlock Text="file name" /> <TextBox Name="txtbox" TextChanged="OnTextBoxTextChanged" /> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="*" /> </Grid.ColumnDefinitions>
<Button Name="saveButton" Content="save" Grid.Column="0" IsEnabled="False" Click="OnSaveButtonClick" />
<Button Content="cancel" Grid.Column="2" Click="OnCancelButtonClick" /> </Grid> </StackPanel> </Grid>
|
The code-behind file also defines a public method named SetTitle. A program that makes use of SaveFileDialog can call that method to set the title of the page with the application name:
Example 3. Silverlight Project: Petzold.Phone.Silverlight File: SaveFileDialog.xaml.cs (excerpt)
public partial class SaveFileDialog : PhoneApplicationPage { PhoneApplicationService appService = PhoneApplicationService.Current; bool okPressed; string filename;
public SaveFileDialog() { InitializeComponent(); }
public void SetTitle(string appTitle) { ApplicationTitle.Text = appTitle; }
void OnTextBoxTextChanged(object sender, TextChangedEventArgs args) { saveButton.IsEnabled = txtbox.Text.Length > 0; }
void OnSaveButtonClick(object sender, RoutedEventArgs args) { okPressed = true; filename = txtbox.Text; this.NavigationService.GoBack(); }
void OnCancelButtonClick(object sender, RoutedEventArgs args) { okPressed = false; this.NavigationService.GoBack(); } . . . }
|
Notice also that the “save” button is disabled unless the TextBox contains at least a one-character filename.
The navigation overrides need to handle a couple jobs. The OnNavigatedTo method checks if the query string contains an initial filename.The methods also handle tombstoning by saving the application title and any filename the user might have entered:
Example 4. Silverlight Project: Petzold.Phone.Silverlight File: SaveFileDialog.xaml.cs (excerpt)
protected override void OnNavigatedTo(NavigationEventArgs args) { if (appService.State.ContainsKey("filename")) txtbox.Text = appService.State["filename"] as string;
if (appService.State.ContainsKey("apptitle")) ApplicationTitle.Text = appService.State["apptitle"] as string;
if (this.NavigationContext.QueryString.ContainsKey("FileName")) txtbox.Text = this.NavigationContext.QueryString["FileName"];
base.OnNavigatedTo(args); }
protected override void OnNavigatedFrom(NavigationEventArgs args) { if (!String.IsNullOrEmpty(txtbox.Text)) appService.State["filename"] = txtbox.Text; appService.State["apptitle"] = ApplicationTitle.Text;
if (args.Content is ISaveFileDialogCompleted) (args.Content as ISaveFileDialogCompleted). SaveFileDialogCompleted(okPressed, filename);
base.OnNavigatedFrom(args); }
|
The most important part of OnNavigagedFrom is at the bottom of the method, where it checks if the page it’s navigating to implements the ISaveFileDialogCompleted interface and if so, calls the SaveFileDialogCompleted method in that page.
In the Monochromize program itself, the content area in the XAML file contains only an Image element with no bitmap:
Example 5. Silverlight Project: Monochromize File: MainPage.xaml (excerpt)
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"> <Image Name="img" /> </Grid>
|
The ApplicationBar has two buttons for load and save:
Example 6. Silverlight Project: Monochromize File: MainPage.xaml (excerpt)
<phone:PhoneApplicationPage.ApplicationBar> <shell:ApplicationBar> <shell:ApplicationBarIconButton x:Name="appbarLoadButton" IconUri="/Images/appbar.folder.rest.png" Text="load" Click="OnAppbarLoadClick" />
<shell:ApplicationBarIconButton x:Name="appbarSaveButton" IconUri="/Images/appbar.save.rest.png" Text="save" IsEnabled="False" Click="OnAppbarSaveClick" /> </shell:ApplicationBar> </phone:PhoneApplicationPage.ApplicationBar>
|
In the code-behind file, the fields are few, and the only one that’s really necessary is the PhotoChooserTask. (The PhoneApplicationService field is only a convenience, and after the program creates WriteableBitmap object, it is also stored as the Source property of the Image element.)
Example 7. Silverlight Project: Monochromize File: MainPage.xaml.cs (excerpt)
public partial class MainPage : PhoneApplicationPage, ISaveFileDialogCompleted { PhoneApplicationService appService = PhoneApplicationService.Current; PhotoChooserTask photoChooser = new PhotoChooserTask(); WriteableBitmap writeableBitmap;
public MainPage() { InitializeComponent();
appbarLoadButton = this.ApplicationBar.Buttons[0] as ApplicationBarIconButton; appbarSaveButton = this.ApplicationBar.Buttons[1] as ApplicationBarIconButton;
photoChooser.Completed += OnPhotoChooserCompleted; } . . . }
|
Notice that the class implements the ISaveFileDialogCompleted interface.
Clicking the “load” button causes the PhotoChooserTask to be invoked; on return the Completed handler creates a WriteableBitmap and then changes every member of the Pixels array by applying standard weights to the Red, Green, and Blue values.
Example 8. Silverlight Project: Monochromize File: MainPage.xaml.cs (excerpt)
void OnAppbarLoadClick(object sender, EventArgs args) { appbarSaveButton.IsEnabled = false; photoChooser.Show(); }
void OnPhotoChooserCompleted(object sender, PhotoResult args) { if (args.Error == null && args.ChosenPhoto != null) { BitmapImage bitmapImage = new BitmapImage(); bitmapImage.SetSource(args.ChosenPhoto); writeableBitmap = new WriteableBitmap(bitmapImage);
// Monochromize for (int pixel = 0; pixel < writeableBitmap.Pixels.Length; pixel++) { int color = writeableBitmap.Pixels[pixel]; byte A = (byte)(color & 0xFF000000 >> 24); byte R = (byte)(color & 0x00FF0000 >> 16); byte G = (byte)(color & 0x0000FF00 >> 8); byte B = (byte)(color & 0x000000FF); byte gray = (byte)(0.30 * R + 0.59 * G + 0.11 * B);
color = (A << 24) | (gray << 16) | (gray << 8) | gray; writeableBitmap.Pixels[pixel] = color; } img.Source = writeableBitmap; appbarSaveButton.IsEnabled = true; } }
|
The “monochromized” WriteableBitmap is set to the Source property of the Image element and the save button is enabled.
Pressing the save button navigates to the SaveFileDialog.xaml page in the Petzold.Phone.Silverlight library. As you’ve just seen, the SaveFileDialog class handles its OnNavigatedFrom override by calling the SaveFileDialogCompleted method in the class that it’s navigating to:
Example 9. Silverlight Project: Monochromize File: MainPage.xaml.cs (excerpt)
void OnAppbarSaveClick(object sender, EventArgs args) { this.NavigationService.Navigate( new Uri("/Petzold.Phone.Silverlight;component/SaveFileDialog.xaml", UriKind.Relative)); }
public void SaveFileDialogCompleted(bool okPressed, string filename) { if (okPressed) { MemoryStream memoryStream = new MemoryStream(); writeableBitmap.SaveJpeg(memoryStream, writeableBitmap.PixelWidth, writeableBitmap.PixelHeight, 0, 75); memoryStream.Position = 0;
MediaLibrary mediaLib = new MediaLibrary(); mediaLib.SavePicture(filename, memoryStream); } }
|
The SaveFileDialogCompleted method uses the filename entered by the user to write the bitmap to the pictures library. This happens in two steps: First the SaveJpegWriteableBitmap to a MemoryStream in JPEG format. The Position on the MemoryStream is then reset, and the stream is saved to the pictures library. method writes the
The Monochromize program also handles tombstoning. The OnNavigatedFrom method uses the SaveJpeg extension method to write to a MemoryStream and then saves the byte array. This method is also responsible for calling SetTitle on the SaveFileDialog if navigating to that page:
Example 10. Silverlight Project: Monochromize File: MainPage.xaml.cs (excerpt)
protected override void OnNavigatedFrom(NavigationEventArgs args) { if (writeableBitmap != null) { MemoryStream stream = new MemoryStream(); writeableBitmap.SaveJpeg(stream, writeableBitmap.PixelWidth, writeableBitmap.PixelHeight, 0, 75); appService.State["jpegBits"] = stream.GetBuffer(); }
if (args.Content is SaveFileDialog) { SaveFileDialog page = args.Content as SaveFileDialog; page.SetTitle(ApplicationTitle.Text); }
base.OnNavigatedFrom(args); }
|
The OnNavigatedTo method is responsible for re-activating after tombstoning. The byte array is converted by to a WriteableBitmap, and the save button is enabled:
Example 11. Silverlight Project: Monochromize File: MainPage.xaml.cs (excerpt)
protected override void OnNavigatedTo(NavigationEventArgs args) { if (appService.State.ContainsKey("jpegBits")) { byte[] bitmapBits = (byte[])appService.State["jpegBits"]; MemoryStream stream = new MemoryStream(bitmapBits); BitmapImage bitmapImage = new BitmapImage(); bitmapImage.SetSource(stream); writeableBitmap = new WriteableBitmap(bitmapImage); img.Source = writeableBitmap; appbarSaveButton.IsEnabled = true; } base.OnNavigatedTo(args); }
|