July 2012

Volume 27 Number 07

Windows Phone - Writing a Compass Application for Windows Phone

By Donn Morse | July 2012

As a writerresponsible for the sensor platform documentation for Windows 8, I want to see as many developers as possible adopt our new platform. And, because Metro-style apps can be written using XAML and C#, the Windows Phone developer is an ideal candidate for this migration. Windows Phone developers already have expe­rience with XAML, and a number also have experience with sensors (because the accelerometer, compass, gyro and GPS sensors are exposed in the most recent Windows Phone release).

To help me better understand Windows Phone developers and their development platform, I decided to write a simple compass app last fall. Once I wrote it, I submitted a free version to the Windows Phone Marketplace using the App Hub. Since acceptance, the app has been downloaded by Windows Phone users from as far away as Switzerland and Malaysia.

This article covers the development of the application.

The Compass Application

The compass application uses the compass, or magnetometer, built in to the Windows Phone device. This app provides a heading with respect to true north as well as a reciprocal heading that can be useful when navigating in a boat or orienteering in a remote area with a map. In addition, the app allows the user to toggle from a numeric heading (for example, “090” degrees) to an alpha heading (for example, “E” for east). The app also allows the user to lock the current heading. This is useful when users need the pointer to remain stationary so they can take a bearing on a specific landmark or reference point on a map or chart.

Figure 1shows the application running on a Samsung Focus. The image on the left displays a numeric heading and the image on the right displays an alpha heading.

The Running App, Showing a Numeric Heading (Left) and an Alpha Heading (Right)
Figure 1 The Running App, Showing a Numeric Heading (Left) and an Alpha Heading (Right)

Designing the UI

As a developer accustomed to writing apps for the PC, I initially felt limited by the reduced screen real estate on the phone. However, this wasn’t a showstopper. I just needed to give a little more thought—and careful consideration—to the features in my app with respect to the new screen dimensions. My compass app has two screens: a calibration screen and the main navigation screen.

The Calibration Screen The compass, or magnetometer, installed on a Windows Phone device requires calibration after the device is powered up. In addition, these sensors might require periodic recalibration. To help you programmatically detect when calibration is necessary, the platform supports a HeadingAccuracy property that you can use to detect the current calibration. In addition, the platform supports a Calibrate event that’s fired if the compass requires calibration.

My app handles the Calibrate event, which, in turn, displays a calibration screen (calibrationStackPanel) that prompts the user to manually calibrate the device by sweeping the phone in a figure 8 motion. As the user sweeps the phone, the current accuracy is displayed in the CalibrationTextBlock with a red font until the desired accuracy is achieved, as shown in Figure 2. Once the returned accuracy is less than or equal to 10°, the numeric values are cleared and a green “Complete!” appears.

The Calibration Screen
Figure 2 The Calibration Screen

The corresponding code that supports calibration is found in the module MainPage.xaml.cs within the compass_Current­ValueChanged event handler, as shown in Figure 3.

Figure 3 Calibrating the Compass

...
else
{
 if (HeadingAccuracy <= 10)
 {
  CalibrationTextBlock.Foreground = 
    new SolidColorBrush(Colors.Green);
  CalibrationTextBlock.Text = "Complete!";
 }
 else
 {
  CalibrationTextBlock.Foreground = 
    new SolidColorBrush(Colors.Red);
  CalibrationTextBlock.Text = 
    HeadingAccuracy.ToString("0.0");
 }
}

Once the desired accuracy is achieved, the user is prompted to press the Done button, which hides the calibration screen and displays the app’s primary screen.

The Primary Screen This screen displays a numeric or alpha heading and reciprocal value. In addition, it renders the face of a compass that’s oriented with respect to true north. Finally, the primary screen displays the four controls (or buttons) that let the user alter the output as well as lock the heading value and compass face.

Figure 4 shows my application’s primary screen, MainPage.xaml, as it appears in Visual Studio.

The App’s Primary Screen in Visual Studio
Figure 4 The App’s Primary Screen in Visual Studio

Most of the UI elements in the primary screen are simple TextBlock and Button controls. The text blocks identify the heading and its reciprocal value. The buttons enable the user to control the output. However, the compass face is a little more involved.

The Compass Face The compass face consists of three components: a background image, a foreground image, and a larger, bordering ellipse in which the foreground and background images rotate. The background image contains the letters corresponding to the four points on a compass, a horizontal line and a vertical line. The foreground image creates the smoked-glass effect.

The Background Image In the XAML, the background image is named CompassFace (this variable name is referenced later in the code that rotates the compass face):

