Plan du site  
français  English
pixel
pixel

Articles - Étudiants SUPINFO

Introduction

Ce chapitre sera dédié à deux éléments essentiels pour le succès d'une application UWP : l'optimisation de l'expérience utilisateur selon son équipement (responsive design) et la gestion des capteurs spécifiques Windows 10.

A la fin de ce chapitre, vous posséderez toutes les connaissances nécessaires pour créer une expérience utilisateur soignée quel que soit l'équipement choisi.

Gestion des équipements par UWP

Comme présenté dans le premier chapitre de ce cours, UWP fait le choix de séparer les équipements non pas par système d'exploitation ou par résolution, mais par famille d'équipements. Ce parti pris est possible notamment grâce à la gestion des pixels effectifs et à l'abstraction de l'équipement derrière la librairie UWP.

Ainsi, on pourra sélectionner à son gré une ou plusieurs familles d'équipements compatibles parmi les ordinateurs, les équipements mobiles (smartphone/tablette), Xbox, IoT ou HoloLens.

Figure 1.1. Familles d'équipements (Source: docs.microsoft.com)

Familles d'équipements (Source: docs.microsoft.com)

La famille universelle est sélectionnée par défaut, et offre l'ensemble des APIs compatibles avec toutes les familles d'équipements plus spécifiques. Le panorama de la plateforme universelle présenté depuis le premier chapitre de ce cours est d'ailleurs compatible avec toutes les familles d'équipement.

[Note]

Pour savoir si une fonctionnalité est disponible dans une famille d'équipements, il est nécessaire de se rendre sur la documentation officielle et naviguer jusqu'à la section "Device Family". Vous trouverez un exemple de fonctionnalité disponible uniquement pour la famille Desktop avec la classe Print3DManager en suivant ce lien.

La gestion en famille d'équipements n'est pas comparable avec les contrôles VisualState présentés au chapitre précédent. Si VisualState vous permet de changer l'affichage en fonction de la résolution de l'équipement (ou de la taille de la fenêtre sur PC), les familles d'équipements permettent de cibler précisément un type d'équipement, quelle que soit sa résolution.

Microsoft impose pour chacun des équipements fonctionnant avec Windows 10 un ensemble de fonctionnalités minimum requises, facilitant le développement et permettant cette séparation en famille d'équipements. Ainsi, en tant que développeur, il devient aisé de planifier les fonctionnalités d'une application quel que soit l'équipement utilisé. Pour l'utilisateur, c'est l'assurance de pouvoir installer et exécuter une application en utilisant toutes ses possibilités. Vous pourrez retrouver l'ensemble de ces fonctionnalités sur la documentation officielle.

Familles d'équipements

Les concepts UX et de design adaptatif présentés au premier chapitre (Réarchitecture notamment) peuvent se faire facilement selon la famille d'équipements. Le principe sous-jacent est de créer différentes vues portant le même nom, et d'utiliser la vue adaptée selon l'équipement.

Pour cibler une famille d'équipements précise, il suffit de créer un dossier appelé "DeviceFamily-" suivi de la famille d'équipements ciblée. Par exemple, pour cibler la famille "Mobile" (smartphone/tablette), il faudra appeler ce dossier "DeviceFamily-Mobile".

Au sein de ce dossier, il suffira de reproduire l'architecture du dossier principal pour que la plateforme UWP sélectionne la bonne page selon l'équipement. Ainsi, si la page MainPage est déclinée pour le mobile, il suffit de créer une page MainPage dans le dossier "DeviceFamily-Mobile".

Figure 1.2. Structure de projet pour familles d'équipements

Structure de projet pour familles d'équipements

Si la page est visitée par un mobile, l'utilisateur n'affichera pas la page MainPage du dossier principal, mais bien celle présente dans le dossier "DeviceFamily-Mobile".

<!-- Page MainPage.xaml. Attributs Page omis -->
<Page>
  <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <TextBlock Text="Contenu chargé par autre chose qu'un mobile" Margin="20" />
  </Grid>
</Page>
<!-- Page DeviceFamily-Mobile/MainPage.xaml. Attributs Page omis -->
<Page>
  <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <TextBlock Text="Contenu chargé par un mobile" Margin="20" />
  </Grid>
</Page>

Figure 1.3. Rendu de l'exemple précédent

Rendu de l'exemple précédent

Cette architecture reproduite peut également fonctionner avec des sous-dossiers comme vu dans la figure 1.2.

[Note]

Pour naviguer vers un sous-dossier, il suffit de préciser l'espace de noms (namespace) du sous-dossier, car chaque sous-dossier crée un espace de noms dédié. Voici un exemple de navigation pour la page "SubPage".

Frame.Navigate(typeof(SubFolder.SubPage));

Etape 8 : Liste d'articles spécifique pour mobiles

Liste d'articles spécifique pour mobiles

1.1.

Ajoutez un nouveau dossier "DeviceFamily-Mobile" dans votre application. A l'intérieur, ajoutez un nouvel élément "XAML View" appelé MainPage.

Au sein de cette page, utilisez un contrôle SplitView :

  • L'attribut DisplayMode doit être défini à CompactOverlay.

  • L'attribut IsPaneOpen doit être défini à False.

A l'intérieur du panneau, utilisez un bouton avec :

  • Pour contenu "&#xE700;"

  • En police de caractère "Segoe MDL2 Assets"

  • Une hauteur et une largeur de 50

  • Une couleur de fond transparente

Au clic sur le bouton, le panneau doit s'ouvrir ou se fermer selon son état. Si le panneau est ouvert, il faudra le fermer. Si le panneau est fermé, il faudra l'ouvrir.

<!-- Page DeviceFamily-Mobile/MainPage.xaml.cs -->
<Page
  x:Class="ArticlesApp.MainPage"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:local="using:ArticlesApp.Controls"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  mc:Ignorable="d">
  <SplitView x:Name="MonSplitView" DisplayMode="CompactOverlay" IsPaneOpen="False">
    <SplitView.Pane>
      <Grid>
        <Grid.RowDefinitions>
          <RowDefinition Height="auto"/>
          <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <Button Grid.Row="0" x:Name="Hamburger" FontFamily="Segoe MDL2 Assets" 
                   Content="&#xE700;" Width="50" Height="50" 
                   Background="Transparent" Click="Hamburger_Click"/>
      </Grid>
    </SplitView.Pane>
  </SplitView>
</Page>
// Fichier MainPage.xaml.cs
private void Hamburger_Click(object sender, RoutedEventArgs e)
{
  if (MonSplitView != null) MonSplitView.IsPaneOpen = !MonSplitView.IsPaneOpen;
}

1.2.

Ajoutez dans le reste du panneau le contrôle utilisateur listant les catégories.

Ajoutez dans le contenu du contrôle SplitView :

  • Le titre "Derniers articles" ayant pour style "HeaderTextStyle"

  • Un contrôle ScrollViewer, contenant en son sein le contrôle utilisateur ArticlesList.

Aux événements de rechargement des articles et de changement de catégorie, fermez le panneau de navigation.

<!-- Page DeviceFamily-Mobile/MainPage.xaml, Contrôle SplitView -->
<SplitView.Pane>
  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="auto"/>
      <RowDefinition Height="*" />
    </Grid.RowDefinitions>
    <Button Grid.Row="0" x:Name="Hamburger" FontFamily="Segoe MDL2 Assets" 
               Content="&#xE700;" Width="50" Height="50" 
               Background="Transparent" Click="Hamburger_Click"/>
    <local:NavPane Grid.Row="1" x:Name="NavPane" />
  </Grid>
