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

WPF DataGrid选择矩形滚动时不跟随内容移动问题求助

WPF DataGrid 选择矩形随滚动失效问题解决

我在WPF DataGrid中复刻Windows文件资源管理器的选择矩形功能,通过Adorner实现了框选逻辑,但滚动时出现异常:当DataGrid自带的ScrollViewer滚动内容时,选择矩形保持固定不动,导致原本与矩形相交的行因滚动偏移被错误取消选择,矩形像是悬浮在DataGrid控件上方,而非绑定到滚动内容上。需要实现像资源管理器那样,选择矩形随DataGrid内容同步滚动的效果。

原始代码

XAML部分

<DataGrid
    Grid.Row="2"
    x:Name="FileDataGrid"
    HeadersVisibility="Column"
    HorizontalAlignment="Stretch"
    VerticalAlignment="Stretch"
    AutoGenerateColumns="False"
    CanUserResizeColumns="True"
    GridLinesVisibility="None"
    IsReadOnly="False"
    HorizontalScrollBarVisibility="Auto"
    PreviewMouseLeftButtonDown="DataGrid_PreviewMouseLeftButtonDown"
    PreviewMouseLeftButtonUp="DataGrid_PreviewMouseLeftButtonUp"
    RowHeight="25"
    Loaded="Grid_Loaded"
    PreviewMouseMove="DataGrid_PreviewMouseMove"
    ScrollViewer.ScrollChanged="FileDataGrid_ScrollChanged"
    SelectionChanged="FileDataGrid_SelectionChanged"
    SelectionMode="Extended">

    <DataGrid.CellStyle>
        <Style TargetType="{x:Type DataGridCell}">
            <Style.Triggers>
                <Trigger Property="DataGridCell.IsSelected" Value="True">
                    <Setter Property="BorderBrush">
                        <Setter.Value>
                            <SolidColorBrush Color="Transparent" />
                        </Setter.Value>
                    </Setter>
                    <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" />
                    <Setter Property="Background">
                        <Setter.Value>
                            <SolidColorBrush Color="Yellow" />
                        </Setter.Value>
                    </Setter>
                </Trigger>
            </Style.Triggers>
        </Style>
    </DataGrid.CellStyle>

    <DataGrid.RowStyle>
        <Style BasedOn="{StaticResource @DataGridRowStyle}" TargetType="{x:Type DataGridRow}">
            <Setter Property="IsHitTestVisible" Value="True" />
            <EventSetter Event="MouseEnter" Handler="DataGridRow_MouseEnter"/>
        </Style>
    </DataGrid.RowStyle>

    <DataGrid.Columns>
        <DataGridTemplateColumn
            x:Name="LeftName"
            Width="1.5*"
            MinWidth="180"
            CanUserResize="True"
            CanUserSort="True"
            Header="Name"
            IsReadOnly="False"
            SortMemberPath="Name">
            <DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <Image
                            Width="17"
                            Height="17"
                            Margin="15,0,-25,0"
                            HorizontalAlignment="Right"
                            Source="Images/sms-mobile.png" />
                        <TextBlock
                            Margin="35,4,0,0"
                            Text="{Binding Name}"
                            ToolTip="{Binding Folder}" />
                    </StackPanel>
                </DataTemplate>
            </DataGridTemplateColumn.CellTemplate>
            <DataGridTemplateColumn.CellEditingTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <Image
                            Width="17"
                            Height="17"
                            Margin="15,0,-25,0"
                            HorizontalAlignment="Right"
                            Source="Images/sms-mobile.png" />
                        <TextBox
                            Width="auto"
                            Height="Auto"
                            Margin="35,0,10,0"
                            Padding="0"
                            HorizontalAlignment="Left"
                            VerticalAlignment="Center"
                            BorderBrush="Blue"
                            BorderThickness="1"
                            Loaded="TextBox_Loaded"
                            Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}" />
                    </StackPanel>
                </DataTemplate>
            </DataGridTemplateColumn.CellEditingTemplate>
        </DataGridTemplateColumn>

        <DataGridTextColumn
            x:Name="sizecolumn"
            MaxWidth="100"
            Binding="{Binding FormattedSize}"
            Header="Size" />

        <DataGridTextColumn
            x:Name="lastModifiedColumn"
            MaxWidth="120"
            Binding="{Binding LastModified}"
            Header="Date Modified " />
    </DataGrid.Columns>      
</DataGrid> 

C#部分

private void DataGrid_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    isleftbuttonpressed = true;
    isSelecting = true;
    selectionStartPoint = e.GetPosition(FileDataGrid);

    adorner = new SelectionAdorner(FileDataGrid, selectionStartPoint);
    var adornerLayer = AdornerLayer.GetAdornerLayer(FileDataGrid);
    adornerLayer.Add(adorner);
    adorner.SetStartPoint(selectionStartPoint);
}

private void DataGrid_PreviewMouseMove(object sender, MouseEventArgs e)
{
    if (e.LeftButton == MouseButtonState.Pressed)
    {
        isleftbuttonpressed = true;
    }

    if (isSelecting && adorner != null)
    {
        adorner.SetEndPoint(e.GetPosition(FileDataGrid));
        var selectionRect = adorner.GetSelectionRect();
        SelectItemsInRectangle(selectionRect, FileDataGrid, sender, e);
    }  
}

