Home > Enterprise >  How to make a control snap to a Grid.Row/Grid.Column in WPF at runtime?
How to make a control snap to a Grid.Row/Grid.Column in WPF at runtime?

Time:01-22

I have a grid with some ColumnDefinitions and RowDefinitions. What I like to do is drag a control at runtime and have it snap to a given GridColumn/GridRow when the control is over that GridColumn/GridRow. I was not able to find any resources on this. Perhaps I am using the wrong key words. Thanks in advance!

CodePudding user response:

The short answer is to put that control inside something which fills that cell. You could just put it in that grid cell by adding it to the grid children and setting grid row and column attached properties but there is a gotcha.

A grid cell is sort of conceptual.

The grid looks at it's content, looks at it's definitions for rows and columns and works out where to put it's content using measure arrange passes.

Which is a wordy way of saying there's nothing there to drag your control into.

You need a drop target to drag drop anything into. As it's name suggests, you need some sort of a receptacle for the thing you are dragging.

Wpf, however has these things called content controls.

A button actually inherits from content control to allow it to have things like a string in it.

There is also a content control itself. Which is just kind of like a receptacle for something or other.

One of these things can be used in a given cell as a sort of a place holder. And then you have something in a cell that you can drop into.

I think if you just throw a contentcontrol in a grid without anything inside it you might have problems hit testing.

Some experimentation in a scratch project is advisable.

But basically you could have something like:

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <Rectangle Fill="Red"
                   Name="DraggAbleThing"
                   MouseMove="DraggAbleThing_MouseMove"
                   />
        <ContentControl Grid.Row="1"
                        Grid.Column="1"
                        x:Name="BottomRight"
                        AllowDrop="True"
                        >
            <Rectangle Fill="Yellow"/>
        </ContentControl>
    </Grid>

There's a fair bit to implement in order to do drag drop but the idea here is you have something in the bottom right cell which you can drop into. You might have to set ishitestable=false on that yellow rectangle.

I'd have to implement all the drag drop methods to try it out.

If I did and drop works ok then when the contentcontrol gets draggablething dropped into it.

Set the content property of the contentcontrol to draggablething and it is now in the bottom right cell.

It will fill that cell because the grid arranges it's contents to fill whichever logical cell it decides they're "in".

CodePudding user response:

You should extend Grid to handle the drop position. Let the Grid add the dropped element to the appropriate cell.

The following simple but working example shows how to enable to drag any UIElement from a Panel such as StackPanel or Grid to the custom DrockingGrid. The custom Grid simply overrides the relevant drag&drop overrides. It's a minimal but working example, therefore only OnDragEnter and OnDrop are overriden:

DockingGrid.cs

public class DockingGrid : Grid
{
  private bool AcceptsDrop { get; set; }

  public DockingGrid()
  {
    this.AllowDrop = true;

    // To disable grid lines, set to false (or remove the line)
    this.ShowGridLines = true;
  }

  protected override void OnDragEnter(DragEventArgs e)
  {
    base.OnDragEnter(e);
    this.AcceptsDrop = e.Data.GetDataPresent(typeof(UIElement));
  }

  protected override void OnDrop(DragEventArgs e)
  {
    base.OnDrop(e);
    if (!this.AcceptsDrop)
    {
      return;
    }

    var droppedElement = e.Data.GetData(typeof(UIElement)) as UIElement;
    if (!TryRemoveDroppedElementFromSourceContainer(droppedElement))
    {
      return;
    }

    _ = this.Children.Add(droppedElement);

    Point dropPosition = e.GetPosition(this);
    SetColumn(droppedElement, dropPosition.X);
    SetRow(droppedElement, dropPosition.Y);
  }

  private void SetRow(UIElement? droppedElement, double verticalOffset)
  {
    double totalRowHeight = 0;
    int targetRowIndex = 0;
    foreach (RowDefinition? rowDefinition in this.RowDefinitions)
    {
      totalRowHeight  = rowDefinition.ActualHeight;
      if (totalRowHeight >= verticalOffset)
      {
        Grid.SetRow(droppedElement, targetRowIndex);
        break;
      }

      targetRowIndex  ;
    }
  }

  private void SetColumn(UIElement? droppedElement, double horizontalOffset)
  {
    double totalColumnWidth = 0;
    int targetColumntIndex = 0;
    foreach (ColumnDefinition? columnDefinition in this.ColumnDefinitions)
    {
      totalColumnWidth  = columnDefinition.ActualWidth;
      if (totalColumnWidth >= horizontalOffset)
      {
        Grid.SetColumn(droppedElement, targetColumntIndex);
        break;
      }

      targetColumntIndex  ;
    }
  }

  // This implementation only supports dragging out of a Panel.
  // It is easily extensible to support other parent hosts or complex scenarios.
  private bool TryRemoveDroppedElementFromSourceContainer(UIElement droppedElement)
  {
    Panel container = VisualTreeHelper.GetParent(droppedElement) as Panel;
    container?.Children.Remove(droppedElement);
    return container is not null;
  }
}

Usage

Use it like a normal Grid.

<local:DockingGrid>
  <Grid.ColumnDefinitions>
    <ColumnDefinition Width="100" />
    <ColumnDefinition Width="200" />
    <ColumnDefinition />
  </Grid.ColumnDefinitions>

  <Grid.RowDefinitions>
    <RowDefinition Height="100" />
    <RowDefinition Height="300" />
    <RowDefinition />
  </Grid.RowDefinitions>
</local:DockingGrid>

In the parent host of the drag&drop context for example the WIndow, enable drag behavior: MainWindow.xaml.cs

partial class MainWindow : Window
{
  protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e)
  {
    base.OnPreviewMouseLeftButtonDown(e);

    if (e.Source is UIElement uIElement)
    {
      _ = DragDrop.DoDragDrop(uIElement, new DataObject(typeof(UIElement), uIElement), DragDropEffects.Copy);
    }
  }
}

See Microsoft Docs: Drag and Drop Overview to learn more about the feature.

  • Related