WPF如何检测父/子ScrollViewer滚动触发源并实现指定滚动逻辑
实现类似Stack Overflow的父子ScrollViewer滚动交互逻辑
我现在的WPF应用里有一个主ScrollViewer,里面包含多个带内嵌ScrollViewer的DataGrid。参考相关内容已经实现了基础的父子滚动交互,但现在需要改成Stack Overflow的滚动逻辑:
- 当鼠标在DataGrid区域滚动时:只滚动当前DataGrid的内嵌ScrollViewer,滚到边界后不再把事件传递给主ScrollViewer;如果这个DataGrid没有可滚动内容(比如数据很少,不需要滚动),则直接由主ScrollViewer响应滚动。
- 当鼠标在主内容区域(非DataGrid)滚动时:所有子ScrollViewer都不响应,只滚动主ScrollViewer。
我之前尝试过参考一些方案但不适用,考虑过IsFocusable属性但不知道怎么在现有代码里校验。以下是我当前的实现代码:
当前C#实现代码
public class ScrollViewerCorrector { public static void SetApplyScrolling(DependencyObject obj, bool value) { obj.SetValue(ApplyScrollingProperty, value); } public static readonly DependencyProperty ApplyScrollingProperty = DependencyProperty.RegisterAttached("ApplyScrolling", typeof(bool), typeof(ScrollViewerCorrector), new FrameworkPropertyMetadata(false, OnApplyScrollingPropertyChanged)); public static void OnApplyScrollingPropertyChanged(object sender, DependencyPropertyChangedEventArgs e) { var viewer = sender as ScrollViewer; if (viewer == null) return; if ((bool)e.NewValue) viewer.PreviewMouseWheel += OnPreviewMouseWheel; else viewer.PreviewMouseWheel -= OnPreviewMouseWheel; } private static List<MouseWheelEventArgs> _wheelEventArgsList = new List<MouseWheelEventArgs>(); private static void OnPreviewMouseWheel(object sender, MouseWheelEventArgs e) { var scrollControl = sender as ScrollViewer; if (!e.Handled && sender != null && !_wheelEventArgsList.Select(args => (args.Source)).Contains(sender)) { var previewEventArg = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta) { RoutedEvent = UIElement.PreviewMouseWheelEvent, Source = sender }; var originalSource = e.OriginalSource as UIElement; _wheelEventArgsList.Add(previewEventArg); originalSource.RaiseEvent(previewEventArg); _wheelEventArgsList.Remove(previewEventArg); //check if any of children handled the event if (!previewEventArg.Handled && ((e.Delta > 0 && scrollControl.VerticalOffset == 0) || (e.Delta <= 0 && scrollControl.VerticalOffset >= scrollControl.ExtentHeight - scrollControl.ViewportHeight))) { e.Handled = true; var eventArg = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta); eventArg.RoutedEvent = UIElement.MouseWheelEvent; eventArg.Source = sender; var parent = (UIElement)((FrameworkElement)sender).Parent; parent.RaiseEvent(eventArg); } } }
当前XAML代码
<rui:ReactiveUserControl x:TypeArguments="viewModels:MainViewModel" x:Class="AnalyticalReporting.UI.Views.MainView" xmlns:viewModels="clr-namespace:AnalyticalReporting.UI.ViewModels" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:rui="http://reactiveui.net" xmlns:controls="clr-namespace:MNS.Common.UI.Controls;assembly=MNS.Common.UI" mc:Ignorable="d" d:DesignHeight="1024" d:DesignWidth="1280" Background="{DynamicResource WhiteBrush}"> <UserControl.Resources> <ResourceDictionary> <Style TargetType="{x:Type ScrollViewer}"> <Style.Setters> <Setter Property="controls:ScrollViewerCorrector.ApplyScrolling" Value="True" /> </Style.Setters> </Style> <DataTemplate x:Key="ListTemplate"> <StackPanel> <rui:ViewModelViewHost x:Name="DisplayingViewModelViewHost" HorizontalContentAlignment="Stretch" ViewModel="{Binding .}"/> </StackPanel> </DataTemplate> </ResourceDictionary> </UserControl.Resources> <ScrollViewer x:Name="MainScrollViewer" VerticalScrollBarVisibility="Auto" VerticalAlignment="Top"> <ItemsControl x:Name="InfoCardList" ItemTemplate="{StaticResource ListTemplate}" > <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <StackPanel Orientation="Vertical"/> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> </ItemsControl> </ScrollViewer> </rui:ReactiveUserControl>
解决方案:调整滚动事件处理逻辑
我帮你梳理下核心思路并修改代码,关键是要准确识别滚动触发的目标元素,并且严格控制事件的传递路径,确保只有目标区域的ScrollViewer响应:
修改后的ScrollViewerCorrector代码
using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; public class ScrollViewerCorrector { public static void SetApplyScrolling(DependencyObject obj, bool value) { obj.SetValue(ApplyScrollingProperty, value); } public static readonly DependencyProperty ApplyScrollingProperty = DependencyProperty.RegisterAttached("ApplyScrolling", typeof(bool), typeof(ScrollViewerCorrector), new FrameworkPropertyMetadata(false, OnApplyScrollingPropertyChanged)); private static void OnApplyScrollingPropertyChanged(object sender, DependencyPropertyChangedEventArgs e) { var viewer = sender as ScrollViewer; if (viewer == null) return; if ((bool)e.NewValue) viewer.PreviewMouseWheel += OnPreviewMouseWheel; else viewer.PreviewMouseWheel -= OnPreviewMouseWheel; } private static void OnPreviewMouseWheel(object sender, MouseWheelEventArgs e) { var currentScrollViewer = sender as ScrollViewer; if (currentScrollViewer == null || e.Handled) return; // 从原始触发元素向上查找最近的ScrollViewer var originalSource = e.OriginalSource as DependencyObject; var targetScrollViewer = FindClosestScrollViewer(originalSource); // 情况1:滚动触发在子ScrollViewer(比如DataGrid的内嵌ScrollViewer)范围内 if (targetScrollViewer != null && targetScrollViewer == currentScrollViewer) { // 检查当前子ScrollViewer是否还有滚动空间 bool canScrollVertically = (e.Delta > 0 && currentScrollViewer.VerticalOffset > 0) || (e.Delta < 0 && currentScrollViewer.VerticalOffset < currentScrollViewer.ExtentHeight - currentScrollViewer.ViewportHeight); if (canScrollVertically) { // 可以滚动,标记事件已处理,只让当前子ScrollViewer响应 e.Handled = true; } else { // 已经滚到边界,不传递给主ScrollViewer(符合需求) e.Handled = true; } } // 情况2:滚动触发在主ScrollViewer区域,或者没有找到子ScrollViewer else if (IsMainScrollViewer(currentScrollViewer)) { // 主ScrollViewer响应,阻止子级干扰 e.Handled = true; // 手动控制滚动幅度,和默认行为一致 currentScrollViewer.ScrollToVerticalOffset(currentScrollViewer.VerticalOffset - e.Delta / 3.0); } } // 查找当前元素最近的父级ScrollViewer private static ScrollViewer FindClosestScrollViewer(DependencyObject obj) { while (obj != null) { if (obj is ScrollViewer viewer) return viewer; obj = VisualTreeHelper.GetParent(obj); } return null; } // 判断当前ScrollViewer是否是主ScrollViewer(最顶层的那个) private static bool IsMainScrollViewer(ScrollViewer viewer) { var parent = VisualTreeHelper.GetParent(viewer); while (parent != null) { if (parent is ScrollViewer) return false; parent = VisualTreeHelper.GetParent(parent); } return true; } }
代码说明
FindClosestScrollViewer方法:从滚动事件的原始触发元素(比如DataGrid的单元格、主内容的文本块)向上遍历视觉树,找到最近的ScrollViewer,这样就能精准定位滚动触发的目标区域。- 事件处理分支:
- 当触发区域属于子ScrollViewer时:检查是否还有滚动空间,如果有就拦截事件,只让这个子ScrollViewer滚动;如果已经滚到边界,同样拦截事件,不传递给主ScrollViewer,完全符合你的第一个需求。
- 当触发区域属于主ScrollViewer时:直接拦截事件,手动控制主ScrollViewer滚动,同时阻止所有子ScrollViewer响应,满足第二个需求。
IsMainScrollViewer方法:通过遍历父级元素判断当前ScrollViewer是否是最顶层的主ScrollViewer,避免子ScrollViewer误处理主区域的滚动事件。
XAML补充说明
你已经在全局Style里给所有ScrollViewer应用了ApplyScrolling属性,这个设置没问题。如果某个DataGrid不需要这个滚动逻辑,可以单独覆盖它的ScrollViewer样式:
<DataGrid> <DataGrid.Resources> <Style TargetType="ScrollViewer"> <Setter Property="controls:ScrollViewerCorrector.ApplyScrolling" Value="False" /> </Style> </DataGrid.Resources> </DataGrid>
内容的提问来源于stack exchange,提问作者Александра Юреня
相关产品推荐
相关产品推荐