private void SelectItemsInRectangle(Rect selectionRect, DataGrid dataGrid, object sender, MouseEventArgs e)
{
    foreach (var item in dataGrid.Items)
    {
        var row = (DataGridRow)dataGrid.ItemContainerGenerator.ContainerFromItem(item);
        if (row != null)
        {
            var rowBounds = GetRowBoundsRelativeToDataGrid(row, dataGrid);
            // 检查行是否与选择矩形相交
            if (selectionRect.IntersectsWith(rowBounds))
            {
                row.IsSelected = true;
            }
            else
            {
                row.IsSelected = false;
            }
        }
    }
}

问题根源

原来的实现将Adorner添加到了DataGrid控件本身的AdornerLayer,但DataGrid的滚动内容实际承载在内部的ScrollContentPresenter控件中,这个Presenter会随ScrollViewer滚动而移动。因此Adorner固定在DataGrid控件层面,无法跟随内容滚动。

解决方案

1. 修改Adorner的挂载目标

将Adorner添加到DataGrid内部的ScrollContentPresenter的AdornerLayer,而非DataGrid本身。

2. 调整坐标计算逻辑

所有鼠标坐标和行边界的计算都基于ScrollContentPresenter的坐标系,确保滚动时坐标匹配。

修改后的代码

新增视觉元素查找辅助方法

private T FindVisualChild<T>(DependencyObject parent) where T : DependencyObject
{
    for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
    {
        var child = VisualTreeHelper.GetChild(parent, i);
        if (child is T tChild)
            return tChild;
        var result = FindVisualChild<T>(child);
        if (result != null)
            return result;
    }
    return null;
}

修改鼠标按下事件

private void DataGrid_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    isleftbuttonpressed = true;
    isSelecting = true;
    
    // 获取DataGrid内部的ScrollContentPresenter
    var scrollContentPresenter = FindVisualChild<ScrollContentPresenter>(FileDataGrid);
    if (scrollContentPresenter == null) return;
    
    // 基于ScrollContentPresenter获取鼠标起始点
    selectionStartPoint = e.GetPosition(scrollContentPresenter);
    
    // 将Adorner挂载到ScrollContentPresenter上
    adorner = new SelectionAdorner(scrollContentPresenter, selectionStartPoint);
    var adornerLayer = AdornerLayer.GetAdornerLayer(scrollContentPresenter);
    adornerLayer.Add(adorner);
    adorner.SetStartPoint(selectionStartPoint);
}

修改鼠标移动事件

private void DataGrid_PreviewMouseMove(object sender, MouseEventArgs e)
{
    if (e.LeftButton == MouseButtonState.Pressed)
    {
        isleftbuttonpressed = true;
    }

    if (isSelecting && adorner != null)
    {
        var scrollContentPresenter = FindVisualChild<ScrollContentPresenter>(FileDataGrid);
        if (scrollContentPresenter == null) return;
        
        // 基于ScrollContentPresenter获取鼠标终点
        adorner.SetEndPoint(e.GetPosition(scrollContentPresenter));
        var selectionRect = adorner.GetSelectionRect();
        SelectItemsInRectangle(selectionRect, FileDataGrid, sender, e);
    }
}

修改行选择判断逻辑

private void SelectItemsInRectangle(Rect selectionRect, DataGrid dataGrid, object sender, MouseEventArgs e)
{
    var scrollContentPresenter = FindVisualChild<ScrollContentPresenter>(dataGrid);
    if (scrollContentPresenter == null) return;
    
    foreach (var item in dataGrid.Items)
    {
        var row = (DataGridRow)dataGrid.ItemContainerGenerator.ContainerFromItem(item);
        if (row != null)
        {
            // 将行的坐标转换为ScrollContentPresenter的相对坐标
            var rowTopLeft = row.TransformToAncestor(scrollContentPresenter).Transform(new Point(0, 0));
            var rowBounds = new Rect(rowTopLeft, new Size(row.ActualWidth, row.ActualHeight));
            
            if (selectionRect.IntersectsWith(rowBounds))
            {
                row.IsSelected = true;
            }
            else
            {
                row.IsSelected = false;
            }
        }
    }
}

确保SelectionAdorner实现正确

public class SelectionAdorner : Adorner
{
    private Point startPoint;
    private Point endPoint;
    private readonly Pen pen = new Pen(Brushes.Blue, 1) { DashStyle = DashStyles.Dash };
    private readonly Brush brush = new SolidColorBrush(Color.FromArgb(50, 0, 0, 255));

    public SelectionAdorner(UIElement adornedElement, Point start) : base(adornedElement)
    {
        startPoint = start;
        endPoint = start;
        IsHitTestVisible = false; // 不干扰DataGrid的鼠标事件
    }

    public void SetStartPoint(Point point)
    {
        startPoint = point;
        InvalidateVisual();
    }

    public void SetEndPoint(Point point)
    {
        endPoint = point;
        InvalidateVisual();
    }

    public Rect GetSelectionRect()
    {
        return new Rect(
            Math.Min(startPoint.X, endPoint.X),
            Math.Min(startPoint.Y, endPoint.Y),
            Math.Abs(startPoint.X - endPoint.X),
            Math.Abs(startPoint.Y - endPoint.Y));
    }

    protected override void OnRender(DrawingContext drawingContext)
    {
        base.OnRender(drawingContext);
        var rect = GetSelectionRect();
        drawingContext.DrawRectangle(brush, pen, rect);
    }
}

效果说明

修改后,选择矩形会跟随ScrollContentPresenter一起滚动,坐标计算始终基于滚动内容的容器,确保滚动时矩形与行的相交判断准确,完全复刻Windows文件资源管理器的框选滚动行为。

内容的提问来源于stack exchange,提问作者Amit

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 07:52:02