Windows Phone

Windows Phone 向けにコンパス アプリを作成する

Donn Morse

コード サンプルのダウンロード

プラットフォームの作成者として、Windows 8 用のセンサー プラットフォーム ドキュメントに対する責任があります。そのため、この新しいプラットフォームをできる限り多くの開発者に採用してもらうことを願っています。また、Metro スタイル アプリは XAML と C# を使用して作成できるため、このプラットフォームの移行を利用する理想的な候補は Windows Phone 開発者です。Windows Phone 開発者は XAML の使用経験があり、その多くはセンサーの使用経験もあります (Windows Phone の最新バージョンでは、加速度計、コンパス、ジャイロ センサー、および GPS センサーが公開されています)。

Windows Phone 開発者とその開発プラットフォームのことをさらによく理解するため、昨年秋、簡単なコンパス アプリを作成することに決めました。作成したアプリは、App Hub を使用して、無償版として Windows Phone Marketplace に提出しました。認定を受けたこのコンパス アプリは、スイスやマレーシアなど遠い国の Windows Phone ユーザーによってダウンロードされています。

今回は、このアプリの開発について説明します。

コンパス アプリ

コンパス アプリは、Windows Phone の携帯電話に組み込まれているコンパス (磁力計) を使用します。このアプリでは、真北を基準とする方位と、航行するときや地図を使用して見知らぬ土地でオリエンテーリングに参加するときに役立つ、反方位を提供します。また、このアプリを使用するユーザーは、方位角 ("090" 度など) と 4 方位 (東を表す "E" など) を切り替えることができます。現在の方位をロックすることも可能です。これは、地図上で特定の目印になる建物や基準点から方位を測るため、ユーザーがポインターを固定する必要があるときに役立ちます。

図 1 は、Samsung Focus で実行しているアプリを示します。左の図は方位角を、右の図は 4 方位を表しています。

The Running App, Showing a Numeric Heading (Left) and an Alpha Heading (Right)図 1 方位角 (左) と 4 方位 (右) を表示する実行中のアプリ

UI をデザインする

これまでコンピューター向けアプリの作成に従事してきたことから、当初、携帯電話の狭い画面には限界を感じていました。しかし、それほど深刻な問題ではありません。新しい画面のサイズという観点から、アプリの機能について少し慎重に検討する必要があるだけです。今回のコンパス アプリには、調整画面とメイン ナビゲーション画面の 2 つがあります。

調整画面: Windows Phone 携帯電話にインストールされているコンパス (磁力計) は、携帯電話の電源を入れた後に調整が必要です。また、このようなセンサーは、定期的な再調整が必要になることがあります。調整が必要な状況をプログラムから検出できるように、プラットフォームでは現在の調整状況の検出に使用できる HeadingAccuracy プロパティと、コンパスで調整が必要になったときに発生する Calibrate イベントをサポートします。

アプリではこの Calibrate イベントをハンドルすることで、調整画面 (calibrationStackPanel) を表示し、ユーザーに携帯電話の画面を八の字にスイープして、携帯電話を手動調整するよう求めます。ユーザーが画面をスイープすると、目標の精度に達するまで、現在の精度を CalibrationTextBlock に赤色のフォントで表示します (図 2 参照)。返される精度が 10 度以下になると、精度の数字を消し、緑色の文字で "Complete!" (完了) と表示します。

The Calibration Screen図 2 調整画面

調整をサポートするコードは、MainPage.xaml.cs モジュールの compass_CurrentValueChanged イベント ハンドラー内にあります (図 3 参照)。

図 3 コンパスの調整

...
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");
 }
}

目標の精度に達すると、[Done] (完了) ボタンを押すようユーザーに求めます。このボタンを押すと、調整画面が非表示になり、アプリの主要画面が表示されます。

主要画面: この画面は、方位角または 4 方位と、反方位を表示します。真北を基準とするコンパス面も表示します。また、主要画面には、ユーザーが出力方法を変更したり、方位やコンパス面をロックできる、4 つのコントロール (ボタン) も用意します。

図 4 は、アプリの主要画面 (Visual Studio では MainPage.xaml) を示しています。

The App’s Primary Screen in Visual Studio
図 4 Visual Studio でのアプリの主要画面

主要画面の UI 要素のほとんどは、簡単な TextBlock コントロールと Button コントロールです。TextBlock コントロールは、方位と反方位を特定します。ユーザーは Button コントロールを使用して、出力方法を制御できます。ただし、コンパス面だけはやや複雑です。

コンパス面: コンパス面は、背景画像、前景画像、そして境界を示す大きな楕円の 3 つのコンポーネントで構成され、この楕円の中で背景画像と前景画像を回転します。背景画像には、コンパスの 4 方位に対応する文字、水平線、垂直線を含めます。前景画像では、スモークガラスの効果を生み出します。

