共用方式為


程式化焦點導航

鍵盤、遙控器與方向鍵

要在 Windows 應用程式中程式化移動焦點,你可以使用 FocusManager.TryMoveFocus 方法或 FindNextElement 方法。

TryMoveFocus 嘗試將焦點從有焦點的元素轉移到指定方向下下一個可聚焦元素,而 FindNextElement 則以 DependencyObject 形式取得該元素,該元素將根據指定導航方向獲得焦點(僅方向導覽,無法用來模擬分頁導航)。

備註

我們建議使用 FindNextElement 方法取代 FindNextFocusableElement ,因為 FindNextFocusableElement 會取得 UIElement,若下一個可聚焦元素不是 UIElement(例如超連結物件),則 UIElement 會回傳 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 與 F 重疊

使用導航提示的自訂對焦導航行為

使用導航提示的自訂對焦導航行為

NoFocusCandidateFound 活動

當按下 Tab 鍵或方向鍵且指定方向沒有焦點候選時,會觸發 UIElement.NoFocusCandidateFound 事件。 此事件不會對 TryMoveFocus 觸發。

由於這是路由事件,它會從焦點元素一路延伸,經過連續的父物件,直到物件樹的根節點。 這樣你就能在適當的地方處理事件。

在這裡,我們展示了當使用者嘗試將焦點移到最左側可聚焦控制項的左側時,網格如何開啟分割視圖(參見 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. 失焦 若焦點重置回失去焦點元素或嘗試 取消 成功,則不會觸發更多事件。
  2. GettingFocus 如果焦點重置回失去焦點的元素,或者 TryCancel 成功,則不會觸發更多事件。
  3. LostFocus
  4. GotFocus

下圖顯示,當XYFocus從A向右移動時,如何選擇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 的文字樹)。 作用域在呼叫中指定(若參數為空,則作用域為目前視窗)。

若無法在範圍內識別出焦點候選,則回傳為空。

在此,我們示範如何指定 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;
        }
    }
}