程式設計焦點瀏覽

Keyboard, remote, and D-pad

若要在 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 會在向右瀏覽時將 I 識別為焦點候選項目。 這樣做的原因是:

  • 由於 A 上的 HintRect,起始參考是 A,而不是 B
  • C 不是候選項目,因為 MyPanel 已指定為 SearchRoot
  • F 不是候選項目,因為 ExclusionRect 與它重疊

Custom focus navigation behavior using navigation hints

使用瀏覽提示的自訂焦點瀏覽行為

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。

Changing focus navigation target on GettingFocus event

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