背景画像: 背景画像の名前は XAML では CompassFace です (この変数名は、後で、コンパス面を回転するコードで参照します)。

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

前景画像: コンパス面の前景は、XAML 自体では EllipseGlass として定義しています。スモークガラスの効果は、線状グラデーション ブラシを使用して作成します。この楕円は Microsoft Expression Blend 4 を使用して作成しました。Expression Blend ツールは Visual Studio と互換性があり、アプリの XAML を読み込み、グラフィックスをカスタマイズして UI を強化することができます。図 5 は、影付きの楕円を作成するときに使用した Expression Blend エディターを示しています。

Creating a Shaded Ellipse in Expression Blend
図 5 Expression Blend で作成する影付きの楕円

Expression Blend で楕円の編集を完了したら、次の XML を使用して Visual Studio プロジェクトの XAML を更新しました。

<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>

EllipseGlass のサイズ (263 x 263 ピクセル) が compass.png のサイズと正確に一致していることに注目してください。また、EllipseGlass というオブジェクト名は、後でコンパス面を回転するコードで参照します。

コンパス面の境界: コンパス面は、赤色の境界線で縁取りした白い大きな楕円内で回転します。この楕円は XAML で EllipseBorder という名前で定義しています。

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

UI の分離コード

コードは、このページの冒頭でダウンロードするコード サンプルの MainPage.xaml.cs ファイルに含まれています。このコードを使用して、アプリで必要な名前空間にアクセスし、センサーを初期化して、レポート間隔を設定し、調整、コンパス面の回転、方位角と 4 方位の切り替えなど、さまざまなアプリの機能を処理します。

コードでコンパスにアクセスする: コンパス アプリ (または携帯電話のセンサーを使用する任意のアプリ) を作成するには、まず、Microsoft.Devices.Sensors 名前空間で公開されるセンサー オブジェクトにアクセスできるようにします。そのためには、MainPage.xaml.cs で次の using ディレクティブを使用します。

using Microsoft.Devices.Sensors;

ファイルでこの using ディレクティブを使用したら、compass 変数を作成して、携帯電話の実際のセンサーにプログラムからアクセスできるようになります。

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

この変数を使用して、コンパスの開始や停止、現在の方位の精度の取得、レポート間隔の設定などを行います。

コンパスを開始してレポートの頻度を設定する: compass 変数を作成したら、いくつかメソッドを呼び出し、オブジェクトのプロパティを設定します。最初に呼び出すメソッドは Start メソッドです。このメソッドにより、センサーからデータの受け取りを開始できます。コンパスを開始したら、レポート間隔 (センサーを更新する間隔) を 400 ミリ秒に設定します (TimeBetweenUpdates プロパティには 20 ミリ秒の倍数を指定する必要があります)。

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

400 ミリ秒という値は、試行錯誤の結果決定しました。既定のレポート間隔は非常に短く、既定値のままアプリを実行すると、コンパス面が頻繁に回転し、不安定に見えます。

コンパスのイベント ハンドラーを指定する: コンパス アプリは 2 つのイベント ハンドラーをサポートします。1 つは調整ページ (calibrationStackPanel) を表示し、もう 1 つは現在の方位を表示してコンパス面を回転します。

調整のイベント ハンドラーを定義して指定する: 調整のイベント ハンドラーには、比較的少量のコードしかありません。このコード (図 6 参照) では 2 つの主要タスクを実行します。まず、MainPage.xaml ファイルで定義した調整画面を表示し、次に、ブール値変数の calibrating を true に設定します。

図 6 調整のイベント ハンドラー

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);
  }
}

このイベント ハンドラーは、バックグラウンド スレッドから呼び出され、UI スレッドには直接アクセスしません。そのため、調整画面を表示するには、Dispatcher オブジェクトの BeginInvoke メソッドを呼び出す必要があります。

ブール値変数の calibrating は、値の変化に応答するイベント ハンドラー (compass_CurrentValueChanged) のコードで調べます。この変数が true のときはコンパスを無視し、最新の調整データで調整画面を更新します。この変数が false のときはコンパスの読み取りデータを更新し、コンパス面の回転を実行します。

このイベント ハンドラーは、次のコードを使用して MainPage コンストラクターで指定します。

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

値の変化に応答するイベント ハンドラーを定義して指定する: 値の変化に応答するイベント ハンドラー (compass_CurrentValueChanged) は、コンパスから新しくデータを読み取るたびに呼び出されます。また、calibrating 変数の値に応じて、調整画面を更新するか、主要画面を更新します。

主要画面を更新する場合、イベント ハンドラーでは次のタスクを実行します。

  • 真北を基準に、真方位と反方位を計算します。
  • コンパス面を回転します。
  • 現在の方位と反方位を表示します。

方位を計算する: 次のコードは、イベント ハンドラーで SensorReading オブジェクトの TrueHeading プロパティを使用して、真北を基準に方位を取得する方法を示しています。

 

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