</SplitView.Pane>

<SplitView.Content>
  <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Grid.RowDefinitions>
      <RowDefinition Height="60" />
      <RowDefinition Height="*" />
    </Grid.RowDefinitions>
    <StackPanel Margin="10,10,0,0" Grid.Row="0" Orientation="Horizontal">
      <TextBlock Text="Derniers articles" TextWrapping="Wrap"
                    Style="{StaticResource HeaderTextStyle}" />
    </StackPanel>
    <ScrollViewer Grid.Row="1" HorizontalScrollBarVisibility="Disabled" 
                  VerticalScrollBarVisibility="Auto" VerticalScrollMode="Enabled">
      <local:ArticlesList x:Name="ArticlesList" />
    </ScrollViewer>
  </Grid>
</SplitView.Content>
private void Refresh_Click(object sender, RoutedEventArgs e)
{
  RssReader.Reset();
  if (MonSplitView != null) MonSplitView.IsPaneOpen = false;
}

private void NavPane_RefreshCalled(object sender, RefreshCalledEventArgs e)
{
  // Reste de la méthode omise
  if (MonSplitView != null) MonSplitView.IsPaneOpen = false;
}

Cibler une ou plusieurs famille d'équipements

Bien qu'un des intérêts des applications universelles soit de fonctionner sur tous les équipements, il est tout à fait possible de spécifier une ou plusieurs familles d'équipements compatibles avec votre application. Cette configuration se trouve dans le fichier Package.appxmanifest. Cette déclaration n'étant pas disponible dans l'interface d'édition graphique, il faudra éditer ce fichier avec un clic-droit puis sélectionner l'option "Afficher le code".

On retrouve dans la balise Package la section suivante :

<Dependencies>
  <TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.0.0" MaxVersionTested="10.0.0.0"/>
</Dependencies>

Dans cet exemple, la famille d'équipements "Universal" est sélectionnée, soit toutes les familles. Pour sélectionner uniquement les mobiles, il suffira de changer l'attribut Name de la balise TargetDeviceFamily comme suit :

<Dependencies>
  <TargetDeviceFamily Name="Windows.Mobile" MinVersion="10.0.0.0" MaxVersionTested="10.0.0.0" />
</Dependencies>
[Note]

Pour plus d'exemples à ce sujet, n'hésitez pas à consulter la documentation officielle (MSDN) en suivant ce lien.

Spécifier une famille d'équipements compatible plutôt qu'une autre aura une conséquence simple mais majeure : l'application ne sera tout simplement pas visible sur le Windows Store si l'équipement utilisé n'est pas inclus dans la ou les familles d'équipements sélectionnées. Dans le cas d'une application de développement fournie en dehors du Store, elle ne sera tout simplement pas exécutable.

Quelle que soit la famille d'équipements ciblée, il est nécessaire de tester la présence de fonctionnalités. Non seulement celles-ci peuvent être optionnelles, mais votre expérience utilisateur peut être améliorée par la présence d'un équipement plutôt dédié au mobile sur un PC, comme un gyroscope.

Capteurs de position

De nombreux capteurs sont aujourd'hui présents dans nos équipements : réseau, accéléromètre, magnétomètre, capteur de lumière, caméra, GPS...

Tous ces capteurs peuvent être utilisés de manière différente dans les applications :

  • Offrir une nouvelle expérience utilisateur : on peut lister dans cette catégorie les applications proposant de passer au contenu suivant en secouant son téléphone

  • Présenter une fonctionnalité : c'est le cas par exemple des applications de réalité augmentée qui se basent à la fois sur la caméra et un gyroscope pour incruster des éléments en trois dimensions.

  • Adapter l'affichage : comme beaucoup d'applications le font, l'affichage peut être modifié en fonction du mode d'affichage de l'équipement (portrait ou paysage)

  • Déclencher des actions : à l'écoute de la perte ou récupération de connectivité, les applications nécessitant une connexion internet peuvent profiter du capteur réseau pour adapter leur comportement.

Le système utilise d'ailleurs ces différents capteurs pour améliorer l'utilisation de l'équipement ou son autonomie : avec Windows 10, l'affichage peut entre autres s'adapter à la luminosité ambiante.

Les capteurs se trouvent dans l'espace de noms Windows.Devices.Sensors. Pour accéder à un capteur, la méthode GetDefault() est nécessaire : elle retournera une instance représentant les données de celui-ci s'il existe, et null dans le cas contraire. Une fois le capteur récupéré, l'événement ReadingChanged permet de souscrire à une modification de valeurs.

Figure 1.4. Panorama non-exhaustif des capteurs (Anglais)


Accéléromètre

Ce capteur permet de récupérer l'accélération de l'équipement dans l'espace avec trois axes : X, Y et Z.

Figure 1.5. Accéléromètre (Source : docs.microsoft.com)

Accéléromètre (Source : docs.microsoft.com)

Présentons ces trois valeurs dans un contrôle Grid. Les trois derniers contrôles TextBlock n'ont pas de texte pour le moment, car ils seront définis à l'exécution du code-behind.

<Grid>
  <Grid.RowDefinitions>
    <RowDefinition Height="auto" />
    <RowDefinition Height="auto" />
    <RowDefinition Height="auto" />
  </Grid.RowDefinitions>
  <Grid.ColumnDefinitions>
    <ColumnDefinition Width="*" />
    <ColumnDefinition Width="*" />
  </Grid.ColumnDefinitions>
  <TextBlock Grid.Row="0" Grid.Column="0" Text="Accélération X:" />
  <TextBlock Grid.Row="1" Grid.Column="0" Text="Accélération Y:" />
  <TextBlock Grid.Row="2" Grid.Column="0" Text="Accélération Z:" />
  <TextBlock Grid.Row="0" Grid.Column="1" x:Name="AccelerationX" />
  <TextBlock Grid.Row="1" Grid.Column="1" x:Name="AccelerationY" />
  <TextBlock Grid.Row="2" Grid.Column="1" x:Name="AccelerationZ" />
</Grid>
// Déclaration de l'accéléromètre
private Accelerometer _accelerometer;
public MainPage()
{
  this.InitializeComponent();
  // Initialisation de l'object avec la méthode GetDefault
  _accelerometer = Accelerometer.GetDefault();

  // Toujours vérifier si le capteur est présent
  if (_accelerometer != null)
  {
    // Les accéléromètres ont une durée minimale de mise à jour des valeurs,
    // essentiellement pour des réduire la consommation de ressources.
    uint minReportInterval = _accelerometer.MinimumReportInterval;
    // En fonction de vos besoins, la période de mise à jour des valeurs peut
    // être modifiée ; selon le type d'application, utilisez une valeur raisonnable.
    // Note : l'intervale de report est défini en millisecondes.
    _accelerometer.ReportInterval = (minReportInterval > 16 ? minReportInterval : 16);

    // On écoute l'événement déclenché au changement des valeurs
    _accelerometer.ReadingChanged += 
      new TypedEventHandler<Accelerometer, AccelerometerReadingChangedEventArgs>(ReadingChanged);
  }

  // Méthode déclenché à la mise à jour des valeurs
  private async void ReadingChanged(object sender, AccelerometerReadingChangedEventArgs e)
  {
    // On récupère les valeurs modifiées. L'utilisation de la classe Dispatcher est
    // conseillé, car elle utilise une tâche en arrière plan pour réaliser la modification
    // de la vue.
    // L'utilisation de l'énumération CoreDispatcherPriority permet de notifier à UWP
    // l'importance de la tâche en cours.
    // Notons l'utilisation d'une closure avec la syntaxe () =>
    await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
    {
      // Récupération des valeurs
      AccelerometerReading reading = e.Reading;
      // Modification de l'affichage avec les valeurs récupérées
      AccelerationX.Text = string.Format("{0,5:0.00}", reading.AccelerationX);
      AccelerationY.Text = string.Format("{0,5:0.00}", reading.AccelerationY);
      AccelerationZ.Text = string.Format("{0,5:0.00}", reading.AccelerationZ);
    });
  }
}

