列表项中的嵌套 UI

嵌套 UI 是一种用户界面(UI),它展示嵌套在容器内的可操作控件,该容器也可以单独获取焦点。

可以使用嵌套 UI 向用户提供有助于加速执行重要作的其他选项。 您暴露的操作越多,用户界面就会变得越复杂。 选择使用此 UI 模式时,需要格外小心。 本文提供了指导,可帮助你确定特定的 UI 的最佳行动方案。

重要 APIListView 类GridView 类

在本文中,我们将讨论在 ListViewGridView 项中创建嵌套 UI。 虽然本部分不讨论其他嵌套 UI 案例,但这些概念是可转移的。 在开始之前,应熟悉在 UI 中使用 ListView 或 GridView 控件的一般指南,这些控件在 列表列表视图和网格视图 文章中找到。

在本文中,我们使用此处定义的术语 列表列表项嵌套 UI

  • 列表是指列表 视图或网格视图中包含的项的集合。
  • 列表项 是指用户可以在列表中执行操作的独立项。
  • 嵌套 UI 是指在列表项中,用户可以对其中的 UI 元素单独操作,而不影响对列表项本身的操作。

嵌套的UI各部分的屏幕截图。

注意 ListView 和 GridView 都派生自 ListViewBase 类,因此它们具有相同的功能,但以不同的方式显示数据。 在本文中,当我们讨论列表时,信息适用于 ListView 和 GridView 控件。

主要操作和次要操作

使用列表创建 UI 时,请考虑用户可能从这些列表项执行哪些操作。

  • 用户是否可以单击该项来执行作?
    • 通常,单击列表项会启动一个动作,但不一定会启动。
  • 用户是否可以执行多项操作?
    • 例如,点击列表中的电子邮件将打开该电子邮件。 但是,用户可能希望在不打开电子邮件的情况下进行的其他操作,如删除电子邮件。 用户如果能够直接在列表中访问此操作,将会受益。
  • 应如何向用户公开这些行为?
    • 请考虑所有输入类型。 某些形式的嵌套 UI 非常适用于一种输入方法,但可能不适用于其他方法。

主要动作是用户按下列表项时所期望的结果。

次要操作 通常是加速器,与列表项关联。 这些加速器可用于列表管理或与列表项相关的操作。

辅助操作选项

创建列表 UI 时,首先需要确保考虑到 Windows 支持的所有输入方法。 有关不同类型的输入的详细信息,请参阅 输入入门

确保您的应用支持 Windows 所支持的所有输入后,您应确定应用的辅助操作是否重要到足以在主列表中作为加速器来展示。 请记住,公开的操作越多,界面就变得越复杂。 是否确实需要在主列表 UI 中显示次要操作,还是可以将它们放在其他位置?

当需要通过任何输入随时访问这些额外的操作时,您可能会考虑在主列表UI中显示这些操作。

如果确定不需要将辅助作置于主列表 UI 中,可通过其他几种方式向用户公开它们。 下面是一些可供您考虑放置辅助操作的位置选项。

在详情页上放置次要操作

将辅助作放在列表项在按下时导航到的页面上。 在使用列表/详细信息模式时,详细信息页通常是放置辅助操作的好位置。

有关详细信息,请参阅 列表/详细信息模式

在上下文菜单中放入辅助动作

将次要操作放在用户可通过右键单击或长按访问的上下文菜单中。 这样可以让用户执行作(例如删除电子邮件),而无需加载详细信息页。 最好在详细信息页上提供这些选项,因为上下文菜单旨在作为加速器而不是主要 UI。

若要在输入来自游戏手柄或远程控件时显示次要操作,建议使用上下文菜单。

有关详细信息,请参阅 上下文菜单和浮出控件

将次级操作放置于悬停用户界面中以优化指针输入

如果希望应用经常与指针输入(如鼠标和笔)一起使用,并且希望使辅助作随时可用于这些输入,则只能在悬停时显示辅助作。 仅当使用指针输入时,此加速器才可见,因此请务必使用其他选项来支持其他输入类型。

悬停时显示的嵌套 UI

有关详细信息,请参阅 鼠标交互

主要操作和辅助操作的 UI 位置

如果你决定应在主列表用户界面中显示次要操作,我们建议遵循以下准则。

在使用主要和次要操作创建列表项时,将主要操作放在左侧,将次要操作放在右侧。 在从左到右阅读习惯的文化中,用户将列表项左侧的操作视为主要操作。

在这些示例中,我们将讨论列表用户界面,其中的项目主要横向排列(宽度大于高度)。 但是,你可能会有形状更接近正方形或高度超过其宽度的列表项。 通常,这些项在网格中使用。 对于这些项目,如果列表不垂直滚动,可以将辅助作放置在列表项的底部,而不是放在右侧。