<Image Height="263" HorizontalAlignment="Left" Margin="91,266,0,0"
  Name="CompassFace" VerticalAlignment="Top" Width="263"  
  Source="/Compass71;component/compass.png" Stretch="None" />

The Foreground Image The foreground of the compass face, EllipseGlass, is defined in the XAML itself. The smoked-glass effect is created using a linear-gradient brush. I created this ellipse using Microsoft Expression Blend 4. The Expression Blend tool is compatible with Visual Studio and lets you load your application’s XAML and enhance the UI by customizing the graphics. Figure 5 shows the Expression Blend editor as it appeared when creating the shaded ellipse.

Creating a Shaded Ellipse in Expression Blend
Figure 5 Creating a Shaded Ellipse in Expression Blend

After I finished editing the ellipse in Expression Blend, the XAML in my Visual Studio project was updated with the following XML:

<Ellipse Height="263"  Width="263" x:Name="EllipseGlass" 
  Margin="91,266,102,239" Stroke="Black" StrokeThickness="1">
  <Ellipse.Fill>
    <LinearGradientBrush EndPoint="1,0.5" StartPoint="0,0.5">
    <GradientStop Color="#A5000000" Offset="0" />
    <GradientStop Color="#BFFFFFFF" Offset="1" />
    </LinearGradientBrush>
  </Ellipse.Fill>

Note that the dimensions of EllipseGlass (263 x 263 pixels) are an exact match of the dimensions of compass.png. Also note that the object name, EllipseGlass, is referenced later in the code that performs the rotation of the compass face.

The Compass Face Border The compass face rotates within a larger, white ellipse with a red border. This ellipse is defined in the XAML and named EllipseBorder:

<Ellipse Height="385" HorizontalAlignment="Left" Margin="31,0,0,176"
  Name="EllipseBorder" Stroke="#FFF80D0D" StrokeThickness="2"
  VerticalAlignment="Bottom" Width="385" Fill="White" />

The Code Behind the UI

The code resides in the file MainPage.xaml.cs in the accompanying code download, and it accesses the namespaces required by the application, initializes the sensor, sets a reporting interval and handles the various application features: calibration, rotating the compass face, toggling between numeric and alpha output and so on.

Accessing the Compass in Your Code The first step in writing a compass application (or any application that accesses one of the phone sensors) is to obtain access to the sensor objects exposed by the namespace Microsoft.Devices.Sensors. This is accomplished with the following using directive in MainPage.xaml.cs:

using Microsoft.Devices.Sensors;

Once this using directive appears in the file, I can create a compass variable that gives me programmatic access to the actual device in the phone:

namespace Compass71
{
  public partial class MainPage : PhoneApplicationPage
  {
    Compass compass = new Compass();

I’ll use this variable to start the compass, stop it, retrieve the current heading accuracy, set the report interval and so on.

Starting the Compass and Setting the Report Frequency Once I create the compass variable, I can begin invoking methods and setting properties on the object. The first method I invoked is the Start method, which allows me to begin receiving data from the sensor. After I start the compass, I set the report interval—the time between sensor updates—to 400 ms (note that the TimeBetweenUpdates property requires a multiple of 20 ms):

compass.TimeBetweenUpdates = 
  TimeSpan.FromMilliseconds(400);  // Must be multiple of 20
compass.Start();

The 400 ms value was chosen by trial and error. The default report interval is extremely short. If you attempt to run the application at this default value, the compass face is rotated so frequently that it appears to be unstable.

Establishing the Compass Event Handlers The compass app supports two event handlers: one that displays the calibration page (calibration­StackPanel), and another that renders the current heading and rotates the compass face.

Defining and Establishing the Calibration Event Handler The calibration event handler contains relatively few lines of code. This code, shown in Figure 6, accomplishes two primary tasks: First, it displays the calibration screen that’s defined in the MainPage.xaml file; second, it sets a Boolean variable calibrating to true.

Figure 6 The Calibration Event Handler

void compass_Calibrate(object sender, 
  CalibrationEventArgs e)
{
  try
  {
    Dispatcher.BeginInvoke(() => 
    { calibrationStackPanel.Visibility =
      Visibility.Visible; 
    });
    calibrating = true;
  }
  catch (Exception ex)
  {
    MessageBox.Show(ex.Message.ToString(), 
       "Error!", MessageBoxButton.OK);
  }
}

Because this event handler is called from a background thread, it doesn’t have direct access to the UI thread. So, to display the calibration screen, I need to call the BeginInvoke method on the Dispatcher object.

The Boolean variable calibrating is examined within the code for the value-changed event handler (compass_CurrentValueChanged). When this variable is true, I ignore the compass and update the calibration screen with the most recent calibration data. When the variable is false, I update the compass readings and perform rotations of the compass face.

This event handler is established in the MainPage constructor with the following line of code:

compass.Calibrate += new EventHandler<CalibrationEventArgs>(compass_Calibrate);

Defining and Establishing the Value-Changed Handler The value-changed event handler (compass_CurrentValueChanged) is invoked each time a new reading arrives from the compass. And, depending on the state of the calibrating variable, it either updates the calibration screen or it updates the primary screen.

When it’s updating the primary screen, the event handler performs the following tasks:

