2009-10-12 13 views
20

casi he conseguí este trabajo, aparte de una pequeña cosa molesta ...WPF arrastrar y soltar desde ListBox con múltiple SelectionMode

Debido a que la selección ListBox sucede en el ratón hacia abajo, si se inicia el arrastre con el ratón hacia abajo al seleccionar el último elemento para arrastrar, funciona bien, pero si selecciona todos los elementos para arrastrar primero y luego hace clic en la selección para comenzar a arrastrarlo, el que hace clic en no se selecciona y se deja atrás después del arrastre.

¿Alguna idea sobre la mejor manera de evitar esto?

<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; 
     } 
    } 
} 

Respuesta

13

Así ..., convertidos el orgulloso propietario de una tarjeta de identificación rodadora, tengo de nuevo en esto para tratar & encontrar una manera de evitarlo. ;-)

No estoy seguro de que me guste la solución, así que todavía estoy abierto a cualquier mejor enfoque.

Básicamente, lo que terminé haciendo es recordar en qué ListBoxItem se hizo clic por última vez en &, luego asegúrese de que se agrega a los elementos seleccionados antes de arrastrar. Esto también significaba observar qué tan lejos se mueve el mouse antes de iniciar un arrastre, ya que al hacer clic en un elemento seleccionado para deseleccionarlo algunas veces se puede volver a seleccionar si el rebote del mouse inicia una pequeña operación de arrastre.

Finalmente, agregué un seguimiento en caliente a los elementos de la lista, por lo que si presiona el mouse sobre un elemento seleccionado, se deseleccionará pero aún recibirá comentarios para indicar que se incluirá en la operación de arrastre.

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; 
} 

cambios XAML para incluir el seguimiento activo ...

<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

Una opción sería la de no permitir ListBox o ListView para eliminar los elementos seleccionados hasta MouseLeftButtonUp se dispara. Código de ejemplo:

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(); 
     } 
    } 
+0

Se siente increíblemente mal, pero esto me solucionó el mismo problema. –

2

me sorprende que la diferencia de comportamiento entre ListBox y el Explorador de Windows no ha sido cuidado después de 4 años a través de 3 grandes actualizaciones del marco .NET.

Volví a este problema en Silverlight 3. Terminé anulando el mouse hacia abajo y el manejador de eventos del mouse para simular completamente el comportamiento de Windows Explorer.

no tengo el código fuente más pero la lógica debería ser:

Cuando ratón

  • si el elemento de destino no está seleccionado, clara selección existente
    • si la tecla Ctrl está presionada, agregue el elemento de destino a la selección
    • si la tecla Shift está presionada
      • si hay un elemento seleccionado previamente, añadir todos los elementos entre este punto del objetivo y punto anterior a la selección
      • demás sólo añadir elemento de destino a la selección
  • si el elemento de destino se selecciona de-selección sólo Si tecla Ctrl está abajo

Cuando ratón hacia arriba (en el mismo artículo)

  • si se selecciona el elemento de destino
    • si tecla Ctrl está abajo, retire elemento de la selección
    • si la tecla Mayús está abajo
      • si hay un elemento seleccionado previamente, eliminar todos los elementos entre este punto del objetivo y previa elemento de selección
      • demás sólo quitar elemento de destino de la selección

Sin embargo, Este debería ser realmente el trabajo de Microsoft para actualizar el comportamiento para que sea coherente con el sistema operativo y sea más intuitivo. Lo he enviado como un error a Microsoft si algún cuerpo quiere votar por él: http://connect.microsoft.com/VisualStudio/feedback/details/809192/

10

He encontrado una manera muy simple de habilitar el Explorador de Windows como el comportamiento de arrastrar/soltar cuando se seleccionan varios elementos. La solución reemplaza la ListBox común con una pequeña cuña derivada que reemplaza la ListBoxItem con una versión más inteligente. De esta forma, podemos encapsular el estado de clic en el nivel correcto y llamar a la maquinaria de selección protegida del ListBox. Aquí está la clase relevante. Para un ejemplo completo, vea my repo on 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); 
     } 
    } 
} 
+0

Esta es una gran solución. Acabo de probarlo y funciona como un encanto. Creo que esta debería ser la respuesta seleccionada, ya que es más limpio y más confiable IMO. – mandarin

+0

Simple y funciona como un encanto. –

+0

funciona muy bien. Dios te bendiga – Youngjae