带有单选和点击取消选择功能的 ListBox 列表控件...?

19

我需要一个列表框,第一次单击时选择项目,第二次单击时取消选择,以便任何时候只选定零个或一个项目。

当您按住ctrl键时,在选择模式为“Single”的列表框中实现了选择/取消选择,但不幸的是,我的所有用户都不能指望知道这一点。

在SelectionMode =“Multiple”下,我们具有所需的完全功能,但是您可以选择多个项......

更多背景信息: 我希望用户首先选择要登录的安装程序,然后提供凭据(和其他选项)

为此,我使用了一个带有扩展内容的列表框。为了帮助扩展,我在列表框项的左侧制作了一个三角形,当未展开时向右指向,当您选择列表框项时,它会转向向下。

所以,首先用户看到安装程序列表,然后在通过选择它来选择他想要的项后,列表框项扩展到他需要输入的其余信息。 这很好用,效果很好,但测试报告称他们希望单击三角形进行第二次选择以取消选择(从而折叠已展开的部分)。 我必须承认,我也点击了这个^%&箭头,期望该操作导致折叠... :-(

是否有人知道如何实现这一点(最好不需要代码后台)?

5个回答

32

通常做法是将SelectionMode模式设置为Multiple,然后在SelectionChanged事件中取消选中除了新选择的项目之外的所有项目。

查看以下链接:

这里有一个可以实现此功能的附加行为,可以像这样使用:

<ListBox local:ListBoxSelectionBehavior.ClickSelection="True"
         ...>

ListBoxSelectionBehavior

public static class ListBoxSelectionBehavior 
{
    public static readonly DependencyProperty ClickSelectionProperty = 
        DependencyProperty.RegisterAttached("ClickSelection", 
                                            typeof(bool),
                                            typeof(ListBoxSelectionBehavior),
                                            new UIPropertyMetadata(false, OnClickSelectionChanged));
    public static bool GetClickSelection(DependencyObject obj) 
    {
        return (bool)obj.GetValue(ClickSelectionProperty); 
    }
    public static void SetClickSelection(DependencyObject obj, bool value) 
    {
        obj.SetValue(ClickSelectionProperty, value); 
    }
    private static void OnClickSelectionChanged(DependencyObject dpo, 
                                                             DependencyPropertyChangedEventArgs e) 
    {
        ListBox listBox = dpo as ListBox;
        if (listBox != null) 
        { 
            if ((bool)e.NewValue == true) 
            {
                listBox.SelectionMode = SelectionMode.Multiple;
                listBox.SelectionChanged += OnSelectionChanged;
            } 
            else 
            {
                listBox.SelectionChanged -= OnSelectionChanged;
            } 
        } 
    }
    static void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        if (e.AddedItems.Count > 0)
        {
            ListBox listBox = sender as ListBox;
            var valid = e.AddedItems[0];
            foreach (var item in new ArrayList(listBox.SelectedItems))
            {
                if (item != valid)
                {
                    listBox.SelectedItems.Remove(item);
                }
            }
        }
    }
}

2
我喜欢这个解决方案胜过被接受的那个,因为它不涉及与UI元素的技巧。 - Romain Hautefeuille

14

尝试这样做:

使用ToggleButton作为详细内容的"展开器"。 您可以将ToggleButton的"IsChecked"属性绑定到该项的"IsSelected"属性。

这里是代码:

<ListBox SelectionMode="Single">
   <ListBox.ItemsSource>
      <x:Array Type="{x:Type sys:String}">
         <sys:String>test1</sys:String>
         <sys:String>test2</sys:String>
         <sys:String>test3</sys:String>
         <sys:String>test4</sys:String>
         <sys:String>test5</sys:String>
         <sys:String>test6</sys:String>
      </x:Array>
   </ListBox.ItemsSource>
   <ListBox.ItemTemplate>
      <DataTemplate>
         <StackPanel Orientation="Horizontal">
            <ToggleButton IsChecked="{Binding 
                          RelativeSource={RelativeSource FindAncestor, 
                          AncestorType={x:Type ListBoxItem}},
                          Path=IsSelected}"
            >btn</ToggleButton>
         </StackPanel>
      </DataTemplate>
   </ListBox.ItemTemplate>
</ListBox>

如何工作: 在Listbox中只能选择一个项。当我们选择一个项时,Toggler会展开,因为它的IsChecked与父ListBoxItem的ListBoxItem.IsSelected(ListBoxItem是一个控件,它包装了每个项的内容)绑定在一起。 由于SelectionMode是单选模式,因此一旦选择另一个项目,就会发生以下情况:

  • 取消选择当前选择的项目
  • 通过绑定,Toggler也将变为未选中状态
  • 选择新项目
  • 通过其绑定,新项目的Toggler被选中

