从 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.WindowsSystem.Windows.InputSystem.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父级鼠标移入移出事件中的子集影响父级的处理方法

获取对话框中视图相对于其父级的位置

如何使用 Swift 从其父级的笔尖创建视图

JS-阻止冒泡事件与事件委托

25.Flutter的ListView监听滚动事件之ScrollController

获取相对于其父级的对话框中的视图位置