프로그래밍 방식 포커스 탐색

Keyboard, remote, and D-pad

Windows 애플리케이션에서 포커스를 프로그래밍 방식으로 이동하려면 FocusManager.TryMoveFocus 메서드 또는 FindNextElement 메서드를 사용하면 됩니다.

TryMoveFocus는 지정된 방향으로 현재 포커스가 있는 요소에서 다음 포커스 가능한 요소로 포커스를 변경하려고 하는 반면, FindNextElement는 지정된 탐색 방향을 토대로 포커스를 받을 요소(DependencyObject)를 검색합니다(방향 탐색에만 해당됨, 탭 탐색 에뮬레이션에는 사용할 수 없음).

참고 항목

FindNextFocusableElement는 UIElement를 검색하며, 다음 포커스 가능 요소가 UIElement가 아닌 경우(하이퍼링크 개체 등) null을 반환하므로 FindNextFocusableElement보다는 FindNextElement 메서드를 사용하는 것이 좋습니다.

범위 내 포커스 후보 찾기

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은 시각적 트리의 루트에서부터 검색을 시작함을 나타냅니다.

Important

방향 영역 외부에 후보를 배치하는 SearchRoot 하위 항목에 하나 이상의 변형이 적용되면 이러한 요소는 여전히 후보로 간주됩니다.

  • ExclusionRect - 중첩되는 모든 개체가 탐색 포커스에서 제외되는 "가상" 경계 사각형을 사용하여 포커스 탐색 후보가 식별됩니다. 이 사각형은 계산에만 사용되며 시각적 트리에는 추가되지 않습니다.
  • HintRect - 포커스를 받을 가능성이 가장 높은 요소를 식별하는 "가상" 경계 사각형을 사용하여 포커스 탐색 후보가 식별됩니다. 이 사각형은 계산에만 사용되며 시각적 트리에는 추가되지 않습니다.
  • XYFocusNavigationStrategyOverride - 포커스 탐색 전략을 사용하여 포커스를 받는 최상의 후보 요소가 식별됩니다.

다음은 이러한 개념 중 일부를 보여 주는 이미지입니다.

요소 B에 포커스가 있으면, 오른쪽으로 탐색 시 FindNextElement가 I를 포커스 후보로 식별합니다. 그 이유는 다음과 같습니다.

  • HintRect가 A에 있으므로 시작 참조가 B가 아닌 A입니다.
  • MyPanel이 SearchRoot로 지정되었으므로 C는 후보가 아닙니다.
  • ExclusionRect가 F와 중첩되므로 F는 후보가 아닙니다.

Custom focus navigation behavior using navigation hints

탐색 힌트를 사용한 사용자 지정 포커스 탐색 동작

NoFocusCandidateFound 이벤트

탭 및 화살표 키를 눌렀는데 지정된 방향에 포커스 후보가 없는 경우 UIElement.NoFocusCandidateFound 이벤트가 발생합니다. 이 이벤트는 TryMoveFocus에 대해 발생하지 않습니다.

이 이벤트는 라우팅된 이벤트이기 때문에 포커스가 설정된 요소에서 다음 부모 개체를 지나 개체 트리의 루트로 버블링됩니다. 이를 통해 적절한 위치에서 이벤트를 처리할 수 있습니다.

다음은 사용자가 가장 왼쪽에 있는 포커스 가능 컨트롤의 왼쪽으로 포커스를 이동하려고 시도할 때 그리드가 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 속성을 통해 변경될 수 있습니다. 대상이 변경되더라도 이벤트는 계속 버블링되며 대상이 다시 변경될 수도 있습니다.

재진입 문제를 방지하기 위해 이러한 이벤트가 버블링되는 동안 포커스를 이동하려고(TryMoveFocus 또는 Control.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;
        }
    }
}

다음은 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;
        }
    }
}