Figure 1.6. Utilisation de l'accéléromètre

Utilisation de l'accéléromètre

[Note]

Pour afficher l'écran de droite, il faudra cliquer sur l'icône >> (Outils) dans la barre d'outils à droite de l'émulateur. Ces options avancées nous seront utiles tout au long de l'utilisation des capteurs, pour simuler des changements au sein de l'émulateur.

Pour "déplacer" le téléphone dans l'espace, sélectionnez le point rouge central sur un point du disque matérialisé en pointillés. Les valeurs présentes en bas à gauche du simulateur correspondent aux valeur affichées dans l'application.

Gyromètre

Le gyromètre mesure une vélocité angulaire le long des axes X, Y et Z, soit la rotation autour de ces axes.

Figure 1.7. Gyromètre (Source : docs.microsoft.com)

Gyromètre (Source : docs.microsoft.com)

Ajoutons ces valeurs au sein de notre élément Grid :

<!-- Reste du fichier omis -->
<TextBlock Grid.Row="3" Grid.Column="0" Text="Vélocité X:" />
<TextBlock Grid.Row="4" Grid.Column="0" Text="Vélocité Y:" />
<TextBlock Grid.Row="5" Grid.Column="0" Text="Vélocité Z:" />
<TextBlock Grid.Row="3" Grid.Column="1" x:Name="VelociteX" />
<TextBlock Grid.Row="4" Grid.Column="1" x:Name="VelociteY" />
<TextBlock Grid.Row="5" Grid.Column="1" x:Name="VelociteZ" />
private Gyrometer _gyrometer;
public MainPage() {
  // Reste du constructeur omis
  _gyrometer = Gyrometer.GetDefault();
  if(_gyrometer != null)
  {
    uint minReportInterval = _accelerometer.MinimumReportInterval;
    _gyrometer.ReportInterval = minReportInterval > 16 ? minReportInterval : 16;
    _gyrometer.ReadingChanged += _gyrometer_ReadingChanged;
  }
}

private async void _gyrometer_ReadingChanged(Gyrometer sender, 
      GyrometerReadingChangedEventArgs args)
{
  await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
  {
    GyrometerReading reading = args.Reading;
    VelociteX.Text = String.Format("{0,5:0.00}", reading.AngularVelocityX);
    VelociteY.Text = String.Format("{0,5:0.00}", reading.AngularVelocityY);
    VelociteZ.Text = String.Format("{0,5:0.00}", reading.AngularVelocityZ);
  });
}

Inclinomètre

Ce capteur n'en est pas vraiment un : il mélange les données de l'accéléromètre et du gyromètre pour offrir un objet contenant les propriétés nécessaires à la réalisation d'une application utilisant le mouvement de l'équipement comme un élément d'expérience utilisateur.

Figure 1.8. Inclinomètre (Source : docs.microsoft.com)

Inclinomètre (Source : docs.microsoft.com)

Ajoutons ces élément à notre contrôle Grid.

<TextBlock Grid.Row="6" Grid.Column="0" Text="Tangage :" />
<TextBlock Grid.Row="7" Grid.Column="0" Text="Roulis :" />
<TextBlock Grid.Row="8" Grid.Column="0" Text="Lacet :" />
<TextBlock Grid.Row="6" Grid.Column="1" x:Name="Tangage" />
<TextBlock Grid.Row="7" Grid.Column="1" x:Name="Roulis" />
<TextBlock Grid.Row="8" Grid.Column="1" x:Name="Lacet" />
private Inclinometer _inclinometer;
public MainPage() {
  // Reste du constructeur omis
  _inclinometer = Inclinometer.GetDefault();
  if(_inclinometer != null)
  {
    uint minReportInterval = _inclinometer.MinimumReportInterval;
    _inclinometer.ReportInterval = minReportInterval > 16 ? minReportInterval : 16;
    _inclinometer.ReadingChanged += _inclinometer_ReadingChanged;
  }
}

private async void _inclinometer_ReadingChanged(Inclinometer sender, 
      InclinometerReadingChangedEventArgs args)
{
  await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
  {
    InclinometerReading reading = args.Reading;
    Tangage.Text = String.Format("{0,5:0.00}", reading.PitchDegrees);
    Roulis.Text = String.Format("{0,5:0.00}", reading.RollDegrees);
    Lacet.Text = String.Format("{0,5:0.00}", reading.YawDegrees);
  });
}

Boussole

Un autre capteur de représentation dans l'espace est la Boussole, permettant de déterminer avec précision la direction du nord et celle du nord magnétique. Ce capteur combine les données récupérées par le gyromètre et le magnétomètre (décrit plus tard dans cette liste) pour offrir des données stables à votre application.

Figure 1.9. Boussole (Source : docs.microsoft.com)

Boussole (Source : docs.microsoft.com)

[Warning]

Le capteur Boussole n'est pas disponible au sein d'un émulateur. Il est nécessaire d'utiliser un équipement réel comme votre ordinateur ou une tablette pour tester ce capteur.

C'est avec la classe Compass qu'on accède à ce capteur qui se comporte comme tous les précédents :

<TextBlock Grid.Row="9" Grid.Column="0" Text="Orientation Nord (magnétique) :" />
<TextBlock Grid.Row="10" Grid.Column="0" Text="Orientation Nord (réel) :" />
<TextBlock Grid.Row="9" Grid.Column="1" x:Name="NordMagnetique" />
<TextBlock Grid.Row="10" Grid.Column="1" x:Name="NordReel" />
private Compass _compass;
public MainPage() {
  // Reste du constructeur omis
  _compass = Compass.GetDefault();
  if(_compass != null)
  {
    uint minReportInterval = _compass.MinimumReportInterval;
    _compass.ReportInterval = minReportInterval > 16 ? minReportInterval : 16;
    _compass.ReadingChanged += _compass_ReadingChanged;
  }
}

private async void _compass_ReadingChanged(Compass sender, CompassReadingChangedEventArgs args)
{
  await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
  {
    CompassReading reading = args.Reading;
    NordMagnetique.Text = String.Format("{0,5:0.00}", reading.HeadingMagneticNorth);
    if (reading.HeadingTrueNorth.HasValue)
    {
      NordReel.Text = String.Format("{0,5:0.00}", reading.HeadingTrueNorth);
    }
    else
    {
      NordReel.Text = "Aucune valeur à afficher.";
    }
  });
}

Capteur d'orientation simple

Pour terminer cette liste de capteurs permettant de représenter l'équipement utilisé dans l'espace, le capteur d'orientation simple est utilisé pour déterminer les quatre possibilités d'orientation de l'équipement : Portrait, Paysage, Portrait inversé et Paysage inversé. Il offre également le sens de l'appareil par rapport au sol, l'écran positionné vers le ciel ou vers le sol.

Figure 1.10. Capteur d'orientation simple

Capteur d'orientation simple

[Note]

SimpleOrientationSensor est d'un niveau d'abstraction supérieur à ceux listés précédemment, car il se base sur un capteur d'orientation, lui-même basé sur l'accéléromètre, le magnétomètre et le gyromètre. Le résultat est offert via la classe OrientationSensor qui est complexe d'utilisation, étant basé sur des concepts comme le quaternion et les matrices de rotation. Pour la majorité des besoins d'une application, SimpleOrientationSensor offre une information suffisante.

