Alarm Clock (Settings,Toggle Switch, Custom Font)

2
267

The Alarm Clock app mimics a somewhat-retro digital alarm clock. It pretends to have a fixed display with segments that can be turned on or off as needed. It displays the current time and day of the week, and it enables setting a snoozable alarm. (The alarm only goes off if the app is still running, even if it’s under the lock screen.)

Alarm Clock exposes several user-configurable settings related to the color and formatting of the clock and its alarm vibration behavior. It also persists state such as whether the alarm is on and what time it should go off. All of this is done with the Setting class that has been used in almost every app so far. Now, given the topic of Part III, “Storing & Retrieving Local Data,” it is time to look into how the Setting class works and understand more about storing and retrieving data.

Setting and Isolated Storage

Isolated storage is a special folder provided to each app where arbitrary data files can be stored. The files in this folder are isolated to the designated app. One app can never access the isolated storage for another app. Even if an app wants to share the data in its storage with others, it cannot.

This is a static IsolatedStorageSettings.ApplicationSettings dictionary in the System.IO.IsolatedStorage namespace. You can store any serializable object in this dictionary with a string key. When an application exits (whether closed or deactivated), the contents of the ApplicationSettings dictionary is automatically serialized to a file in isolated storage. And when an application is launched or activated, the dictionary is automatically populated with any previously persisted data. Listing 20.1 contains the implementation of the generic Setting class that wraps the use of the ApplicationSettings dictionary.

LISTING 20.1 Setting.cs—The Setting Class Automatically Persists a Named Value to Isolated Storage

[code]

using System.IO.IsolatedStorage;
namespace WindowsPhoneApp
{
// Encapsulates a key/value pair stored in Isolated Storage ApplicationSettings
public class Setting<T>
{
string name;
T value;
T defaultValue;
bool hasValue;
public Setting(string name, T defaultValue)
{
this.name = name;
this.defaultValue = defaultValue;
}
public T Value
{
get
{
// Check for the cached value
if (!this.hasValue)
{
// Try to get the value from Isolated Storage
if (!IsolatedStorageSettings.ApplicationSettings.TryGetValue(
this.name, out this.value))
{
// It hasn’t been set yet
this.value = this.defaultValue;
IsolatedStorageSettings.ApplicationSettings[this.name] = this.value;
}
this.hasValue = true;
}
return this.value;
}
set
{
// Save the value to Isolated Storage
IsolatedStorageSettings.ApplicationSettings[this.name] = value;
this.value = value;
this.hasValue = true;
}
}
public T DefaultValue
{
get { return this.defaultValue; }
}
// “Clear” cached value:
public void ForceRefresh()
{
this.hasValue = false;
}
}
}

[/code]

Although placing items in the ApplicationSettings dictionary and retrieving them is straightforward, the Setting class cuts down on the amount of code needed by apps compared to using ApplicationSettings directly. This is especially true because users of ApplicationSettings must be prepared for keys that are not in the dictionary (typically for the first run of the app). The Setting class supports the specification of a default value, and it caches its value so it doesn’t need to fetch it from the dictionary every time.

The ForceRefresh method resolves cases where the underlying dictionary entry is modified from external code. This is leveraged by every app that uses this book’s color picker, because the color picker’s page updates ApplicationSettings directly.

The Settings Page

Alarm Clock has four pages: a main page, an alarm page, a settings page, and an instructions page. Because settings pages are one of this chapter’s lessons, we’ll look at Alarm Clock’s settings page first. Shown in Figure 20.1, the page’s XAML is in Listing 20.2.

FIGURE 20.1 Alarm Clock’s settings page exposes several settings.
FIGURE 20.1 Alarm Clock’s settings page exposes several settings.

LISTING 20.2 SettingsPage.xaml—The User Interface for Alarm Clock’s Settings Page

[code]

