목록 항목의 중첩된 UI

중첩된 UI는 컨테이너 내부에 묶인 중첩된 실행 가능한 컨트롤을 노출하는 UI(사용자 인터페이스)로, 독립적인 포커스를 취할 수도 있습니다.

중첩된 UI를 사용하여 사용자에게 중요한 액션 수행을 가속화하는 데 도움이 되는 추가 옵션을 표시할 수 있습니다. 그러나 노출하는 액션이 많을수록 UI가 더 복잡해집니다. 이 UI 패턴을 사용하기로 선택할 때는 더욱 주의해야 합니다. 이 문서에서는 특정 UI에 가장 적합한 액션 과정을 결정하는 데 도움이 되는 지침을 제공합니다.

중요 API: ListView 클래스, GridView 클래스

이 문서에서는 ListViewGridView 항목 중 중첩된 UI 만들기에 대해 설명합니다. 이 섹션에서는 다른 중첩된 UI 사례에 대해 이야기하지 않지만 이러한 개념은 전송할 수 있습니다. 시작하기 전에 Lists와 List view, grid view 문서에 있는 UI에서 ListView 또는 GridView 컨트롤을 사용하는 데 필요한 일반 지침을 숙지해야 합니다.

이 문서에서는 여기에 정의된 대로 용어 list, list 항목중첩된 UI를 사용합니다.

  • List는 list view 또는 grid view에 포함된 항목의 컬렉션을 나타냅니다.
  • List 항목은 사용자가 list에서 액션을 수행할 수 있는 개별 항목을 나타냅니다.
  • 중첩된 UI는 사용자가 list 항목 자체에 대한 액션을 수행하는 것과 별도로 액션을 수행할 수 있는 list 항목 내의 UI 요소를 나타냅니다.

Screenshot showing the parts of a Nested U I.

ListView 및 GridView 둘 다 ListViewBase 클래스에서 파생되므로, 동일한 functionality를 갖지만 데이터를 다르게 표시합니다. 이 문서에서 list에 대해 논의할 때, 해당 정보가 ListView 및 GridView 컨트롤 모두에 적용됩니다.

주 액션 및 보조 액션

list을 사용하여 UI를 만들 때 사용자가 해당 list 항목에서 어떤 액션을 수행할 수 있는지를 고려합니다.

  • 사용자가 항목을 클릭하여 액션을 수행할 수 있나요?
    • 통상 list 항목을 클릭하면 액션이 시작되지만 그럴 필요는 없습니다.
  • 사용자가 수행할 수 있는 액션이 두 개 이상 있나요?
    • 예를 들어 list에서 이메일을 탭하면 해당 메일이 열립니다. 그러나 사용자가 이메일을 먼저 열지 않고 다른 액션(예: 이메일 삭제)을 수행하려고 할 수 있습니다. list에서 직접 이 액션에 액세스하는 것이 사용자에게 도움이 됩니다.
  • 사용자에게 액션을 노출하려면 어떻게 해야 하나요?
    • 모든 입력 타입을 고려합니다. 일부 중첩된 UI는 한 가지 입력 메서드에서 잘 작동하지만 다른 메서드에서는 작동하지 않을 수 있습니다.

주요 액션은 사용자가 list 항목을 누를 때 발생할 것으로 예상됩니다.

보조 액션은 통상 list 항목과 연관된 가속기를 지칭합니다. 이러한 가속기는 list 관리 또는 list 항목과 관련된 액션에 사용할 수 있습니다.

보조 액션에 대한 옵션

목록 UI를 만들 때는 먼저 Windows를 지원하는 모든 입력 방법을 고려해야 합니다. 다양한 종류의 입력에 대한 자세한 내용은 입력 프라이머를 참조하세요.

Windows에서 지원되는 모든 입력을 앱에서 지원하는 것이 확인된 후에는 앱의 보조 작업이 기본 목록에 바로 가기로 표시될 정도로 중요한지 결정해야 합니다. 노출하는 액션이 많을수록 UI가 더 복잡해진다는 점 기억하십시오. 주요 list UI에서 보조 액션을 노출해야 합니까, 아니면 다른 곳에 배치할 수 있나요?

항상 입력으로 해당 액션에 액세스할 수 있어야 하는 경우 주요 list UI에 추가 작업을 노출하는 것이 좋습니다.

주요 list UI에 보조 액션을 배치할 필요가 없다고 결정한 경우 몇 가지 다른 방법을 통해 사용자에게 노출될 수 있습니다. 다음은 보조 액션을 배치할 위치에 대해 고려할 수 있는 옵션 몇 가지입니다.

