WPF ListBox多选模式下的拖放

32

除了一个小小的烦人问题,我几乎解决了这个问题...

由于 ListBox 的选择是在鼠标按下时发生的,如果你在选择要拖动的最后一项时用鼠标按下开始拖动,那么它就能正常工作,但如果你先选择所有要拖动的项,然后再点击选择开始拖动,你所点击的那个被取消选中并被留在原地。

有什么好的方法可以解决这个问题吗?

<DockPanel LastChildFill="True">
    <ListBox ItemsSource="{Binding SourceItems}"
             SelectionMode="Multiple"
             PreviewMouseLeftButtonDown="HandleLeftButtonDown"
             PreviewMouseLeftButtonUp="HandleLeftButtonUp"
             PreviewMouseMove="HandleMouseMove"
             MultiSelectListboxDragDrop:ListBoxExtension.SelectedItemsSource="{Binding SelectedItems}"/>
    <ListBox ItemsSource="{Binding DestinationItems}"
             AllowDrop="True"
             Drop="DropOnToDestination"/>
<DockPanel>

...

public partial class Window1
{
    private bool clickedOnSourceItem;

    public Window1()
    {
        InitializeComponent();
        DataContext = new WindowViewModel();
    }

    private void DropOnToDestination(object sender, DragEventArgs e)
    {
        var viewModel = (WindowViewModel)
                            e.Data.GetData(typeof(WindowViewModel));
        viewModel.CopySelectedItems();
    }

    private void HandleLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        var sourceElement = (FrameworkElement)sender;
        var hitItem = sourceElement.InputHitTest(e.GetPosition(sourceElement))
                      as FrameworkElement;

        if(hitItem != null)
        {
            clickedOnSourceItem = true;
        }
    }

    private void HandleLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
        clickedOnSourceItem = false;
    }

    private void HandleMouseMove(object sender, MouseEventArgs e)
    {
        if(clickedOnSourceItem)
        {
            var sourceItems = (FrameworkElement)sender;
            var viewModel = (WindowViewModel)DataContext;

            DragDrop.DoDragDrop(sourceItems, viewModel, DragDropEffects.Move);
            clickedOnSourceItem = false;
        }
    }
}
5个回答

23

我找到了一种非常简单的方法,可以在选择多个项目时启用类似Windows Explorer的拖放行为。该解决方案将常见的ListBox替换为一个小型的派生外壳,该外壳将ListBoxItem替换为更智能的版本。这样,我们可以在正确的层次上封装点击状态,并调用ListBox的受保护选择机制。下面是相关的类。有完整示例,请参见我的Github库

public class ListBoxEx : ListBox
{
    protected override DependencyObject GetContainerForItemOverride()
    {
        return new ListBoxItemEx();
    }

    class ListBoxItemEx : ListBoxItem
    {
        private bool _deferSelection = false;

        protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
        {
            if (e.ClickCount == 1 && IsSelected)
            {
                // the user may start a drag by clicking into selected items
                // delay destroying the selection to the Up event
                _deferSelection = true;
            }
            else
            {
                base.OnMouseLeftButtonDown(e);
            }
        }

        protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
        {
            if (_deferSelection)
            {
                try
                {
                    base.OnMouseLeftButtonDown(e);
                }
                finally
                {
                    _deferSelection = false;
                }
            }
            base.OnMouseLeftButtonUp(e);
        }

        protected override void OnMouseLeave(MouseEventArgs e)
        {
            // abort deferred Down
            _deferSelection = false;
            base.OnMouseLeave(e);
        }
    }
}

这是一个很棒的解决方案。我刚刚测试了一下,它运行得非常好。我认为这应该是被选中的答案,因为在我看来它更加简洁可靠。 - mandarin
完美的解决方案。 - Javid
不错!虽然有点可怕,但这是必要的...一个问题:为什么要使用 try...finally 块?会出现什么问题,它可以安全地忽略吗? - mike
finally语句块确保在事件处理结束时正确重置_deferSelection。那是五年前的事了,所以不知道实际上会有什么异常导致那里出错,但我猜测任何其他处理该事件的程序都可能搞砸。 - David Schmitt
太棒了! :) 对于 ListView 也适用。 - Oliver Giesen
谢谢您的回答!这正是我所需要的,我正在用于ListView!我不得不稍微修改一下,但这是可以预料的!Viva StackOverflow! - Ryan