<phone:PhoneApplicationPage
x:Class=”WindowsPhoneApp.SettingsPage”
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”
xmlns:local=”clr-namespace:WindowsPhoneApp”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”PortraitOrLandscape” shell:SystemTray.IsVisible=”True”>
<Grid>
<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=”alarm clock”
Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<ScrollViewer Grid.Row=”1”>
<Grid Margin=”{StaticResource PhoneMargin}”>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock Text=”Foreground color” Margin=”12,7,12,8”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
<Rectangle x:Name=”ForegroundColorRectangle” Grid.Row=”1” Height=”90”
Margin=”12,0,12,18” StrokeThickness=”3” local:Tilt.IsEnabled=”True”
Stroke=”{StaticResource PhoneForegroundBrush}”
MouseLeftButtonUp=”ForegroundColorRectangle_MouseLeftButtonUp”/>
<TextBlock Text=”Background color” Grid.Column=”1” Margin=”12,7,12,8”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
<Rectangle x:Name=”BackgroundColorRectangle” Grid.Row=”1” Grid.Column=”1”
Margin=”12,0,12,18” Height=”90” StrokeThickness=”3”
Stroke=”{StaticResource PhoneForegroundBrush}”
local:Tilt.IsEnabled=”True”
MouseLeftButtonUp=”BackgroundColorRectangle_MouseLeftButtonUp”/>
<toolkit:ToggleSwitch x:Name=”DisableScreenLockToggleSwitch” Grid.Row=”2”
Grid.ColumnSpan=”2” Header=”Disable screen time-out”/>
<toolkit:ToggleSwitch x:Name=”Show24HourToggleSwitch” Grid.Row=”3”
Grid.ColumnSpan=”2” Header=”24-hour clock”/>
<toolkit:ToggleSwitch x:Name=”ShowSecondsToggleSwitch” Grid.Row=”4”
Grid.ColumnSpan=”2” Header=”Show seconds”/>
<toolkit:ToggleSwitch x:Name=”EnableVibrationToggleSwitch” Grid.Row=”5”
Grid.ColumnSpan=”2” Header=”Enable vibration”/>
</Grid>
</ScrollViewer>
</Grid>
</phone:PhoneApplicationPage>

[/code]

This page makes use of several toggle switch controls from the Silverlight for Windows Phone Toolkit. Programmatically, a toggle switch acts like a check box. Visually, its default appearance is an on/off sliding switch.

The Windows Phone team publishes several guidelines for how an app’s settings page should look and behave:

  • The page should have the standard two-text-block header, and it should never scroll, even if the rest of the page does.
  • The text block that typically shows the application name should say “SETTINGS” instead, as if the user got transported to the phone’s Settings app.
  • The text block that typically shows the page title should be the application name, again to match the appearance of pages in the Settings app.
  • When the user changes a setting, it should take effect immediately. There should be no page-level apply/cancel buttons.
  • Specific actions that are irreversible should prompt the user with a message box that gives the opportunity to cancel.
  • Try to keep the settings page as concise as possible.
  • Avoid creating more than one settings page. Making use of scrolling and/or a pivot control (covered in Part IV, “Pivot, Panorama, Charts, & Graphs) is a good way to handle content that won’t fit on the screen.

The page’s code-behind is shown in Listing 20.3. It makes use of the some of the following settings defined in Settings.cs:

[code]

public static class Settings
{
// Persistent user settings from the settings page
public static readonly Setting<Color> ForegroundColor = new Setting<Color>(
“ForegroundColor”, (Color)Application.Current.Resources[“PhoneAccentColor”]);
public static readonly Setting<Color> BackgroundColor = new Setting<Color>(
“BackgroundColor”, Colors.Black);
public static readonly Setting<bool> DisableScreenLock =
new Setting<bool>(“DisableScreenLock”, true);
public static readonly Setting<bool> ShowSeconds =
new Setting<bool>(“ShowSeconds”, true);
public static readonly Setting<bool> Show24Hours =
new Setting<bool>(“Show24Hours”, false);
public static readonly Setting<bool> EnableVibration =
new Setting<bool>(“EnableVibration”, true);
// Persistent user settings from the alarm page
public static readonly Setting<DateTime> AlarmTime = new Setting<DateTime>(
“AlarmTime”, new DateTime(2010, 1, 1, 8, 0, 0));
public static readonly Setting<bool> IsAlarmOn =
new Setting<bool>(“IsAlarmOn”, false);
// Persistent state
public static readonly Setting<SupportedPageOrientation> SupportedOrientations
= new Setting<SupportedPageOrientation>(“SupportedOrientations”,
SupportedPageOrientation.PortraitOrLandscape);
public static readonly Setting<DateTime?> SnoozeTime =
new Setting<DateTime?>(“SnoozeTime”, null);
}

[/code]

LISTING 20.3 SettingsPage.xaml.cs—The Code-Behind for Alarm Clock’s Settings Page

[code]