Le capteur d'orientation simple ne se base pas sur un intervalle de report comme les autres capteurs, mais sur un événement se déclenchant au changement d'orientation :

<TextBlock Grid.Row="11" Grid.Column="0" Text="Orientation:" />
<TextBlock Grid.Row="11" Grid.Column="1" x:Name="Orientation" />
private SimpleOrientationSensor _simpleorientation;

public TestPage()
{
  // Reste du constructeur omis
  _simpleorientation = SimpleOrientationSensor.GetDefault();
  if(_simpleorientation != null)
  {
    _simpleorientation.OrientationChanged += _simpleorientation_OrientationChanged;
  }
}

private async void _simpleorientation_OrientationChanged(SimpleOrientationSensor sender, 
      SimpleOrientationSensorOrientationChangedEventArgs args)
{
  await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
  {
    SimpleOrientation orientation = args.Orientation;
    switch (orientation)
    {
      case SimpleOrientation.NotRotated:
        Orientation.Text = "Aucune rotation";
        break;
      case SimpleOrientation.Rotated90DegreesCounterclockwise:
        Orientation.Text = "Rotation de 90° antihoraire";
        break;
      case SimpleOrientation.Rotated180DegreesCounterclockwise:
        Orientation.Text = "Rotation 180° antihoraire";
        break;
      case SimpleOrientation.Rotated270DegreesCounterclockwise:
        Orientation.Text = "Rotation 270° antihoraire";
        break;
      case SimpleOrientation.Faceup:
        Orientation.Text = "Ecran vers le ciel";
        break;
      case SimpleOrientation.Facedown:
        Orientation.Text = "Ecran vers le sol";
        break;
      default:
        Orientation.Text = "Orientation inconnue";
        break;
    }
  });
}

Magnétomètre

Plusieurs des capteurs présentés précédemment se basent sur le magnétomètre, un capteur recensant la force des champs magnétiques dans les trois dimensions. C'est un capteur pour qui la précision peut décroître temporairement en fonction de facteurs environnementaux.

Son utilité apparaît avec l'énumération MagnetometerAccuracy permettant de déterminer la précision actuelle de ce capteur. Elle possède quatre valeurs :

  • Unknown : Impossible de récupérer la précision du capteur. Dans le cas où votre application nécessite absolument un capteur de position, il est conseillé d'inviter l'utilisateur à le recalibrer.

  • Unreliable : Il est conseillé d'inviter l'utilisateur à recalibrer le magnétomètre.

  • Approximate : Le magnétomètre est approximatif, ce qui est peut-être suffisant pour votre application.

  • High : La plus haute valeur, la précision du magnétomètre est haute et permet sans aucune approximation l'utilisation des capteurs liés.

Pour obtenir cette valeur, il suffit d'utiliser la propriété associée dans la propriété "Reading" de chaque retour de capteur utilisant le magnétomètre.

private async void _magnetometer_ReadingChanged(Magnetometer sender, 
      MagnetometerReadingChangedEventArgs args)
{
  await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
  {
    MagnetometerReading reading = args.Reading;
    switch(reading.DirectionalAccuracy)
    {
      case MagnetometerAccuracy.Unknown:
        PrecisionMagnetometre.Text = "Précision inconnue";
        break;
      case MagnetometerAccuracy.Unreliable:
        PrecisionMagnetometre.Text = "Précision insuffisante";
        break;
      case MagnetometerAccuracy.Approximate:
        PrecisionMagnetometre.Text = "Précision acceptable";
        break;
      case MagnetometerAccuracy.High:
        PrecisionMagnetometre.Text = "Précision maximale";
        break;
    }
  });
}

private async void _compass_ReadingChanged(Compass sender, CompassReadingChangedEventArgs args)
{
  await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
  {
    CompassReading reading = args.Reading;
    // CompassReading possède également une propriété précision dépendante du magnétomètre
    if(reading.HeadingAccuracy == MagnetometerAccuracy.High)
    {
      // ...
    }
  });
}

Pour calibrer le magnétomètre, il est nécessaire d'inviter l'utilisateur à déplacer l'équipement dans les trois axes. La vidéo officielle Microsoft ci-dessous vous présentera quand et comment recalibrer le capteur.

Figure 1.11. Quand et comment recalibrer le magnétomètre (Anglais)


Exercice : Square Eater

Exercice : Snake Eater

1.1.

Nous allons réaliser un simple jeu utilisant les capacités de l'inclinomètre. Le joueur incarne un carré rouge sur l'écran, qui se déplace en fonction de l'inclinaison de l'équipement. Des carrés bleus apparaissent et il faudra les ingérer par contact. Plus le joueur ingère des bonus, plus son avatar grandit et se déplace vite. Le but du jeu est d'obtenir le score le plus élevé sans toucher les bords de l'écran.

Créez un nouveau projet SquareEater. Au sein de la page principale, ajoutez un contrôle Canvas avec le nom "MonCanvas". Ce contrôle contiendra un contrôle Rectangle avec la propriété Fill définie à Red, et pour nom "Joueur".

<!-- Fichier MainPage.xaml -->
<Page> <!-- Attributs omis pour la lisibilité -->
  <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Canvas x:Name="MonCanvas">
      <Rectangle Fill="Red" x:Name="Joueur"/>
    </Canvas>
  </Grid>
</Page>

1.2.

Dans la classe MainPage, créez une méthode privée Start() ne retournant rien. Dans le constructeur, inscrivez une méthode écoutant l'événement Loaded de la page. Dans cette méthode déclenchée à l'événement Loaded, appelez la méthode Start().

public MainPage()
{
    this.InitializeComponent();
    Loaded += MainPage_Loaded;
}

private void MainPage_Loaded(object sender, RoutedEventArgs e)
{
    Start();
}

private void Start()
{

}

1.3.

Ajoutez à la classe MainPage les propriétés suivantes :

  • TailleJoueurDefaut (int) : Cette propriété retournera toujours 80 et ce 80 ne doit pas être modifiable par un élément du code.

  • FacteurDefaut (float) : Cette propriété retournera toujours 1F et ne doit pas être modifiable par un élément du code.

  • BonusTaille (int) : Retournera toujours 10 sans être modifiable par un élément du code.

  • BonusFacteur (float) : Retournera toujours 0.1F sans être modifiable par un élément du code.

  • TailleJoueur (int)

  • Facteur (float)

  • Score (int)

Dans la méthode Start, initialisez TailleJoueur à TailleJoueurDefaut et Facteur à FacteurDefaut. Initialisez Score à 0. Définissez également la largeur et la hauteur de Joueur avec la taille par défaut.

private static int TailleJoueurDefaut { get { return 80; } }
private static float FacteurDefaut { get { return 1F; } }
private static int BonusTaille { get { return 10; } }
private static float BonusFacteur { get { return 0.1F; } }

private static int TailleJoueur;
private float Facteur;
private int Score;

private void Start()
{
  Joueur.Width = TailleJoueurDefaut;
  Joueur.Height = TailleJoueurDefaut;
  TailleJoueur = TailleJoueurDefaut;
  Facteur = FacteurDefaut;
  Score = 0;
}

1.4.

Ajoutez une propriété de MainPage appelée _inclinometer de type Inclinometer. Dans la méthode Start, utilisez la méthode GetDefault() pour initialiser l'inclinomètre, et inspirez-vous de l'exemple d'utilisation plus haut pour définir une méthode écoutant l'événement ReadingChanged.