14

我已经荣获一个飘荡之草徽章的骄傲所有者,所以我重新开始尝试并寻找解决方法。

我不确定自己是否喜欢这个解决方案,所以我仍然非常乐意接受更好的方法。

基本上,我最终做的是记住最后点击的ListBoxItem,然后确保在拖动之前将其添加到所选项目中。这也意味着看一下鼠标移动多远才开始拖动,因为如果点击一个已选择的项目来取消选择,有时会导致它再次被选择,如果鼠标反弹开始了一个小的拖动操作。

最后,我向列表框项目添加了一些热跟踪,因此,如果您在选定的项上按下鼠标,它将被取消选择,但您仍会获得一些反馈,指示它将包含在拖动操作中。

private void HandleLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    var source = (FrameworkElement)sender;
    var hitItem = source.InputHitTest(e.GetPosition(source)) as FrameworkElement;
    hitListBoxItem = hitItem.FindVisualParent<ListBoxItem>();
    origPos = e.GetPosition(null);
}
private void HandleLeftButtonUp(object sender, MouseButtonEventArgs e)
{
    hitListBoxItem = null;
}
private void HandleMouseMove(object sender, MouseEventArgs e)
{
    if (ShouldStartDrag(e))
    {
        hitListBoxItem.IsSelected = true;

        var sourceItems = (FrameworkElement)sender;
        var viewModel = (WindowViewModel)DataContext;
        DragDrop.DoDragDrop(sourceItems, viewModel, DragDropEffects.Move);
        hitListBoxItem = null;
    }
}
private bool ShouldStartDrag(MouseEventArgs e)
{
    if (hitListBoxItem == null)
        return false;

    var curPos = e.GetPosition(null);
    return
  Math.Abs(curPos.Y-origPos.Y) > SystemParameters.MinimumVerticalDragDistance ||
  Math.Abs(curPos.X-origPos.X) > SystemParameters.MinimumHorizontalDragDistance;
}

XAML更新以包括热跟踪...

<Style TargetType="ListBoxItem">
    <Setter Property="Margin" Value="1"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type ListBoxItem}">
                <Grid>
                  <Border Background="{TemplateBinding Background}" />
                  <Border Background="#BEFFFFFF" Margin="1">
                    <Grid>
                      <Grid.RowDefinitions>
                        <RowDefinition /><RowDefinition />
                      </Grid.RowDefinitions>
                      <Border Margin="1" Grid.Row="0" Background="#57FFFFFF" />
                    </Grid>
                  </Border>
                  <ContentPresenter Margin="8,5" />
                </Grid>
                <ControlTemplate.Triggers>
                  <Trigger Property="IsSelected" Value="True">
                    <Setter Property="Background" Value="PowderBlue" />
                  </Trigger>
                  <MultiTrigger>
                    <MultiTrigger.Conditions>
                      <Condition Property="IsMouseOver" Value="True" />
                      <Condition Property="IsSelected" Value="False"/>
                    </MultiTrigger.Conditions>
                    <Setter Property="Background" Value="#5FB0E0E6" />
                  </MultiTrigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

4

一个选择是不允许 ListBox 或 ListView 在触发 MouseLeftButtonUp 之前移除选定项。 示例代码:

    List<object> removedItems = new List<object>();

    private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        if (e.RemovedItems.Count > 0)
        {
            ListBox box = sender as ListBox;
            if (removedItems.Contains(e.RemovedItems[0]) == false)
            {
                foreach (object item in e.RemovedItems)
                {
                    box.SelectedItems.Add(item);
                    removedItems.Add(item);
                }
            }
        }
    }

    private void ListBox_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
        if (removedItems.Count > 0)
        {
            ListBox box = sender as ListBox;
            foreach (object item in removedItems)
            {
                box.SelectedItems.Remove(item);
            }
            removedItems.Clear();
        }
    }

