Поделиться через


Программное перемещение фокуса

Клавиатура, пульт и D-пад

Для программного перемещения фокуса в приложении Windows можно использовать метод FocusManager.TryMoveFocus или метод FindNextElement .

TryMoveFocus пытается перевести фокус с текущего элемента на следующий доступный элемент в указанном направлении, тогда как FindNextElement находит элемент (в виде DependencyObject), который получит фокус в соответствии с заданным направлением навигации (только для направленной навигации, нельзя использовать для эмуляции табуляции).

Замечание

Рекомендуется использовать метод FindNextElement вместо FindNextFocusableElement , так как FindNextFocusableElement получает uiElement, который возвращает значение NULL, если следующий фокусируемый элемент не является элементом UIElement (например, объектом Гиперссылки).

Найдите кандидатуру фокуса в рамках области

Вы можете настроить поведение навигации фокуса для TryMoveFocus и FindNextElement, включая поиск следующего кандидата фокуса в определенном дереве пользовательского интерфейса или исключение определенных элементов из рассмотрения.

В этом примере используется игра TicTacToe для демонстрации методов TryMoveFocus и 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);
    }
}

Используйте FindNextElementOptions, чтобы дополнительно настроить определение кандидатов фокуса. Этот объект предоставляет следующие свойства:

  • SearchRoot — ограничение области поиска кандидатов на навигацию фокуса до дочерних элементов этого объекта DependencyObject. Null указывает, чтобы начать поиск с корня визуального дерева.

Это важно

Если одно или несколько преобразований применяются к потомкам SearchRoot , которые размещают их за пределами направления, эти элементы по-прежнему считаются кандидатами.

  • ExclusionRect — кандидаты фокусной навигации определяются с помощью "вымышленного" ограничивающего прямоугольника, где все перекрывающиеся объекты исключены из фокусной навигации. Этот прямоугольник используется только для вычислений и никогда не добавляется в визуальное дерево.
  • HintRect — кандидаты для навигации фокуса определяются через «фиктивный» ограничивающий прямоугольник, который идентифицирует элементы, наиболее вероятные для получения фокуса. Этот прямоугольник используется только для вычислений и никогда не добавляется в визуальное дерево.
  • XYFocusNavigationStrategyOverride — стратегия навигации фокуса, используемая для идентификации наиболее подходящего элемента-кандидата для получения фокуса.

На следующем рисунке показаны некоторые из этих понятий.

Когда элемент B имеет фокус, FindNextElement идентифицирует I как кандидата на получение фокуса при навигации вправо. Ниже приведены причины:

  • Из-за HintRect на A начальная ссылка — это A, а не B.
  • C не является кандидатом, так как MyPanel был указан как SearchRoot
  • F не является кандидатом, так как исключениеRect перекрывает его

Пользовательское поведение навигации фокуса с помощью подсказок навигации

Пользовательское поведение навигации с фокусом с использованием подсказок навигации

Событие NoFocusCandidateFound

Событие UIElement.NoFocusCandidateFound запускается при нажатии клавиш табуляции или стрелок, если в указанном направлении нет кандидата фокуса. Это событие не запускается для TryMoveFocus.

Так как это маршрутизируемое событие, оно поднимается от фокусированного элемента по иерархии родительских объектов к корню дерева объектов. Это позволяет обрабатывать событие там, где это уместно.

Здесь показано, как сетка открывает SplitView , когда пользователь пытается переместить фокус слева от наиболее фокусируемого элемента управления (см. статью "Проектирование для Xbox и телевизора").

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

События GotFocus и LostFocus

События UIElement.GotFocus и UIElement.LostFocus запускаются, когда элемент получает фокус или теряет фокус соответственно. Это событие не запускается для TryMoveFocus.

Так как это перенаправленные события, они пузырятся из фокусированного элемента вверх через последовательные родительские объекты в корень дерева объектов. Это позволяет обрабатывать событие там, где это целесообразно.

События GettingFocus и LosingFocus

События UIElement.GettingFocus и UIElement.LosingFocus происходят перед соответствующими событиями UIElement.GotFocus и UIElement.LostFocus.

Так как это маршрутизируемые события, они распространяются вверх через иерархию родительских объектов к корню дерева объектов. Так как это происходит перед изменением фокуса, вы можете перенаправить или отменить изменение фокуса.

GettingFocus и LosingFocus являются синхронными событиями, поэтому фокус не будет перемещен в то время как эти события бурные. Тем не менее события GotFocus и LostFocus являются асинхронными, что означает, что нет гарантии, что фокус не переместится снова перед выполнением обработчика.

Если фокус перемещается через вызов Control.Focus, GettingFocus вызывается во время вызова, а GotFocus вызывается после вызова.

Целевой объект навигации фокуса можно изменить во время событий GettingFocus и LosingFocus (перед перемещением фокуса) через свойство GettingFocusEventArgs.NewFocusedElement . Даже если целевой объект изменен, событие по-прежнему пузырится и целевой объект можно изменить снова.

Чтобы избежать проблем повторного ввода данных, исключение возникает при попытке переместить фокус (с помощью TryMoveFocus или Control.Focus) во время выполнения этих событий.

Эти события запускаются независимо от причины перемещения фокуса (включая навигацию по вкладкам, направление навигации и программную навигацию).

Ниже приведен порядок выполнения для фокусируемых событий.

  1. LosingFocus Если фокус сбрасывается обратно на элемент, который его теряет, или если TryCancel выполняется успешно, дальнейшие события не инициируются.
  2. GettingFocus Если фокус сбрасывается обратно в элемент, теряющий фокус, или TryCancel успешен, дальнейшие события не происходят.
  3. LostFocus
  4. GotFocus

На следующем рисунке показано, как при движении вправо от A XYFocus выбирает B4 в качестве кандидата. Затем B4 запускает событие GettingFocus, где ListView имеет возможность переназначить фокус на B3.

Изменение целевого объекта навигации фокуса во время события GettingFocus

Изменение целевого объекта навигации фокуса в событии GettingFocus

Здесь мы покажем, как обрабатывать событие GettingFocus и перенаправлять фокус.

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

Здесь показано, как обрабатывать событие LosingFocus для панели команд и задавать фокус при закрытии меню.

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

Поиск первого и последнего фокусируемых элементов

Методы FocusManager.FindFirstFocusableElement и FocusManager.FindLastFocusableElement перемещают фокус на первый или последний фокусируемый элемент в области объекта (дерево элементов uiElement или текстовое дерево TextElement). Область указана в вызове (если аргумент имеет значение NULL, область — текущее окно).

Если в области не удается определить кандидатов фокуса, возвращается значение NULL.

Здесь мы покажем, как задать поведение кнопок панели команд с возможностью кругового перемещения (см. Взаимодействие с клавиатурой).

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