private Inclinometer _inclinometer;

private void Start() 
{
  // Reste de la méthode omise
  _inclinometer = Inclinometer.GetDefault();
  if (_inclinometer != null)
  {
    uint minimumReportInterval = _inclinometer.MinimumReportInterval;
    _inclinometer.ReportInterval = minimumReportInterval > 16 ? minimumReportInterval : 16;
    _inclinometer.ReadingChanged += _inclinometer_ReadingChanged;
  }
}

private async void _inclinometer_ReadingChanged(Inclinometer sender, 
      InclinometerReadingChangedEventArgs args)
{
  await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
  {
  });
}

1.5.

Ajoutons cette fois le déplacement de l'avatar du joueur. Pour ce faire, initialisez tout d'abord le rectangle au centre de l'écran dans la méthode Start.

[Note]

Pour placer un élément dans un contrôle Canvas, il faut utiliser les méthodes de classe Canvas.SetTop(Elément, coordonnée) et Canvas.SetLeft(Elément, coordonnée).

Pour déterminer les coordonnées, il vous faudra utiliser les propriétés existantes de MainPage, ActualWidth et ActualHeight

Ensuite, dans la corps de la méthode lambda de Dispatcher.RunAsync, adaptez les coordonnées du rectangle du joueur avec les retours de l'inclinomètre. Nous allons pour cela utiliser les formules suivantes :

  • PositionX = PositionX + (Facteur * args.Reading.RollDegrees)

  • PositionY = PositionY + (Facteur * args.Reading.PitchDegrees)

[Note]

Pour obtenir les coordonnées d'un élément dans un contrôle Canvas, il faut utiliser les méthodes de classe Canvas.GetTop(Elément) et Canvas.GetLeft(Elément).

N'oubliez pas de borner les nouvelles valeurs de PositionX et PositionY entre 0 et la taille de l'écran pour garder le joueur dans la zone visible !

Enfin, définissez au joueur les nouvelles coordonnées obtenues.

private void Start()
{
  // Reste de la méthode omise
  Canvas.SetLeft(Joueur, ActualWidth / 2 - (TailleJoueur / 2));
  Canvas.SetTop(Joueur, ActualHeight / 2 - (TailleJoueur / 2));
}

private async void _inclinometer_ReadingChanged(Inclinometer sender, 
      InclinometerReadingChangedEventArgs args)
{
  await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
  {
    var positionX = Canvas.GetLeft(Joueur);
    var positionY = Canvas.GetTop(Joueur);

    positionX += (Facteur * args.Reading.RollDegrees);
    if(positionX <= 0) positionX = 0;
    if(positionX >= ActualWidth - TailleJoueur) positionX = ActualWidth - TailleJoueur;

    positionY += (Facteur * args.Reading.PitchDegrees);
    if(positionY <= 0) positionY = 0;
    if(positionY >= ActualHeight - TailleJoueur) positionY = ActualHeight - TailleJoueur;

    Canvas.SetLeft(Joueur, positionX);
    Canvas.SetTop(Joueur, positionY);
  });
}

1.6.

En testant votre application, vous devriez obtenir un carré se déplaçant dans les bornes de l'écran.

Ajoutons maintenant les bonus récupérables. Créez une nouvelle propriété de MainPage, une liste d'instances de la classe Rectangle appelée Collectables et initialisez la. Ajoutez une seconde propriété de type System.Threadings.Timer appelée Timer ayant pour valeur null. Dans la méthode Start, initialisez la propriété Timer comme appelant la méthode AjoutCollectable toutes les secondes à partir du lancement du programme. Dans la méthode AjoutElement, utilisez Dispatcher.RunAsync.

[Note]

Vous trouverez un exemple de la classe Timer à cette adresse.

private List<Rectangle> Collectables = new List<Rectangle>();
private Timer Timer;

private void Start() {
  // Reste de la méthode omise
  Timer = new Timer(AjoutElement, null, TimeSpan.FromSeconds(0), TimeSpan.FromSeconds(1));
}

private async void AjoutElement(object state)
{
  await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
  {
  });
}

1.7.

Dans le corps de la méthode lambda appelée dans AjoutElement, utilisez l'objet Random pour définir les coordonnées du nouvel élément collectable à ajouter. Pour cela, instanciez un nouvel objet Random et utilisez la méthode Next(de, a) où de est la borne basse et a la borne haute.

Instanciez un nouvel objet Rectangle et utilisez les méthodes de classe Canvas SetLeft et SetTop pour placer ce rectangle dans l'affichage. Colorez ce rectangle en bleu avec sa propriété Fill, sa largeur et sa hauteur à 60. Ajoutez ce nouveau rectangle à la fois dans la liste de MainPage Collectables et en tant qu'enfant de votre contrôle MonCanvas.

[Note]

Pour définir la couleur d'un élément XAML, utilisez la syntaxe suivante :

element.Fill = new SolidColorBrush(Colors.Blue);

Pour finir, créez une propriété de MainPage non-modifiable appelée "TailleCollectable" qui retournera toujours 60. Utilisez cette propriété pour la hauteur et la largeur de votre rectangle.

private int TailleCollectable = 60;
private async void AjoutElement(object state)
{
  await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
  {
    Random r = new Random();
    int positionX = r.Next(0, (int)ActualWidth - TailleCollectable);
    int positionY = r.Next(0, (int)ActualHeight - TailleCollectable);
    Rectangle collectable = new Rectangle();
    collectable.Fill = new SolidColorBrush(Colors.Blue);
    collectable.Height = TailleCollectable;
    collectable.Width = TailleCollectable;
    MonCanvas.Children.Add(collectable);
    Collectables.Add(collectable);
    Canvas.SetLeft(collectable, positionX);
    Canvas.SetTop(collectable, positionY);
  });
}

1.8.

Passons maintenant au système de collisions. Quand le personnage est au contact d'un collectable, ce dernier doit disparaître. Pour ce faire, déterminez dans la méthode _inclinometer_ReadingChanged après mise à jour de la position du joueur les coordonnées des quatre coins du personnage.

Ensuite, pour chaque élément Rectangle de la liste Collectables, déterminez les coordonnées des quatre coins de ce collectable. Utilisez ces huit coordonnées pour déterminer la collision.

Il est conseillé d'utiliser la classe Point pour représenter les coordonnées X et Y de vos éléments.

[Note]

Pour qu'il y ait collision, il faut :

  • Qu'un coin du collectable soit entre le coin Haut-Gauche et le coin Bas-Droite du personnage

  • Ou qu'un coin du personnage soit entre le coin Haut-Gauche et le coin Bas-Droite du collectable

private async void _inclinometer_ReadingChanged(Inclinometer sender, 
      InclinometerReadingChangedEventArgs args)
{
  await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
  {
    // Reste du code omis
    Point hautGauche = new Point(positionX, positionY);
    Point hautDroite = new Point(positionX + TailleJoueur, positionY);
    Point basGauche = new Point(positionX, positionY + TailleJoueur);
    Point basDroite = new Point(positionX + TailleJoueur, positionY + TailleJoueur);
    
    foreach (Rectangle c in Collectables)
    {
      var gauche = Canvas.GetLeft(c);
      var haut = Canvas.GetTop(c);
      var bordures = new
      {
        HautGauche = new Point(gauche, haut),
        HautDroite = new Point(gauche + TailleCollectable, haut),
        BasGauche = new Point(gauche, haut + TailleCollectable),
        BasDroite = new Point(gauche + TailleCollectable, haut + TailleCollectable)
      };
      if (
        EstEntre(bordures.HautGauche, hautGauche, basDroite) ||
        EstEntre(bordures.HautDroite, hautGauche, basDroite) ||
        EstEntre(bordures.BasGauche, hautGauche, basDroite) ||
        EstEntre(bordures.BasDroite, hautGauche, basDroite) ||
        EstEntre(hautGauche, bordures.HautGauche, bordures.BasDroite) ||
        EstEntre(hautDroite, bordures.HautGauche, bordures.BasDroite) ||
        EstEntre(basGauche, bordures.HautGauche, bordures.BasDroite) ||
        EstEntre(basDroite, bordures.HautGauche, bordures.BasDroite)
      )
      {
        // Collision
      }
    }
  });
}

