我正在尝试在 WPF DataGrid 中复制 Windows 文件资源管理器的选择行为。具体来说,我的目标是实现一个模仿文件资源管理器选择机制功能的选择矩形。目前,我创建了一个装饰矩形来方便选择项目。但是,当我尝试在滚动时选择行时遇到问题。当 DataGrid 的内容在其自己的 ScrollViewer 内滚动时,与选择矩形不相交的行将被取消选择,因为选择矩形是静态的,并且在滚动期间不会随数据网格内容一起移动。就像矩形在空中一样,没有与数据网格粘贴在一起。为了让您更清楚地了解该问题,我在下面提供了图片以供参考。
我尝试了多种解决方案,但没有任何效果。我期望这个矩形在滚动期间应与数据网格内容一起移动,正如我们在 Windows 文件资源管理器中看到的那样。
`<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="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>
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);
// Check if the row intersects with the selection rectangle
if (selectionRect.IntersectsWith(rowBounds))
{
// Select the item and add it to the collection of selected items
row.IsSelected = true;
}
else
{
row.IsSelected = false;
}
}
}
} `
由于
DataGrid
已经支持使用鼠标按下+鼠标移动进行多选,因此您只需处理装饰器剪辑到可滚动区域(ScrollContentPresenter
)。
我建议扩展
DataGrid
并将装饰器处理移至此自定义 DataGrid
。
请参阅完整示例,包括修复的
FileExplorer
(树结构中存在一些错误),位于 GitHub:DataGrid_Drag_Select_Example。
这只是原始代码。您可能必须改进错误方面的行为,并且您想要美化外观。但是,该示例是可用的并且是一个很好的起点。
装饰器.cs
public partial class SelectionAdorner : Adorner
{
private Point startPoint;
private Point endPoint;
public SelectionAdorner(UIElement adornedElement)
: base(adornedElement)
{
this.startPoint = new Point(0, 0);
this.endPoint = new Point(0, 0);
this.IsHitTestVisible = false;
}
protected override void OnRender(DrawingContext drawingContext)
{
var rectangle = new Rect(this.startPoint, this.endPoint);
drawingContext.DrawRectangle(new SolidColorBrush(Colors.LightBlue) { Opacity = 0.5 }, null, rectangle);
}
public void SetStartPoint(Point point)
{
this.startPoint = point;
this.startPoint = ClipLocation(this.startPoint);
InvalidateVisual();
}
public void SetEndPoint(Point point)
{
this.endPoint = point;
this.endPoint = ClipLocation(this.endPoint);
InvalidateVisual();
}
public void Move(double horizontalOffset, double verticalOffset)
{
this.startPoint.Offset(horizontalOffset, verticalOffset);
this.startPoint = ClipLocation(this.startPoint);
InvalidateVisual();
}
// Clip new location:
// Y = 0 < location_Y < adorned_element_height
// X = 0 < location_X < adorned_element_width
private Point ClipLocation(Point point)
{
double horizontalOffset = point.X;
double coercedHorizontalOffset = Math.Min(Math.Max(0, horizontalOffset), ((FrameworkElement)this.AdornedElement).ActualWidth);
double verticalOffset = point.Y;
double coercedVerticalOffset = Math.Min(Math.Max(0, verticalOffset), ((FrameworkElement)this.AdornedElement).ActualHeight);
return new Point(coercedHorizontalOffset, coercedVerticalOffset);
}
}
DragSelectDataGrid.cs
public class DragSelectDataGrid : DataGrid
{
private SelectionAdorner selectionAdorner;
private AdornerLayer adornerLayer;
private double actualRowHeight;
private ScrollViewer scrollViewer;
private ScrollContentPresenter scrollContentPresenter;
private bool canPerfromDragSelect;
public DragSelectDataGrid()
{
this.Loaded += OnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
this.adornerLayer = AdornerLayer.GetAdornerLayer(this);
if (this.adornerLayer == null)
{
throw new InvalidOperationException("No AdornerDecorator found in the parent tree.");
}
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
if (!TryFindVisualChild(this, out this.scrollViewer))
{
throw new InvalidOperationException("No ScrollViewer found");
}
this.scrollViewer.ScrollChanged += OnScrollChanged;
if (!scrollViewer.IsLoaded)
{
this.scrollViewer.Loaded += OnScrollViewerLoaded;
}
else
{
EnsureScrollContentPresenter();
}
}
protected override void OnLoadingRow(DataGridRowEventArgs e)
{
base.OnLoadingRow(e);
if (e.Row.ActualHeight == 0 || this.actualRowHeight == 0)
{
return;
}
this.actualRowHeight = e.Row.ActualHeight;
}
protected override void OnBeginningEdit(DataGridBeginningEditEventArgs e)
{
base.OnBeginningEdit(e);
this.canPerfromDragSelect = false;
}
protected override void OnCellEditEnding(DataGridCellEditEndingEventArgs e)
{
base.OnCellEditEnding(e);
this.canPerfromDragSelect = true;
}
private void OnScrollViewerLoaded(object sender, RoutedEventArgs e)
=> EnsureScrollContentPresenter();
private void EnsureScrollContentPresenter()
{
if (!TryFindVisualChild(this.scrollViewer, out this.scrollContentPresenter))
{
throw new InvalidOperationException("No valid ScrollViewer found");
}
this.scrollContentPresenter.PreviewMouseLeftButtonDown += OnPreviewMouseLeftButtonDown;
this.scrollContentPresenter.PreviewMouseMove += OnPreviewMouseMove;
this.scrollContentPresenter.PreviewMouseLeftButtonUp += OnPreviewMouseLeftButtonUp;
this.selectionAdorner = new SelectionAdorner(this.scrollContentPresenter);
}
private void OnScrollChanged(object sender, ScrollChangedEventArgs e)
{
bool canContentScroll = ScrollViewer.GetCanContentScroll(this);
double verticalOffset = canContentScroll
? this.actualRowHeight * e.VerticalChange
: e.VerticalChange;
double horizontalOffset = e.HorizontalChange;
this.selectionAdorner?.Move(-horizontalOffset, -verticalOffset);
}
private void OnPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (!this.canPerfromDragSelect)
{
return;
}
if (this.actualRowHeight == 0 && this.HasItems)
{
var rowItemContainer = (FrameworkElement)this.ItemContainerGenerator.ContainerFromIndex(0);
this.actualRowHeight = rowItemContainer.ActualHeight;
}
Point selectionStartPoint = e.GetPosition(this.scrollContentPresenter);
this.selectionAdorner.SetStartPoint(selectionStartPoint);
this.selectionAdorner.SetEndPoint(selectionStartPoint);
this.adornerLayer.Add(this.selectionAdorner);
}
private void OnPreviewMouseMove(object sender, MouseEventArgs e)
{
base.OnPreviewMouseMove(e);
if (e.LeftButton != MouseButtonState.Pressed)
{
return;
}
Point selectionEndPoint = e.GetPosition(this.scrollContentPresenter);
this.selectionAdorner.SetEndPoint(selectionEndPoint);
}
private void OnPreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
base.OnPreviewMouseLeftButtonUp(e);
this.adornerLayer.Remove(this.selectionAdorner);
}
private bool TryFindVisualChild<TChild>(DependencyObject parent, out TChild child)
where TChild : DependencyObject
{
child = null;
if (parent is Popup popup)
{
parent = popup.Child;
}
if (parent == null)
{
return false;
}
for (int childElementIndex = 0; childElementIndex < VisualTreeHelper.GetChildrenCount(parent); childElementIndex++)
{
DependencyObject childElement = VisualTreeHelper.GetChild(parent, childElementIndex);
if (childElement is TChild resultChildElement)
{
child = resultChildElement;
return true;
}
if (TryFindVisualChild(childElement, out child))
{
return true;
}
}
return false;
}
}