세부 정보 페이지에 보조 액션을 배치하십시오

목록 항목을 누르면 이동되는 페이지에 보조 작업을 배치합니다. 목록/세부 정보 패턴을 사용하는 경우 세부 정보 페이지에 보조 작업을 배치하는 것이 좋습니다.

자세한 내용은 목록/세부 정보 패턴을 참조하세요.

컨텍스트 메뉴에 보조 액션을 배치하십시오

사용자가 마우스 우클릭하거나 길게 눌러 액세스할 수 있는 컨텍스트 메뉴에 보조 액션을 배치합니다. 이렇게 하면 사용자가 세부 정보 페이지를 로드하지 않고도 이메일 삭제와 같은 액션을 수행할 수 있습니다. 컨텍스트 메뉴는 주요 UI가 아니라 액셀러레이터가 되는 것을 목표로 하기 때문에, 세부 정보 페이지에서 이러한 옵션을 사용할 수 있도록 하는 것이 좋습니다.

게임 패드 또는 리모컨에서 입력이 제공되는 경우에 보조 액션을 노출하려면, 컨텍스트 메뉴를 사용하는 것이 좋습니다.

자세한 내용은 컨텍스트 메뉴와 플라이아웃을 참조하세요.

포인터 입력을 최적화하려면 hover UI에 보조 작업을 배치하십시오

앱이 마우스 및 펜과 같은 포인터 입력과 함께 자주 사용되며 해당 입력에서만 앱을 통해 보조 액션이 쉽게 사용 가능할 수 있도록 하려는 경우, 마우스로 가리키면 보조 액션만 표시할 수 있습니다. 이 가속기는 포인터 입력을 사용하는 경우에만 표시되므로, 다른 옵션을 사용해야만 다른 입력 타입을 지원 가능합니다.

Nested UI shown on hover

자세한 내용은 마우스 조작을 참조하세요.

주 액션 및 보조 액션에 대한 UI 배치

보조 액션이 주요 list UI에 노출되도록 결정하는 경우 다음 지침을 적용하는 것이 좋습니다.

주 액션 및 보조 액션을 사용하여 list 항목을 만들면 기본 액션은 왼쪽에, 보조 액션은 오른쪽에 배치합니다. 왼쪽에서 오른쪽으로 읽는 문화권에서는 사용자가 list 항목의 왼쪽에 있는 액션을 주 액션으로 연결합니다.

이 예제에서는 항목이 더 가로로 이어지는 list UI에 대해 설명합니다(자체 heigh보다 넓음). 그러나 list 항목이 shape 측면에서 정사각형이거나 width 값보다 길 수도 있습니다. 일반적으로 그리드에서 사용되는 항목입니다. 이러한 항목의 경우 list가 수직으로 스크롤되지 않는 경우, 보조 액션을 오른쪽 말고 list 항목의 맨 아래에 배치할 수 있습니다.

모든 입력을 고려하십시오

중첩된 UI를 사용하기로 하면, 모든 입력 타입의 사용자 환경도 평가하십시오. 앞에서 언급된 바처럼, 중첩된 UI는 일부 입력 타입에 아주 적합합니다. 그러나, 항상 다른 타입에서 잘 작동하는 것은 아닙니다. 특히 키보드, 컨트롤러 및 원격 입력은 중첩된 UI 요소에 액세스하는 데 어려움을 겪을 수 있습니다. Windows가 모든 입력 유형에 대해 올바르게 작동하려면 아래 지침을 따라야 합니다.

중첩된 UI 처리

list 항목에 둘 이상의 작업이 중첩되어 있는 경우 키보드, 게임 패드, 원격 제어 또는 기타 비 포인터 입력을 사용하여 탐색 처리하는 것이 좋습니다.

list 항목이 액션을 수행하는 중첩된 UI

중첩 요소가 있는 list UI가 호출, 선택(단일 또는 다중) 또는 드래그 앤 드롭 오퍼레이션과 같은 액션을 지원하는 경우, 이러한 화살표 기술을 사용하여 중첩된 UI 요소를 탐색하는 것이 좋습니다.

Screenshot showing nested U I elements labeled with the letters A, B, C, and D.

Gamepad

