清單項目中的巢狀 UI

巢狀 UI 是一種使用者介面 (UI),可公開容器內所封入的巢狀可操作控制件,也可以取得獨立焦點。

您可以使用巢狀UI向用戶呈現其他選項,以協助加速採取重要動作。 不過,您公開的動作越多,UI 就越複雜。 當您選擇使用此 UI 模式時,需要特別小心。 本文提供指導方針,可協助您判斷特定UI的最佳動作路線。

重要 APIListView 類別GridView 類別

在本文中,我們會討論在 ListView 和 GridView 專案中建立巢狀 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 支援的所有輸入之後,您應該決定 app 的次要動作是否重要,足以公開為主要清單中的快速鍵。 請記住,您公開的動作越多,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 上。
  • B,右方向鍵會將焦點放在 C 上。
  • C,右方向鍵不是 op,或者如果清單右邊有可設定焦點的 UI 元素,請將焦點放在該處。
  • C,左方向鍵會將焦點 放在 B 上。
  • B,左方向鍵會將焦點放在 A
  • A,左方向鍵不是操作,或者如果清單右邊有可設定焦點的 UI 元素,請將焦點放在該處。
  • ABC 向下方向鍵將焦點放在 D 上。
  • 從清單專案左邊的UI元素,向右鍵會將焦點放在A
  • 從清單專案右邊的UI元素,左方向鍵會將焦點放在A

鍵盤

當輸入來自鍵盤時,這是使用者取得的體驗:

  • A 中,定位鍵會將焦點放在 B
  • B 中,定位鍵會將焦點放在 C 上。
  • C 中,Tab 鍵會將焦點放在定位順序中的下一個可設定焦點 UI 元素。
  • C,shift+tab 鍵會將焦點 放在 B 上。
  • B、shift+tab 或向左鍵將焦點放在 A
  • A,shift+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>

當清單專案未執行動作時,建議您使用遊戲板或鍵盤來處理導覽。

遊戲台

當輸入來自遊戲板時,請提供下列用戶體驗:

  • 從清單專案,向下鍵會將焦點放在下一個清單專案上。
  • 從清單專案,左/右鍵不是op,或者如果清單右邊有可設定焦點的UI元素,請將焦點放在該處。
  • 從 [列表專案],[A] 按鈕會將焦點放在巢狀 UI 的上/左/右優先順序。
  • 在巢狀UI內,遵循 XY 焦點流覽模型。 焦點只能巡覽目前列表專案內所包含的巢狀 UI,直到使用者按下 『B』 按鈕,才能將焦點放回清單專案。

鍵盤

當輸入來自鍵盤時,這是使用者取得的體驗:

  • 從清單專案,向下鍵會將焦點放在下一個清單專案上。
  • 從 [列表專案] 中,按左/向右鍵不會執行任何作業。
  • 在 [列表專案] 中,按 Tab 鍵會將焦點放在巢狀 UI 專案之間的下一個製表位。
  • 從其中一個巢狀 UI 專案,按 Tab 鍵會以定位順序周遊巢狀 UI 專案。 一旦移至所有巢狀 UI 項目之後,它會將焦點放在 ListView 之後的下一個控件上。
  • Shift+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;
    }
}