Пузырьковые события прокрутки от ListView до его родителя
В моем приложении WPF у меня есть ListView
чья ScrollViewer.VerticalScrollBarVisibility
установлен в Disabled
, Он содержится в ScrollViewer
, Когда я пытаюсь использовать колесо мыши над ListView
, внешний ScrollViewer
не прокручивает, потому что ListView
захватывает события прокрутки.
Как я могу заставить ListView
чтобы позволить событиям прокрутки всплывать до ScrollViewer
?
7 ответов
Вам нужно захватить событие колеса мыши для предварительного просмотра во внутреннем представлении списка
ctl.PreviewMouseWheel += PreviewMouseWheel;
затем остановите прокрутку просмотра списка события и вызовите событие в родительском просмотре списка.
private static void PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
if (!e.Handled)
{
e.Handled = true;
var eventArg = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta);
eventArg.RoutedEvent = UIElement.MouseWheelEvent;
eventArg.Source = sender;
var parent = ((Control)sender).Parent as UIElement;
parent.RaiseEvent(eventArg);
}
}
Кредиты отправляются @robert-wagner, который решил это для меня несколько месяцев назад.
Еще одно приятное решение с использованием прикрепленного поведения. Мне это нравится, потому что оно лишает решения контроля.
Создайте поведение без прокрутки, которое перехватит событие PreviewMouseWheel(Tunneling) и вызовет новое MouseWheelEvent(Bubbling)
public sealed class IgnoreMouseWheelBehavior : Behavior<UIElement>
{
protected override void OnAttached( )
{
base.OnAttached( );
AssociatedObject.PreviewMouseWheel += AssociatedObject_PreviewMouseWheel ;
}
protected override void OnDetaching( )
{
AssociatedObject.PreviewMouseWheel -= AssociatedObject_PreviewMouseWheel;
base.OnDetaching( );
}
void AssociatedObject_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
e.Handled = true;
var e2 = new MouseWheelEventArgs(e.MouseDevice,e.Timestamp,e.Delta);
e2.RoutedEvent = UIElement.MouseWheelEvent;
AssociatedObject.RaiseEvent(e2);
}
}
Затем присоедините поведение к любому UIElement с вложенным случаем ScrollViewers.
<ListBox Name="ForwardScrolling">
<i:Interaction.Behaviors>
<local:IgnoreMouseWheelBehavior />
</i:Interaction.Behaviors>
</ListBox>
все заслуги в блоге Джоша Эйнштейна
Если вы приходите сюда в поисках решения, которое ТОЛЬКО для всплытия события, если ребенок находится сверху и прокручивает вверх или вниз и прокручивает вниз, вот решение. Я только протестировал это с DataGrid, но он должен работать и с другими элементами управления.
public class ScrollParentWhenAtMax : Behavior<FrameworkElement>
{
protected override void OnAttached()
{
base.OnAttached();
this.AssociatedObject.PreviewMouseWheel += PreviewMouseWheel;
}
protected override void OnDetaching()
{
this.AssociatedObject.PreviewMouseWheel -= PreviewMouseWheel;
base.OnDetaching();
}
private void PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
var scrollViewer = GetVisualChild<ScrollViewer>(this.AssociatedObject);
var scrollPos = scrollViewer.ContentVerticalOffset;
if ((scrollPos == scrollViewer.ScrollableHeight && e.Delta < 0)
|| (scrollPos == 0 && e.Delta > 0))
{
e.Handled = true;
var e2 = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta);
e2.RoutedEvent = UIElement.MouseWheelEvent;
AssociatedObject.RaiseEvent(e2);
}
}
private static T GetVisualChild<T>(DependencyObject parent) where T : Visual
{
T child = default(T);
int numVisuals = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < numVisuals; i++)
{
Visual v = (Visual)VisualTreeHelper.GetChild(parent, i);
child = v as T;
if (child == null)
{
child = GetVisualChild<T>(v);
}
if (child != null)
{
break;
}
}
return child;
}
}
Чтобы прикрепить это поведение, добавьте следующие XMLNS и XAML к вашему элементу:
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
<i:Interaction.Behaviors>
<shared:ScrollParentWhenAtMax />
</i:Interaction.Behaviors>
Существуют разные подходы в зависимости от вашей конкретной ситуации, но я нашел, что это хорошо работает. Предполагая, что ваша основная ситуация такова:
<Window Height="200" Width="200">
<Grid>
<ScrollViewer Name="sViewer">
<StackPanel>
<Label Content="Scroll works here" Margin="10" />
<ListView Name="listTest" Margin="10"
PreviewMouseWheel="listTest_PreviewMouseWheel"
ScrollViewer.VerticalScrollBarVisibility="Disabled">
<ListView.ItemsSource>
<Int32Collection>
1,2,3,4,5,6,7,8,9,10
</Int32Collection>
</ListView.ItemsSource>
<ListView.View>
<GridView>
<GridViewColumn Header="Column 1" />
</GridView>
</ListView.View>
</ListView>
</StackPanel>
</ScrollViewer>
</Grid>
</Window>
Поднятие MouseWheel Событие себя во время PreviewMouseWheel, кажется, заставляет ScrollViewer работать. Хотел бы я знать почему, это кажется очень нелогичным.
private void listTest_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
e.Handled = true;
MouseWheelEventArgs e2 = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta);
e2.RoutedEvent = UIElement.MouseWheelEvent;
listTest.RaiseEvent(e2);
}
Вы также можете достичь того же, используя прикрепленное поведение. Это имеет то преимущество, что не требуется библиотека System.Windows.Interactivity. Логика была взята из других ответов, отличается только реализация.
public static class IgnoreScrollBehaviour
{
public static readonly DependencyProperty IgnoreScrollProperty = DependencyProperty.RegisterAttached("IgnoreScroll", typeof(bool), typeof(IgnoreScrollBehaviour), new PropertyMetadata(OnIgnoreScollChanged));
public static void SetIgnoreScroll(DependencyObject o, string value)
{
o.SetValue(IgnoreScrollProperty, value);
}
public static string GetIgnoreScroll(DependencyObject o)
{
return (string)o.GetValue(IgnoreScrollProperty);
}
private static void OnIgnoreScollChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
bool ignoreScoll = (bool)e.NewValue;
UIElement element = d as UIElement;
if (element == null)
return;
if (ignoreScoll)
{
element.PreviewMouseWheel += Element_PreviewMouseWheel;
}
else
{
element.PreviewMouseWheel -= Element_PreviewMouseWheel;
}
}
private static void Element_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
UIElement element = sender as UIElement;
if (element != null)
{
e.Handled = true;
var e2 = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta);
e2.RoutedEvent = UIElement.MouseWheelEvent;
element.RaiseEvent(e2);
}
}
}
А потом в XAML:
<DataGrid ItemsSource="{Binding Items}">
<DataGrid.RowDetailsTemplate>
<DataTemplate>
<ListView ItemsSource="{Binding Results}"
behaviours:IgnoreScrollBehaviour.IgnoreScroll="True">
<ListView.ItemTemplate>
<DataTemplate>
...
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</DataTemplate>
</DataGrid.RowDetailsTemplate>
<DataGrid.Columns>
...
</DataGrid.Columns>
</DataGrid>
Спасибо Кейл
Я адаптировал ваш ответ как метод расширения RX
public static IDisposable ScrollsParent(this ItemsControl itemsControl)
{
return Observable.FromEventPattern<MouseWheelEventHandler, MouseWheelEventArgs>(
x => itemsControl.PreviewMouseWheel += x,
x => itemsControl.PreviewMouseWheel -= x)
.Subscribe(e =>
{
if(!e.EventArgs.Handled)
{
e.EventArgs.Handled = true;
var eventArg = new MouseWheelEventArgs(e.EventArgs.MouseDevice, e.EventArgs.Timestamp, e.EventArgs.Delta)
{
RoutedEvent = UIElement.MouseWheelEvent,
Source = e.Sender
};
var parent = ((Control)e.Sender).Parent as UIElement;
parent.RaiseEvent(eventArg);
}
});
}
Использование:
myList.ScrollsParent().DisposeWith(disposables);
Мой вариант использования был немного другим. У меня очень большой просмотрщик прокруток, а внизу еще один просмотрщик прокруток, максимальная высота которого составляет 600. Я хочу прокрутить всю страницу до конца, пока не передам скроллвент для внутреннего просмотра прокрутки. Это гарантирует, что вы сначала увидите весь scrollviewer, прежде чем начнете прокручивать.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interactivity;
using System.Windows.Media;
namespace CleverScroller.Helper
{
public class ScrollParentWhenAtMax : Behavior<FrameworkElement>
{
protected override void OnAttached()
{
base.OnAttached();
this.AssociatedObject.PreviewMouseWheel += PreviewMouseWheel;
}
protected override void OnDetaching()
{
this.AssociatedObject.PreviewMouseWheel -= PreviewMouseWheel;
base.OnDetaching();
}
private void PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
if (e.Delta < 0)
{
var outerscroller = GetVisualParent<ScrollViewer>(this.AssociatedObject);
if (outerscroller.ContentVerticalOffset < outerscroller.ScrollableHeight)
{
e.Handled = true;
var e2 = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta);
e2.RoutedEvent = UIElement.MouseWheelEvent;
AssociatedObject.RaiseEvent(e2);
}
}
else
{
var scrollViewer = GetVisualChild<ScrollViewer>(this.AssociatedObject);
var scrollPos = scrollViewer.ContentVerticalOffset;
if ((scrollPos == scrollViewer.ScrollableHeight && e.Delta < 0)
|| (scrollPos == 0 && e.Delta > 0))
{
e.Handled = true;
var e2 = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta);
e2.RoutedEvent = UIElement.MouseWheelEvent;
AssociatedObject.RaiseEvent(e2);
}
}
}
private static T GetVisualChild<T>(DependencyObject parent) where T : Visual
{
T child = default(T);
int numVisuals = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < numVisuals; i++)
{
Visual v = (Visual)VisualTreeHelper.GetChild(parent, i);
child = v as T;
if (child == null)
{
child = GetVisualChild<T>(v);
}
if (child != null)
{
break;
}
}
return child;
}
private static T GetVisualParent<T>(DependencyObject parent) where T : Visual
{
T obj = default(T);
Visual v = (Visual)VisualTreeHelper.GetParent(parent);
do
{
v = (Visual)VisualTreeHelper.GetParent(v);
obj = v as T;
} while (obj == null);
return obj;
}
}
}
Хорошо, я давно был на SO, но мне пришлось это прокомментировать. Любые предварительные туннели событий, так почему же мы его запутываем? Остановите туннель у родителя и покончите с этим. в родительском добавить событие PreviewMouseWheel.
private void UIElement_OnPreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
var scrollViewer = FindName("LeftPanelScrollViwer"); // name your parent mine is a scrollViewer
((ScrollViewer) scrollViewer)?.ScrollToVerticalOffset(e.Delta);
e.Handled = true;
}