private bool EstEntre(Point valeur, Point de, Point a)
{
  return valeur.X > de.X && valeur.X < a.X && valeur.Y > de.Y && valeur.Y < a.Y;
}

1.9.

A la collision, il faudra :

  • Ajouter à la propriété Facteur la propriété FacteurBonus

  • Ajouter à la propriété TailleJoueur la propriété TailleBonus

  • Augmenter la taille du contrôle XAML Joueur en accord avec cet agrandissement

  • Supprimer le collectable du Canvas et de la liste Collectables

  • Augmenter le score en suivant la formule suivante :

    Score += (int)((Math.Abs(args.Reading.RollDegrees) + 
                    Math.Abs(args.Reading.PitchDegrees) + 1) * Facteur);
List<Rectangle> aSupprimer = new List<Rectangle>();
// Si collision
{
  Facteur += BonusFacteur;
  Joueur.Width += BonusTaille;
  Joueur.Height += BonusTaille;
  TailleJoueur += (int)BonusTaille;
  Score += (int)((Math.Abs(args.Reading.RollDegrees) + 
                  Math.Abs(args.Reading.PitchDegrees) + 1) * Facteur);
  aSupprimer.Add(c);
}
foreach(Rectangle r in aSupprimer) {
  MonCanvas.Children.Remove(r);
  Collectables.Remove(r);
}

1.10.

A l'exécution de votre programme, le personnage devrait être présent au milieu de l'écran. Piloté par la variation d'angle de l'équipement, il devrait glisser uniquement dans la surface de l'écran. Chaque seconde, un rectangle collectable bleu apparaît à un endroit aléatoire de l'écran, et à son contact, le personnage et sa vitesse de déplacement grandissent.

Pour l'étape suivante, agrémentons l'affichage de deux éléments visibles :

  • La taille du personnage

  • Le score

Ces éléments doivent être présents à l'extrême Haut-Droite de l'écran, l'un en dessous de l'autre. Ces éléments ne doivent pas réduire la zone de jeu. Leur taille doit être de 24, leur police grasse et le texte aligné à droite.

[Note]

Pour que ces éléments soient affichés au dessus, il vous faudra les placer plus bas que le contrôle Canvas dans le code XAML.

Figure 1.12. Rendu attendu

Rendu attendu

Ces contrôles TextBlock doivent être mis à jour en cas de collision.

<!-- MainPage.xaml. Reste de la page omise -->
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
  <Grid.RowDefinitions>
    <RowDefinition Height="auto" />
    <RowDefinition Height="*" />
  </Grid.RowDefinitions>
  <Canvas Grid.Row="0" x:Name="MonCanvas">
    <Rectangle Fill="Red" x:Name="Joueur"/>
  </Canvas>
  <Grid Grid.Row="0">
    <Grid>
      <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="auto" />
      </Grid.ColumnDefinitions>
      <StackPanel Grid.Column="1">
        <TextBlock x:Name="txtTaille" Text="Taille :" 
               FontSize="24" FontWeight="Bold" 
               TextAlignment="Right" />
        <TextBlock x:Name="txtScore" Text="Score :" 
               FontSize="24" FontWeight="Bold" 
               TextAlignment="Right"/>
      </StackPanel>
    </Grid>
  </Grid>
</Grid>
private async void _inclinometer_ReadingChanged(Inclinometer sender, 
      InclinometerReadingChangedEventArgs args)
{
  await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
  {
    // Reste du code omis
    txtScore.Text = "Score : " + Score;
    txtTaille.Text = "Size : " + TailleJoueur;
  });
}

1.11.

Dernière étape de notre application, les conditions de défaite. Le joueur perd quand son avatar touche les bords de l'écran. Pour ce faire, créez une méthode Stop() ne retournant rien dans la classe MainPage et appelez-la en cas de défaite.

Cette méthode Stop() aura pour but de :

  • Appeler la méthode Dispose() sur l'objet Timer

  • Affecter à Timer la valeur null

  • Retirer l'écoute à l'événement ReadingChanged de l'inclinomètre

Ajoutez dans le code XAML un contrôle Border dans la seconde ligne de votre grille principale. Celui-ci contiendra un contrôle StackPanel, contenant lui-même :

  • Le texte "Perdu !". Ce texte doit être centré horizontalement et verticalement et être de taille 48.

  • Un bouton "Rejouer". Celui-ci doit prendre la largeur du texte "Perdu !". Pour cela, définissez l'attribut HorizontalAlignment à "Stretch". Au clic sur ce bouton, la méthode Start() doit être appelée pour relancer l'ensemble du jeu à son état initial.

Ce contrôle Border doit avoir l'attribut Visibility défini à Collapsed pour être caché au lancement du jeu. N'oubliez pas de placer le contrôle Border après le contrôle Canvas dans votre code XAML pour le faire passer au-dessus.

Pour finir, le contrôle Border doit être affiché à la méthode Stop, et caché à la méthode Start.

<!-- Fichier Main.Page.xaml. Reste de la page omise -->
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
  <!-- Reste de l'élément Grid omis -->
  <Border Grid.Row="1" HorizontalAlignment="Center" VerticalAlignment="Center" 
          x:Name="BorderStop" Visibility="Collapsed">
    <StackPanel>
      <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center"
                 FontSize="48" Text="Perdu !" />
      <Button Content="Rejouer" HorizontalAlignment="Stretch" Click="Button_Click" />
    </StackPanel>
  </Border>
</Grid>
private async void _inclinometer_ReadingChanged(Inclinometer sender, 
      InclinometerReadingChangedEventArgs args)
{
  await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
  {
    // Transformation du code précédent
    var positionX = Canvas.GetLeft(Joueur);
    var positionY = Canvas.GetTop(Joueur);
    positionX += (Facteur * args.Reading.RollDegrees);
    if(positionX <= 0 || positionX >= ActualWidth - TailleJoueur)
    {
      Stop();
      return;
    }

    positionY += (Facteur * args.Reading.PitchDegrees);
    if(positionY <= 0 || positionY >= ActualHeight - TailleJoueur)
    {
      Stop();
      return;
    }
    // Reste du code omis
  });
}

private void Stop()
{
  if (Timer != null) { Timer.Dispose(); }
  Timer = null;
  _inclinometer.ReadingChanged -= _inclinometer_ReadingChanged;
  BorderStop.Visibility = Visibility.Visible;
}

public void Start() {
  // Reste du code omis
  BorderStop.Visibility = Visibility.Collapsed;
}

private void Button_Click(object sender, RoutedEventArgs e)
{
  Start();
}

Autres capteurs

Si les capteurs de position permettent de travailler avec l'orientation, l'inclinaison et l'accélération de l'équipement, d'autres capteurs sont également utilisés pour améliorer le confort ou proposer des fonctionnalités exclusives à vos applications.

Luminomètre

