Selection using Selection Rectangle
Can anyone please tell me how to make this kind of behaviour in our own WPF app. Right now i am making a File explorer kind of behaviour using the WPF datagrid. Can someone please help me with the logic or post some code.
Here if we see the rows are getting selected using rectangle and if we scroll upwards then it also autoscroll. Can someone please tell me how can i achieve this kind of behaviour.
Windows
Windows Presentation Foundation
C#
-
Hui Liu-MSFT 48,256 Reputation points • Microsoft Vendor
2024-04-24T06:13:39.29+00:00 Hi,@Amit.Welcom to Microsoft Q&A. I tested the solution here and it works great. You could refer to it.
-
Amit 20 Reputation points
2024-04-24T06:29:32.3066667+00:00 Hi @Hui Liu yes that's me only who asked the question there. In my case i am not able to start the drawing the rectangle from them empty space in the datagrid and also the autoscroll behaviour is missing there when i start the selection from the empty space in the datagrid. So please help me in this.
-
Amit 20 Reputation points
2024-04-24T06:56:32.1433333+00:00 Here this code https://github.com/BionicCodeStackoverflow/DataGrid_Drag_Select_Example is working fine when we want to select from inside the rows. Can anyone please help me how to achieve the autoscroll behaviour when we start selecting from outside the rows(empty space in the datagrid). And also how to draw the rectangle when from empty space in the datagrid and select rows using that.
-
Hui Liu-MSFT 48,256 Reputation points • Microsoft Vendor
2024-04-24T07:43:31.1233333+00:00 Hi,@Amit. I stretch the window to expand the control's width. Then I click on an empty space on the name column and drag, and it scrolls to a row that I didn't see before. Are my test results not what you need? What did I miss? Could you share a screenshot to show the actual results and describe the expected results?
-
Amit 20 Reputation points
2024-04-24T08:06:45.73+00:00 See i moved the last column to the left side and starting drawing rectangle. Now you can see i am not able to select the rows. But the same thing if you see in my question can be achieved with MS file explorer. And one more thing you can test as if we draw the rectangle from outside the rows (empty space) then the datagrid is not autoscrolled. Can you help me with these things?
-
Amit 20 Reputation points
2024-04-24T09:59:06.5533333+00:00 is it possible to check what all rows are under this selection rectangle ? So whatever rows that come under rectangle we will make them selected otherwise will make them unselected.
is it possible? is it a right approach? -
Amit 20 Reputation points
2024-04-24T16:32:49.67+00:00 namespace DataGridDragSelectExample { using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.TextFormatting; using System.Windows.Threading; public class DragSelectDataGrid : DataGrid { private readonly SelectionAdorner selectionAdorner; private AdornerLayer adornerLayer; private double actualRowHeight; private ScrollViewer scrollViewer; private bool canPerfromDragSelect; private readonly HashSet<object> dragSelectedItemsTable; private readonly Stack<object> dragSelectedItemsHistory; private Point selectionStartPoint; private Point oldMousePosition; private DataGridColumnHeadersPresenter PART_ColumnHeadersPresenter; private ScrollBar PART_HorizontalScrollBar; private ScrollBar PART_VerticalScrollBar; private bool isVisualDragSelectActive; private bool isCustomSelectionActive; private ScrollDirection autoScrollDirection; private const int AutoScrollStep = 1; private readonly DispatcherTimer autoScrollTimer; internal enum ScrollDirection { Undefined = 0, Up, Down } public DragSelectDataGrid() { this.selectionAdorner = new SelectionAdorner(this); this.SelectionMode = DataGridSelectionMode.Extended; this.Loaded += OnLoaded; this.canPerfromDragSelect = true; this.dragSelectedItemsTable = new HashSet<object>(); this.dragSelectedItemsHistory = new Stack<object>(); this.autoScrollTimer = new DispatcherTimer(DispatcherPriority.Background, this.Dispatcher) { Interval = TimeSpan.FromMilliseconds(500) }; this.Unloaded += OnUnloaded; } private void OnUnloaded(object sender, RoutedEventArgs e) { this.autoScrollTimer.Stop(); this.autoScrollTimer.Tick -= AutoScrollTimer_Tick; } 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 typedChild) { return typedChild; } else { T foundChild = FindVisualChild<T>(child); if (foundChild != null) { return foundChild; } } } return null; } private void OnLoaded(object sender, RoutedEventArgs e) { var scrollViewer = FindVisualChild<ScrollViewer>(this); if (scrollViewer != null) { // Find the ScrollContentPresenter in the ScrollViewer's visual tree var scrollContentPresenter = FindVisualChild<ScrollContentPresenter>(scrollViewer); if (scrollContentPresenter != null) { // Subscribe to the MouseLeave event scrollContentPresenter.MouseLeave += OnScrollContentPresenterMouseLeave; scrollContentPresenter.MouseEnter += OnScrollContentPresenterMouseEnter; } } Window parentWindow = Window.GetWindow(this); // Track global mouse up to ensure the unloading of the adorner parentWindow.AddHandler(PreviewMouseLeftButtonUpEvent, new MouseButtonEventHandler(OnWindowPreviewMouseLeftButtonUp)); this.adornerLayer = AdornerLayer.GetAdornerLayer(this); if (this.adornerLayer == null) { throw new InvalidOperationException("No AdornerDecorator found in the parent tree."); } EnsureScrollViewer(); } private void EnsureScrollViewer() { if (this.scrollViewer is null) { StopAutoScroll(); throw new InvalidOperationException("No ScrollViewer found"); } if (!this.scrollViewer.CanContentScroll) { StopAutoScroll(); throw new NotSupportedException("ScrollViewer.CanContentScroll must be set to TRUE."); } } private void OnScrollContentPresenterMouseLeave(object sender, MouseEventArgs e) { var scrollContentPresenter = (ScrollContentPresenter)sender; Rect scrollContentPresenterBounds = LayoutInformation.GetLayoutSlot(scrollContentPresenter); var currentMousePosition = e.GetPosition(scrollContentPresenter); if (currentMousePosition.Y >= scrollContentPresenterBounds.Bottom && e.LeftButton==MouseButtonState.Pressed) { StartAutoScroll(ScrollDirection.Down); } else { if (e.LeftButton == MouseButtonState.Pressed) StartAutoScroll(ScrollDirection.Up); } } private void OnScrollContentPresenterMouseEnter(object sender, MouseEventArgs e) => StopAutoScroll(); private void StartAutoScroll(ScrollDirection scrollDirection) { EnsureScrollViewer(); if (!CanAutoScroll()) { return; } this.autoScrollDirection = scrollDirection; this.autoScrollTimer.Tick += AutoScrollTimer_Tick; this.autoScrollTimer.Start(); } private void StopAutoScroll() { this.autoScrollTimer.Stop(); this.autoScrollTimer.Tick -= AutoScrollTimer_Tick; } private void AutoScrollTimer_Tick(object sender, EventArgs e) { if (!CanAutoScroll()) { StopAutoScroll(); return; } double newVerticalOffset = 0; switch (this.autoScrollDirection) { case ScrollDirection.Up: newVerticalOffset = this.scrollViewer.VerticalOffset - DragSelectDataGrid.AutoScrollStep; this.scrollViewer.ScrollToVerticalOffset(newVerticalOffset); break; case ScrollDirection.Down: newVerticalOffset = this.scrollViewer.VerticalOffset + DragSelectDataGrid.AutoScrollStep; this.scrollViewer.ScrollToVerticalOffset(newVerticalOffset); break; default: throw new NotImplementedException(); } this.scrollViewer.ScrollToVerticalOffset(newVerticalOffset); var currentRowContainer = (DataGridRow)this.ItemContainerGenerator.ContainerFromIndex((int)newVerticalOffset); currentRowContainer.IsSelected = true; // TODO::Store currentRowContainer in Stack and HashSet } private bool CanAutoScroll() { EnsureScrollViewer(); if ((this.autoScrollDirection is ScrollDirection.Up && this.scrollViewer.VerticalOffset == 0) || (this.autoScrollDirection is ScrollDirection.Down && this.scrollViewer.VerticalOffset >= this.scrollViewer.ScrollableHeight)) { return false; } return true; } public override void OnApplyTemplate() { base.OnApplyTemplate(); if (!this.TryFindVisualChild(out this.scrollViewer)) { throw new InvalidOperationException($"No {typeof(ScrollViewer).FullName} found!"); } this.scrollViewer.ScrollChanged += OnScrollChanged; if (this.scrollViewer.IsLoaded) { EnsureDataGridColumnHeadersPresenter(); } else { this.scrollViewer.Loaded += OnScrollViewerLoaded; } } private void OnScrollViewerLoaded(object sender, RoutedEventArgs e) { _ = this.scrollViewer.TryFindVisualChildElementByName(nameof(this.PART_HorizontalScrollBar), out this.PART_HorizontalScrollBar); _ = this.scrollViewer.TryFindVisualChildElementByName(nameof(this.PART_HorizontalScrollBar), out this.PART_VerticalScrollBar); EnsureDataGridColumnHeadersPresenter(); } private void EnsureDataGridColumnHeadersPresenter() { if (!this.scrollViewer.TryFindVisualChild(out this.PART_ColumnHeadersPresenter)) { throw new InvalidOperationException($"No {typeof(DataGridColumnHeadersPresenter).FullName} found!"); } } protected override void OnPreviewMouseDown(MouseButtonEventArgs e) { base.OnPreviewMouseDown(e); this.isCustomSelectionActive = this.canPerfromDragSelect; } protected override void OnPreviewMouseMove(MouseEventArgs e) { base.OnPreviewMouseMove(e); if (e.LeftButton != MouseButtonState.Pressed || this.isCustomSelectionActive) { return; } Point mousePosition = e.GetPosition(this); Rect selectionBounds = GetSelectionBounds(); // Don't draw the adorner on the column headers if (!selectionBounds.Contains(mousePosition)) { return; } if (this.isVisualDragSelectActive) { Point selectionEndPoint = mousePosition; UpdateVisualDragSelect(selectionEndPoint); } else { this.selectionStartPoint = mousePosition; StartVisualDragSelect(this.selectionStartPoint); } } protected override void OnBeginningEdit(DataGridBeginningEditEventArgs e) { base.OnBeginningEdit(e); this.canPerfromDragSelect = false; } protected override void OnCellEditEnding(DataGridCellEditEndingEventArgs e) { base.OnCellEditEnding(e); this.canPerfromDragSelect = true; } protected override void PrepareContainerForItemOverride(DependencyObject element, object item) { base.PrepareContainerForItemOverride(element, item); if (element is UIElement uIElement) { uIElement.MouseEnter += OnDataGridRowMouseEnter; uIElement.PreviewMouseLeftButtonDown += OnDataGridRowPreviewMouseLeftButtonDown; } } protected override void ClearContainerForItemOverride(DependencyObject element, object item) { base.ClearContainerForItemOverride(element, item); if (element is UIElement uIElement) { uIElement.MouseEnter -= OnDataGridRowMouseEnter; uIElement.PreviewMouseLeftButtonDown -= OnDataGridRowPreviewMouseLeftButtonDown; } } protected override void OnSelectionChanged(SelectionChangedEventArgs e) { base.OnSelectionChanged(e); if (!this.isCustomSelectionActive) { this.dragSelectedItemsTable.Clear(); this.dragSelectedItemsHistory.Clear(); foreach (object item in this.SelectedItems) { this.dragSelectedItemsHistory.Push(item); this.dragSelectedItemsTable.Add(item); } } } protected override void OnLoadingRow(DataGridRowEventArgs e) { base.OnLoadingRow(e); if (e.Row.ActualHeight == 0 || this.actualRowHeight == 0) { return; } // Get the lates row height. Row height is relevant when CanContentScroll is TRUE // in order to convert the actual scroll offset from item to pixel. this.actualRowHeight = e.Row.ActualHeight; } private void OnDataGridRowMouseEnter(object sender, MouseEventArgs e) { // Don't execute selection if the select operation was not triggered outside the DataGrid if (e.LeftButton != MouseButtonState.Pressed || !this.isCustomSelectionActive) { return; } var rowItemContainer = (DataGridRow)sender; HandleItemSelect(rowItemContainer); } private void HandleItemSelect(DependencyObject rowItemContainer) { object rowItem = this.ItemContainerGenerator.ItemFromContainer(rowItemContainer); bool isItemContainerAlreadyVisited = this.dragSelectedItemsTable.Contains(rowItem); // Unselected the DataGridRow that we left before entering the current DataGridRow. // The fact that this DataGridRow was already visited before means the user has changed mouse direction. if (isItemContainerAlreadyVisited) { if (this.dragSelectedItemsTable.Count <= 1) { return; } UnselectItem(); } else { SelectItem(rowItem); } } private void UnselectItem() { object unselectedRowItem = this.dragSelectedItemsHistory.Pop(); _ = this.dragSelectedItemsTable.Remove(unselectedRowItem); this.SelectedItems.Remove(unselectedRowItem); } private void SelectItem(object rowItem) { this.dragSelectedItemsHistory.Push(rowItem); _ = this.dragSelectedItemsTable.Add(rowItem); this.SelectedItems.Add(rowItem); } private void OnDataGridRowPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) { this.isCustomSelectionActive = false; Point selectionStartPoint = e.GetPosition(this); StartVisualDragSelect(selectionStartPoint); } private void UpdateVisualDragSelect(Point selectionEndPoint) { this.selectionAdorner.SelectionBounds = GetSelectionBounds(); this.selectionAdorner.SetEndPoint(selectionEndPoint); } private void OnScrollChanged(object sender, ScrollChangedEventArgs e) { /* Update adorner position when the ScrollViewer was scrolled */ if (this.actualRowHeight == 0) { if (!double.IsNaN(this.RowHeight)) { this.actualRowHeight = this.RowHeight; } else { var rowItemContainer = (FrameworkElement)this.ItemContainerGenerator.ContainerFromIndex((int)this.scrollViewer.VerticalOffset); this.actualRowHeight = rowItemContainer?.ActualHeight ?? 0; } } double verticalOffset = this.scrollViewer.CanContentScroll ? this.actualRowHeight * e.VerticalChange : e.VerticalChange; double horizontalOffset = e.HorizontalChange; this.selectionAdorner?.Move(-horizontalOffset, -verticalOffset); } private void OnWindowPreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e) { StopVisualDragSelect(); this.isCustomSelectionActive = false; } private void ClearSelection() { this.dragSelectedItemsHistory.Clear(); this.dragSelectedItemsTable.Clear(); this.SelectedItems.Clear(); } private void StopVisualDragSelect() { this.adornerLayer.Remove(this.selectionAdorner); this.isVisualDragSelectActive = false; } private void StartVisualDragSelect(Point selectionStartPoint) { // For example, do not perform a select while any row is in edit mode if (!this.canPerfromDragSelect) { return; } this.selectionAdorner.SelectionBounds = GetSelectionBounds(); this.selectionAdorner.SetStartPoint(selectionStartPoint); this.selectionAdorner.SetEndPoint(selectionStartPoint); this.adornerLayer.Add(this.selectionAdorner); this.isVisualDragSelectActive = true; } private Rect GetSelectionBounds() { Rect bounds = LayoutInformation.GetLayoutSlot(this); double columnHeaderHeight = double.IsNaN(this.ColumnHeaderHeight) ? this.PART_ColumnHeadersPresenter.ActualHeight : this.ColumnHeaderHeight; // Bounds position must be relative to 'this' and not screen bounds.Location = new Point(0, 0); bounds.Offset(0, columnHeaderHeight); bounds.Height = this.ActualHeight - columnHeaderHeight - this.PART_HorizontalScrollBar.ActualHeight; bounds.Width = this.ActualWidth - this.PART_VerticalScrollBar.ActualHeight; return bounds; } } }
-
Amit 20 Reputation points
2024-04-24T16:35:23.95+00:00 I tried above code for autoscroll behaviour. Its working good for upward autoscroll but not for downside autoscroll. Am i missing something? Can someone please help?
-
Amit 20 Reputation points
2024-04-24T17:36:54.68+00:00 Ok please ignore the above comment. I figured out that. The only thing now is if we start selecting from the right side of the datagrid then the selection is not happening. RIght side means as i posted earlier if we compress the last header to left side then neither autoscroll is working nor auto scrolling but MS file explorer support it. Can you please guide me @Hui Liu-MSFT in this code. We can use the Git code as you have suggested and start working on it.
-
Hui Liu-MSFT 48,256 Reputation points • Microsoft Vendor
2024-04-26T09:35:27.5366667+00:00 Hello,@Amit. I'm trying to find a solution and test it in a project to implement the functionality, I'll come back if there are any updates. If you find a solution, please feel free to share it here.
-
Amit 20 Reputation points
2024-04-27T05:11:46.4666667+00:00 Hello @Hui Liu-MSFT i can implement if i get the right logic. I tried various solutions but they are not working as expected.
Sign in to comment