Navigation en mode focus programmé

Clavier, distant et pavé D

Pour déplacer le focus par programmation dans votre application Windows, vous pouvez utiliser la méthode FocusManager.TryMoveFocus ou la méthode FindNextElement .

TryMoveFocus tente de changer le focus de l’élément avec le focus sur l’élément focusable suivant dans la direction spécifiée, tandis que FindNextElement récupère l’élément (en tant que DependencyObject) qui recevra le focus en fonction du sens de navigation spécifié (navigation directionnelle uniquement, ne peut pas être utilisé pour émuler la navigation par onglet).

Remarque

Nous vous recommandons d’utiliser la méthode FindNextElement au lieu de FindNextFocusableElement, car FindNextFocusableElement récupère un UIElement, qui retourne null si l’élément focusable suivant n’est pas un élément UIElement (tel qu’un objet Hyperlink).

Rechercher un candidat de focus dans une étendue

Vous pouvez personnaliser le comportement de navigation du focus pour TryMoveFocus et FindNextElement, notamment la recherche du candidat focus suivant dans une arborescence d’interface utilisateur spécifique ou l’exclusion d’éléments spécifiques à prendre en compte.

Cet exemple utilise un jeu TicTacToe pour illustrer les méthodes TryMoveFocus et FindNextElement .

<StackPanel Orientation="Horizontal"
                VerticalAlignment="Center"
                HorizontalAlignment="Center" >
    <Button Content="Start Game" />
    <Button Content="Undo Movement" />
    <Grid x:Name="TicTacToeGrid" KeyDown="OnKeyDown">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="50" />
            <ColumnDefinition Width="50" />
            <ColumnDefinition Width="50" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="50" />
            <RowDefinition Height="50" />
            <RowDefinition Height="50" />
        </Grid.RowDefinitions>
        <myControls:TicTacToeCell 
            Grid.Column="0" Grid.Row="0" 
            x:Name="Cell00" />
        <myControls:TicTacToeCell 
            Grid.Column="1" Grid.Row="0" 
            x:Name="Cell10"/>
        <myControls:TicTacToeCell 
            Grid.Column="2" Grid.Row="0" 
            x:Name="Cell20"/>
        <myControls:TicTacToeCell 
            Grid.Column="0" Grid.Row="1" 
            x:Name="Cell01"/>
        <myControls:TicTacToeCell 
            Grid.Column="1" Grid.Row="1" 
            x:Name="Cell11"/>
        <myControls:TicTacToeCell 
            Grid.Column="2" Grid.Row="1" 
            x:Name="Cell21"/>
        <myControls:TicTacToeCell 
            Grid.Column="0" Grid.Row="2" 
            x:Name="Cell02"/>
        <myControls:TicTacToeCell 
            Grid.Column="1" Grid.Row="2" 
            x:Name="Cell22"/>
        <myControls:TicTacToeCell 
            Grid.Column="2" Grid.Row="2" 
            x:Name="Cell32"/>
    </Grid>
</StackPanel>
private void OnKeyDown(object sender, KeyRoutedEventArgs e)
{
    DependencyObject candidate = null;

    var options = new FindNextElementOptions ()
    {
        SearchRoot = TicTacToeGrid,
        XYFocusNavigationStrategyOverride = XYFocusNavigationStrategyOverride.Projection
    };

    switch (e.Key)
    {
        case Windows.System.VirtualKey.Up:
            candidate = 
                FocusManager.FindNextElement(
                    FocusNavigationDirection.Up, options);
            break;
        case Windows.System.VirtualKey.Down:
            candidate = 
                FocusManager.FindNextElement(
                    FocusNavigationDirection.Down, options);
            break;
        case Windows.System.VirtualKey.Left:
            candidate = FocusManager.FindNextElement(
                FocusNavigationDirection.Left, options);
            break;
        case Windows.System.VirtualKey.Right:
            candidate = 
                FocusManager.FindNextElement(
                    FocusNavigationDirection.Right, options);
            break;
    }
    // Also consider whether candidate is a Hyperlink, WebView, or TextBlock.
    if (candidate != null && candidate is Control)
    {
        (candidate as Control).Focus(FocusState.Keyboard);
    }
}

Utilisez FindNextElementOptions pour personnaliser davantage la façon dont les candidats focus sont identifiés. Cet objet fournit les propriétés suivantes :

  • SearchRoot - Étendue de la recherche de candidats de navigation focus aux enfants de cet DependencyObject. Null indique de démarrer la recherche à partir de la racine de l’arborescence visuelle.

Important