如果仅取消选择当前选定项目的Toggler,则通过绑定,该项目将取消选择...


我对“选择时展开”的功能没有问题,问题是使列表框一次只能选择一个项目,并在第二次点击时取消选择。 - Tor Thorbergsen
但它确实做到了这一点。在ToggleButtonClick上,它会展开并选择该项。第二次点击时,它会折叠并取消选择该项。最多只能选择1个项目。诀窍在于ListBoxItem的IsSelected属性的双向绑定。试试看吧。 - fixagon
你知道为什么我无法通过同一个ToggleButton取消选择ListBoxItem吗?只有选择功能可用。 - Daniel
看起来不错,但如何消除ToggleButton,为ListBoxItem显示自定义内容(基本上是两个TextBlock),并仍能实现选择切换结果? - Romain Hautefeuille
@RomainHautefeuille:只需将自定义内容放置在切换按钮内即可(如果您有多个项目在StackPanel或类似控件中)。 - fixagon
显示剩余2条评论

0

比这更简单的方法是,只需添加标志组合SelectionMode="Multiple"

 private bool _ignoreSelectionFlag = false;
    private void LbHistory_OnSelectionChanged(object sender,SelectionChangedEventArgs e)
    {
        if (_ignoreSelectionFlag)
            return;

        if (e.AddedItems.Count > 0)
        {
            ListBox listBox = sender as ListBox;
            var valid = e.AddedItems[0];

            _ignoreSelectionFlag = true;
            LbHistory.UnselectAll();
            LbHistory.SelectedItems.Add(e.AddedItems[0]);
            e.Handled = true;
            _ignoreSelectionFlag = false;


        }
    }

0

我允许自己为UWP和.NET Framework 4.7补充Fredrik的答案:

public static class ListBoxSelectionBehavior
{
    public static readonly DependencyProperty ClickSelectionProperty =
    DependencyProperty.RegisterAttached("ClickSelection",
                                        typeof(bool),
                                        typeof(ListBoxSelectionBehavior),
                                        new PropertyMetadata(false, OnClickSelectionChanged));

    public static bool GetClickSelection(DependencyObject obj)
    {
        return (bool)obj.GetValue(ClickSelectionProperty);
    }
    public static void SetClickSelection(DependencyObject obj, bool value)
    {
        obj.SetValue(ClickSelectionProperty, value);
    }
    private static void OnClickSelectionChanged(DependencyObject dpo, DependencyPropertyChangedEventArgs e)
    {
        if (dpo is ListBox listBox)
        {
            if ((bool)e.NewValue == true)
            {
                listBox.SelectionMode = SelectionMode.Multiple;
                listBox.SelectionChanged += OnSelectionChanged;
            }
            else
            {
                listBox.SelectionChanged -= OnSelectionChanged;
            }
        }
    }
    static void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        if (e.AddedItems.Count > 0)
        {
            ListBox listBox = sender as ListBox;
            var valid = e.AddedItems[0];
            foreach (var item in new ArrayList(listBox.SelectedItems.ToArray()))
            {
                if (item != valid)
                {
                    listBox.SelectedItems.Remove(item);
                }
            }
        }
    }
}

虽然这段代码可能回答了问题,但是提供关于为什么和/或如何回答问题的额外上下文可以提高其长期价值。 - Alex Riabov

0
我的解决方案是将ListBox的SelectionMode设置为Multiple,添加forbidSelectionButOne方法在Click事件中,并且之后只允许选择一个项目,如下所示:
Private Sub forbidSelectionButOne(sender As Object, e As MouseButtonEventArgs)
    Dim lv As ListView = TryCast(sender, ListView)
    If lv IsNot Nothing Then
        If lv.SelectedIndex <> getCausesListViewItemIndex(sender, e) Then
            lv.SelectedIndex = getCausesListViewItemIndex(sender, e)
            e.Handled = True
        End If
        lv.Focus()
    End If
End Sub

同时,提供一个帮助函数,用于查找被鼠标点击的ListViewItem:

Private Function getCausesListViewItemIndex(ByVal sender As Object, e As RoutedEventArgs) As Integer
    Dim dep As DependencyObject = TryCast(e.OriginalSource, DependencyObject)
    Do While dep IsNot Nothing AndAlso Not TypeOf (dep) Is ListViewItem
        dep = VisualTreeHelper.GetParent(dep)
    Loop
    If dep Is Nothing Then
        Return -1
    Else
        Dim lv As ListView = TryCast(sender, ListView)
        If lv IsNot Nothing Then
            Dim i As Integer = lv.ItemContainerGenerator.IndexFromContainer(dep)
            Return i
        Else
            Return -1
        End If
    End If
End Function

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接