地圖和位置概觀

Windows 應用程式 SDK 與 WinUI 3 提供顯示地圖、偵測使用者位置及設置地理圍欄的 API 與控制。 利用這些功能來建立能顯示帶有圖釘的互動地圖、追蹤使用者位置,並在使用者進出地理區域時觸發動作的應用程式。

本文介紹每項能力,並包含一個完整的MapControl範例,將 、 GeolocatorGeofenceMonitor 、 整合於單一可用應用程式中。

使用 MapControl 顯示地圖

MapControl 顯示由 Azure 地圖服務 驅動的互動地圖。 你可以新增標記和圖層,並回應使用者的互動操作,例如平移、縮放和點擊。

MapControl 需要 Azure 地圖服務 帳號。 請參閱「管理您的 Azure 地圖服務 帳號」以建立帳號並取得服務代幣。

詳細使用說明請參閱 MapControl。

<MapControl x:Name="myMap"
            MapServiceToken="YOUR_AZURE_MAPS_TOKEN"
            Height="400" />

附註

UWP MapControl 與 Windows。Services.Maps API 已被棄用,未來版本的 Windows 可能無法使用。 WinUI 3 應用程式應該使用上述的新 MapControl。 欲了解更多資訊,請參閱 已棄用功能資源。

偵測使用者的位置

Windows。Devices.Geolocation API 讓您能取得裝置的地理位置。 這些 API 可在 UWP 和 Windows 應用程式 SDK(WinUI 3)應用程式中運作。 您可以:

如需逐步操作指南,請參閱 取得使用者的位置。

設定地理柵欄

地楔定義了地理邊界。 當使用者進入或離開邊界時,應用程式會收到通知。 地理圍欄對於基於位置的提醒、警示或內容傳遞非常有用。

關於建立與監控地理圍欄的說明,請參見 「設置地理圍欄」。

定位能力與隱私

所有位置 API 都需要在您的應用程式套件資訊清單中宣告 Location 功能。 你也必須在執行時呼叫 Geolocator.RequestAccessAsync ,才能存取位置資料。

Windows 允許用戶透過設定>、隱私與安全>、位置資訊來控制哪些應用程式能存取他們的位置。 你的應用程式應該能處理使用者拒絕或撤銷位置存取的情況。

完整範例

以下範例將 MapControl、 Geolocator、 GeofenceMonitor 合併於單一 WinUI 3 視窗中。 如果沒有設定 Azure 地圖服務 鍵,地圖會優雅地退化,而地理定位和地理圍欄功能則持續運作。

先決條件

  • Windows 應用程式 SDK 2.2 或更新版本
  • 一個 Azure 地圖服務 金鑰 — 顯示地圖圖塊所需。 如果沒有有效的金鑰,MapControl 會渲染但地圖是空白的。
  • 定位裝置能力宣告於Package.appxmanifest:
<DeviceCapability Name="location" />

在執行應用程式前,先將你的 Azure 地圖服務 鍵數設為環境變數:

$env:AZURE_MAPS_KEY = "your-key-here"

MainWindow.xaml

左側為 300 像素寬的控制面板,包含按鈕和狀態文字,右側則為 MapControl。 當 Azure 地圖服務 鍵缺失時,會出現一個覆蓋層。

<?xml version="1.0" encoding="utf-8" ?>
<Window
    x:Class="MapLocationDemo.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Map Location Demo">
    <Window.SystemBackdrop>
        <MicaBackdrop />
    </Window.SystemBackdrop>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <TitleBar Title="Map Location Demo" />

        <Grid Grid.Row="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="300" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>

            <Grid Grid.Column="0" Margin="16" RowSpacing="12">
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="*"/>
                </Grid.RowDefinitions>

                <TextBlock Text="Location Demo" FontSize="20" FontWeight="Bold"/>
                <StackPanel Grid.Row="1" Spacing="8">
                    <Button x:Name="FindMeButton" Content="Find My Location"
                            Click="FindMeButton_Click" HorizontalAlignment="Stretch"/>
                    <Button x:Name="AddGeofenceButton" Content="Add Geofence Here"
                            Click="AddGeofenceButton_Click" HorizontalAlignment="Stretch"/>
                </StackPanel>
                <TextBlock x:Name="StatusText" Grid.Row="2"
                           Text="Click 'Find My Location' to begin." TextWrapping="Wrap"/>
                <ListView x:Name="EventLog" Grid.Row="3" Header="Event Log"/>
            </Grid>

            <Grid Grid.Column="1">
                <MapControl x:Name="MyMap" />
                <StackPanel x:Name="MapKeyMissing" Visibility="Collapsed"
                            HorizontalAlignment="Center" VerticalAlignment="Center"
                            Spacing="8">
                    <FontIcon Glyph="&#xE783;" FontSize="48"
                              HorizontalAlignment="Center"
                              Foreground="{ThemeResource SystemFillColorCautionBrush}" />
                    <TextBlock Text="Azure Maps key not configured"
                               FontSize="18" FontWeight="SemiBold"
                               HorizontalAlignment="Center" />
                    <TextBlock x:Name="MapKeyHint" TextWrapping="Wrap" MaxWidth="400"
                               HorizontalAlignment="Center" TextAlignment="Center"
                               Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
                </StackPanel>
            </Grid>
        </Grid>
    </Grid>