Si une ou plusieurs transformations sont appliquées aux descendants de SearchRoot qui les placent en dehors de la zone directionnelle, ces éléments sont toujours considérés comme candidats.

  • ExclusionRect - Les candidats de navigation focus sont identifiés à l’aide d’un rectangle englobant « fictif » où tous les objets qui se chevauchent sont exclus du focus de navigation. Ce rectangle est utilisé uniquement pour les calculs et n’est jamais ajouté à l’arborescence visuelle.
  • HintRect - Les candidats de navigation focus sont identifiés à l’aide d’un rectangle englobant « fictif » qui identifie les éléments les plus susceptibles de recevoir le focus. Ce rectangle est utilisé uniquement pour les calculs et n’est jamais ajouté à l’arborescence visuelle.
  • XYFocusNavigationStrategyOverride : stratégie de navigation focus utilisée pour identifier le meilleur élément candidat à recevoir le focus.

L’image suivante illustre certains de ces concepts.

Lorsque l’élément B a le focus, FindNextElement identifie i comme candidat de focus lors de la navigation vers la droite. Les raisons sont les suivantes :

  • En raison de hintRect sur A, la référence de départ est A, et non B
  • C n’est pas un candidat, car MyPanel a été spécifié comme SearchRoot
  • F n’est pas candidat, car l’ExclusionRect se chevauche

Comportement de navigation focus personnalisé à l’aide d’indicateurs de navigation

Comportement de navigation focus personnalisé à l’aide d’indicateurs de navigation

Événement NoFocusCandidateFound

L’événement UIElement.NoFocusCandidateFound est déclenché lorsque l’onglet ou les touches de direction sont enfoncées et qu’il n’existe aucun candidat au focus dans la direction spécifiée. Cet événement n’est pas déclenché pour TryMoveFocus.

Étant donné qu’il s’agit d’un événement routé, il se déplace de l’élément ciblé vers le haut par le biais d’objets parents successifs à la racine de l’arborescence d’objets. Cela vous permet de gérer l’événement, où qu’il soit approprié.

Ici, nous montrons comment une grille ouvre un SplitView lorsque l’utilisateur tente de déplacer le focus à gauche du contrôle le plus focusable à gauche (voir Conception pour Xbox et TV).

<Grid NoFocusCandidateFound="OnNoFocusCandidateFound">
...
</Grid>
private void OnNoFocusCandidateFound (
    UIElement sender, NoFocusCandidateFoundEventArgs args)
{
    if(args.NavigationDirection == FocusNavigationDirection.Left)
    {
        if(args.InputDevice == FocusInputDeviceKind.Keyboard ||
        args.InputDevice == FocusInputDeviceKind.GameController )
            {
                OpenSplitPaneView();
            }
        args.Handled = true;
    }
}

Événements GotFocus et LostFocus

Les événements UIElement.GotFocus et UIElement.LostFocus sont déclenchés lorsqu’un élément obtient le focus ou perd le focus, respectivement. Cet événement n’est pas déclenché pour TryMoveFocus.

Étant donné qu’ils sont routés, ils sont en bulles de l’élément ciblé jusqu’à la racine de l’arborescence d’objets. Cela vous permet de gérer l’événement, où qu’il soit approprié.

Événements GettingFocus et LosingFocus

Les événements UIElement.GettingFocus et UIElement.LosingFocus se déclenchent avant les événements UIElement.GotFocus et UIElement.LostFocus respectifs.

Étant donné qu’ils sont routés, ils sont en bulles de l’élément ciblé jusqu’à la racine de l’arborescence d’objets. Comme cela se produit avant qu’une modification de focus ne se produise, vous pouvez rediriger ou annuler la modification du focus.

GettingFocus et LosingFocus sont des événements synchrones afin que le focus ne soit pas déplacé pendant que ces événements sont en cours de propagation. Toutefois, GotFocus et LostFocus sont des événements asynchrones, ce qui signifie qu’il n’existe aucune garantie que le focus ne se déplace plus avant l’exécution du gestionnaire.

Si le focus passe par un appel à Control.Focus, GettingFocus est déclenché pendant l’appel, tandis que GotFocus est déclenché après l’appel.

La cible de navigation focus peut être modifiée pendant les événements GettingFocus et LosingFocus (avant le déplacement du focus) via la propriété GettingFocusEventArgs.NewFocusedElement . Même si la cible est modifiée, l’événement reste en bulles et la cible peut être modifiée à nouveau.

Pour éviter les problèmes de réentrance, une exception est levée si vous essayez de déplacer le focus (à l’aide de TryMoveFocus ou control.Focus) pendant que ces événements sont en cours de propagation.

Ces événements sont déclenchés, quelle que soit la raison du déplacement du focus (y compris la navigation par onglet, la navigation directionnelle et la navigation programmatique).