using System;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class SettingsPage : PhoneApplicationPage
{
public SettingsPage()
{
InitializeComponent();
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Respect the saved settings
this.ForegroundColorRectangle.Fill =
new SolidColorBrush(Settings.ForegroundColor.Value);
this.BackgroundColorRectangle.Fill =
new SolidColorBrush(Settings.BackgroundColor.Value);
this.DisableScreenLockToggleSwitch.IsChecked =
Settings.DisableScreenLock.Value;
this.Show24HourToggleSwitch.IsChecked = Settings.Show24Hours.Value;
this.ShowSecondsToggleSwitch.IsChecked = Settings.ShowSeconds.Value;
this.EnableVibrationToggleSwitch.IsChecked = Settings.EnableVibration.Value;
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
// Save the settings (except the colors, already saved by the color picker)
Settings.DisableScreenLock.Value =
this.DisableScreenLockToggleSwitch.IsChecked.Value;
Settings.Show24Hours.Value = this.Show24HourToggleSwitch.IsChecked.Value;
Settings.ShowSeconds.Value = this.ShowSecondsToggleSwitch.IsChecked.Value;
Settings.EnableVibration.Value =
this.EnableVibrationToggleSwitch.IsChecked.Value;
}
void ForegroundColorRectangle_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.ForegroundColor.Value.ToString().Substring(1);
string defaultColorString =
Settings.ForegroundColor.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.ForegroundColor.ForceRefresh();
// Navigate to the color picker
this.NavigationService.Navigate(new Uri(
“/Shared/Color Picker/ColorPickerPage.xaml?”
+ “&currentColor=” + currentColorString
+ “&defaultColor=” + defaultColorString
+ “&settingName=ForegroundColor”, UriKind.Relative));
}
void BackgroundColorRectangle_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.BackgroundColor.Value.ToString().Substring(1);
string defaultColorString =
Settings.BackgroundColor.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.BackgroundColor.ForceRefresh();
// Navigate to the color picker
this.NavigationService.Navigate(new Uri(
“/Shared/Color Picker/ColorPickerPage.xaml?”
+ “&currentColor=” + currentColorString
+ “&defaultColor=” + defaultColorString
+ “&settingName=BackgroundColor”, UriKind.Relative));
}
}
}

[/code]

The color picker page writes the chosen color value directly to the IsolatedStorageSettings. ApplicationSettings dictionary, using the passed-in settingName as the key. This is how it is able to work with the Setting objects used by any app, and why a call to ForceRefresh is required before the Setting is accessed again.

The Alarm Page

The alarm page, shown in Figure 20.2, is basically a second settings page, but dedicated to turning the alarm on and off, and settings its time. It also has a button for testing the alarm volume, so users can make sure it is loud enough to wake them up.

FIGURE 20.2 The alarm page exposes the most important settings in the app.
FIGURE 20.2 The alarm page exposes the most important settings in the app.

Although apps should avoid having more than one settings page, this page is distinctand important-enough to warrant its own page. A user may wish to visit the alarm page every night to ensure the alarm is set correctly, whereas they may never have the need to visit the settings page. Listing 20.4 contains its XAML and Listing 20.5 contains its codebehind.

LISTING 20.4 AlarmPage.xaml—The User Interface for Alarm Clock’s Alarm Page

[code]

<phone:PhoneApplicationPage
x:Class=”WindowsPhoneApp.AlarmPage”
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”
xmlns:local=”clr-namespace:WindowsPhoneApp”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”PortraitOrLandscape” shell:SystemTray.IsVisible=”True”>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”*”/>
</Grid.RowDefinitions>
<!– The standard settings header –>
<StackPanel Grid.Row=”0” Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock Text=”ALARM CLOCK”
Style=”{StaticResource PhoneTextTitle0Style}”/>
<TextBlock Text=”set alarm” Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<ScrollViewer Grid.Row=”1”>
<StackPanel Margin=”{StaticResource PhoneMargin}”>
<toolkit:ToggleSwitch x:Name=”ToggleSwitch” Header=”Alarm”
Checked=”ToggleSwitch_IsCheckedChanged”
Unchecked=”ToggleSwitch_IsCheckedChanged”/>
<toolkit:TimePicker x:Name=”TimePicker”
ValueChanged=”TimePicker_ValueChanged”/>
<ToggleButton x:Name=”TestVolumeButton” Content=”test volume”
Margin=”0,36,0,0” Checked=”TestVolumeButton_Checked”
Unchecked=”TestVolumeButton_Unchecked”
local:Tilt.IsEnabled=”True”/>
</StackPanel>
</ScrollViewer>
</Grid>
</phone:PhoneApplicationPage>

[/code]

Listing 20.4 leverages two controls from the Silverlight for Windows Phone Toolkit: the toggle switch and the time picker. Toggle switches don’t have an IsCheckedChanged event, so this listing attaches the ToggleSwitch_IsCheckedChanged event handler to two individual events—Checked and Unchecked.

LISTING 20.5 AlarmPage.xaml.cs—The Code-Behind for Alarm Clock’s Alarm Page

[code]