考虑所有输入

在决定使用嵌套 UI 时,还可以评估所有输入类型的用户体验。 如前所述,嵌套 UI 适用于某些输入类型。 但是,它并不总是对其他一些情况效果很好。 具体而言,键盘、控制器和远程输入难以访问嵌套 UI 元素。 请务必遵循以下指南,确保 Windows 适用于所有输入类型。

嵌套 UI 管理

在列表项中嵌套了多个操作时,我们建议遵循此指南,以便使用键盘、游戏手柄、遥控器或其他非指针输入设备处理导航。

执行动作的列表项的嵌套用户界面

如果具有嵌套元素的列表 UI 支持调用、选择(单个或多个)或拖放作等作,我们建议使用这些箭头技术在嵌套的 UI 元素中导航。

显示用字母 A、B、C 和 D 标记的嵌套 U I 元素的屏幕截图。

游戏板

输入来自游戏板时,请提供以下用户体验:

  • A,右方向键将焦点放在 B 上。
  • B 中,右方向键将焦点放在 C 上。
  • C 开始,右方向键要么不执行任何操作,要么如果列表的右侧有可聚焦的 UI 元素,则将焦点移动到该元素上。
  • C 中,左方向键将焦点放在 B 上。
  • B 中,左方向键将焦点放在 A 上。
  • A 开始,左方向键要么无操作,要么在列表右侧有可聚焦的 UI 元素时将焦点移到该元素上。
  • ABC 向下方向键将焦点放在 D 上。
  • 从 UI 元素到列表项左侧,右方向键将焦点放在 A 上。
  • 从 UI 元素到列表项右侧,左方向键将焦点放在 A 上。

键盘

输入来自键盘时,这是用户获取的体验:

  • A 中,Tab 键将焦点放在 B 上。
  • B 中,Tab 键将焦点放在 C 上。
  • C 中,Tab 键将焦点置于 Tab 键顺序中的下一个可聚焦 UI 元素上。
  • C 开始,Shift+Tab 键将焦点放在 B 上。
  • B 开始,Shift+Tab 或向左键将焦点放在 A 上。
  • A 中,Shift+Tab 键将焦点置于反向 Tab 键顺序中的下一个可聚焦 UI 元素上。
  • ABC 向下键将焦点放在 D 上。
  • 从 UI 元素到列表项左侧,Tab 键将焦点放在 A 上。
  • 从 UI 元素到列表项右侧,Shift Tab 键将焦点放在 C 上。

若要实现此 UI,请在列表中将 IsItemClickEnabled 设置为 trueSelectionMode 可以是任何值。

有关实现此代码的代码,请参阅本文的 “示例 ”部分。

嵌套界面中,列表项不执行操作

可以使用列表视图,因为它提供虚拟化和优化的滚动行为,但没有与列表项关联的操作。 这些 UI 通常仅使用列表项对元素进行分组,并确保它们作为一个集滚动。

此类 UI 往往比前面的示例要复杂得多,其中包含许多嵌套元素,用户可以对这些元素进行操作。

复杂嵌套 U 的屏幕截图,其中显示了用户可以与之交互的许多嵌套元素。

若要实现此 UI,请在列表中设置以下属性:

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

当列表项不执行操作时,我们建议遵循本指南,使用游戏手柄或键盘来处理导航。

游戏板

输入来自游戏板时,请提供以下用户体验:

  • 在列表项中,向下方向键将焦点放在下一个列表项上。
  • 在列表项中,左/右键要么不起作用,要么如果列表右侧有可聚焦的 UI 元素,则将焦点移动到该元素上。
  • 从列表项中,按下“A”按钮时,会按上/下、左/右的优先顺序将焦点放在嵌套的 UI 上。
  • 在嵌套 UI 中,遵循 XY 焦点导航模型。 焦点只能在当前列表项中包含的嵌套 UI 周围导航,直到用户按下“B”按钮,这会将焦点放回列表项上。

键盘

输入来自键盘时,这是用户获取的体验:

  • 在列表项中,下箭头键将焦点放在下一个列表项上。
  • 在列表项中,按向左/右键没有任何操作。
  • 在列表项中,按 Tab 键将焦点放在嵌套 UI 项之间的下一个制表位上。
  • 在嵌套 UI 项中的某一项上按 Tab 键,即可按照 Tab 顺序遍历这些嵌套的 UI 项。 一旦导航到所有嵌套的 UI 项目,焦点将置于 ListView 后的下一个控件上。
  • Shift+Tab 的操作方向与 Tab 键相反。

Example

此示例演示如何实现 嵌套 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;
    }
}