</Window>

MainWindow.xaml.cs

using System;
using System.Collections.Generic;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Windows.Devices.Geolocation;
using Windows.Devices.Geolocation.Geofencing;

namespace MapLocationDemo;

public sealed partial class MainWindow : Window
{
    private Geolocator? _geolocator;
    private BasicGeoposition _lastPosition;
    private bool _mapAvailable;

    public MainWindow()
    {
        InitializeComponent();
        _mapAvailable = TryConfigureMap();
        GeofenceMonitor.Current.GeofenceStateChanged += OnGeofenceStateChanged;
    }

    // Read the Azure Maps key from an environment variable.
    // If missing, collapse the map and show an informational overlay.
    private bool TryConfigureMap()
    {
        var key = Environment.GetEnvironmentVariable("AZURE_MAPS_KEY");
        if (string.IsNullOrWhiteSpace(key))
        {
            MyMap.Visibility = Visibility.Collapsed;
            MapKeyMissing.Visibility = Visibility.Visible;
            MapKeyHint.Text = "Set the AZURE_MAPS_KEY environment variable "
                + "and restart.\nGeolocation and geofencing still work "
                + "without the map.";
            Log("Azure Maps key not found — map disabled");
            return false;
        }
        MyMap.MapServiceToken = key;
        return true;
    }

    private async void FindMeButton_Click(object sender, RoutedEventArgs e)
    {
        FindMeButton.IsEnabled = false;
        StatusText.Text = "Requesting location access...";

        var access = await Geolocator.RequestAccessAsync();
        if (access != GeolocationAccessStatus.Allowed)
        {
            StatusText.Text =
                "Location access denied. Check Settings > Privacy > Location.";
            FindMeButton.IsEnabled = true;
            return;
        }

        _geolocator = new Geolocator { DesiredAccuracyInMeters = 100 };
        try
        {
            var pos = await _geolocator.GetGeopositionAsync();
            var lat = pos.Coordinate.Point.Position.Latitude;
            var lon = pos.Coordinate.Point.Position.Longitude;
            _lastPosition = new BasicGeoposition
            {
                Latitude = lat, Longitude = lon
            };

            StatusText.Text =
                $"Location: {lat:F5}, {lon:F5}  ({pos.Coordinate.Accuracy:F0} m)";
            Log($"Position: {lat:F5}, {lon:F5}");

            if (_mapAvailable)
            {
                var pt = new Geopoint(_lastPosition);
                MyMap.Center = pt;
                MyMap.ZoomLevel = 15;

                var layer = new MapElementsLayer();
                layer.MapElements = new List<MapElement>
                {
                    new MapIcon { Location = pt }
                };
                MyMap.Layers.Clear();
                MyMap.Layers.Add(layer);
            }
        }
        catch (Exception ex) { StatusText.Text = $"Error: {ex.Message}"; }
        finally { FindMeButton.IsEnabled = true; }
    }

    private void AddGeofenceButton_Click(object sender, RoutedEventArgs e)
    {
        if (_lastPosition.Latitude == 0 && _lastPosition.Longitude == 0)
        {
            StatusText.Text = "Get your location first.";
            return;
        }

        var fence = new Geofence("MyGeofence",
            new Geocircle(_lastPosition, 200),
            MonitoredGeofenceStates.Entered | MonitoredGeofenceStates.Exited,
            false, TimeSpan.FromSeconds(5));
        GeofenceMonitor.Current.Geofences.Add(fence);

        StatusText.Text = $"Geofence added at "
            + $"{_lastPosition.Latitude:F5}, {_lastPosition.Longitude:F5}";
        Log("Geofence registered");
    }

    private void OnGeofenceStateChanged(
        GeofenceMonitor sender, object args)
    {
        var reports = sender.ReadReports();
        DispatcherQueue.TryEnqueue(() =>
        {
            foreach (var r in reports)
            {
                var msg = r.NewState switch
                {
                    GeofenceState.Entered => $"Entered: {r.Geofence.Id}",
                    GeofenceState.Exited  => $"Exited: {r.Geofence.Id}",
                    GeofenceState.Removed => $"Removed: {r.Geofence.Id}",
                    _ => null
                };
                if (msg != null) { StatusText.Text = msg; Log(msg); }
            }
        });
    }

    private void Log(string msg) =>
        EventLog.Items.Insert(0, $"[{DateTime.Now:HH:mm:ss}] {msg}");
}

關鍵模式

  • MapControl內建於 Windows 應用程式 SDK 1.6 及更新版本中。 設定MapServiceToken為你的 Azure 地圖服務 鍵。
  • TryConfigureMap 啟動時檢查 AZURE_MAPS_KEY 環境變數。 如果變數是空的,地圖會崩潰,並有覆蓋層說明如何修復——不會當機,也不會出現空白地圖。
  • DeviceCapability Name="location" 在 Package.appxmanifest 中是必需或 Geolocator.RequestAccessAsync 返回 Denied。
  • GeofenceMonitor.GeofenceStateChanged 會在背景執行緒觸發,所以用 DispatcherQueue.TryEnqueue 來更新 UI。