Navegación con foco mediante programación

Teclado, remoto y D-pad

Para mover el foco mediante programación en la aplicación Windows, puede usar el método FocusManager.TryMoveFocus o el método FindNextElement .

TryMoveFocus intenta cambiar el foco del elemento con foco al siguiente elemento con foco en la dirección especificada, mientras que FindNextElement recupera el elemento (como dependencyObject) que recibirá el foco en función de la dirección de navegación especificada (solo navegación direccional, no se puede usar para emular la navegación por tabulación).

Nota:

Se recomienda usar el método FindNextElement en lugar de FindNextFocusableElement porque FindNextFocusableElement recupera un UIElement, que devuelve null si el siguiente elemento con foco no es un UIElement (por ejemplo, un objeto Hyperlink).

Búsqueda de un candidato de enfoque dentro de un ámbito

Puede personalizar el comportamiento de navegación de foco para TryMoveFocus y FindNextElement, incluida la búsqueda del siguiente candidato de enfoque dentro de un árbol de interfaz de usuario específico o la exclusión de elementos específicos de la consideración.

En este ejemplo se usa un juego TicTacToe para mostrar los métodos TryMoveFocus y 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);
    }
}

Use FindNextElementOptions para personalizar aún más cómo se identifican los candidatos de foco. Este objeto proporciona las siguientes propiedades:

  • SearchRoot : ámbito de la búsqueda de candidatos de navegación de foco a los elementos secundarios de este DependencyObject. Null indica que se inicia la búsqueda desde la raíz del árbol visual.

Importante

Si se aplican una o varias transformaciones a los descendientes de SearchRoot que los colocan fuera del área direccional, estos elementos se siguen considerando candidatos.

  • ExclusionRect : los candidatos de navegación de foco se identifican mediante un rectángulo delimitador "ficticio" en el que todos los objetos superpuestos se excluyen del foco de navegación. Este rectángulo solo se usa para cálculos y nunca se agrega al árbol visual.
  • HintRect : los candidatos de navegación de foco se identifican mediante un rectángulo delimitador "ficticio" que identifica los elementos con más probabilidades de recibir el foco. Este rectángulo solo se usa para cálculos y nunca se agrega al árbol visual.
  • XYFocusNavigationStrategyOverride : la estrategia de navegación de foco utilizada para identificar el mejor elemento candidato para recibir el foco.

En la imagen siguiente se muestran algunos de estos conceptos.

Cuando el elemento B tiene el foco, FindNextElement identifica I como candidato de enfoque al navegar a la derecha. Las razones para esto son las siguientes:

  • Debido a HintRect en A, la referencia inicial es A, no B
  • C no es un candidato porque MyPanel se ha especificado como SearchRoot
  • F no es un candidato porque ExclusionRect se superpone

Comportamiento de navegación de foco personalizado mediante sugerencias de navegación

Comportamiento de navegación de foco personalizado mediante sugerencias de navegación

Evento NoFocusCandidateFound

El evento UIElement.NoFocusCandidateFound se desencadena cuando se presionan las teclas de dirección o tabulación y no hay ningún candidato de foco en la dirección especificada. Este evento no se desencadena para TryMoveFocus.

Dado que se trata de un evento enrutado, se propaga desde el elemento centrado a través de objetos primarios sucesivos a la raíz del árbol de objetos. Esto le permite controlar el evento siempre que sea necesario.

Aquí se muestra cómo una cuadrícula abre un SplitView cuando el usuario intenta mover el foco a la izquierda del control más enfocado a la izquierda (consulta Diseño para Xbox y 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;
    }
}

Eventos GotFocus y LostFocus

Los eventos UIElement.GotFocus y UIElement.LostFocus se desencadenan cuando un elemento obtiene el foco o pierde el foco, respectivamente. Este evento no se desencadena para TryMoveFocus.

Dado que se trata de eventos enrutados, se propagan desde el elemento centrado a través de objetos primarios sucesivos a la raíz del árbol de objetos. Esto le permite controlar el evento siempre que sea necesario.

Eventos GettingFocus y LosingFocus

Los eventos UIElement.GettingFocus y UIElement.LosingFocus se desencadenan antes de los eventos UIElement.GotFocus y UIElement.LostFocus correspondientes.

Dado que se trata de eventos enrutados, se propagan desde el elemento centrado a través de objetos primarios sucesivos a la raíz del árbol de objetos. Como esto sucede antes de que se produzca un cambio de foco, puede redirigir o cancelar el cambio de foco.

GettingFocus y LosingFocus son eventos sincrónicos, por lo que el foco no se moverá mientras estos eventos se propagan. Sin embargo, GotFocus y LostFocus son eventos asincrónicos, lo que significa que no hay ninguna garantía de que el foco no se mueva de nuevo antes de que se ejecute el controlador.

Si el foco se mueve a través de una llamada a Control.Focus, se genera GettingFocus durante la llamada, mientras que GotFocus se genera después de la llamada.

El destino de navegación de foco se puede cambiar durante los eventos GettingFocus y LosingFocus (antes de que se mueva el foco) a través de la propiedad GettingFocusEventArgs.NewFocusedElement . Incluso si se cambia el destino, el evento todavía se propaga y el destino se puede volver a cambiar.

Para evitar problemas de reentrada, se produce una excepción si intenta mover el foco (mediante TryMoveFocus o Control.Focus) mientras estos eventos se propagan.

Estos eventos se desencadenan independientemente del motivo del movimiento del foco (incluida la navegación por tabulación, la navegación direccional y la navegación mediante programación).

Este es el orden de ejecución de los eventos de enfoque:

  1. LosingFocus Si el foco se restablece al elemento de foco que pierde o TryCancel se realiza correctamente, no se desencadenan más eventos.
  2. GettingFocus Si el foco se restablece al elemento de foco que pierde o TryCancel se realiza correctamente, no se desencadenan más eventos.
  3. LostFocus
  4. GotFocus

En la imagen siguiente se muestra cómo, al moverse a la derecha desde A, el XYFocus elige B4 como candidato. A continuación, B4 desencadena el evento GettingFocus donde ListView tiene la oportunidad de reasignar el foco a B3.

Cambio del destino de navegación de foco en el evento GettingFocus

Cambio del destino de navegación de foco en el evento GettingFocus

Aquí se muestra cómo controlar el evento GettingFocus y el foco de redireccionamiento.

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

Aquí se muestra cómo controlar el evento LosingFocus de una barra de comandos y establecer el foco cuando se cierra el menú.

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

Buscar el primer y último elemento con foco

Los métodos FocusManager.FindFirstFocusableElement y FocusManager.FindLastFocusableElement mueven el foco al primer o último elemento enfocado dentro del ámbito de un objeto (el árbol de elementos de un UIElement o el árbol de texto de un TextElement). El ámbito se especifica en la llamada (si el argumento es null, el ámbito es la ventana actual).

Si no se puede identificar ningún candidato de foco en el ámbito, se devuelve null.

Aquí se muestra cómo especificar que los botones de una barra de comandos tienen un comportamiento direccional de ajuste (consulte Interacciones de teclado).

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