Un luminomètre est un capteur dédié à l'analyse de la lumière ambiante. Celui-ci est utilisé dans le cadre du système d'exploitation pour adapter automatiquement le rétro-éclairage du téléphone pour économiser de la batterie et améliorer la lisibilité et le confort.

Le luminomètre s'utilise comme les précédents capteurs.

<TextBlock Grid.Row="12" Grid.Column="0" Text="Luminosité:" />
<TextBlock Grid.Row="12" Grid.Column="1" x:Name="Luminosite" />
private LightSensor _lightsensor;

public MainPage()
{
  _lightsensor = LightSensor.GetDefault();
  if(_lightsensor != null)
  {
    uint minimumReportInterval = _lightsensor.MinimumReportInterval;
    _lightsensor.ReportInterval = minimumReportInterval > 16 ? minimumReportInterval : 16;
    _lightsensor.ReadingChanged += _lightsensor_ReadingChanged;
  }
}

private async void _lightsensor_ReadingChanged(LightSensor sender, 
      LightSensorReadingChangedEventArgs args)
{
  await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
  {
    LightSensorReading reading = args.Reading;
    Luminosite.Text = string.Format("{0,5:0.00}", reading.IlluminanceInLux);
  });
}
[Note]

Le luminomètre ne peut être testé au sein d'un émulateur car la luminosité sera toujours égale à 1 (maximum).

Localisation

Le capteur GPS permet de détecter la position de l'utilisateur. Cette fonctionnalité peut être utilisée tant pour des applications de navigation (cartes, itinéraires, ...) que pour offrir des informations pertinentes autour de l'utilisateur.

Votre application devra mentionner cette capacité dans le fichier Package.appxmanifest. Pour cela, deux possibilités :

  • Afficher le code pour ajouter la mention suivante :

    <!-- Fichier Package.appxmanifest. La balise Capabilities est présente par défaut. -->
    <Capabilities>
      <DeviceCapability Name="location"/>
    </Capabilities>
  • Double-cliquer sur le fichier, sélectionner l'onglet "Capabilities" et cocher la case "Location" :

    Figure 1.13. Ajout de capacité localisation

    Ajout de capacité localisation

[Note]

L'utilisateur aura toujours le dernier mot. Ainsi, même si votre application est référencée avec les permissions de localisation, l'utilisateur peut toujours refuser d'être localisé.

Une fois cette étape réalisée, il faudra utiliser l'objet Geolocator pour récupérer les coordonnées de l'utilisateur :

<TextBlock Grid.Row="14" Grid.Column="0" Text="Statut localisation :" />
<TextBlock Grid.Row="14" Grid.Column="1" x:Name="StatutLocalisation" />
<TextBlock Grid.Row="15" Grid.Column="0" Text="Latitude :" />
<TextBlock Grid.Row="15" Grid.Column="1" x:Name="Latitude" />
<TextBlock Grid.Row="16" Grid.Column="0" Text="Longitude :" />
<TextBlock Grid.Row="16" Grid.Column="1" x:Name="Longitude" />
public MainPage()
{
  this.InitializeComponent();
  // ...
  Geolocalisation();
}

private async void Geolocalisation()
{
  // Il faudra récupérer le statut de la requête d'accès avant toute chose
  // Cette requête d'accès représente l'utilisateur acceptant ou non la
  // localisation. Cette demande n'est pas forcément affichée à l'utilisateur,
  // par exemple si l'utilisateur a paramétré son équipement pour désactiver
  // ce type de fonctionnalité.
  GeolocationAccessStatus statut = await Geolocator.RequestAccessAsync();
  switch(statut)
  {
    // Accès autorisé
    case GeolocationAccessStatus.Allowed:
      StatutLocalisation.Text = "En cours de résolution...";

      // Dans ce cas, nous créons un objet Geolocator représentant le
      // capteur. Celui-ci aura une précision demandée de 10 mètres, et
      // se mettra à jour toutes les deux secondes.
      // Il faudra adapter cet intervale de report en fonction des besoins
      // de votre application.
      Geolocator geolocator = new Geolocator() {
        DesiredAccuracyInMeters = 10,
        ReportInterval = 2000
      };

      // Ecoute de l'événement au changement de position
      geolocator.PositionChanged += Geolocator_PositionChanged;
      break;

    // Accès refusé
    case GeolocationAccessStatus.Denied:
      StatutLocalisation.Text = "Permission refusée pour localiser.";
      break;

    // Statut non spécifié (Erreur)
    case GeolocationAccessStatus.Unspecified:
      StatutLocalisation.Text = "Erreur inconnue.";
      break;
  }
}

private async void Geolocator_PositionChanged(Geolocator sender, PositionChangedEventArgs args)
{
  await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
  {
    // Récupération de la position mise à jour
    Geoposition position = args.Position;
    // Affichage de la latitude et longitude avec un format adapté
    Latitude.Text = string.Format("{0,5:0.0000}", position.Coordinate.Point.Position.Latitude);
    Longitude.Text = string.Format("{0,5:0.0000}", position.Coordinate.Point.Position.Longitude);
  });
}
[Note]

La propriété CivicAddress de Geoposition n'est plus supportée est retournera toujours null. Pour obtenir une adresse en fonction d'une géoposition, voir plus bas.

L'émulateur Windows permet d'agir avec précision sur les coordonnées d'un utilisateur fictif pour tester votre application. Ainsi, il est possible d'utiliser la position réelle de l'ordinateur, ou d'épingler une position. En épinglant, on pourra sélectionner une ou plusieurs localisations pour constituer un scénario de déplacement, et lire ces localisations par intervale défini.

Figure 1.14. Utilisation de l'émulateur pour localiser

Utilisation de l'émulateur pour localiser

Le capteur GPS permet également de déterminer des itinéraires et des adresses postales. L'API Maps est utilisée pour ces fonctionnalités.

[Note]

La gestion des cartes demande une autorisation auprès de l'API Maps. Un tutoriel pas à pas dédié à l'obtention de cette autorisation est disponible sur le site officiel Microsoft en suivant ce lien.

Pour intégrer un contrôle MapControl dans notre application, il suffit d'ajouter l'espace de noms maps dans les attributs de notre Page, puis de placer ce dit contrôle.

<Page ...
  xmlns:maps="using:Windows.UI.Xaml.Controls.Maps">
  <!-- Reste du fichier omis -->
  <maps:MapControl x:Name="myMap" />
</Page>

Ce contrôle possède de nombreux attributs allant de l'affichage du trafic avec l'attribut TrafficFlowVisible, le style de la carte (vue 3D, carte, ...) avec l'attribut Style, le niveau de zoom avec ZoomLevel ou encore l'affichage ou non des caractéristiques piétonnes comme les escaliers avec l'attribut PedestrianFeaturesVisible.

// Exemple via le code-behind
myMap.PedestrianFeaturesVisible = true;
myMap.TrafficFlowVisible = true;
myMap.Style = MapStyle.Aerial3DWithRoads;
myMap.ZoomLevel = 10;
[Note]

Le niveau de Zoom est un entier allant de 1 à 19, 1 étant une vue entière du globe, et 19 une zone où un pixel représente 30 centimètres (donc un écran de 1000 pixels de large affichera une zone de 30 mètres).

Pour obtenir la relation pixel/mètre des différents niveaux de zoom, Bing a réalisé un article de blog les détaillant accessible à cette adresse.

C'est grâce à l'API Maps que l'on peut également récupérer une adresse postale par rapport à une géoposition :

<Page ...
  xmlns:maps="using:Windows.UI.Xaml.Controls.Maps">
  <!-- Reste du fichier omis -->
  <maps:MapControl Grid.Row="18" Grid.ColumnSpan="2" x:Name="myMap" Loaded="myMap_Loaded" />
