Почему список WPF изменяет выбор на кнопке мыши, а не на кнопке вверх?

Я никогда не замечал этого раньше, но WPF ListBox, кажется, меняет свой SelectedItem, когда мышь не работает, но еще не выпущена. В качестве быстрого примера просто создайте простой ListBox с несколькими ListBoxItems, например:

<ListBox>
  <ListBoxItem>Hello</ListBoxItem>
  <ListBoxItem>World</ListBoxItem>
  <ListBoxItem>ListBox</ListBoxItem>
  <ListBoxItem>Test</ListBoxItem>
</ListBox>

запустите приложение, нажмите кнопку мыши (не отпустите его!) и передвиньте мышь. При перемещении мыши элемент SelectedItem изменится. Это иллюстрирует большую проблему (по крайней мере, для меня), что ListBox SelectedItem будет установлен сразу же, как вы нажимаете мыши, а не при наведении мыши. Обычно это не проблема, но в моем случае я бы хотел включить перетаскивание элементов в моем ListBox без явного выбора элементов.

Я предполагаю, что мой единственный ресурс - создать пользовательский элемент ItemsControl или Selector с семантикой стиля выбора, подобной ListBox, так что действительно мой вопрос - это больше, почему ListBox работает таким образом? Кто-нибудь знает это?

Ответ 1

Это может быть немного не по теме, но я просто подошел к подобной проблеме. Я не хочу делать перетаскивание, но я хочу выбрать элементы в ListBox на MouseUp, а не MouseDown. Хотя псевдо-код Sheena может дать какой-то намек, это все равно заняло у меня некоторое время, прежде чем я нашел правильное решение. Так что это мое решение для моей проблемы.

public class ListBoxSelectionItemChangedOnMouseUp : ListBox
{
    protected override void OnMouseUp(MouseButtonEventArgs e)
    {
        if (e.ChangedButton == MouseButton.Left)
        {
            DependencyObject obj = this.ContainerFromElement((Visual)e.OriginalSource);
            if (obj != null)
            {
                FrameworkElement element = obj as FrameworkElement;
                if (element != null)
                {
                    ListBoxItem item = element as ListBoxItem;
                    if (item != null && this.Items.Contains(item))
                    {
                        this.SelectedItem = item;
                    }
                }
            }
        }
    }

    protected override void OnPreviewMouseDown(MouseButtonEventArgs e)
    {
        e.Handled = true;
    }
}

Я также хотел выбрать только левую кнопку мыши. В случае перетаскивания необходимо сохранить выбранный элемент в событии мыши, а затем использовать его в событии mouse up. Надеюсь, это поможет кому-то.

Ответ 2

Я лично предпочитаю MVVM и прикрепленные свойства для настройки поведения элементов.

Кроме того, решение, предложенное Томасом Косаром, похоже, не работает, когда свойство ItemsSource привязано.

Вот что я сейчас использую (синтаксис С# 7)

public static class SelectorBehavior
{
    #region bool ShouldSelectItemOnMouseUp

    public static readonly DependencyProperty ShouldSelectItemOnMouseUpProperty = 
        DependencyProperty.RegisterAttached(
            "ShouldSelectItemOnMouseUp", typeof(bool), typeof(SelectorBehavior), 
            new PropertyMetadata(default(bool), HandleShouldSelectItemOnMouseUpChange));

    public static void SetShouldSelectItemOnMouseUp(DependencyObject element, bool value)
    {
        element.SetValue(ShouldSelectItemOnMouseUpProperty, value);
    }

    public static bool GetShouldSelectItemOnMouseUp(DependencyObject element)
    {
        return (bool)element.GetValue(ShouldSelectItemOnMouseUpProperty);
    }

    private static void HandleShouldSelectItemOnMouseUpChange(
        DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is Selector selector)
        {
            selector.PreviewMouseDown -= HandleSelectPreviewMouseDown;
            selector.MouseUp -= HandleSelectMouseUp;

            if (Equals(e.NewValue, true))
            {
                selector.PreviewMouseDown += HandleSelectPreviewMouseDown;
                selector.MouseUp += HandleSelectMouseUp;
            }
        }
    }

    private static void HandleSelectMouseUp(object sender, MouseButtonEventArgs e)
    {
        var selector = (Selector)sender;

        if (e.ChangedButton == MouseButton.Left && e.OriginalSource is Visual source)
        {
            var container = selector.ContainerFromElement(source);
            if (container != null)
            {
                var index = selector.ItemContainerGenerator.IndexFromContainer(container);
                if (index >= 0)
                {
                    selector.SelectedIndex = index;
                }
            }
        }
    }

    private static void HandleSelectPreviewMouseDown(object sender, MouseButtonEventArgs e)
    {
        e.Handled = e.ChangedButton == MouseButton.Left;
    }

    #endregion

}

Теперь вы можете применить это к любому классу ListBox (или к селектору), например

<ListBox ItemsSource="{Binding ViewModelItems}" 
    SelectedItem="{Binding SelectedViewModelItem}" 
    ui:SelectorBehavior.ShouldSelectItemOnMouseUp="True" />