using System;
using System.Windows;
using System.Windows.Navigation;
using System.Windows.Threading;
using Microsoft.Devices;
using Microsoft.Phone.Controls;
using Microsoft.Xna.Framework.Audio; // For SoundEffectInstance
namespace WindowsPhoneApp
{
public partial class AlarmPage : PhoneApplicationPage
{
// For the sound and vibration
SoundEffectInstance alarmSound;
DispatcherTimer timer =
new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
public AlarmPage()
{
InitializeComponent();
this.timer.Tick += Timer_Tick;
// Initialize the alarm sound
this.alarmSound = SoundEffects.Alarm.CreateInstance();
this.alarmSound.IsLooped = true;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Respect the saved settings
this.ToggleSwitch.IsChecked = Settings.IsAlarmOn.Value;
this.TimePicker.Value = Settings.AlarmTime.Value;
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
// Save the settings (except AlarmTime, handled in TimePicker_ValueChanged)
Settings.IsAlarmOn.Value = this.ToggleSwitch.IsChecked.Value;
// Stop the vibration/sound effect if still playing
this.timer.Stop();
this.alarmSound.Stop();
}
void TimePicker_ValueChanged(object sender, DateTimeValueChangedEventArgs e)
{
// To prevent getting clobbered on way back in
Settings.AlarmTime.Value = this.TimePicker.Value.Value;
Settings.SnoozeTime.Value = null;
}
void ToggleSwitch_IsCheckedChanged(object sender, RoutedEventArgs e)
{
// If we’re currently snoozing, cancel it
Settings.SnoozeTime.Value = null;
}
void TestVolumeButton_Checked(object sender, RoutedEventArgs e)
{
// Vibrate, only if its enabled
if (Settings.EnableVibration.Value)
this.timer.Start();
// Play the sound
this.alarmSound.Play();
}
void TestVolumeButton_Unchecked(object sender, RoutedEventArgs e)
{
// Stop the sound and vibration
this.timer.Stop();
this.alarmSound.Stop();
}
void Timer_Tick(object sender, EventArgs e)
{
// Vibrate for half a second
VibrateController.Default.Start(TimeSpan.FromSeconds(.5));
}
}
}

[/code]

Notes:

  • To produce the alarm sound, this app uses sound effect APIs covered in Part V, “Audio & Video.”

The Main Page

The main page is the page that acts like a physical digital alarm clock, with its time display, day-of-the-week display, and alarm information. Its root grid contains many columns, mainly for evenly distributing the seven days of the week. Listing 20.6 contains its XAML, and Figure 20.3 shows this page with its root grid temporarily marked with ShowGridLines=”True”.

FIGURE 20.3 The main page, with grid lines showing.
FIGURE 20.3 The main page, with grid lines showing.

LISTING 20.6 MainPage.xaml—The User Interface for Alarm Clock’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:local=”clr-namespace:WindowsPhoneApp”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
SupportedOrientations=”PortraitOrLandscape”>
<phone:PhoneApplicationPage.Resources>
<!– A style shared by SUN,MON,TUE,WED,THU,FRI,SAT –>
<Style x:Name=”DayOfWeekStyle” TargetType=”TextBlock”>
<Setter Property=”HorizontalAlignment” Value=”Center”/>
<Setter Property=”VerticalAlignment” Value=”Center”/>
<Setter Property=”Grid.Row” Value=”2”/>
</Style>
</phone:PhoneApplicationPage.Resources>
<!– The application bar, with three buttons and two menu items –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar Opacity=”.95”>
<shell:ApplicationBarIconButton Text=”set alarm”
IconUri=”/Images/appbar.alarm.png” Click=”AlarmButton_Click”/>
<shell:ApplicationBarIconButton Text=”settings”
IconUri=”/Shared/Images/appbar.settings.png” Click=”SettingsButton_Click”/>
<shell:ApplicationBarIconButton Text=”lock screen”
IconUri=”/Shared/Images/appbar.orientationUnlocked.png”
Click=”OrientationLockButton_Click”/>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”instructions”
Click=”InstructionsMenuItem_Click”/>
<shell:ApplicationBarMenuItem Text=”about” Click=”AboutMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<Grid x:Name=”Grid”>
<Grid.RowDefinitions>
<RowDefinition/> <!– Top margin –>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”75”/>
<RowDefinition Height=”Auto”/>
<RowDefinition/> <!– Bottom margin –>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name=”LeftMargin” Width=”0”/>
<ColumnDefinition/> <!– SUN –>
<ColumnDefinition/> <!– MON –>
<ColumnDefinition/> <!– TUE –>
<ColumnDefinition/> <!– WED –>
<ColumnDefinition/> <!– THU –>
<ColumnDefinition/> <!– FRI –>
<ColumnDefinition/> <!– SAT –>
<ColumnDefinition x:Name=”RightMargin” Width=”0”/>
</Grid.ColumnDefinitions>
<!– The current time –>
<local:TimeDisplay x:Name=”MainTimeDisplay” Grid.Row=”1” Grid.Column=”1”
Grid.ColumnSpan=”7” HorizontalAlignment=”Center”/>
<!– Two simple labels –>
<TextBlock x:Name=”AlarmOnTextBlock” Grid.Row=”3” Grid.Column=”1”
Grid.ColumnSpan=”3” Text=”ALARM ON” Margin=”0,-24,0,0”
HorizontalAlignment=”Right” VerticalAlignment=”Center”/>
<TextBlock x:Name=”SnoozeTextBlock” Grid.Row=”3” Grid.Column=”1”
Grid.ColumnSpan=”3” Text=”SNOOZING UNTIL” Margin=”0,24,0,0”
HorizontalAlignment=”Right” VerticalAlignment=”Center”/>
<!– The alarm/snooze time –>
<local:TimeDisplay x:Name=”AlarmTimeDisplay” ShowSeconds=”False”
Grid.Row=”3” Grid.Column=”3” Grid.ColumnSpan=”5”
HorizontalAlignment=”Right”/>
</Grid>
</phone:PhoneApplicationPage>