Voici l’ordre d’exécution des événements de focus :

  1. LosingFocus Si le focus est réinitialisé à l’élément de focus perdu ou TryCancel réussit, aucun autre événement n’est déclenché.
  2. GettingFocus Si le focus est réinitialisé à l’élément de focus perdant ou TryCancel réussit, aucun autre événement n’est déclenché.
  3. LostFocus
  4. GotFocus

L’image suivante montre comment, lorsque vous passez à droite à partir de A, le XYFocus choisit B4 en tant que candidat. B4 déclenche ensuite l’événement GettingFocus où ListView a la possibilité de réaffecter le focus à B3.

Modification de la cible de navigation focus sur l’événement GettingFocus

Modification de la cible de navigation focus sur l’événement GettingFocus

Ici, nous montrons comment gérer l’événement GettingFocus et rediriger le focus.

<StackPanel Orientation="Horizontal">
    <Button Content="A" />
    <ListView x:Name="MyListView" SelectedIndex="2" GettingFocus="OnGettingFocus">
        <ListViewItem>LV1</ListViewItem>
        <ListViewItem>LV2</ListViewItem>
        <ListViewItem>LV3</ListViewItem>
        <ListViewItem>LV4</ListViewItem>
        <ListViewItem>LV5</ListViewItem>
    </ListView>
</StackPanel>
private void OnGettingFocus(UIElement sender, GettingFocusEventArgs args)
{
    //Redirect the focus only when the focus comes from outside of the ListView.
    // move the focus to the selected item
    if (MyListView.SelectedIndex != -1 && 
        IsNotAChildOf(MyListView, args.OldFocusedElement))
    {
        var selectedContainer = 
            MyListView.ContainerFromItem(MyListView.SelectedItem);
        if (args.FocusState == 
            FocusState.Keyboard && 
            args.NewFocusedElement != selectedContainer)
        {
            args.TryRedirect(
                MyListView.ContainerFromItem(MyListView.SelectedItem));
            args.Handled = true;
        }
    }
}

Ici, nous montrons comment gérer l’événement LosingFocus pour un CommandBar et définir le focus lorsque le menu est fermé.

<CommandBar x:Name="MyCommandBar" LosingFocus="OnLosingFocus">
     <AppBarButton Icon="Back" Label="Back" />
     <AppBarButton Icon="Stop" Label="Stop" />
     <AppBarButton Icon="Play" Label="Play" />
     <AppBarButton Icon="Forward" Label="Forward" />

     <CommandBar.SecondaryCommands>
         <AppBarButton Icon="Like" Label="Like" />
         <AppBarButton Icon="Share" Label="Share" />
     </CommandBar.SecondaryCommands>
 </CommandBar>
private void OnLosingFocus(UIElement sender, LosingFocusEventArgs args)
{
    if (MyCommandBar.IsOpen == true && 
        IsNotAChildOf(MyCommandBar, args.NewFocusedElement))
    {
        if (args.TryCancel())
        {
            args.Handled = true;
        }
    }
}

Rechercher le premier et le dernier élément focusable

Les méthodes FocusManager.FindFirstFocusableElement et FocusManager.FindLastFocusableElement déplacent le focus sur le premier ou le dernier élément focusable dans l’étendue d’un objet (l’arborescence d’éléments d’un UIElement ou l’arborescence de texte d’un TextElement). L’étendue est spécifiée dans l’appel (si l’argument a la valeur Null, l’étendue est la fenêtre active).

Si aucun candidat de focus ne peut être identifié dans l’étendue, la valeur Null est retournée.

Ici, nous montrons comment spécifier que les boutons d’une barre de commandes ont un comportement directionnel encapsulé (voir Interactions clavier).

<CommandBar x:Name="MyCommandBar" LosingFocus="OnLosingFocus">
    <AppBarButton Icon="Back" Label="Back" />
    <AppBarButton Icon="Stop" Label="Stop" />
    <AppBarButton Icon="Play" Label="Play" />
    <AppBarButton Icon="Forward" Label="Forward" />

    <CommandBar.SecondaryCommands>
        <AppBarButton Icon="Like" Label="Like" />
        <AppBarButton Icon="ReShare" Label="Share" />
    </CommandBar.SecondaryCommands>
</CommandBar>
private void OnLosingFocus(UIElement sender, LosingFocusEventArgs args)
{
    if (IsNotAChildOf(MyCommandBar, args.NewFocussedElement))
    {
        DependencyObject candidate = null;
        if (args.Direction == FocusNavigationDirection.Left)
        {
            candidate = FocusManager.FindLastFocusableElement(MyCommandBar);
        }
        else if (args.Direction == FocusNavigationDirection.Right)
        {
            candidate = FocusManager.FindFirstFocusableElement(MyCommandBar);
        }
        if (candidate != null)
        {
            args.NewFocusedElement = candidate;
            args.Handled = true;
        }
    }
}