【问题标题】:Bubbling scroll events from a ListView to its parent从 ListView 到其父级的冒泡滚动事件
【发布时间】:2010-12-07 19:31:04
【问题描述】:

在我的 WPF 应用程序中,我有一个 ListView,其 ScrollViewer.VerticalScrollBarVisibility 设置为 Disabled。它包含在ScrollViewer 中。当我尝试在ListView 上使用鼠标滚轮时,外部ScrollViewer 不会滚动,因为ListView 正在捕获滚动事件。

如何强制ListView 允许滚动事件冒泡到ScrollViewer

【问题讨论】:

    标签: wpf listview scroll event-bubbling


    【解决方案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。

    【讨论】:

    • 这点我不敢恭维:stackoverflow.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;
            }
            

            【讨论】:

              猜你喜欢
              • 2021-09-05
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2015-09-30
              • 1970-01-01
              • 2017-05-02
              • 1970-01-01
              相关资源
              最近更新 更多