[/code]

The XAML uses two instances of a TimeDisplay user control, shown in the next section. The code-behind is in Listing 20.7.

LISTING 20.7 MainPage.xaml.cs—The Code-Behind for Alarm Clock’s Main Page

[code]

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Navigation;
using System.Windows.Threading;
using Microsoft.Devices;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
using Microsoft.Xna.Framework.Audio; // For SoundEffectInstance
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
// A timer, so we can update the display every second
DispatcherTimer timer =
new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
IApplicationBarIconButton orientationLockButton;
TextBlock[] dayOfWeekTextBlocks = new TextBlock[7];
SoundEffectInstance alarmSound;
bool tappedAlarmOff;
public MainPage()
{
InitializeComponent();
this.orientationLockButton =
this.ApplicationBar.Buttons[2] as IApplicationBarIconButton;
this.timer.Tick += Timer_Tick;
this.timer.Start();
// Initialize the alarm sound effect
SoundEffects.Initialize();
this.alarmSound = SoundEffects.Alarm.CreateInstance();
this.alarmSound.IsLooped = true;
// Add the seven day-of-week text blocks here, assigning them to an array
this.dayOfWeekTextBlocks[0] = new TextBlock { Text = “SUN”,
Style = this.DayOfWeekStyle };
Grid.SetColumn(this.dayOfWeekTextBlocks[0], 1);
this.dayOfWeekTextBlocks[1] = new TextBlock { Text = “MON”,
Style = this.DayOfWeekStyle };
Grid.SetColumn(this.dayOfWeekTextBlocks[1], 2);
this.dayOfWeekTextBlocks[2] = new TextBlock { Text = “TUE”,
Style = this.DayOfWeekStyle };
Grid.SetColumn(this.dayOfWeekTextBlocks[2], 3);
this.dayOfWeekTextBlocks[3] = new TextBlock { Text = “WED”,
Style = this.DayOfWeekStyle };
Grid.SetColumn(this.dayOfWeekTextBlocks[3], 4);
this.dayOfWeekTextBlocks[4] = new TextBlock { Text = “THU”,
Style = this.DayOfWeekStyle };
Grid.SetColumn(this.dayOfWeekTextBlocks[4], 5);
this.dayOfWeekTextBlocks[5] = new TextBlock { Text = “FRI”,
Style = this.DayOfWeekStyle };
Grid.SetColumn(this.dayOfWeekTextBlocks[5], 6);
this.dayOfWeekTextBlocks[6] = new TextBlock { Text = “SAT”,
Style = this.DayOfWeekStyle };
Grid.SetColumn(this.dayOfWeekTextBlocks[6], 7);
for (int i = 0; i < this.dayOfWeekTextBlocks.Length; i++)
this.Grid.Children.Add(dayOfWeekTextBlocks[i]);
// Allow the app to run (making the alarm sound and vibration)
// even when the phone is locked.
// Once disabled, you cannot re-enable the default behavior!
PhoneApplicationService.Current.ApplicationIdleDetectionMode =
IdleDetectionMode.Disabled;
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
// Restore the ability for the screen to auto-lock when on other pages
PhoneApplicationService.Current.UserIdleDetectionMode =
IdleDetectionMode.Enabled;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
this.tappedAlarmOff = false;
// Respect the saved settings
this.Foreground = new SolidColorBrush(Settings.ForegroundColor.Value);
this.Grid.Background = new SolidColorBrush(Settings.BackgroundColor.Value);
this.ApplicationBar.ForegroundColor = Settings.ForegroundColor.Value;
this.ApplicationBar.BackgroundColor = Settings.BackgroundColor.Value;
// While on this page, don’t allow the screen to auto-lock
if (Settings.DisableScreenLock.Value)
PhoneApplicationService.Current.UserIdleDetectionMode =
IdleDetectionMode.Disabled;
// Restore the orientation setting to whatever it was last time
this.SupportedOrientations = Settings.SupportedOrientations.Value;
// If the restored value is not PortraitOrLandscape, then the orientation
// has been locked. Change the state of the application bar button to
// reflect this.
if (this.SupportedOrientations !=
SupportedPageOrientation.PortraitOrLandscape)
{
this.orientationLockButton.Text = “unlock”;
this.orientationLockButton.IconUri = new Uri(
“/Shared/Images/appbar.orientationLocked.png”, UriKind.Relative);
}
RefreshDisplays();
// Don’t wait for the next tick
Timer_Tick(this, EventArgs.Empty);
}
protected override void OnOrientationChanged(OrientationChangedEventArgs e)
{
base.OnOrientationChanged(e);
RefreshDisplays();
}
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
if (this.alarmSound.State == SoundState.Playing)
{
// Turn the alarm off
this.tappedAlarmOff = true;
this.alarmSound.Stop();
// Set the snooze time to five minutes from now
DateTime currentTimeWithoutSeconds = DateTime.Now;
currentTimeWithoutSeconds =
currentTimeWithoutSeconds.AddSeconds(-currentTimeWithoutSeconds.Second);
Settings.SnoozeTime.Value = currentTimeWithoutSeconds.AddMinutes(5);
RefreshDisplays();
}
else
{
// Toggle the application bar visibility
this.ApplicationBar.IsVisible = !this.ApplicationBar.IsVisible;
}
}
void RefreshDisplays()
{
if (IsMatchingOrientation(PageOrientation.Portrait))
{
// Adjust the margins for portrait
this.LeftMargin.Width = new GridLength(12);
this.RightMargin.Width = new GridLength(12);
// Set the font size accordingly
if (Settings.ShowSeconds.Value)
this.MainTimeDisplay.FontSize = 182;
else
this.MainTimeDisplay.FontSize = 223;
}
else
{
// Adjust the margins for landscape
this.LeftMargin.Width = new GridLength(92);
this.RightMargin.Width = new GridLength(92);
// Set the font size accordingly
if (Settings.ShowSeconds.Value)
this.MainTimeDisplay.FontSize = 251;
else
this.MainTimeDisplay.FontSize = 307;
}
this.AlarmTimeDisplay.FontSize = this.MainTimeDisplay.FontSize / 2;
// Respect the settings in the two time displays
this.MainTimeDisplay.Show24Hours = Settings.Show24Hours.Value;
this.AlarmTimeDisplay.Show24Hours = Settings.Show24Hours.Value;
this.MainTimeDisplay.ShowSeconds = Settings.ShowSeconds.Value;
this.MainTimeDisplay.Initialize();
this.AlarmTimeDisplay.Initialize();
if (Settings.IsAlarmOn.Value)
{
if (Settings.SnoozeTime.Value != null)
{
// Show that we’re snoozing
this.AlarmOnTextBlock.Opacity = .1;
this.SnoozeTextBlock.Opacity = 1;
this.AlarmTimeDisplay.Time = Settings.SnoozeTime.Value;
}
else
{
// Show when the alarm will go off
this.AlarmOnTextBlock.Opacity = 1;
this.SnoozeTextBlock.Opacity = .1;
this.AlarmTimeDisplay.Time = Settings.AlarmTime.Value;
}
}
else
{
// No alarm, no snooze
this.AlarmOnTextBlock.Opacity = .1;
this.SnoozeTextBlock.Opacity = .1;
this.AlarmTimeDisplay.Time = null;
}
}
void Timer_Tick(object sender, EventArgs e)
{
// Refresh the current time
this.MainTimeDisplay.Time = DateTime.Now;
// Keep the day of the week up-to-date
for (int i = 0; i < this.dayOfWeekTextBlocks.Length; i++)
this.dayOfWeekTextBlocks[i].Opacity = .2;
this.dayOfWeekTextBlocks[(int)DateTime.Now.DayOfWeek].Opacity = 1;
// If the alarm sound is playing, accompany it with vibration
// (if that setting is enabled)
if (this.alarmSound.State == SoundState.Playing
&& Settings.EnableVibration.Value)
VibrateController.Default.Start(TimeSpan.FromSeconds(.5));
if (Settings.IsAlarmOn.Value)
{
TimeSpan timeToAlarm =
Settings.AlarmTime.Value.TimeOfDay – DateTime.Now.TimeOfDay;
// Let the alarm go off up to 60 seconds after the designated time
// (in case the app wasn’t running at the beginning of the minute or it
// was on a different page)
if (!this.tappedAlarmOff && this.alarmSound.State != SoundState.Playing
&& timeToAlarm.TotalSeconds <= 0 && timeToAlarm.TotalSeconds > -60)
{
this.alarmSound.Play();
return; // Don’t bother with snooze
}
}
if (Settings.SnoozeTime.Value != null)
{
TimeSpan timeToSnooze =
Settings.SnoozeTime.Value.Value.TimeOfDay – DateTime.Now.TimeOfDay;
// Let the snoozed alarm go off up to 60 seconds after the designated time
// (in case the app wasn’t running at the beginning of the minute or it
// was on a different page)
if (this.alarmSound.State != SoundState.Playing
&& timeToSnooze.TotalSeconds <= 0 && timeToSnooze.TotalSeconds > -60)
{
this.alarmSound.Play();
}
}
}
bool IsMatchingOrientation(PageOrientation orientation)
{
return ((this.Orientation & orientation) == orientation);
}
// Application bar handlers
void AlarmButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/AlarmPage.xaml”,
UriKind.Relative));
}
void SettingsButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/SettingsPage.xaml”,
UriKind.Relative));
}
// The “orientation lock” feature
void OrientationLockButton_Click(object sender, EventArgs e)
{
// Check the value of SupportedOrientations to see if we’re currently
// “locked” to a value other than PortraitOrLandscape.
if (this.SupportedOrientations !=
SupportedPageOrientation.PortraitOrLandscape)
{
// We are locked, so unlock now
this.SupportedOrientations = SupportedPageOrientation.PortraitOrLandscape;
// Change the state of the application bar button to reflect this
this.orientationLockButton.Text = “lock screen”;
this.orientationLockButton.IconUri = new Uri(
“/Shared/Images/appbar.orientationUnlocked.png”, UriKind.Relative);
}
else
{
// We are unlocked, so lock to the current orientation now
if (IsMatchingOrientation(PageOrientation.Portrait))
this.SupportedOrientations = SupportedPageOrientation.Portrait;
else
this.SupportedOrientations = SupportedPageOrientation.Landscape;
// Change the state of the application bar button to reflect this
this.orientationLockButton.Text = “unlock”;
this.orientationLockButton.IconUri = new Uri(
“/Shared/Images/appbar.orientationLocked.png”, UriKind.Relative);
}
// Remember the new setting after the page has been left
Settings.SupportedOrientations.Value = this.SupportedOrientations;
}
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=Alarm Clock”, UriKind.Relative));
}
}
}

