Почему список 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;
}
}