</Page>
public MainPage()
{
  this.InitializeComponent();
  // Il faudra remplacer "APIKEY" par la clef fournie
  myMap.MapServiceToken = "APIKEY";

  // Reste de la méthode omise
}

private async void Geolocator_PositionChanged(Geolocator sender, PositionChangedEventArgs args)
{
  await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, async () =>
  {
    Geoposition position = args.Position;
    Latitude.Text = string.Format("{0,5:0.0000}", position.Coordinate.Point.Position.Latitude);
    Longitude.Text = string.Format("{0,5:0.0000}", position.Coordinate.Point.Position.Longitude);

    MapLocationFinderResult resultat = 
          await MapLocationFinder.FindLocationsAtAsync(position.Coordinate.Point);
    if (resultat.Status == MapLocationFinderStatus.Success)
    {
      MapLocation localisation = resultat.Locations[0];
      string text = string.Empty;
      if(localisation.DisplayName != string.Empty)
      {
        text += localisation.DisplayName + " ";
      }
      if(localisation.Address.StreetNumber != string.Empty)
      {
        text += localisation.Address.StreetNumber + ", ";
      }
      text += localisation.Address.Street + " - " +
        localisation.Address.FormattedAddress;
      Adresse.Text = text;
    } 
    else
    {
      Adresse.Text = "Impossible d'obtenir l'adresse.";
    }
  });
}

Il est également possible d'effectuer une recherche par adresse pour obtenir une liste de résultats pertinents. Cette recherche s'effectue par une chaîne de caractères et peut spécifier ou non un point de référence agissant comme un indice facilitant les recherches.

// Chaîne représentant la recherche d'un utilisateur
string recherche = "Champs Elysées";

// Une localisation proche pour aider la recherche.
// Cette localisation est le centre de la ville de Paris
BasicGeoposition indice = new BasicGeoposition();
indice.Latitude = 48.8566;
indice.Longitude = 2.3522;
Geopoint pointIndice = new Geopoint(indice);

// Recherche de l'adresse spécifiée avec le centre de la ville
// de Paris comme indice. Le troisième paramètre (ici 3) spécifie
// le nombre de résultats maximum souhaités.
MapLocationFinderResult resultat =
    await MapLocationFinder.FindLocationsAsync(
        recherche,
        pointIndice,
        3);

// Si la recherche est un succès, affichons le premier résultat.
if (resultat.Status == MapLocationFinderStatus.Success)
{
  Geopoint point = resultat.Locations[0].Point;
  ResultatRecherche.Text = 
      string.Format("{0,5:0.0000}", point.Position.Latitude) + "," +
      string.Format("{0,5:0.0000}", point.Position.Longitude);
}

Enfin, il est possible d'utiliser Bing Maps pour effectuer des itinéraires complets entre deux points. Pour ce faire, il faudra déterminer le point de départ, le point d'arrivée et la méthode de déplacement.

// Définition de l'itinéraire. Ces coordonnées peuvent être récupées
// par résultat de recherche ou localisation de l'utilisateur.
// Pour cet exemple, il sera pris deux points fixes et définis 
// dans le code.
Geopoint champsElysees = new Geopoint(new BasicGeoposition()
{
  Latitude = 48.8705,
  Longitude = 2.3048
});
Geopoint tourMontparnasse = new Geopoint(new BasicGeoposition()
{
  Latitude = 48.8421,
  Longitude = 2.3220
});

// La recherche permet de choisir entre l'itinéraire à pied (GetWalkingRouteAsync)
// ou en voiture (GetDrivingRouteAsync).
MapRouteFinderResult result = 
      await MapRouteFinder.GetWalkingRouteAsync(champsElysees, tourMontparnasse);
if (result.Status == MapRouteFinderStatus.Success)
{
  // Création d'un objet MapRouteView représentant l'itinéraire
  MapRouteView routeView = new MapRouteView(result.Route);

  // Ajout de cet élément dans la carte
  this.myMap.Routes.Add(routeView);
  
  // Pour modifier l'affichage de la carte, on utilisera
  // TrySetViewBoundsAsync prenant en paramètre une zone
  // entourant les points de l'itinéraire (BoundingBox),
  // une marge autour des points pour une meilleure lisibilité
  // (ici de 10), et une animation de zoom linéaire.
  await myMap.TrySetViewBoundsAsync(
        routeView.Route.BoundingBox, 
        new Thickness(10), 
        MapAnimationKind.Linear);
  
}

Réseau

Avec cette méthode, on s'intéressera au type de réseau sur lequel est connecté l'équipement. Les réseaux peuvent être limités ou illimités, réseau par téléphonie mobile ou par WiFi. Selon le type de connection, il est possible d'adapter son application, ce qui est par exemple fait par le système pour le téléchargement de cartes hors lignes ou autres opérations demandant un téléchargement conséquent de données.

Pour ce faire, on utilisera les classes NetworkInterface et NetworkInformation.

bool connexionInternet = NetworkInterface.GetIsNetworkAvailable();

La différenciation de la connection mobile (WWAN) ou WiFi (WLAN) se fait via l'instance de classe ConnectionProfile, comme ceci :

ConnectionProfile profilConnexionInternet = NetworkInformation.GetInternetConnectionProfile();
bool connexionWiFi = false;
bool connexionMobile = false;
if(profilConnexionInternet != null)
{
  connexionWifi = profilConnexionInternet.IsWlanConnectionProfile;
  connexionMobile = profilConnexionInternet.IsWwanConnectionProfile;
}

La différenciation entre un réseau limité ou illimité se fait grâce aux informations fournies par le réseau. Il est possible de déterminer programmatiquement le type de réseau actuel si ce dernier communique les informations :

ConnectionCost coutConnexion = profilConnexionInternet.GetConnectionCost();
switch(coutConnexion.NetworkCostType) {
  case NetworkCostType.Unknown:
  case NetworkCostType.Unrestricted:
    // Réseau ne communiquant pas l'information ou illimité
    break;
  default:
    // Réseau limité
    break;
}

Un événement est également disponible pour prévenir du changement de connexion. Cet événement est utile pour prévenir l'utilisateur que la connectivité est désormais disponible ou indisponible, ou encore pour reprendre des opérations de synchronisation.

public MainPage() {
  // Reste de la méthode omise
  NetworkInformation.NetworkStatusChanged += NetworkInformation_NetworkStatusChanged;
}
private async void NetworkInformation_NetworkStatusChanged(object sender)
{
  ConnectionProfile profilConnexionInternet = NetworkInformation.GetInternetConnectionProfile();
  // Utilisez cet objet pour déterminer le nouveau type de connexion et prendre des décisions
}
A propos de SUPINFO | Contacts & adresses | Enseigner à SUPINFO | Presse | Conditions d'utilisation & Copyright | Respect de la vie privée | Investir
Logo de la société Cisco, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société IBM, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Sun-Oracle, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Apple, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Sybase, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Novell, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Intel, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Accenture, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société SAP, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo de la société Prometric, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management Logo du IT Academy Program par Microsoft, partenaire pédagogique de SUPINFO, la Grande École de l'informatique, du numérique et du management

SUPINFO International University
Ecole d'Informatique - IT School
École Supérieure d'Informatique de Paris, leader en France
La Grande Ecole de l'informatique, du numérique et du management
Fondée en 1965, reconnue par l'État. Titre Bac+5 certifié au niveau I.
SUPINFO International University is globally operated by EDUCINVEST Belgium - Avenue Louise, 534 - 1050 Brussels