게임 패드에서 입력하는 경우, 다음 사용자 환경을 제공합니다.

  • A에서 오른쪽 방향 키가 B에 포커스를 놓습니다.
  • B에서 오른쪽 방향 키가 C에 포커스를 놓습니다.
  • C에서 오른쪽 방향 키는 op가 아니거나 list 오른쪽에 있는 포커스가 있는 UI 요소가 있는 경우 포커스를 배치합니다.
  • C에서 왼쪽 방향 키가 B에 포커스를 놓습니다.
  • B에서 왼쪽 방향 키가 A에 포커스를 놓습니다.
  • A에서 왼쪽 방향 키가 op가 아니거나 list 오른쪽에 있는 포커스가 있는 UI 요소가 있는 경우, 포커스를 배치합니다.
  • A, B, 또는, C에서, 아래 쪽 방향 키가 D에 포커스를 둡니다.
  • UI 요소에서 List 항목의 왼쪽까지, 오른쪽 방향 키는 포커스를 A에 배치합니다.
  • UI 요소에서 List 항목의 오른쪽까지, 왼쪽 방향 키는 포커스를 A에 배치합니다.

키보드

키보드에서 입력하는 경우 사용자가 얻는 경험은 다음과 같습니다.

  • A에서 탭 키는 B에 포커스를 놓습니다.
  • B에서 탭 키는 C에 포커스를 놓습니다.
  • C에서 탭 키는 포커스가 있을 법한 다음 UI 요소에 포커스를 탭 순서로 배치합니다.
  • C에서 shift+tab 키를 누르면 포커스가 B에 배치됩니다
  • B에서 shift+tab 또는 왼쪽 화살표 키는 A에 포커스를 놓습니다.
  • A에서 shift+tab 키가 포커스가 있을 법한 다음 UI 요소에 포커스를 역방향 탭 순서로 배치합니다.
  • A, B, 또는, C에서, 아래 쪽 화살표 키가 D에 포커스를 둡니다.
  • UI 요소에서부터 List Item의 왼쪽까지, 탭 키는 포커스를 A에 배치합니다.
  • UI 요소부터 List Item의 오른쪽까지, Shift Tab 키를 누르면 포커스가 C에 배치됩니다.

이 UI를 달성하려면, list에서 IsItemClickEnabled을 true로 설정해야만 합니다. SelectionMode 는 모든 값일 수 있습니다.

이를 구현하는 코드는 이 문서의 Example 섹션을 참조하세요.

list 항목이 액션을 수행하지 않는 중첩된 UI

list view는 가상화 및 최적화된 스크롤 동작을 제공하지만 list 항목과 연관된 액션이 없기 때문에 사용할 수 있습니다. 이러한 UI는 일반적으로 list 항목을 사용하여 요소를 그룹화하고 세트로서 스크롤하는지 확인합니다.

이러한 종류의 UI는 사용자가 액션을 수행할 수 있는 중첩된 요소가 많기 때문에 이전 예제보다 훨씬 더 복잡한 경향이 있습니다.

Screenshot of a complex Nested U I showing a lot of nested elements that the user can interact with.

이 UI를 수행하려면 list에서 다음 속성을 설정합니다.

<ListView SelectionMode="None" IsItemClickEnabled="False" >
    <ListView.ItemContainerStyle>
         <Style TargetType="ListViewItem">
             <Setter Property="IsFocusEngagementEnabled" Value="True"/>
         </Style>
    </ListView.ItemContainerStyle>
</ListView>

list 항목이 액션을 수행하지 않는 경우, 게임 패드 또는 키보드로 탐색을 처리하는 데 이 지침을 사용하는 것이 좋습니다.

Gamepad

게임 패드에서 입력하는 경우, 다음 사용자 환경을 제공합니다.

  • List Item에서부터, 아래쪽 방향 키는 다음 List Item에 포커스를 놓습니다.
  • List Item에서 왼쪽/오른쪽 키가 op가 아니거나 list 오른쪽에 있는 포커스가 있을 법한 UI 요소가 있는 경우, 포커스를 거기에 배치합니다.
  • List Item에서 'A' 버튼은 중첩된 UI에 포커스를 왼쪽 위/아래/오른쪽 우선 순위로 배치합니다.
  • 중첩된 UI 내에 있는 동안 XY 포커스 탐색 모델을 따릅니다. 포커스는 사용자가 'B' 단추를 눌러 포커스를 List Item으로 되돌릴 때까지 현재 List Item 내에 포함된 중첩된 UI를 탐색할 수 있습니다.

키보드

