Подтвердить что ты не робот

WPF Перетаскивание из списка ListBox с помощью SelectionMode Multiple

Я почти разобрался в этой работе с одной маленькой досадой...

Поскольку выбор 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;
        }
    }
}
4b9b3361

Ответ 1

Итак... став гордым владельцем значка перелива, я вернулся к этому, чтобы попытаться найти способ обойти его.; -)

Я не уверен, что мне нравится решение, поэтому я все еще очень открыт для любых лучших подходов.

В основном, то, что я закончил делать, это помнить, на что последний раз был нажат 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>

Ответ 2

Я нашел очень простой способ включить проводник Windows, например, при перемещении нескольких элементов. Это решение заменяет обычный 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);
        }
    }
}

Ответ 3

Один из вариантов - не разрешить 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();
        }
    }

Ответ 4

Я удивлен, что различие в поведении между ListBox и проводником Windows не было учтено через 4 года через 3 основных обновления среды .NET.

Я столкнулся с этой проблемой еще в Silverlight 3. В итоге я переопределил обработчик событий мыши и мыши, чтобы полностью имитировать поведение проводника Windows.

У меня больше нет исходного кода, но логика должна быть:

Когда мышь вниз

  • если целевой элемент не выбран, очистить существующий выбор
    • Если Ctrl не нажата, добавьте целевой элемент в список
    • если клавиша Shift нажата
      • если есть ранее выбранный элемент, добавьте все элементы между целевым элементом и предыдущим элементом для выбора.
      • else только добавляет целевой элемент к выбору
  • если выбранный элемент выбран, отмените выбор, только если клавиша Ctrl опущена.

Когда мышь вверх (на том же элементе)

  • если выбран целевой объект
    • если Ctrl не нажата, удалите элемент из выделения
    • если клавиша Shift нажата
      • если есть выделенный ранее элемент, удалите все элементы между целевым элементом и предыдущим элементом из выделения.
      • else удаляет целевой объект из выделения

Однако Это действительно должно быть заданием Microsoft, чтобы обновить поведение, чтобы оно соответствовало операционной системе и было более интуитивным. Я представил его как ошибку для Microsoft, если какой-либо орган хочет проголосовать за нее: http://connect.microsoft.com/VisualStudio/feedback/details/809192/