[/code]

Notes:

  • The seven day-of-the-week text blocks are constructed and added to their parent grid in code-behind rather than in XAML. This is done because having them in an array is convenient for the code that needs to illuminate the appropriate one.
  • This page sets ApplicationIdleDetectionMode to Disabled so it runs while the screen is locked (and off). This is the most likely what users would want to do, rather than leaving the screen on all night. However, if they wish to do so, this page also sets UserIdleDetectionMode to Disabled so it won’t automatically lock. If the users want to turn off their screen, they must do it manually.
  • This page does something a bit unique; it applies the chosen foreground and background colors to the application bar in addition to the main display. The chosen background color is applied to the grid rather than the page. Recall that setting a page’s background color has no effect.
  • If the alarm is going off, tapping the screen snoozes it by five minutes. (This is five minutes from the beginning of the current minute, as it would be confusing if the alarm didn’t go off at a minute boundary.) Otherwise, tapping the screen toggles the visibility of the application bar. This provides a way for the user to get a more realistic and minimalistic display.
  • The various text blocks are illuminated with an opacity of 1, and they are “turned off” with an opacity of either .1 or .2. (The variation is there just to give more realism.) The code inside of Timer_Tick leverages the array of day-of-the-week text blocks, using DateTime.Now.DayOfWeek (a number from 0 to 6, inclusive) as the index into the array.

