Partilhar via


Navegação de foco programático

Teclado, comando e D-pad

Para mover o foco programaticamente na sua aplicação Windows, pode usar o método FocusManager.TryMoveFocus ou o método FindNextElement .

O TryMoveFocus tenta mudar o foco do elemento com foco para o próximo elemento focalizável na direção especificada, enquanto o 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 separador).

Observação

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

Encontre um candidato de foco dentro de um âmbito

Pode personalizar o comportamento de navegação de foco tanto para o TryMoveFocus como para o FindNextElement, incluindo procurar o próximo candidato a foco dentro de uma árvore de interface específica ou excluir elementos específicos da consideração.

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

  • SearchRoot - Limitar a pesquisa de candidatos para navegação de foco aos filhos deste DependencyObject. Nulo indica iniciar a pesquisa na árvore visual a partir da raiz.

Importante

Se uma ou mais transformações forem aplicadas aos descendentes do SearchRoot que os colocam fora da área direcional, estes elementos continuam a ser considerados candidatos.

  • ExclusionRect - Candidatos à navegação de foco são identificados usando um "retângulo de delimitação fictício" onde todos os objetos sobrepostos são excluídos do foco de navegação. Este 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 indica os elementos mais propensos a receber foco. Este 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 destinatário do foco.

A imagem seguinte ilustra alguns destes conceitos.

Quando o elemento B tem foco, FindNextElement identifica I como candidato a foco ao navegar para a direita. As razões para isto são:

  • Devido ao HintRect em A, a referência inicial é A, não B
  • C não é candidato porque o MyPanel foi especificado como o SearchRoot
  • F não é candidato porque o ExclusionRect sobrepõe-se a ele

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

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

Evento NoFocusCandidateFound

O evento UIElement.NoFocusCandidateFound é ativado quando as teclas tab ou setas são pressionadas e não há candidato a foco na direção especificada. Este evento não é disparado para TryMoveFocus.

Como este é um evento encaminhado, ele propaga-se a partir do elemento focado, pelos objetos pais sucessivos, até à raiz da árvore de objetos. Isto permite-lhe gerir o evento sempre que for apropriado.

Aqui, mostramos como uma grelha abre um SplitView quando o utilizador tenta mover o foco para a esquerda do controlo mais à esquerda (ver Design 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 disparados quando um elemento recebe ou perde o foco, respetivamente. Este evento não é disparado para TryMoveFocus.

Como estes são eventos encaminhados, eles propagam-se do elemento focado por sucessivos objetos-pai até à raiz da árvore de objetos. Isto permite-lhe tratar o evento em qualquer local apropriado.

Eventos GettingFocus e LosingFocus

Os eventos UIElement.GettingFocus e UIElement.LosingFocus são ativados antes dos respetivos eventos UIElement.GotFocus e UIElement.LostFocus.

Como estes são eventos roteados, eles propagam-se do elemento em foco através de sucessivos objetos pais até à raiz da árvore de objetos. Como isto acontece antes de ocorrer uma mudança de foco, pode redirecionar ou cancelar a mudança de foco.

GettingFocus e LosingFocus são eventos síncronos, por isso o foco não será movido enquanto estes eventos estão a borbulhar. No entanto, GotFocus e LostFocus são eventos assíncronos, o que significa que não há garantia de que o focus não se mova novamente antes de o handler ser executado.

Se o foco se mover através de uma chamada para Control.Focus, GettingFocus é levantado durante a chamada, enquanto GotFocus é levantado após a chamada.

O alvo de navegação do foco pode ser alterado durante os eventos GettingFocus e LosingFocus (antes do movimento do foco) através da propriedade GettingFocusEventArgs.NewFocusedElement . Mesmo que o alvo seja alterado, o evento continua a propagar-se e o alvo pode ser alterado novamente.

Para evitar problemas de reentrada, é feita uma exceção se tentares mover o foco (usando TryMoveFocus ou Control.Focus) enquanto estes eventos estão a borbulhar.

Estes eventos são acionados independentemente do motivo da mudança de 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. Perder o Foco Se o foco for reiniciado para o elemento que perdeu o foco ou se TryCancel for bem-sucedido, não são lançados mais eventos.
  2. GettingFocus Se o foco for redefinido no elemento de perda de foco ou TryCancel for bem-sucedido, nenhum evento adicional é desencadeado.
  3. LostFocus
  4. GotFocus

A imagem seguinte mostra como, ao mover-se para a direita a partir de A, o XYFocus escolhe B4 como candidato. O B4 lança então o evento GettingFocus, onde o ListView tem a oportunidade de reatribuir o foco ao B3.

Alterar o alvo de navegação do foco no evento GettingFocus

Alterar o alvo de navegação do 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 gerir o evento LosingFocus para uma Barra de Comandos e definir o foco quando o menu está 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 focável

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

Se nenhum candidato de foco for identificado no âmbito, é retornado null.

Aqui, mostramos como especificar que os botões de uma Barra de Comandos têm um comportamento direcional envolvente (ver Interações com o 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;
        }
    }
}