  • It computes the true and reciprocal headings with respect to true north.
  • It rotates the compass face.
  • It renders the current and reciprocal headings.

Computing the Headings The following code demonstrates how the event handler retrieves the heading with respect to true north using the TrueHeading property on the SensorReading object:

 

TrueHeading = e.SensorReading.TrueHeading;
  if ((180 <= TrueHeading) && (TrueHeading <= 360))
    ReciprocalHeading = TrueHeading - 180;
  Else
    ReciprocalHeading = TrueHeading + 180;

Figure 7 demonstrates how the event handler updates the current and reciprocal headings.

Figure 7 Updating Current and Reciprocal Headings

if (!Alphabetic) // Render numeric heading
{
  HeadingTextBlock.Text = TrueHeading.ToString();
  RecipTextBlock.Text = ReciprocalHeading.ToString();
}
else // Render alpha heading
{
  if (((337 <= TrueHeading) && (TrueHeading < 360)) ||
    ((0 <= TrueHeading) && (TrueHeading < 22)))
  {
    HeadingTextBlock.Text = "N";
    RecipTextBlock.Text = "S";
  }
  else if ((22 <= TrueHeading) && (TrueHeading < 67))
  {
    HeadingTextBlock.Text = "NE";
    RecipTextBlock.Text = "SW";
  }
  else if ((67 <= TrueHeading) && (TrueHeading < 112))
  {
    HeadingTextBlock.Text = "E";
    RecipTextBlock.Text = "W";
  }
  else if ((112 <= TrueHeading) && (TrueHeading < 152))
  {
    HeadingTextBlock.Text = "SE";
    RecipTextBlock.Text = "NW";
  }
  else if ((152 <= TrueHeading) && (TrueHeading < 202))
  {
    HeadingTextBlock.Text = "S";
    RecipTextBlock.Text = "N";
  }
  else if ((202 <= TrueHeading) && (TrueHeading < 247))
  {
    HeadingTextBlock.Text = "SW";
    RecipTextBlock.Text = "NE";
  }
  else if ((247 <= TrueHeading) && (TrueHeading < 292))
  {
    HeadingTextBlock.Text = "W";
    RecipTextBlock.Text = "E";
  }
  else if ((292 <= TrueHeading) && (TrueHeading < 337))
  {
    HeadingTextBlock.Text = "NW";
    RecipTextBlock.Text = "SE";
  }
}

Rotating the Compass Face The following code snippet demonstrates how the app rotates the two ellipses that constitute the background and foreground of the compass face:

CompassFace.RenderTransformOrigin = new Point(0.5, 0.5);
EllipseGlass.RenderTransformOrigin = new Point(0.5, 0.5);
transform.Angle = 360 - TrueHeading;
CompassFace.RenderTransform = transform;
EllipseGlass.RenderTransform = transform;

The variable CompassFace corresponds to the background image that contains the four points of the compass (N, E, W and S) and the horizontal and vertical line. The variable EllipseGlass corresponds to the smoked-glass layer.

Before I can apply a rotation transform, I need to ensure that the transform is centered on the two objects that I’ll rotate. This is done by invoking the RenderTransformOrigin method on each object and supplying the coordinates (0.5, 0.5). (For more information about this method and its use, refer to the MSDN Library page, “UIElement.RenderTransformOrigin Property,” at bit.ly/KIn8Zh.)

Once I’ve centered the transform, I can compute the angle and perform the rotation. I compute the angle by subtracting the current heading from 360. (This is the heading I just received in the event handler.) I apply this new angle with the RenderTransform property.

Locking and Unlocking the Compass The locking and unlock­ing feature was intended for someone outdoors who’s using the app to navigate (whether in a boat or hiking on a trail with map in hand). This feature is simple; it invokes the Stop method on the compass to lock the heading and then it invokes the Start method to resume heading retrieval.

The Stop method is invoked when the user presses LockButton:

private void LockButton_Click(object sender, 
  RoutedEventArgs e)
{
  try
  {
    compass.Stop();
  }
  catch (Exception ex)
  {
    MessageBox.Show(ex.Message.ToString(), 
      "Error!", MessageBoxButton.OK);
  }
}

The Start method is invoked when the user presses UnlockButton:

private void UnlockButton_Click(object sender, 
  RoutedEventArgs e)
{
  try
  {
    compass.Start();
    compass.TimeBetweenUpdates =
      TimeSpan.FromMilliseconds(400);  
      // Must be multiple of 20
  }
  catch (Exception ex)
  {
    MessageBox.Show(ex.Message.ToString(), 
      "Error!", MessageBoxButton.OK);
  }
}

Note that in addition to restarting the compass, I reset the report interval to 400 ms to ensure consistent behavior.

Toggling Between Numeric and Alpha Headings The code that toggles between alpha and numeric headings is controlled by a single Boolean variable named Alphabetic that’s set when a user presses AlphaButton or NumericButton. When the user presses AlphaButton, this variable is set to True; when the user presses NumericButton, it’s set to False.

Following is the code for the AlphaButton click event:

private void AlphaButton_Click(
    object sender, RoutedEventArgs e)
  {
    try
    {
      Alphabetic = true;
    }
    catch (Exception ex)
    {
      MessageBox.Show(
         ex.Message.ToString(), "Error!",
        MessageBoxButton.OK);
    }
  }

The code within the compass_CurrentValueChanged event handler examines Alphabetic  to determine whether it should render the numeric or alpha headings.

Supporting the Light and Dark Visibility Themes After I created the app and submitted it to the App Hub for certification, I was surprised to receive notification that it had failed because certain UI elements disappeared when the light visibility theme was tested. (I’d been running exclusively with the dark theme and had failed to test the light theme.)

To resolve this issue, I added code to the MainPage constructor, which retrieves the current theme and then sets the foreground color of the UI elements (text blocks and buttons) to work with the given theme. If the light theme is set, the foreground colors of the elements are set to black and red. If the dark theme is set, the foreground colors of the elements are set to dark and light gray. Figure 8 shows this code.

Figure 8 Coordinating Themes and Colors

Visibility isLight = (Visibility)Resources["PhoneLightThemeVisibility"]; // For light theme
if (isLight == System.Windows.Visibility.Visible) // Light theme enabled
{
  // Constructor technique
  SolidColorBrush scb = new SolidColorBrush(Colors.Black);
  SolidColorBrush scb2 = new SolidColorBrush(Colors.Red);
  RecipLabelTextBlock.Foreground = scb;
  HeadingLabelTextBlock.Foreground = scb;
  RecipTextBlock.Foreground = scb2;
  HeadingTextBlock.Foreground = scb2;
  LockButton.Foreground = scb;
  UnlockButton.Foreground = scb;
  AlphaButton.Foreground = scb;
  NumericButton.Foreground = scb;
}
else // Dark color scheme is selected—set text colors accordingly
{
  // Constructor technique
  SolidColorBrush scb = new SolidColorBrush(Colors.DarkGray);
  SolidColorBrush scb2 = new SolidColorBrush(Colors.LightGray);
  RecipLabelTextBlock.Foreground = scb;
  HeadingLabelTextBlock.Foreground = scb;
  RecipTextBlock.Foreground = scb2;
  HeadingTextBlock.Foreground = scb2;
  LockButton.Foreground = scb;
  UnlockButton.Foreground = scb;
  AlphaButton.Foreground = scb;
  NumericButton.Foreground = scb;
}

Fun and Valuable

The creation of this app was a lot of fun, and it was valuable as well. Having worked with a sensor in the Windows Phone platform, I now have a clearer understanding of the differences between this platform and the sensor support in Windows 8. But what struck me the most were the similarities. My hunch is that if you’re a Windows Phone developer who has spent any time with the sensor namespace, you’re going to find the migration to Windows 8 exceptionally straightforward. And, in Windows 8, you’ll find additional sensors such as the inclinometer, orientation sensor and simple orientation sensor. (The orientation sensor is a fusion of multiple sensors that returns a Quaternion, or rotation matrix, which you can use to control complex games. The simple orientation sensor lets you detect whether your device is in portrait or landscape mode, as well as faceup or facedown.)

The development opportunities afforded by all these sensors are exciting, and I look forward to seeing the imaginative ways our creative developer community can put them to use.


Donn Morse is a senior programming writer on the Windows team at Microsoft, who has focused for the past several years on the sensor platform, from the application side to the driver. He’s passionate about, and fascinated by, sensors and their use.

Thanks to the following technical expert for reviewing this article: Jason Scott