The TimeDisplay User Control

What makes this app special is its time display that uses seven-segment digital numerals. There are a few ways to accomplish such a display. For example, you could create vectorbased shapes and illuminate the appropriate segments at an appropriate time with their fill settings. This app takes a somewhat-easier approach: using a custom font. Listing 20.8 contains the XAML for the TimeDisplay user control that implements the seven-segment display.

Before using a custom font,make sure you have permission!

Using a custom font is easy.Using it legally is another story. Be sure you understand the rules for any specific font you wish to use.The custom font used in Listing 20.8 is called “Pendule Ornamental,” created by Scott Lawrence and available at http://fontstruct.fontshop.com/ fontstructions/show/200136. It is licensed under a Creative Commons Attribution Share Alike license (http://creativecommons.org/licenses/by-sa/3.0/).

LISTING 20.8 TimeDisplay.xaml—The User Interface for the TimeDisplay User Control

[code]

<UserControl x:Class=”WindowsPhoneApp.TimeDisplay”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”>
<Grid x:Name=”Grid” Background=”Transparent”>
<Grid.ColumnDefinitions>
<ColumnDefinition Width=”Auto”/>
<ColumnDefinition Width=”Auto”/>
</Grid.ColumnDefinitions>
<!– The background “off” segments –>
<TextBlock FontFamily=”Fonts/pendule_ornamental.ttf#pendule ornamental”
Opacity=”.1”>
<!– It’s important not to have whitespace between the runs!–>
<Run x:Name=”TimeBackgroundRun”>88:88</Run><Run
x:Name=”SecondsBackgroundRun”>88</Run>
</TextBlock>
<!– The foreground “on” segments –>
<TextBlock FontFamily=”Fonts/pendule_ornamental.ttf#pendule ornamental”>
<!– It’s important not to have whitespace between the runs!–>
<Run x:Name=”TimeRun”/><Run x:Name=”SecondsRun”/>
</TextBlock>
<!– AM / PM –>
<TextBlock x:Name=”AMTextBlock” Grid.Column=”1” Text=”AM”
FontSize=”{StaticResource PhoneFontSizeNormal}”
HorizontalAlignment=”Center” Margin=”4,0,0,0”/>
<TextBlock x:Name=”PMTextBlock” Grid.Column=”1” Text=”PM”
FontSize=”{StaticResource PhoneFontSizeNormal}”
HorizontalAlignment=”Center” Margin=”4,24,0,0”/>
</Grid>
</UserControl>

