共用方式為


清單項目中的巢狀 UI

巢狀 UI 這種使用者介面 (UI) 公開的巢狀可操作控制項封裝在容器之中,但也可獨立運作。

您可以使用巢狀 UI 向使用者提供額外選項,協助加速執行重要動作。 不過,公開的動作越多,UI 就越複雜。 選擇使用這類 UI 模式時,需要特別謹慎。 本文提供的指導方針,可協助您判斷特定 UI 最合適的操作方式。

重要 APIListView 類別GridView 類別

本文中,我們會討論在 ListViewGridView 項目中建立巢狀 UI。 雖然本節不討論其他巢狀 UI 案例,但這些概念是可相通的。 開始之前,建議您先熟悉在 UI 中使用 ListView 或 GridView 控制項的一般操作指引,請參閱清單清單檢視和網格檢視文章。

本文中,我們會使用如下定義的詞彙清單清單項目巢狀 UI

  • 清單是指包含在清單檢視或網格檢視中的項目集合。
  • 清單項目是指使用者在清單中可以採取動作的個別項目。
  • 巢狀 UI是指清單項目中的 UI 元素,使用者可以對這些元素分別採取動作,而不是對清單項目本身採取動作。

Screenshot showing the parts of a Nested U I.

注意:ListView 和 GridView 都是衍生自 ListViewBase 類別,因此它們具有相同功能,但會以不同方式顯示資料。 本文中討論的清單相關資訊同時適用於 ListView 和 GridView 控制項。

主要和次要動作

建立清單 UI 時,請考量使用者可能對這些清單項目採取的動作。

  • 使用者是否能按一下項目來執行動作?
    • 一般而言,按一下清單項目會啟動動作,但並非必要。
  • 使用者是否能執行多個動作?
    • 舉例來說,點選清單中的電子郵件會開啟該電子郵件。 不過,點選後可能還有其他動作,例如刪除電子郵件,使用者通常希望不需開啟電子郵件就能採取這個動作。 這種設定的好處是使用者可直接在清單中存取此動作。
  • 這類動作應該如何公開給使用者?
    • 請考量所有輸入類型。 某些形式的巢狀 UI 很適合使用某種輸入方法,但與其他方法可能無法搭配。

主要動作是使用者按下清單項目時預期發生的動作。

次要動作通常是與清單項目相關聯的快速鍵。 這些快速鍵可能用於清單管理或是與清單項目相關的動作。

次要動作選項

建立清單 UI 時,請先確定您已考慮到 Windows 支援的所有輸入方法。 如需詳細瞭解其他輸入類型,請參閱輸入入門

在確認應用程式可支援 Windows 支援的所有輸入後,您接著就要決定應用程式的次要動作是否具有足夠的重要性,需公開為主要清單的快速鍵。 請記住,公開的動作越多,UI 就越複雜。 您是否真的需要在主要清單 UI 中公開次要動作,或是這些動作可以放在別的地方?

如果這些動作需要隨時可由任何輸入存取,建議您考慮在主要清單 UI 公開更多動作。

如果您認為不需要將次要動作放在主要清單 UI 中,您可以使用其他方式可以向使用者公開。 在考慮次要動作的放置位置時,以下是幾個可考量的選項。

將次要動作放在詳細資訊頁面

將次要動作放在按下清單項目時會前往的頁面。 如果您使用清單/詳細資訊模式,詳細資訊頁面通常很適合放置次要動作。

如需瞭解詳情,請參閱清單/詳細資訊模式

將次要動作放在操作功能表

將次要動作放在操作功能表中,使用者可透過按下滑鼠右鍵或按住滑鼠來存取動作。 這麼做的好處是可讓使用者執行刪除電子郵件等動作時,不需要載入詳細資訊頁面。 但將這些選項放在詳細資訊頁面上也是不錯的做法,因為操作功能表的用途是快速鍵,而不是主要 UI。

如果輸入來自遊戲控制器或遙控器,在公開次要動作時,建議您使用操作功能表。

如需瞭解詳情,請參閱操作功能表和飛出視窗

將次要動作放在暫留 UI 以便最佳化指標輸入

如果您預期應用程式會經常搭配滑鼠和手寫筆等指標輸入來使用,且想要讓次要動作隨時可供這些輸入使用,您可以讓次要動作只會暫留顯示。 這類快速鍵只會在您使用指標輸入時顯示,因此請務必同時使用其他選項來支援其他輸入類型。

Nested UI shown on hover

如需瞭解詳情,請參閱滑鼠互動

主要和次要動作的 UI 位置

如果您決定要在主要清單 UI 公開次要動作,建議您遵循下列指導方針。

如果您建立的清單項目包含主要和次要動作,請將主要動作放在左側,次要動作放在右側。 在閱讀方向為從左至右的文化中,使用者會將清單項目左側的動作聯想為主要動作。

在這些範例中,我們討論的清單 UI 中的項目會更大程度採取水平流動 (寬度大於高度)。 不過,有些清單項目的形狀可能較偏方形,或高度大於寬度。 一般而言,這些項目都使用在網格中。 處理這類項目時,如果清單不會垂直捲動,您可以將次要動作放在清單項目的底部,而不是放在右側。

考量所有輸入

決定使用巢狀 UI 時,請一並評估所有輸入類型的使用者體驗。 如前文所述,巢狀 UI 適用於某些輸入類型。 然而,它並不必然適合其他類型。 具體而言,鍵盤、遙控器和遠端輸入可能不太容易存取巢狀 UI 元素。 請務必遵循下列指引,以確保您的 Windows 能夠搭配所有輸入類型使用。

巢狀 UI 的處理

如果您的清單項目中有多個巢狀動作,建議您參考本指南來處理鍵盤、遊戲控制器、遙控器或其他非指標輸入的瀏覽方式。

清單項目會執行動作的巢狀 UI

如果您的清單 UI 包含巢狀元素支援的動作,例如叫用、選取 (單選或多選) 或拖放操作,建議您使用這類指標技術來瀏覽巢狀 UI 元素。

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

遊戲台

輸入來自遊戲控制器時,提供以下使用者體驗:

  • 右方向鍵會從 A 開始,將焦點放在 B 上。
  • 右方向鍵會從 開始,將焦點放在 上。
  • C 開始,右方向鍵若不是沒有動作,就是在清單右側有可當作焦點的 UI 元素時,將焦點放在該處。
  • 左方向鍵會從 開始,將焦點放在 上。
  • 左方向鍵會從 開始,將焦點放在 上。
  • 開始,左方向鍵若不是沒有動作,就是在清單左側有可當作焦點的 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 使用清單項目通常只是用來將元素分組,確保元素整組捲動。

這類 UI 通常比之前的範例更複雜,當中有許多巢狀元素可供使用者採取動作。

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

若要完成上述 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 項目中的下一個 tab 位置。
  • 從巢狀 UI 項目之一開始,按下 tab 鍵會以 tab 順序周遊巢狀 UI 項目。 在周遊過所有巢狀 UI 項目後,焦點會以 tab 順序放在 ListView 後的下一個控制項上。
  • Shift+Tab 會以反方向來執行 tab 鍵的行為。

範例

此範例示範如何實作清單項目會執行動作的巢狀 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;
    }
}