从 ListView 到其父级的冒泡滚动事件
Posted
技术标签:
【中文标题】从 ListView 到其父级的冒泡滚动事件【英文标题】:Bubbling scroll events from a ListView to its parent 【发布时间】:2010-12-07 19:31:04 【问题描述】:在我的 WPF 应用程序中,我有一个 ListView
,其 ScrollViewer.VerticalScrollBarVisibility
设置为 Disabled
。它包含在ScrollViewer
中。当我尝试在ListView
上使用鼠标滚轮时,外部ScrollViewer
不会滚动,因为ListView
正在捕获滚动事件。
如何强制ListView
允许滚动事件冒泡到ScrollViewer
?
【问题讨论】:
【参考方案1】:您需要在内部列表视图中捕获预览鼠标滚轮事件
MyListView.PreviewMouseWheel += HandlePreviewMouseWheel;
或者在 XAML 中
<ListView ... PreviewMouseWheel="HandlePreviewMouseWheel">
然后停止滚动列表视图的事件并在父列表视图中引发事件。
private void HandlePreviewMouseWheel(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。
【讨论】:
这点我不敢恭维:***.com/questions/3498686/… 这适用于我的 UserControl,其中有一个 ListView! :D 对我来说也适用于列表框,多亏了这一点,我的滚动查看器现在按预期同步工作,不仅在悬停滚动条本身时,谢谢!【参考方案2】:使用附加行为的另一个不错的解决方案。 我喜欢它,因为它从 Control 中解开了解决方案。
创建一个无滚动行为,它将捕获 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);
然后将行为附加到任何带有嵌套 ScrollViewers 案例的 UIElement
<ListBox Name="ForwardScrolling">
<i:Interaction.Behaviors>
<local:IgnoreMouseWheelBehavior />
</i:Interaction.Behaviors>
</ListBox>
感谢Josh Einstein Blog
【讨论】:
对于使用此解决方案的任何人,您必须确保已添加using System.Windows.Interactivity
命名空间,可以使用 NuGet 包管理器控制台中的命令 Install-Package Expression.Blend.Sdk
将其添加到当前项目中。此外,您必须使用 xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
将命名空间添加到您的 .xaml 文件中。
另外,System.Windows
、System.Windows.Input
和 System.Windows.Controls
。而且我还找到了另一种引用交互程序集的方法:xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
。
从 .NET Core 3 开始,你应该改用Microsoft.Xaml.Behaviors.Wpf
NuGet 包。
从 .NET Core 3 开始,您还应该在 xaml 中使用 xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
。【参考方案3】:
如果您来这里寻找解决方案,仅当孩子位于顶部并向上滚动或底部并向下滚动时才冒泡事件,这里有一个解决方案。我只使用 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>
【讨论】:
效果很好。谢谢。【参考方案4】:根据您的具体情况,有不同的方法,但我发现这种方法效果很好。假设你的基本情况是这样的:
<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>
在 PreviewMouseWheel 期间自己提高 MouseWheelEvent 似乎会强制 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);
【讨论】:
【参考方案5】:您也可以使用附加的行为来实现相同的目的。这具有不需要 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>
【讨论】:
【参考方案6】:我的用例略有不同。我有一个非常大的滚动查看器,底部有另一个最大高度为 600 的滚动查看器。我想将整个页面滚动到底部,直到我将滚动事件传递给内部滚动查看器。 这可确保您在开始滚动之前首先看到整个滚动查看器。
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;
【讨论】:
【参考方案7】:谢谢凯尔
我将您的答案改编为 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);
【讨论】:
【参考方案8】:好吧,自从我参加 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;
【讨论】:
以上是关于从 ListView 到其父级的冒泡滚动事件的主要内容,如果未能解决你的问题,请参考以下文章
javascript父级鼠标移入移出事件中的子集影响父级的处理方法