共用方式為


清單項目中的巢狀使用者介面

巢狀 UI 是一種使用者介面(UI),它在容器中顯示巢狀的可操作控制項,且該容器也可以獨立聚焦。

你可以使用巢狀介面,向使用者呈現更多選項,幫助加速執行重要行動。 然而,你暴露的動作越多,使用者介面就越複雜。 使用此介面模式時需特別謹慎。 本文提供指引,幫助你判斷最適合你個人介面的行動方案。

重要 APIListView 類別GridView 類別

本文討論在 ListViewGridView 項目中建立巢狀 UI。 雖然本節未討論其他巢狀 UI 案例,但這些概念是可轉移的。 在開始之前,你應該熟悉在 UI 中使用 ListView 或 GridView 控制項的一般指引,該指引可見於 「列表 」和 「清單檢視」及「網格檢視 」條目中。

本文使用以下定義的 清單清單項目巢狀 UI 等術語:

  • 清單 指的是清單檢視或格子檢視中包含的項目集合。
  • 清單項目 指的是使用者在列表中可以對其採取行動的單一項目。
  • 巢狀 UI 指的是清單項目中使用者可以對這些元素採取不同於對清單項目本身的操作。

截圖顯示巢狀 U I. 的各個部分。

注意 ListView 和 GridView 都源自 ListViewBase 類別,因此功能相同,但資料顯示方式不同。 在本文中,當我們談論清單時,資訊同時適用於 ListView 和 GridView 的控制項。

主要與次要行動

在建立包含清單的 UI 時,請考慮使用者可能會從這些清單項目中採取哪些動作。

  • 使用者可以點擊該物品來執行動作嗎?
    • 通常點擊清單項目會啟動一個動作,但不一定非得如此。
  • 使用者可以採取不只一個動作嗎?
    • 例如,點擊郵件清單中的郵件會打開該郵件。 不過,使用者可能還會想先做其他操作,例如刪除郵件,而不必先打開郵件。 使用者若能直接在清單中存取此操作,將對使用者有幫助。
  • 這些動作應該如何向使用者揭露?
    • 考慮所有輸入類型。 有些巢狀介面在某種輸入方式下運作良好,但可能不適合其他輸入方式。

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

次要動作 通常是與清單項目相關的加速器。 這些加速器可以用於清單管理或與清單項目相關的動作。

次要行動的選項

在建立清單介面時,你首先需要確保考慮 Windows 支援的所有輸入法。 關於不同輸入類型的更多資訊,請參見輸入指南

在確定你的應用程式支援 Windows 支援的所有輸入後,你應該決定應用程式的次要動作是否重要到需要在主清單中以加速器形式公開。 記得你暴露的動作越多,介面就越複雜。 你真的需要在主清單介面中公開次要動作,還是可以放在別處?

當這些動作需要隨時透過任何輸入方式存取時,你可以考慮在主清單介面中顯示額外的動作。

如果你決定不需要在主清單介面中放置次要動作,還有其他幾種方法可以讓使用者知道它們。 以下是一些你可以考慮的放置次要動作的選項。

把次要功能放在詳細頁面

當清單項目被按下時,將次要動作放置在其導覽到的頁面上。 當你使用清單/細節模式時,詳細頁面通常是放置次要動作的好地方。

更多資訊請參閱 清單/詳細模式

將次要動作放在上下文選單中

將次要動作放在使用者可透過右鍵點擊或長按進入的內容選單中。 這讓使用者可以執行某些操作,例如刪除電子郵件,而無需載入詳細頁面。 建議也將這些選項提供在詳細頁面,因為上下文選單是加速器而非主要介面。

當輸入來自手把或遙控器時,我們建議你使用上下文選單來顯示次要動作。

更多資訊請參閱 快捷選單與延伸選單

在懸浮介面中放置次要動作,以優化指標輸入

如果你預期應用程式會經常搭配滑鼠和筆等指標輸入使用,並希望次要動作只對這些輸入開放,那麼你可以只在滑鼠懸停時顯示次要動作。 這個加速器只有在使用指標輸入時才會顯示,所以請記得用其他選項來支援其他輸入類型。

