WPF DataGrid鼠标滚轮滚动父容器异常及兼容优化问题
WPF DataGrid 滚动穿透问题解决
需求场景
自定义DataGrid需满足:
- 未设置
MaxHeight时,鼠标在DataGrid内滚动可带动外层父ScrollViewer整体滚动(当前需移出DataGrid才能滚动父容器) - 设置
MaxHeight后,DataGrid自身可正常滚动,滚动到边界时再触发父容器滚动
现有尝试及问题
- 修改
ScrollViewer.CanContentScroll="False"无效 - 给DataGrid添加
PreviewMouseWheel事件手动滚动父容器,无效 - 实现
ScrollViewerCorrector附加属性后,无MaxHeight时正常,但设置MaxHeight后DataGrid自身无法滚动
现有代码
DataGrid样式及布局XAML
<Style TargetType="{x:Type DataGrid}"> <Setter Property="Background" Value="Transparent" /> <Setter Property="VerticalAlignment" Value="Top" /> <Setter Property="RowDetailsVisibilityMode" Value="Collapsed" /> <Setter Property="AutoGenerateColumns" Value="False" /> <Setter Property="IsReadOnly" Value="True" /> <Setter Property="HeadersVisibility" Value="None" /> <Setter Property="VerticalContentAlignment" Value="center" /> <Setter Property="GridLinesVisibility" Value="None" /> <Setter Property="EnableRowVirtualization" Value="False" /> <Setter Property="EnableColumnVirtualization" Value="True" /> <Setter Property="VirtualizingPanel.ScrollUnit" Value="Pixel" /> <Setter Property="VirtualizingPanel.VirtualizationMode" Value="Standard" /> <Setter Property="VirtualizingStackPanel.IsVirtualizing" Value="True" /> <Setter Property="ScrollViewer.CanContentScroll" Value="False" /> <Setter Property="BorderBrush" Value="Transparent" /> </Style> <ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition /> </Grid.RowDefinitions> <!-- 其他组件 --> <DataGrid ItemsSource="{Binding ListOfUsers}" PreviewMouseWheel="ListView_PreviewMouseWheel"> <!-- 自定义行 --> </DataGrid> </ScrollViewer>
后台事件代码
private void ListView_PreviewMouseWheel(object sender, MouseWheelEventArgs e) { // 查找包含当前ListView的父ScrollViewer ScrollViewer scrollViewer = FindParentScrollViewer(PatientsListView); if (scrollViewer != null) { // 根据鼠标滚轮增量滚动父ScrollViewer scrollViewer.ScrollToVerticalOffset(scrollViewer.VerticalOffset - e.Delta); e.Handled = true; // 标记事件已处理 } } private ScrollViewer FindParentScrollViewer(DependencyObject child) { if (child is null) return null; for (DependencyObject parent = VisualTreeHelper.GetParent(child); parent != null; parent = VisualTreeHelper.GetParent(parent)) { if (parent is ScrollViewer scrollViewer) return scrollViewer; } return null; }
已实现的ScrollViewerCorrector附加属性
using System; using System.Collections.Generic; using System.Windows; using System.Windows.Controls; using System.Windows.Input; public class ScrollViewerCorrector { public static bool GetFixScrolling(DependencyObject obj) { return (bool)obj.GetValue(FixScrollingProperty); } public static void SetFixScrolling(DependencyObject obj, bool value) { obj.SetValue(FixScrollingProperty, value); } public static readonly DependencyProperty FixScrollingProperty = DependencyProperty.RegisterAttached( "FixScrolling", typeof(bool), typeof(ScrollViewerCorrector), new FrameworkPropertyMetadata(false, OnFixScrollingPropertyChanged)); private static List<MouseWheelEventArgs> _reentrantList = new List<MouseWheelEventArgs>(); private static void OnFixScrollingPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) { if (sender is ScrollViewer viewer) { if ((bool)e.NewValue) { viewer.PreviewMouseWheel += HandlePreviewMouseWheel; } else { viewer.PreviewMouseWheel -= HandlePreviewMouseWheel; } } } private static void HandlePreviewMouseWheel(object sender, MouseWheelEventArgs e) { var scrollControl = sender as ScrollViewer; if (!e.Handled && scrollControl != null && !_reentrantList.Contains(e)) { var previewEventArg = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta) { RoutedEvent = UIElement.PreviewMouseWheelEvent, Source = sender }; var originalSource = e.OriginalSource as UIElement; _reentrantList.Add(previewEventArg); originalSource.RaiseEvent(previewEventArg); _reentrantList.Remove(previewEventArg); 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) { RoutedEvent = UIElement.MouseWheelEvent, Source = sender }; var parent = (UIElement)((FrameworkElement)sender).Parent; parent.RaiseEvent(eventArg); } } } }
附加属性应用XAML
<Window x:Class="YourNamespace.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:YourNamespace" Title="MainWindow" Height="423" Width="596"> <Window.Resources> <Style TargetType="{x:Type ScrollViewer}"> <Setter Property="local:ScrollViewerCorrector.FixScrolling" Value="True" /> </Style> </Window.Resources> <Grid> <ScrollViewer> <StackPanel> <TextBlock>Content before ListView</TextBlock> <ScrollViewer Name="InnerScrollViewer" Height="235"> <StackPanel> <TextBlock>Scrollable Content 1</TextBlock> <TextBlock>Scrollable Content 2</TextBlock> <TextBlock>Scrollable Content 3</TextBlock> <ListView Background="Red" ItemsSource="{Binding PatientsNotVerified}"> <ListView.ItemTemplate> <DataTemplate> <StackPanel HorizontalAlignment="Right"> <TextBlock> <TextBlock.Text> <MultiBinding StringFormat="{}{0} {1}"> <Binding Path="FirstName" /> <Binding Path="LastName" /> </MultiBinding> </TextBlock.Text> </TextBlock> <TextBlock> <TextBlock.Text> <MultiBinding StringFormat="{}{0}, {1}"> <Binding Path="Email" /> <Binding Path="Cell" /> </MultiBinding> </TextBlock.Text> </TextBlock> </StackPanel> </DataTemplate> </ListView.ItemTemplate> </ListView> </StackPanel> </ScrollViewer> <TextBlock>Content after ListView</TextBlock> </StackPanel> </ScrollViewer> </Grid> </Window>
解决方案调整
问题出在ScrollViewerCorrector的逻辑上,当前逻辑直接拦截了内部ScrollViewer的滚动事件,导致设置MaxHeight后DataGrid自身无法滚动。需修改逻辑:仅当内部ScrollViewer已经滚动到边界时,才将事件冒泡给父容器,否则让内部ScrollViewer正常处理滚动。
修改HandlePreviewMouseWheel方法
private static void HandlePreviewMouseWheel(object sender, MouseWheelEventArgs e) { var scrollControl = sender as ScrollViewer; if (scrollControl == null || e.Handled || _reentrantList.Contains(e)) return; // 判断内部ScrollViewer是否还能继续滚动 bool canScrollInternally = false; if (e.Delta > 0) { // 向上滚动:未到顶部则可内部滚动 canScrollInternally = scrollControl.VerticalOffset > 0; } else { // 向下滚动:未到底部则可内部滚动 canScrollInternally = scrollControl.VerticalOffset < scrollControl.ExtentHeight - scrollControl.ViewportHeight; } if (canScrollInternally) { // 内部可滚动,不拦截事件 return; } // 内部无法滚动,将事件冒泡给父容器 e.Handled = true; var eventArg = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta) { RoutedEvent = UIElement.MouseWheelEvent, Source = sender }; var parent = (UIElement)((FrameworkElement)sender).Parent; parent.RaiseEvent(eventArg); }
调整附加属性应用范围
避免全局设置所有ScrollViewer,仅给DataGrid内部的ScrollViewer应用附加属性,修改DataGrid样式:
<Style TargetType="{x:Type DataGrid}"> <!-- 原有样式设置 --> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type DataGrid}"> <Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="True"> <ScrollViewer x:Name="DG_ScrollViewer" Focusable="false" local:ScrollViewerCorrector.FixScrolling="True"> <ScrollViewer.Template> <ControlTemplate TargetType="{x:Type ScrollViewer}"> <Grid x:Name="Grid" Background="{TemplateBinding Background}"> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> <ColumnDefinition Width="Auto"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <Button Command="{x:Static DataGrid.SelectAllCommand}" Focusable="false" Style="{DynamicResource {ComponentResourceKey ResourceId=DataGridSelectAllButtonStyle, TypeInTargetAssembly={x:Type DataGrid}}}" Visibility="{Binding HeadersVisibility, ConverterParameter={x:Static DataGridHeadersVisibility.All}, Converter={x:Static DataGrid.HeadersVisibilityConverter}, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}" Width="{Binding CellsPanelHorizontalOffset, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"/> <DataGridColumnHeadersPresenter x:Name="PART_ColumnHeadersPresenter" Grid.Column="1" Visibility="{Binding HeadersVisibility, ConverterParameter={x:Static DataGridHeadersVisibility.Column}, Converter={x:Static DataGrid.HeadersVisibilityConverter}, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"/> <ScrollContentPresenter x:Name="PART_ScrollContentPresenter" CanContentScroll="{TemplateBinding CanContentScroll}" Grid.ColumnSpan="2" Grid.Row="1"/> <ScrollBar x:Name="PART_VerticalScrollBar" Grid.Column="2" Maximum="{TemplateBinding ScrollableHeight}" Orientation="Vertical" Grid.Row="1" Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}" Value="{Binding VerticalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}" ViewportSize="{TemplateBinding ViewportHeight}"/> <Grid Grid.Column="1" Grid.Row="2"> <Grid.ColumnDefinitions> <ColumnDefinition Width="{Binding NonFrozenColumnsViewportHorizontalOffset, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <ScrollBar x:Name="PART_HorizontalScrollBar" Grid.Column="1" Maximum="{TemplateBinding ScrollableWidth}" Orientation="Horizontal" Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}" Value="{Binding HorizontalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}" ViewportSize="{TemplateBinding ViewportWidth}"/> </Grid> </Grid> </ControlTemplate> </ScrollViewer.Template> <ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/> </ScrollViewer> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style>
调整后效果:
- 未设置
MaxHeight时,内部ScrollViewer无法滚动,事件直接冒泡给父容器,实现整体滚动 - 设置
MaxHeight后,内部ScrollViewer正常滚动,仅在滚动到边界时触发父容器滚动
内容的提问来源于stack exchange,提问作者Thi Nguyen
相关产品推荐
相关产品推荐

