You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 19:53:10