懸停時顯示巢狀介面

更多資訊請參見 滑鼠互動

主要與次要行動的 UI 配置

如果您決定要在主清單介面中揭露次要動作,我們建議遵循以下指引。

當你建立包含主要和次要動作的清單項目時,將主要動作放在左側,次要動作放在右側。 在由左到右的閱讀文化中,使用者會將清單項目左側的動作視為主要動作。

在這些例子中,我們討論的是清單使用者介面,項目更傾向於橫向排列(即寬度大於高度)。 不過,你可能會有清單上的項目形狀較為方正,或比寬度還高。 通常,這些是用在網格中的項目。 對於這些項目,如果清單沒有垂直捲動,你可以把次要動作放在清單底部,而不是放在右側。

考慮所有輸入

決定使用巢狀介面時,也要評估所有輸入類型的使用者體驗。 如前所述,巢狀介面對某些輸入類型效果很好。 然而,這種方法並不總是對某些人有效。 特別是鍵盤、控制器和遠端輸入在存取巢狀介面元素時可能會遇到困難。 請務必遵循以下指引,確保您的 Windows 能支援所有輸入類型。

巢狀 UI 處理

當你在清單項目中內嵌多個動作時,我們建議使用此指引來使用鍵盤、遊戲手柄、遙控器或其他非指標輸入裝置來進行導航。

巢狀 UI 讓清單項目執行動作

如果您的清單介面包含巢狀元素,支援呼叫、選取(單一或多個)或拖放操作,我們建議用這些箭頭技巧來導航巢狀 UI 元素。

截圖顯示巢狀的 U I 元素,標示為 A、B、C 和 D 字母。

遊戲控制器

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

  • A 開始,右方向鍵會把焦點放在 B
  • B開始,右方向鍵會把焦點放在 C上。
  • C 鍵開始,右方向鍵要麼不執行任何操作,要麼如果 List 右邊有可聚焦的 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 鍵會將焦點移到按反向順序排列的可聚焦 UI 元素。
  • ABC 按下下方向鍵會對焦到 D
  • 從 UI 元素到清單項目左側,按 Tab 鍵會將焦點放在 A
  • 從 UI 元素到清單項目右側,按 Shift Tab 鍵會把焦點放在 C

要達成此 UI,請將 IsItemClickEnabled為 trueSelectionMode 可以是任何值。

關於實作此的程式碼,請參閱本文的 範例 部分。

巢狀 UI,清單項目不會執行任何動作

你可能會使用清單檢視,因為它提供虛擬化和最佳化的捲動行為,但不會與清單項目綁定動作。 這些介面通常只用清單項目來將元素分組,並確保它們一起捲動。

這類使用者介面通常比前面的範例複雜得多,包含許多巢狀元素,使用者可以對這些元素進行操作。

顯示複雜巢狀 UI 的截圖,含有許多使用者可互動的巢狀元素。

要實現這個 UI,請在你的清單中設定以下屬性:

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

當清單項目無法執行任何動作時,我們建議使用此指引來使用手把或鍵盤來進行導航。

遊戲控制器

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

  • 從清單項目往下按方向鍵會聚焦到下一個清單項目。
  • 從清單項目,左右鍵要麼是無操作鍵,要麼如果列表右邊有可聚焦的 UI 元素,就把焦點放在那裡。
  • 從清單項目中,按下「A」鍵會將焦點設置於巢狀使用者介面,按照上下、左右的優先順序移動。
  • 在巢狀介面內,請依照 XY Focus 導航模型操作。 焦點只能在目前清單項目內的巢狀介面中移動,直到使用者按下「B」鍵後,焦點才會回到清單項目。

鍵盤

當輸入來自鍵盤時,使用者會獲得以下體驗:

  • 從清單項目,按向下箭頭鍵會聚焦到下一個清單項目。
  • 在清單項目中,按左鍵或右鍵沒有任何操作效果。
  • 從清單項目中,按下 Tab 鍵會將焦點移至巢狀 UI 元件中的下一個定位點。
  • 從巢狀 UI 項目中的一個開始,按下 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;
    }
}