図 7 は、イベント ハンドラーで現在の方位と反方位を更新する方法を示します。

図 7 現在の方位と反方位の更新

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";
  }
}

コンパス面を回転する: 次のコード スニペットは、アプリでコンパス面の背景と前景を構成する 2 つの楕円を回転する方法を示します。

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;

CompassFace 変数は、コンパスの 4 点 (N、E、W、および S)、水平線、および垂直線を含む背景画像に対応します。EllipseGlass 変数はスモークガラスのレイヤーに対応します。

回転変換を適用するには、まず、回転する 2 つのオブジェクトの中心で変換が行われるようにする必要があります。そのためには、各オブジェクトの RenderTransformOrigin メソッドを呼び出し、座標 (0.5, 0.5) を指定します (このメソッドとその使用方法の詳細については、MSDN ライブラリの「UIElement.RenderTransformOrigin プロパティ」(msdn.microsoft.com/ja-jp/library/system.windows.uielement.rendertransformorigin.aspx) を参照してください)。

変換を中心で行うように設定したら、角度を計算し、回転を実行できます。360 から現在の方位を引いて角度を計算します (現在の方位とは、イベント ハンドラーで受け取った方位です)。この新しい角度を RenderTransform プロパティに適用します。

コンパスをロックおよびロック解除する: ロックおよびロック解除の機能は、アプリを屋外での移動中 (航行や、地図を持参してのハイキングなど) に使用するユーザー向けに作成しました。この機能はシンプルです。コンパスの Stop メソッドが呼び出されると方位をロックし、Start メソッドが呼び出されると方位の取得を再開します。

Stop メソッドは、ユーザーが [Lock] (ロック) ボタンを押したときに呼び出されます。

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

Start メソッドは、ユーザーが [Unlock] (ロック解除) ボタンを押したときに呼び出されます。

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);
  }
}

コンパスを再開するときに、レポート間隔を 400 ミリ秒に再設定して、動作の一貫性を保持するようにします。

方位角と 4 方位を切り替える: 方位角と 4 方位を切り替えるコードは、Alphabetic という 1 つのブール値変数で制御します。Alphabetic は、ユーザーが [Alpha] (4 方位) ボタンや [Numeric] (方位角) ボタンを押したときに設定します。ユーザーが [Alpha] (4 方位) ボタンを押すと、この変数を true に設定し、[Numeric] (方位角) ボタンを押すと false に設定します。

次のコードは、[Alpha] (4 方位) ボタンを押したときのイベントを示しています。

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

compass_CurrentValueChanged イベント ハンドラー内のコードでは、Alphabetic を確認し、方位角または 4 方位のどちらを表示すべきかを判断します。

明暗の表示テーマをサポートする: アプリを作成し、認定を受けるため App Hub に提出すると、明るい表示テーマをテストしたときに一部の UI 要素が消失したため、アプリを認定できないという通知が届き驚きました (暗い表示テーマでしかアプリを実行していなかったため、明るい表示テーマのテストに不合格になりました)。

この問題を解決するため、現在のテーマを取得し、UI 要素 (TextBlock と Button) の前景色が指定のテーマで機能するように設定するコードを MainPage コンストラクターに追加しました。明るい表示テーマが設定されると、要素の前景色を黒と赤に設定します。暗い表示テーマが設定されると、要素の前景色を暗い灰色と明るい灰色に設定します。図 8 はそのコードを示しています。

図 8 テーマと色の調整

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;
}

楽しくて実用的

このアプリを作成するのはとても楽しく、このアプリは実用的です。Windows Phone プラットフォームでセンサーを使用すると、このプラットフォームと Windows 8 でのセンサー サポートの違いがよくわかりました。ただし、最も印象に残ったのは、両者が似ている点です。センサーの名前空間を使用した経験がある Windows Phone 開発者は、実に簡単に Windows 8 に移行できることがわかると思います。また、Windows 8 では、傾斜計センサー、方位センサー、簡易方位センサーなど追加のセンサーを使用できます (方位センサーは、複雑なゲームの制御に使用できる、クオータニオン (回転のマトリックス) を返す複数のセンサーの組み合わせです。簡易方位センサーを使用すると、デバイスが縦モードと横モードのいずれになっているか、画面を上にしているか下にしているかを検出できます)。

これらすべてのセンサーによって優れた開発を実現できます。そのため、創造性豊かな開発者コミュニティが、これらのセンサーをいかに創意工夫に富んだ方法で活用するか楽しみです。

Donn Morse は、マイクロソフトの Windows チームのシニア プログラミング ライターを務めています。彼はここ数年間、アプリからドライバーに至るまで、センサー プラットフォームを担当していました。センサーとその使用方法に夢中で、強い関心を抱いています。

この記事のレビューに協力してくれた技術スタッフの Jason Scot に心より感謝いたします。