Ответ 3

Альтернативный подход, который, кажется, работает для меня:

private class SelectOnMouseUpListViewItem: ListViewItem
{
    protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
    {
        if (IsSelected)
            e.Handled = true;
        base.OnMouseLeftButtonDown(e);
    }

    protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
    {
        if (!IsSelected)
            base.OnMouseLeftButtonDown(e);
        base.OnMouseLeftButtonUp(e);
    }
}

protected override DependencyObject GetContainerForItemOverride() // in ListView
{
    return new SelectOnMouseUpListViewItem();
}

Ответ 4

Я предполагаю, что вы уже пытались сделать новое событие mouse-down, которое делает то, что вы хотите, и переопределите стандартное поведение таким образом... вот какой-то псевдокод, который должен сделать трюк:

ListBoxItem selected;
on_any_event_that_should_change_whats_selected()
{
    selected=whatever_you_want_selected;
}
on_selection_changed()
{
    theListBox.selectedItem=selected;
}

Мои wpf skillz немного ржавые, но я думаю, вам нужно будет сохранить элемент, а затем создать контейнер, поэтому этот псевдокод является огромным упрощением, но алгоритм должен сделать трюк.

Ответ 5

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

Я решил это, получив ListView и обработав событие PreviewMouseDown. Вместо этого я выбрал пункт MouseUp.

Остальная логика перетаскивания реализована с помощью Reactive Extensions.

ListBox похож на ListView, поэтому вы можете просто извлечь из ListBox, и он будет работать.

код:

public class DragDroppableListView : ListView
{
    private IDisposable _subscription;

    public DragDroppableListView()
    {
        Loaded += OnControlLoaded;
        Unloaded += OnControlUnloaded;
    }

    protected override void OnMouseUp(MouseButtonEventArgs e)
    {
        if (e.ChangedButton != MouseButton.Left) return;

        var obj = ContainerFromElement((Visual)e.OriginalSource);
        if (obj == null) return;

        var element = obj as FrameworkElement;
        if (element == null) return;

        var item = element as ListBoxItem;
        if (item == null) return;

        // select item
        item.IsSelected = true;
    }

    private void OnControlUnloaded(object sender, RoutedEventArgs e)
    {
        if (_subscription != null)
            _subscription.Dispose();
    }

    private void OnControlLoaded(object sender, RoutedEventArgs e)
    {
        var mouseDown = Observable.FromEventPattern<MouseButtonEventArgs>(this, "PreviewMouseDown");

        var mouseUp = Observable.FromEventPattern<MouseEventArgs>(this, "MouseUp");
        var mouseMove = Observable.FromEventPattern<MouseEventArgs>(this, "MouseMove");

        _subscription = mouseDown
            .Where(a => a.EventArgs.LeftButton == MouseButtonState.Pressed)
            .Where(o => !IsScrollBar(o.EventArgs))
            .Do(o => o.EventArgs.Handled = true)        // not allow listview select on mouse down
            .Select(ep => ep.EventArgs.GetPosition(this))
            .SelectMany(md => mouseMove
                .TakeWhile(ep => ep.EventArgs.LeftButton == MouseButtonState.Pressed)
                .Where(ep => IsMinimumDragSeed(md, ep.EventArgs.GetPosition(this)))
                .TakeUntil(mouseUp))
            .ObserveOnDispatcher()
            .Subscribe(_ => OnDrag());
    }

    private void OnDrag()
    {
        var item = GetItemUnderMouse();
        if (item == null) return;

        DragDrop.DoDragDrop(
            this,
            new DataObject(typeof(object), item),
            DragDropEffects.Copy | DragDropEffects.Move);
    }

    private ListViewItem GetItemUnderMouse()
    {
        return Items.Cast<object>()
            .Select(item => ItemContainerGenerator.ContainerFromItem(item))
            .OfType<ListViewItem>()
            .FirstOrDefault(lvi => lvi.IsMouseOver);
    }

    private static bool IsMinimumDragSeed(Point start, Point end)
    {
        return Math.Abs(end.X - start.X) >= SystemParameters.MinimumHorizontalDragDistance ||
               Math.Abs(end.Y - start.Y) >= SystemParameters.MinimumVerticalDragDistance;
    }

    private bool IsScrollBar(MouseEventArgs args)
    {
        var res = VisualTreeHelper.HitTest(this, args.GetPosition(this));
        if (res == null) return false;

        var depObj = res.VisualHit;
        while (depObj != null)
        {
            if (depObj is ScrollBar) return true;

            // VisualTreeHelper works with objects of type Visual or Visual3D.
            // If the current object is not derived from Visual or Visual3D,
            // then use the LogicalTreeHelper to find the parent element.
            if (depObj is Visual || depObj is System.Windows.Media.Media3D.Visual3D)
                depObj = VisualTreeHelper.GetParent(depObj);
            else
                depObj = LogicalTreeHelper.GetParent(depObj);
        }

        return false;
    }
}