[/code]

Notes:

  • To use a custom font, simply include the font file in your project (with a Build Action of Content), and then reference it in the FontFamily value on any text block or text box (or element whose font family will be inherited by relevant child elements). The syntax is

    [code] pathAndFilename#fontName [/code]

    You can see the correct value for fontName by opening the font file via Windows Explorer.

  • To give the effect of dim “off” segments in each numeral, this user control actually uses two overlapping text blocks. The one in the back has all segments activated at all times (with a value of 88:88 for the hour/minutes and a value of 88 for the seconds), but is given an opacity of .1. The one in the front displays the actual time at full opacity.
  • Each text block contains two runs, one for the larger hour/minutes and one for the smaller seconds. (The sizes and values are set in code-behind.) A surprising fact about a text block is that its content property is not its Text property, but rather a property called Inlines. Although a type converter enables you to set it to a simple string in XAML, Inlines can be set to a collection of Inline objects. There are two classes that derive from the abstract Inline class: Run and LineBreak.
  • On a run, you can set the same formatting properties available on a text block, such as FontFamily, FontSize, FontStyle, FontWeight, Foreground, and TextDecorations. Runs, therefore, provide a convenient way to create text with mixed formatting all inside a single text block.

LISTING 20.9 TimeDisplay.xaml.cs—The Code-Behind for the TimeDisplay User Control

[code]

using System;
using System.Windows;
using System.Windows.Controls;
namespace WindowsPhoneApp
{
public partial class TimeDisplay : UserControl
{
DateTime? time;
public TimeDisplay()
{
InitializeComponent();
Time = null;
}
public void Initialize()
{
if (!this.ShowSeconds)
{
// Remove the seconds display
this.SecondsRun.Text = null;
this.SecondsBackgroundRun.Text = null;
}
// Hide AM and PM in 24-hour mode
this.AMTextBlock.Visibility =
this.Show24Hours ? Visibility.Collapsed : Visibility.Visible;
this.PMTextBlock.Visibility =
this.Show24Hours ? Visibility.Collapsed : Visibility.Visible;
// The seconds font size is always half of whatever the main font size is
this.SecondsBackgroundRun.FontSize = this.SecondsRun.FontSize =
this.FontSize / 2;
}
public bool ShowSeconds { get; set; }
public bool Show24Hours { get; set; }
public DateTime? Time
{
get { return this.time; }
set
{
this.time = value;
if (this.time == null)
{
// Clear everything
this.TimeRun.Text = null;
this.SecondsRun.Text = null;
this.AMTextBlock.Opacity = .1;
this.PMTextBlock.Opacity = .1;
return;
}
string formatString = this.Show24Hours ? “H:mm” : “h:mm”;
// The hour needs a leading space if it ends up being only one digit
if ((this.Show24Hours && this.time.Value.Hour < 10) ||
(!this.Show24Hours &&
(this.time.Value.Hour % 12 < 10 && this.time.Value.Hour % 12 > 0)))
formatString = “ “ + formatString;
this.TimeRun.Text = this.time.Value.ToString(formatString);
if (this.ShowSeconds)
this.SecondsRun.Text = this.time.Value.ToString(“ss”);
if (!this.Show24Hours)
{
// Show either AM or PM
if (this.time.Value.Hour < 12)
{
this.AMTextBlock.Opacity = 1;
this.PMTextBlock.Opacity = .1;
}
else
{
this.AMTextBlock.Opacity = .1;
this.PMTextBlock.Opacity = 1;
}
}
}
}
}
}

[/code]

The Finished Product

Alarm Clock (Settings,Toggle Switch, Custom Font)

 

2 COMMENTS

Comments are closed.