Navegação por foco programática

Teclado, remoto e direcional

Para mover o foco programaticamente em seu aplicativo do Windows, você pode usar o método FocusManager.TryMoveFocus ou o método FindNextElement .

TryMoveFocus tenta mudar o foco do elemento com foco para o próximo elemento focalizável na direção especificada, enquanto FindNextElement recupera o elemento (como um DependencyObject) que receberá o foco com base na direção de navegação especificada (apenas navegação direcional, não pode ser usado para emular a navegação por guias).

Observação

Recomendamos usar o método FindNextElement em vez de FindNextFocusableElement porque FindNextFocusableElement recupera um UIElement, que retorna nulo se o próximo elemento focalizável não for um UIElement (como um objeto de hiperlink).

Encontre um candidato de foco dentro de um escopo

Você pode personalizar o comportamento de navegação por foco para TryMoveFocus e FindNextElement, incluindo a pesquisa do próximo candidato de foco em uma árvore de interface do usuário específica ou a desconsideração de elementos específicos.

Este exemplo usa um "jogo da velha" para demonstrar os métodos TryMoveFocus e 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 ainda mais a identificação de candidatos de foco. Este objeto fornece as propriedades a seguir:

  • SearchRoot - Definir o escopo da pesquisa de candidatos de navegação por foco aos filhos deste DependencyObject. O valor nulo indica o início da pesquisa na raiz da árvore visual.

Importante

Se uma ou mais transformações forem aplicadas aos descendentes de SearchRoot que as colocam fora da área direcional, esses elementos ainda serão considerados candidatos.

  • ExclusionRect - Candidatos de navegação por foco são identificados usando um retângulo delimitador "fictício" no qual todos os objetos sobrepostos são excluídos do foco de navegação. Esse retângulo é usado apenas para cálculos e nunca é adicionado à árvore visual.
  • HintRect - Candidatos de navegação por foco são identificados usando um retângulo delimitador "fictício" que identifica os elementos com mais chances de receber o foco. Esse retângulo é usado apenas para cálculos e nunca é adicionado à árvore visual.
  • XYFocusNavigationStrategyOverride - A estratégia de navegação por foco usada para identificar o melhor elemento candidato para receber o foco.

A imagem a seguir ilustra alguns desses conceitos.

Quando o elemento B tem o foco, FindNextElement identifica I como o candidato de foco ao navegar para a direita. Os motivos para isso são:

  • Por causa do HintRect em A, a referência inicial é A, não B
  • C não é um candidato porque MyPanel foi especificado como o SearchRoot
  • F não é um candidato porque o ExclusionRect o sobrepõe

Comportamento de navegação por foco personalizada usando dicas de navegação

Comportamento de navegação por foco personalizada usando dicas de navegação

Evento NoFocusCandidateFound

O evento UIElement.NoFocusCandidateFound é acionado quando as teclas de seta ou a tecla tab são pressionadas e não há nenhum candidato de foco na direção especificada. Esse evento não é acionado para TryMoveFocus.

Como é um evento roteado, ele sobe do elemento focalizado por meio de sucessivos objetos pais até a raiz da árvore de objetos. Isso permite que você manipule o evento onde for apropriado.

Aqui, mostramos como uma Grade abre um SplitView quando o usuário tenta mover o foco para a esquerda do controle focalizável na extrema esquerda (consulte Projetando para Xbox e 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 e LostFocus

Os eventos UIElement.GotFocus e UIElement.LostFocus são acionados quando um elemento recebe ou perde o foco, respectivamente. Esse evento não é acionado para TryMoveFocus.

Como esses eventos são roteados, eles são propagados do elemento focalizado por meio de sucessivos objetos pai até a raiz da árvore de objetos. Isso permite que você manipule o evento onde for apropriado.

Eventos GettingFocus e LosingFocus

Os eventos UIElement.GettingFocus e UIElement.LosingFocus são acionados antes dos respectivos eventos UIElement.GotFocus e UIElement.LostFocus.

Como esses eventos são roteados, eles são propagados do elemento focalizado por meio de sucessivos objetos pai até a raiz da árvore de objetos. Como isso acontece antes da mudança de foco, você pode redirecioná-la ou cancelá-la.

GettingFocus e LosingFocus são eventos síncronos, portanto, o foco não será movido enquanto esses eventos são propagados. No entanto, GotFocus e LostFocus são eventos assíncronos, o que significa que não há nenhuma garantia de que o foco não se moverá novamente antes do manipulador ser executado.

Se o foco se mover por meio de uma chamada para Control.Focus, GettingFocus será acionado durante a chamada, enquanto GotFocus será acionado após a chamada.

O destino da navegação por foco pode ser alterado durante os eventos GettingFocus e LosingFocus (antes do foco se mover) por meio da propriedade GettingFocusEventArgs.NewFocusedElement. Mesmo que o destino seja alterado, o evento ainda é propagado e o destino pode ser alterado novamente.

Para evitar problemas de reentrância, uma exceção é gerada se você tentar mover o foco (usando TryMoveFocus ou Control.Focus) enquanto esses eventos são propagados.

Esses eventos são acionados independentemente do motivo para mover o foco (incluindo navegação por guias, direcional e programática).

Aqui está a ordem de execução para os eventos de foco:

  1. LosingFocus Se o foco for restaurado para o elemento que perdia o foco ou se TryCancel for bem-sucedido, nenhum outro evento será acionado.
  2. GettingFocus Se o foco for restaurado para o elemento que perdia o foco ou se TryCancel for bem-sucedido, nenhum outro evento será acionado.
  3. LostFocus
  4. GotFocus

A imagem a seguir mostra como, ao mover de A para a direita, o XYFocus escolhe B4 como um candidato. O B4 dispara o evento GettingFocus, onde o ListView tem a oportunidade de reatribuir foco a B3.

Alterando o destino da navegação por foco no evento GettingFocus

Alterando o destino da navegação por foco no evento GettingFocus

Aqui, mostramos como manipular o evento GettingFocus e redirecionar o foco.

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

Aqui, mostramos como manipular o evento LosingFocus para um CommandBar e definir o foco quando o menu for fechado.

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

Encontre o primeiro e o último elemento focalizável

Os métodos FocusManager.FindFirstFocusableElement e FocusManager.FindLastFocusableElement movem o foco para o primeiro ou o último elemento focalizável no escopo de um objeto (a árvore de elementos de um UIElement ou a árvore de texto de um TextElement). O escopo é especificado na chamada (se o argumento for nulo, o escopo é a janela atual).

Se nenhum candidato de foco puder ser identificado no escopo, o retorno será nulo.

Aqui, mostramos como especificar que os botões de um CommandBar têm um comportamento direcional delimitador (consulte Interações por 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;
        }
    }
}