本主题将带您完成创建罗盘应用程序,该应用程序为您提供罗盘数据的数字显示以及图形表示。本演练还介绍如何实现罗盘校准对话框。
可以使用罗盘或磁力计传感器来确定设备相对于地球磁场北极旋转的角度。应用程序也可以使用原始磁力计读数来检测设备周围的磁力。罗盘传感器对于所有 Windows Phone 设备来说都不是必需的。设计和实现应用程序时应该考虑此内容,这一点非常重要。应用程序应该始终检查传感器是否可用,如果不可用,是提供备用输入机制还是正常失败。
罗盘 API 根据设备的方向使用单个轴来计算航向。如果您想创建一个使用所有轴上的设备方向的应用程序,则应该使用 Motion 类的 RotationMatrix 属性。
设备中的罗盘传感器可能随时间变得不精确,尤其是暴露在磁场中时更是如此。有一个重新校准罗盘的简单用户操作。只要系统检测到航向精度大于 +/- 20 度,就引发 Calibrate 事件。此示例介绍如何实现允许用户校准其罗盘的校准对话框。
以下步骤向您介绍如何创建罗盘应用程序。
创建罗盘应用程序的步骤
在 Visual Studio 中,创建一个新的“Windows Phone 应用程序”项目。此模板在“Silverlight for Windows Phone”类别中。
该应用程序需要引用包含传感器 API 和 XNA Framework 的程序集,因为其中一些罗盘数据采用 XNA Framework Vector3 对象的形式传递。从“项目”菜单中,单击“添加引用...”,选择“Microsoft.Devices.Sensors”和“Microsoft.Xna.Framework”,然后单击“确定”。
在 MainPage.xaml 文件中,将以下 XAML 代码放置在名为“ContentPanel”的 Grid 元素中。此 XAML 代码创建将显示罗盘数据的 UI。TextBlock 元素将用于以数字形式显示罗盘数据。Line 元素将用于以图形方式显示航向和原始罗盘数据。
<StackPanel Orientation="Vertical"> <StackPanel Orientation="Horizontal"> <TextBlock>status: </TextBlock> <TextBlock Name="statusTextBlock"></TextBlock> </StackPanel> <StackPanel Orientation="Horizontal"> <TextBlock>time between updates:</TextBlock> <TextBlock Name="timeBetweenUpdatesTextBlock"></TextBlock> </StackPanel> <StackPanel Orientation="Horizontal"> <TextBlock>magnetic heading: </TextBlock> <TextBlock Name="magneticTextBlock"></TextBlock> </StackPanel> <StackPanel Orientation="Horizontal"> <TextBlock>true heading: </TextBlock> <TextBlock Name="trueTextBlock"></TextBlock> </StackPanel> <StackPanel Orientation="Horizontal"> <TextBlock>heading accuracy: </TextBlock> <TextBlock Name="accuracyTextBlock"></TextBlock> </StackPanel> <StackPanel Orientation="Horizontal"> <TextBlock>compass orientation mode:</TextBlock> <TextBlock Name="orientationTextBlock"></TextBlock> </StackPanel> <Grid Height="200" Name="headingGrid"> <TextBlock Foreground="Yellow" FontSize="16">magnetic heading</TextBlock> <TextBlock Foreground="Orange" FontSize="16" Margin="0,18">true heading</TextBlock> <Line x:Name="magneticLine" X1="240" Y1="100" X2="240" Y2="0" Stroke="Yellow" StrokeThickness="4"></Line> <Line x:Name="trueLine" X1="240" Y1="100" X2="240" Y2="0" Stroke="Orange" StrokeThickness="4"></Line> </Grid> <TextBlock Text="raw magnetometer data:"></TextBlock> <Grid> <TextBlock Height="30" HorizontalAlignment="Left" Name="xTextBlock" Text="X: 1.0" VerticalAlignment="Top" Foreground="Red" FontWeight="Bold"/> <TextBlock Height="30" HorizontalAlignment="Center" Name="yTextBlock" Text="Y: 1.0" VerticalAlignment="Top" Foreground="Green" FontWeight="Bold"/> <TextBlock Height="30" HorizontalAlignment="Right" Name="zTextBlock" Text="Z: 1.0" VerticalAlignment="Top" Foreground="Blue" FontWeight="Bold"/> </Grid> <Grid Height="140"> <Line x:Name="xLine" X1="240" Y1="40" X2="240" Y2="40" Stroke="Red" StrokeThickness="14"></Line> <Line x:Name="yLine" X1="240" Y1="70" X2="240" Y2="70" Stroke="Green" StrokeThickness="14"></Line> <Line x:Name="zLine" X1="240" Y1="100" X2="240" Y2="100" Stroke="Blue" StrokeThickness="14"></Line> </Grid> </StackPanel>
这就是显示 UI 的方式。
接下来,添加用来定义罗盘校准对话框 UI 的 XAML。该 UI 向用户显示一个图像以及有关如何移动设备以便校准罗盘的说明。请注意,外面的 StackPanel 设置为 Visibility.Collapsed 以便对用户隐藏。稍后将在本演练中提供用来显示 UI 的代码。
<!--Calibration UI--> <StackPanel Name="calibrationStackPanel" Background="Black" Opacity="1" Visibility="Collapsed"> <Image Source="/Images/calibrate_compass.png" Opacity=".95" HorizontalAlignment="Center"/> <TextBlock TextWrapping="Wrap" TextAlignment="Center">The compass on your device needs to be calibrated. Hold the device in front of you and sweep it through a figure 8 pattern as shown until the calibration is complete.</TextBlock> <StackPanel Orientation="Horizontal" Margin="0,10" HorizontalAlignment="Center"> <TextBlock>heading accuracy:</TextBlock> <TextBlock Name="calibrationTextBlock">0.0°</TextBlock> </StackPanel> <Button Name="calibrationButton" Content="Done" Click="calibrationButton_Click"></Button> </StackPanel> <!--End Calibration UI-->
这就是校准 UI 的显示方式。
上面定义的罗盘校准对话框使用一个图像来校准罗盘,该图像演示了用户应该扫掠设备的模式。若要向您的解决方案中添加图像,请将该图像复制到项目目录中名为“Images”的子文件夹中。在“解决方案资源管理器”中,右键单击应用程序项目,然后选择“添加 -> 现有项...”。选择您的图像并单击“添加”。添加该图像之后,在“解决方案资源管理器”中右键单击该图像图标,然后选择“属性”。确保“生成操作”属性设置为“内容”。原始传感器数据示例附带此示例中使用的图像,您可以从 Windows Phone 的代码示例中下载该示例。
向 MainPage.xaml 中添加的最后一个 UI 代码是具有一个按钮的应用程序栏的定义,该按钮将开始和停止从罗盘获取数据。将以下代码粘贴到项目模板中包含的已注释掉的应用程序栏代码上。
现在,打开 MainPage.xaml.cs 代码隐藏页面并向该页面顶部的其他 using 指令中添加传感器和 XNA Framework 命名空间的 using 指令。此示例使用计时器来更新 UI,以便也包含 System.Windows.Threading 命名空间。
using Microsoft.Devices.Sensors; using Microsoft.Xna.Framework; using System.Windows.Threading;
在 MainPage 类定义的顶部声明一些成员变量。
public partial class MainPage : PhoneApplicationPage { Compass compass; DispatcherTimer timer; double magneticHeading; double trueHeading; double headingAccuracy; Vector3 rawMagnetometerReading; bool isDataValid; bool calibrating = false;
第一个变量是 Compass 类型的对象,它将用于从罗盘传感器获取数据。接下来,声明一个 DispatcherTimer,它将用于定期更新 UI。然后,便有了一组将包含罗盘数据的变量。这些变量将使用罗盘 API 进行设置,并将显示在 DispatcherTimer 的 Tick 事件中。最后,布尔变量 calibrating 将用于跟踪当前是否正在显示校准对话框。
在页面的构造函数中,查看其上运行应用程序的设备是否支持罗盘传感器。并非所有设备都支持所有传感器,因此使用传感器之前您应该始终进行检查。如果不支持罗盘,则会向用户显示一个消息并且隐藏应用程序栏。如果支持罗盘,则会初始化 DispatcherTimer 并分配一个事件处理程序,但此时不启动计时器。用下面的代码替换现有的页面构造函数。
// Constructor public MainPage() { InitializeComponent(); if (!Compass.IsSupported) { // The device on which the application is running does not support // the compass sensor. Alert the user and hide the // application bar. statusTextBlock.Text = "device does not support compass"; ApplicationBar.IsVisible = false; } else { // Initialize the timer and add Tick event handler, but don't start it yet. timer = new DispatcherTimer(); timer.Interval = TimeSpan.FromMilliseconds(30); timer.Tick += new EventHandler(timer_Tick); } }
为应用程序栏按钮添加单击事件的处理程序。根据您之前在本主题中添加 XAML 代码的方式,Visual Studio 可能会为您添加此处理程序。如果是这样,则删除该处理程序中的任何代码。如果该处理程序是自动添加的,请将以下空函数复制并粘贴到 MainPage 类定义中。
private void ApplicationBarIconButton_Click(object sender, EventArgs e) { }
在应用程序栏按钮单击处理程序中,首先查看 Compass 对象是否不为 null 以及是否正在接收数据。如果是这种情况,则用户单击该按钮以停止罗盘,以便为 Compass 和 DispatcherTimer 调用 Stop()()()()。将以下代码粘贴到空的按钮单击处理程序中。
if (compass != null && compass.IsDataValid) { // Stop data acquisition from the compass. compass.Stop(); timer.Stop(); statusTextBlock.Text = "compass stopped."; }
接下来,该代码将处理用户正在启动罗盘的情况。如果 Compass 对象为 null,则创建一个新的实例。设置所需的更新时间间隔。请注意,不同设备上的传感器支持不同的更新间隔,在此示例中,在设置之后查询属性以便向用户显示传感器的实际间隔。下面,为罗盘有新数据时引发的 CurrentValueChanged 事件以及在罗盘需要校准时引发的 Calibrate 事件添加事件处理程序。将该代码粘贴到按钮单击处理程序中,放置在之前的代码部分之后。
else { if (compass == null) { // Instantiate the compass. compass = new Compass(); // Specify the desired time between updates. The sensor accepts // intervals in multiples of 20 ms. compass.TimeBetweenUpdates = TimeSpan.FromMilliseconds(20); // The sensor may not support the requested time between updates. // The TimeBetweenUpdates property reflects the actual rate. timeBetweenUpdatesTextBlock.Text = compass.TimeBetweenUpdates.TotalMilliseconds + " ms"; compass.CurrentValueChanged += new EventHandler<SensorReadingEventArgs<CompassReading>>(compass_CurrentValueChanged); compass.Calibrate += new EventHandler<CalibrationEventArgs>(compass_Calibrate); }
现在,使用 Start()()()() 方法启动罗盘。调用 Start 有可能会失败,因此您应该将此调用放置在一个 try 块中。在 catch 块中,您可以警告用户罗盘可能无法启动。该代码还启动 DispatcherTimer。将该代码粘贴到“开始”按钮单击处理程序中,放置在之前的代码部分之后。
try { statusTextBlock.Text = "starting compass."; compass.Start(); timer.Start(); // Start accelerometer for detecting compass axis accelerometer = new Accelerometer(); accelerometer.CurrentValueChanged += new EventHandler<SensorReadingEventArgs<AccelerometerReading>>(accelerometer_CurrentValueChanged); accelerometer.Start(); } catch (InvalidOperationException) { statusTextBlock.Text = "unable to start compass."; } }
现在,实现 CurrentValueChanged 事件处理程序。具有新罗盘数据的系统会以使用 TimeBetweenUpdates 指定的频率调用该方法。该处理程序接收包含罗盘数据的 CompassReading 对象。在对 UI 没有访问权限的后台线程上调用该处理程序。因此,如果您想通过该方法修改 UI,则必须使用 Dispatcher.BeginInvoke 在 UI 线程上调用代码。此示例使用一个调度程序计时器更新 UI,因此该方法只是将类成员变量的值设置为 CompassReading 对象的值。Abs 函数用于获取航向精度的绝对值,因为该应用程序只关心精度的量值,而不关心符号。
void compass_CurrentValueChanged(object sender, SensorReadingEventArgs<CompassReading> e) { // Note that this event handler is called from a background thread // and therefore does not have access to the UI thread. To update // the UI from this handler, use Dispatcher.BeginInvoke() as shown. // Dispatcher.BeginInvoke(() => { statusTextBlock.Text = "in CurrentValueChanged"; }); isDataValid = compass.IsDataValid; trueHeading = e.SensorReading.TrueHeading; magneticHeading = e.SensorReading.MagneticHeading; headingAccuracy = Math.Abs(e.SensorReading.HeadingAccuracy); rawMagnetometerReading = e.SensorReading.MagnetometerReading; }
实现 DispatcherTimer Tick 事件处理程序以用当前罗盘读数更新 UI。根据罗盘当前是否正在校准,该方法具有不同的行为。如果未进行校准,则更新状态 TextBlock 以指示正在接收数据。接下来,更新 TextBlock 对象以显示磁场航向(相对于磁场北极的航向)、真实航向(相对于地理北极的航向)以及显示罗盘读数误差的航向精度。然后,更新 Line 对象以采用图形方式演示罗盘读数。Microsoft.Xna.Framework 库中的 MathHelper 类用于将读数从度转换为弧度以便可以用于三角函数。接下来,采用数字和图形的方式显示原始磁力计读数。在 MainPage.xaml.cs 中粘贴以下代码。将在下一步中显示该方法的剩余部分。
void timer_Tick(object sender, EventArgs e) { if (!calibrating) { if (isDataValid) { statusTextBlock.Text = "receiving data from compass."; } // Update the textblocks with numeric heading values magneticTextBlock.Text = magneticHeading.ToString("0.0"); trueTextBlock.Text = trueHeading.ToString("0.0"); accuracyTextBlock.Text = headingAccuracy.ToString("0.0"); // Update the line objects to graphically display the headings double centerX = headingGrid.ActualWidth / 2.0; double centerY = headingGrid.ActualHeight / 2.0; magneticLine.X2 = centerX - centerY * Math.Sin(MathHelper.ToRadians((float)magneticHeading)); magneticLine.Y2 = centerY - centerY * Math.Cos(MathHelper.ToRadians((float)magneticHeading)); trueLine.X2 = centerX - centerY * Math.Sin(MathHelper.ToRadians((float)trueHeading)); trueLine.Y2 = centerY - centerY * Math.Cos(MathHelper.ToRadians((float)trueHeading)); // Update the textblocks with numeric raw magnetometer readings xTextBlock.Text = rawMagnetometerReading.X.ToString("0.00"); yTextBlock.Text = rawMagnetometerReading.Y.ToString("0.00"); zTextBlock.Text = rawMagnetometerReading.Z.ToString("0.00"); // Update the line objects to graphically display raw data xLine.X2 = xLine.X1 + rawMagnetometerReading.X * 4; yLine.X2 = yLine.X1 + rawMagnetometerReading.Y * 4; zLine.X2 = zLine.X1 + rawMagnetometerReading.Z * 4; }
Tick 事件处理程序的第二部分将更新罗盘校准的 UI。请记住,默认情况下,此 UI 不向用户显示。将在后面的步骤中显示显示 UI 的代码。该代码只是计算 HeadingAccuracy 值,如果该值小于或等于 10 度,则考虑校准罗盘,以便文本颜色设置为绿色并且通知用户校准已完成。否则,文本为红色并且显示航向精度的数值。在上一步中的代码后面粘贴以下代码,以完成 Tick 事件处理程序。
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"); } } }
接下来,实现 Calibrate 事件处理程序。如果系统检测到罗盘航向精度大于 +/20 度,则引发此事件。在此事件处理程序中,只是使校准 UI 可见并且将 calibrating 成员变量设置为 true。请注意,使用 Dispatcher.Invoke 调用更新 UI 的代码,因为未在 UI 线程上调用此事件处理程序。
void compass_Calibrate(object sender, CalibrationEventArgs e) { Dispatcher.BeginInvoke(() => { calibrationStackPanel.Visibility = Visibility.Visible; }); calibrating = true; }
最后,为校准 UI 中的按钮实现 Click 事件处理程序。当校准完成时用户将点按此处理程序。该方法只是隐藏校准 UI 并将 calibrating 成员变量设置为 false。
private void calibrationButton_Click(object sender, RoutedEventArgs e) { calibrationStackPanel.Visibility = Visibility.Collapsed; calibrating = false; }
罗盘 API 将根据手机的方向使用不同的轴来计算航向。以下代码修改示例应用程序,以确定在运行时罗盘使用的方向。
确定罗盘方向模式的步骤
首先,在具有另一个成员变量的类顶部添加一个类型为 Accelerometer 的成员变量。
Accelerometer accelerometer;
接下来,在应用程序栏按钮单击处理程序中,调用位于 Compass 和 DispatcherTimer 的 Stop 方法调用之后的加速度计的 Stop()()()() 方法。
… compass.Stop(); timer.Stop(); // Add the following line accelerometer.Stop();
之后,还在应用程序栏按钮单击处理程序中,初始化 Accelerometer 对象,连接 CurrentValueChanged 的事件处理程序,然后调用 Start()()()()。为 Compass 和 DispatcherTimer 调用 Start 之后,添加该代码。
… statusTextBlock.Text = "starting compass."; compass.Start(); timer.Start(); // add the following lines accelerometer = new Accelerometer(); accelerometer.CurrentValueChanged += new EventHandler<SensorReadingEventArgs<AccelerometerReading>>(accelerometer_CurrentValueChanged); accelerometer.Start();
最后,为 Accelerometer 对象的 CurrentValueChanged 事件实现事件处理程序。该方法获取表示加速度读数的 Vector3 对象,然后使用三角函数确定设备的方向。最后,用当前罗盘模式更新 UI。请注意,Dispatcher.BeginInvoke 用于更新 UI,因为不是在 UI 线程上调用此事件。
void accelerometer_CurrentValueChanged(object sender, SensorReadingEventArgs<AccelerometerReading> e) { Vector3 v = e.SensorReading.Acceleration; bool isCompassUsingNegativeZAxis = false; if (Math.Abs(v.Z) < Math.Cos(Math.PI / 4) && (v.Y < Math.Sin(7 * Math.PI / 4))) { isCompassUsingNegativeZAxis = true; } Dispatcher.BeginInvoke(() => { orientationTextBlock.Text = (isCompassUsingNegativeZAxis) ? "portrait mode" : "flat mode"; }); }