Partilhar via


Navegação por foco programática

Teclado, controle remoto e D-pad

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

TryMoveFocus tenta alterar 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 (somente navegação direcional, não pode ser usada para emular a navegação por guia).

Observação

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

Encontre um candidato em foco dentro de um escopo

Você pode personalizar o comportamento de navegação de 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 exclusão de elementos específicos da consideração.

Este exemplo usa um jogo TicTacToe 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 como os candidatos de foco são identificados. Esse objeto fornece as seguintes propriedades:

  • SearchRoot - Defina o escopo da pesquisa de candidatos de navegação de foco para os filhos desse DependencyObject. Nulo indica para iniciar a pesquisa a partir da raiz da árvore visual.

Importante

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

  • ExclusionRect - Os candidatos à navegação de foco são identificados usando um retângulo delimitador "fictício" em que 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 – Os candidatos à navegação de foco são identificados usando um retângulo delimitador "fictício" que identifica os elementos com maior probabilidade de receber foco. Esse retângulo é usado apenas para cálculos e nunca é adicionado à árvore visual.
  • XYFocusNavigationStrategyOverride - A estratégia de navegação de 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 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 se sobrepõe a ele

Comportamento de navegação de foco personalizado usando dicas de navegação

Comportamento de navegação de foco personalizado usando dicas de navegação

Evento NoFocusCandidateFound

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

Como esse é um evento roteado, ele é borbulhado do elemento focalizado por meio de objetos pai sucessivos até a raiz da árvore de objetos. Isso permite que você manipule o evento sempre que apropriado.

Aqui, mostramos como um Grid abre um SplitView quando o usuário tenta mover o foco para a esquerda do controle focalizável mais à 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 obtém o foco ou perde o foco, respectivamente. Esse evento não é acionado para TryMoveFocus.

Como esses são eventos roteados, eles se espalham do elemento focalizado por meio de objetos pai sucessivos até a raiz da árvore de objetos. Isso permite que você manipule o evento sempre que 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 são eventos roteados, eles se espalham do elemento focalizado por meio de objetos pai sucessivos até a raiz da árvore de objetos. Como isso acontece antes de ocorrer uma mudança de foco, você pode redirecionar ou cancelar a mudança de foco.

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

Se o foco passar por uma chamada para Control.Focus, GettingFocus será gerado durante a chamada, enquanto GotFocus será gerado após a chamada.

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

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

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

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

  1. LosingFocus Se o foco for redefinido para o elemento de foco perdido ou TryCancel for bem-sucedido, nenhum outro evento será disparado.
  2. GettingFocus Se o foco for redefinido de volta para o elemento de foco perdido ou TryCancel for bem-sucedido, nenhum outro evento será disparado.
  3. LostFocus
  4. GotFocus

A imagem a seguir mostra como, ao mover para a direita de A, o XYFocus escolhe B4 como candidato. Em seguida, B4 aciona o evento GettingFocus em que o ListView tem a oportunidade de reatribuir o foco a B3.

Alterando o destino de navegação de foco no evento GettingFocus

Alterando o destino de navegação de foco no evento GettingFocus

Aqui, mostramos como lidar com 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 lidar com o evento LosingFocus para um CommandBar e definir o foco quando o menu é 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 último elemento focalizável dentro do 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 será a janela atual).

Se nenhum candidato de foco puder ser identificado no escopo, null será retornado.

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