Search code examples
c#wpfdatagridcheckbox

Click and Drag to Multiselect Checkboxes in WPF/C#


The problem:

My application requires a user to be able to select multiple entries in a datagrid via a column of checkboxes. The desired behavior is that when you click on a checkbox in the column, it behaves like a normal checkbox, but if you drag over it while the left mouse button is down, its selection state changes to the opposite of what is was before.

What I have tried so far:

I have tried subclassing CheckBox and handling OnMouseEnter, but the first checkbox that is clicked seems to capture the mouse so no other checkboxes fire the OnMouseEnter event.

I have tried implementing a drag-and-drop hack, where the user clicks to select a checkbox, and then drags that checkbox over the others so the others recieve a DragOver event and can switch states. This solution causes the cursor to display as a circle with a slash when not over another checkbox during the drag and drop, which is not acceptable for this application.

What I would like:

I would like a method to implement a checkbox that has the functionality I describe, ideally in a xaml style or subclass that I can reuse, as this functionality is needed in multiple places in my application.

Is there an elegant way to achieve this effect?


Solution

  • I did this in my application, very handy when you have to select, say, 30 checkBoxes.
    To do this, i handled the preview mouse event myself : PreviewMouseLeftButtonDown, PreviewMouseMove, PreviewMouseLeftButtonUp.

    In PreviewMouseLeftButtonDown : i get the mouse position relative to the control.
    In PreviewMouseMove : i draw a rectangle from start to current position if i am far enough from firstPoint. then i iterate in CheckBoxes, see if they intersect with rectangle, and highlight them if so (so the user know whiwh chexboxes will swap)
    In PreviewMouseLeftButtonUp : i do the swap for intersecting CheckBoxes.

    if it can help you, here's the code i use. it is not MVVM (:-)) but works fine, it might give you ideas. it is an automatic translation from vb.net code.

    To make it work, you need a Canvas on top of your CheckBoxes (=within the same grid cell for instance), with property IsHitTestVisible="False" .
    Within this Canvas, put a Rectangle nammed "SelectionRectangle" with proper fill and stroke, but with 0.0 Opacity.

    // '' <summary>
    // '' When Left Mouse button is pressed, remember where the mouse move start
    // '' </summary>
    private void EditedItems_PreviewMouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e) {
        StartPoint = Mouse.GetPosition(this);
    }
    
    // '' <summary>
    // '' When mouse move, update the highlight of the selected items.
    // '' </summary>
    private void EditedItems_PreviewMouseMove(object sender, System.Windows.Input.MouseEventArgs e) {
        if ((StartPoint == null)) {
            return;
        }
        PointWhereMouseIs = Mouse.GetPosition(this);
        Rect SelectedRect = new Rect(StartPoint, PointWhereMouseIs);
        if (((SelectedRect.Width < 20) 
                    && (SelectedRect.Height < 20))) {
            return;
        }
        //  show the rectangle again
        Canvas.SetLeft(SelectionRectangle, Math.Min(StartPoint.X, PointWhereMouseIs.X));
        Canvas.SetTop(SelectionRectangle, Math.Min(StartPoint.Y, PointWhereMouseIs.Y));
        SelectionRectangle.Width = Math.Abs((PointWhereMouseIs.X - StartPoint.X));
        SelectionRectangle.Height = Math.Abs((PointWhereMouseIs.Y - StartPoint.Y));
        foreach (CheckBox ThisChkBox in EditedItems.Children) {
            object rectBounds = VisualTreeHelper.GetDescendantBounds(ThisChkBox);
            Vector vector = VisualTreeHelper.GetOffset(ThisChkBox);
            rectBounds.Offset(vector);
            if (rectBounds.IntersectsWith(SelectedRect)) {
                ((TextBlock)(ThisChkBox.Content)).Background = Brushes.LightGreen;
            }
            else {
                ((TextBlock)(ThisChkBox.Content)).Background = Brushes.Transparent;
            }
        }
    }
    
    // '' <summary>
    // '' When Left Mouse button is released, change all CheckBoxes values. (Or do nothing if it is a small move -->
    // '' click will be handled in a standard way.)
    // '' </summary>
    private void EditedItems_PreviewMouseLeftButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e) {
        PointWhereMouseIs = Mouse.GetPosition(this);
        Rect SelectedRect = new Rect(StartPoint, PointWhereMouseIs);
        StartPoint = null;
        SelectionRectangle.Opacity = 0;
        //  hide the rectangle again
        if (((SelectedRect.Width < 20) 
                    && (SelectedRect.Height < 20))) {
            return;
        }
        foreach (CheckBox ThisEditedItem in EditedItems.Children) {
            object rectBounds = VisualTreeHelper.GetDescendantBounds(ThisEditedItem);
            Vector vector = VisualTreeHelper.GetOffset(ThisEditedItem);
            rectBounds.Offset(vector);
            if (rectBounds.IntersectsWith(SelectedRect)) {
                ThisEditedItem.IsChecked = !ThisEditedItem.IsChecked;
            }
            ((TextBlock)(ThisEditedItem.Content)).Background = Brushes.Transparent;
        }
    }
    

    Edit : i used that code within a user control. This control takes a list of booleans and a list of strings (caption) as argument, and builds (with a WrapPanel) an array of CheckBoxes having the right caption. And so you can select/unselect with the rectangle, and there are also two buttons to check all/uncheck all. I tried also to keep good column/rows ratio to handle selection of 7 to 200 booleans with a good column/row balance.

    An exemple of use of the BooleanEdit User Control within a Window