感觉非常不对,但这解决了同样的问题! - Matt Winward

2
我很惊讶的是,在 .NET框架的三个主要更新之后的四年里,ListBox和Windows资源管理器之间的行为差异仍未得到解决。
我在 Silverlight 3 中遇到过这个问题。最终,我覆盖了鼠标按下和松开事件处理程序,完全模拟了 Windows 资源管理器的行为。
我现在没有源代码,但逻辑应该是:
当鼠标按下时
- 如果目标项未被选中,则清除现有选择 - 如果按下 Ctrl 键,则将目标项添加到选择列表 - 如果按下 Shift 键 - 如果已经存在先前选择的项,则将位于目标项和先前项之间的所有项都添加到选择列表 - 否则只将目标项添加到选择列表 - 如果目标项已被选中,则只在按下 Ctrl 键时取消选择。
当鼠标在同一项上松开时
如果选择目标项:
  • 如果按下 Ctrl 键,则从选择中移除该项
  • 如果按下 Shift 键
    • 如果有先前选择的项,则从选择中删除目标项和先前项之间的所有项
    • 否则仅从选择中删除目标项

然而,这应该是微软更新行为以使其与操作系统一致且更加直观的工作。我已将其提交给微软作为一个错误,如果有人想要投票支持它:http://connect.microsoft.com/VisualStudio/feedback/details/809192/


0

我曾经遇到过类似的问题。我从https://www.c-sharpcorner.com/uploadfile/dpatra/drag-and-drop-item-in-listbox-in-wpf/开始使用基本实现,然后进行了修改,变成了这样:

    ListBox dragSource = null;
    ObservableCollection<String> dragItems;

    private void ListBox_Drop(object sender, DragEventArgs e)
    {
        ListBox parent = (ListBox)sender;
        // check if the Items are from an different source
        if(dragSource != parent)
        {
            // Add and remove the Items of both sources
            foreach (var item in dragItems)
            {
                ((ObservableCollection<String>)dragSource.ItemsSource).Remove(item);
                ((ObservableCollection<String>)parent.ItemsSource).Add(item);
            }
        }
    }

    private void ListBox_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        // Check if Modifiers for Selection modes are pressed
        if(Keyboard.Modifiers != ModifierKeys.Control && Keyboard.Modifiers != ModifierKeys.Shift)
        {
            ListBox parent = (ListBox)sender;
            dragSource = parent;
            object data = GetDataFromListBox(dragSource, e.GetPosition(parent));
            dragItems = new ObservableCollection<String>();
            for(int i = 0; i < parent.SelectedItems.Count; i++)
            {
                dragItems.Add(parent.SelectedItems[i] as String);
            }
            //If the Data is currently selected drop whole selection
            if(dragItems.Contains(data as String))
            {
                DragDrop.DoDragDrop(parent, parent.SelectedItems, DragDropEffects.Move);
            }
            // The data is not selected, so clear selection and try to drop the current Item
            else
            {
                dragItems.Clear();
                dragItems.Add(data as String);
                parent.SelectedItems.Clear();
                DragDrop.DoDragDrop(parent, data, DragDropEffects.Move);
            }
        }
    }
    private static object GetDataFromListBox(ListBox source, Point point)
    {
        UIElement element = source.InputHitTest(point) as UIElement;
        if (element != null)
        {
            object data = DependencyProperty.UnsetValue;
            while (data == DependencyProperty.UnsetValue)
            {
                data = source.ItemContainerGenerator.ItemFromContainer(element);

                if (data == DependencyProperty.UnsetValue)
                {
                    element = VisualTreeHelper.GetParent(element) as UIElement;
                }

                if (element == source)
                {
                    return null;
                }
            }

            if (data != DependencyProperty.UnsetValue)
            {
                return data;
            }
        }
        return null;
    }

希望这能帮助到任何遇到这个线程的人。

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