编程焦点导航

键盘、遥控器和方向键

若要以编程方式在 Windows 应用程序中移动焦点,可以使用 FocusManager.TryMoveFocus 方法或 FindNextElement 方法。

TryMoveFocus 尝试将当前焦点从一个元素移动到指定方向的下一个可聚焦元素,而 FindNextElement 则基于指定的导航方向检索将获得焦点的元素(作为 DependencyObject),仅支持定向导航,不能用于模拟选项卡导航。

注释

建议使用 FindNextElement 方法而不是 FindNextFocusableElement,因为 FindNextFocusableElement 检索 UIElement,如果下一个可聚焦元素不是 UIElement(如 Hyperlink 对象),则返回 null。

在一个范围内查找焦点候选者

可以自定义 TryMoveFocusFindNextElement 的焦点导航行为,包括搜索特定 UI 树中的下一个焦点候选项或从考虑中排除特定元素。

此示例使用 TicTacToe 游戏演示 TryMoveFocusFindNextElement 方法。

<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 在导航到右侧时将我标识为候选焦点。 原因如下:

  • 由于 A 上的 HintRect ,因此起始引用为 A,而不是 B
  • C 不是候选项,因为 MyPanel 已被指定为 SearchRoot
  • F 不是候选项,因为 ExclusionRect 与它重叠

使用导航提示的自定义焦点导航行为

使用导航提示的自定义焦点导航行为

NoFocusCandidateFound 事件

按下 Tab 键或箭头键且指定方向上没有候选焦点时,将触发 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.GotFocusUIElement.LostFocus 事件。 不会为 TryMoveFocus 触发此事件。

由于这些是路由事件,因此它们从焦点元素向上冒泡,通过连续的父对象到达对象树的根。 这样就可以在适当情况下处理事件。

GettingFocus 和 LosingFocus 事件

UIElement.GettingFocusUIElement.LosingFocus 事件在相应的 UIElement.GotFocusUIElement.LostFocus 事件之前触发。

由于这些是路由事件,因此它们从焦点元素依次向上通过连续的父对象冒泡到对象树的根。 在焦点切换发生之前,可以重定向或取消此切换。

GettingFocusLosingFocus 是同步事件,因此在这些事件冒泡时,焦点不会被移动。 但是, GotFocusLostFocus 是异步事件,这意味着无法保证焦点在执行处理程序之前不会再次移动。

如果焦点在对 Control.Focus 的调用中移动,则调用期间会引发 GettingFocus ,而在调用后引发 GotFocus

焦点导航目标可以在 GettingFocusLosingFocus 事件(焦点移动前)通过 GettingFocusEventArgs.NewFocusedElement 属性进行更改。 即使目标已更改,事件仍会传播,并且目标可以再次更改。

为了避免重入问题,如果在事件冒泡时尝试移动焦点(使用TryMoveFocusControl.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;
        }
    }
}

在这里,我们将演示如何处理 CommandBarLosingFocus 事件,并在菜单关闭时设置焦点。

<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.FindFirstFocusableElementFocusManager.FindLastFocusableElement 方法将焦点移动到对象范围内的第一个或最后一个可聚焦元素(UIElement 的元素树或 TextElement 的文本树)。 在调用中指定范围(如果参数为 null,则范围为当前窗口)。

如果范围中无法识别任何焦点候选项,则返回 null。

下面介绍如何指定 CommandBar 的按钮具有环绕方向行为(请参阅 键盘交互)。

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