程式設計焦點瀏覽

鍵盤、遠端與方向鍵

若要在 Windows 應用程式中以程式設計方式移動焦點,您可以使用system.windows.input.focusmanager>. TryMoveFocus方法或FindNextElement方法。

TryMoveFocus 會嘗試使用指定方向中下一個可設定為焦點元素的焦點,從元素變更焦點,FindNextElement 則會擷取元素 (作為 DependencyObject),該元素會根據指定的瀏覽方向取得焦點 (僅限方向瀏覽,無法用來模擬 Tab 瀏覽)。

注意

我們建議使用 FindNextElement 方法,而非 FindNextFocusableElement,因為 FindNextFocusableElement 會擷取 UIElement,其會在下一個可設定為焦點的元素並非 UIElement 時傳回 Null (例如超連結 (Hyperlink) 物件)。

在範圍中尋找焦點候選項目

您可以為 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 - 將搜尋焦點瀏覽候選項目的範圍設為此 DependecyObject 的下層。 Null 表示會從視覺化樹狀結構的根開始搜尋。

重要

若一或多個轉換已套用至 SearchRoot 的子系,將其置於方向區域之外,這些元素仍然會被認為是候選項目。

  • ExclusionRect - 會使用「虛擬」的週框識別焦點瀏覽候選項目,所有重疊的物件都會排除在瀏覽焦點之外。 這個矩形只適用於計算,而不會新增到視覺化樹狀結構。
  • HintRect - 會使用「虛擬」的週框識別焦點瀏覽候選項目,取得最有可能取得焦點的元素。 這個矩形只適用於計算,而不會新增到視覺化樹狀結構。
  • XYFocusNavigationStrategyOverride - 用來移動焦點的瀏覽策略。

下圖顯示這些概念的一部分。

當元素 B 取得焦點時,FindNextElement 會在向右巡覽時識別 I 作為焦點候選項目。 這樣做的原因是:

  • 因為 HintRect 位於 A,因此啟動參考為 A 而非 B
  • C 並非候選項目,因為 MyPanel 已識別為 SearchRoot
  • F 並非候選項目,因為 ExclusionRect 與它重疊

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

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

NoFocusCandidateFound event

UIElement.NoFocusCandidateFound 事件會在按下 TAB 鍵或方向鍵,並且在指定方向沒有焦點候選項目時引發。 此事件不會為 TryMoveFocus 引發。

因為這是路由事件,它會從已設定焦點元素,向上到連續父物件,一直到物件樹的根向上反昇。 這可讓您在任何適當地方處理事件。

在這裡,我們顯示 Grid 如何在使用者嘗試在最左側的可設為焦點的控制項上向左移動焦點時開啟 SplitView (請參閱為 Xbox 和 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;
    }
}

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),便會擲回例外狀況。

這些事件無論移動焦點的理由為何都會引發 (包含 Tab 瀏覽、方向瀏覽和程式設計瀏覽)。

以下是焦點事件的執行順序:

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

在這裡,我們顯示如何為 CommandBar 處理 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.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;
        }
    }
}