키보드에서 입력하는 경우 사용자가 얻는 경험은 다음과 같습니다.

  • List Item에서부터, 아래쪽 방향 키는 다음 List Item에 포커스를 놓습니다.
  • List Item에서, 왼쪽/오른쪽 키를 누르나 작동하지 않습니다.
  • List Item에서, 탭 키를 누르면 중첩된 UI 항목 사이에 있는 다음 탭 정지에 포커스가 놓입니다.
  • 중첩된 UI 항목 중 하나에서, 탭을 누르면 중첩된 UI 항목이 탭 순서대로 트래버스됩니다. 한번 중첩된 UI 항목 전체가 이동하면, ListView 다음에 포커스가 탭 순서대로 다음 컨트롤에 배치합니다.
  • Shift+Tab은 탭 동작에서 역방향으로 동작합니다.

예시

이 예제에서는 list item이 액션을 수행하는 중첩된 UI를 구현하는 법을 보여줍니다.

<ListView SelectionMode="None" IsItemClickEnabled="True"
          ChoosingItemContainer="listview1_ChoosingItemContainer"/>
private void OnListViewItemKeyDown(object sender, KeyRoutedEventArgs e)
{
    // Code to handle going in/out of nested UI with gamepad and remote only.
    if (e.Handled == true)
    {
        return;
    }

    var focusedElementAsListViewItem = FocusManager.GetFocusedElement() as ListViewItem;
    if (focusedElementAsListViewItem != null)
    {
        // Focus is on the ListViewItem.
        // Go in with Right arrow.
        Control candidate = null;

        switch (e.OriginalKey)
        {
            case Windows.System.VirtualKey.GamepadDPadRight:
            case Windows.System.VirtualKey.GamepadLeftThumbstickRight:
                var rawPixelsPerViewPixel = DisplayInformation.GetForCurrentView().RawPixelsPerViewPixel;
                GeneralTransform generalTransform = focusedElementAsListViewItem.TransformToVisual(null);
                Point startPoint = generalTransform.TransformPoint(new Point(0, 0));
                Rect hintRect = new Rect(startPoint.X * rawPixelsPerViewPixel, startPoint.Y * rawPixelsPerViewPixel, 1, focusedElementAsListViewItem.ActualHeight * rawPixelsPerViewPixel);
                candidate = FocusManager.FindNextFocusableElement(FocusNavigationDirection.Right, hintRect) as Control;
                break;
        }

        if (candidate != null)
        {
            candidate.Focus(FocusState.Keyboard);
            e.Handled = true;
        }
    }
    else
    {
        // Focus is inside the ListViewItem.
        FocusNavigationDirection direction = FocusNavigationDirection.None;
        switch (e.OriginalKey)
        {
            case Windows.System.VirtualKey.GamepadDPadUp:
            case Windows.System.VirtualKey.GamepadLeftThumbstickUp:
                direction = FocusNavigationDirection.Up;
                break;
            case Windows.System.VirtualKey.GamepadDPadDown:
            case Windows.System.VirtualKey.GamepadLeftThumbstickDown:
                direction = FocusNavigationDirection.Down;
                break;
            case Windows.System.VirtualKey.GamepadDPadLeft:
            case Windows.System.VirtualKey.GamepadLeftThumbstickLeft:
                direction = FocusNavigationDirection.Left;
                break;
            case Windows.System.VirtualKey.GamepadDPadRight:
            case Windows.System.VirtualKey.GamepadLeftThumbstickRight:
                direction = FocusNavigationDirection.Right;
                break;
            default:
                break;
        }

        if (direction != FocusNavigationDirection.None)
        {
            Control candidate = FocusManager.FindNextFocusableElement(direction) as Control;
            if (candidate != null)
            {
                ListViewItem listViewItem = sender as ListViewItem;

                // If the next focusable candidate to the left is outside of ListViewItem,
                // put the focus on ListViewItem.
                if (direction == FocusNavigationDirection.Left &&
                    !listViewItem.IsAncestorOf(candidate))
                {
                    listViewItem.Focus(FocusState.Keyboard);
                }
                else
                {
                    candidate.Focus(FocusState.Keyboard);
                }
            }

            e.Handled = true;
        }
    }
}

private void listview1_ChoosingItemContainer(ListViewBase sender, ChoosingItemContainerEventArgs args)
{
    if (args.ItemContainer == null)
    {
        args.ItemContainer = new ListViewItem();
        args.ItemContainer.KeyDown += OnListViewItemKeyDown;
    }
}
// DependencyObjectExtensions.cs definition.
public static class DependencyObjectExtensions
{
    public static bool IsAncestorOf(this DependencyObject parent, DependencyObject child)
    {
        DependencyObject current = child;
        bool isAncestor = false;

        while (current != null && !isAncestor)
        {
            if (current == parent)
            {
                isAncestor = true;
            }

            current = VisualTreeHelper.GetParent(current);
        }

        return isAncestor;
    }
}