Snap to grid

Very often Diagram Designers like Visio and others give you the possibility to snap shapes to a grid. In this tutorial I’ll show you, how you can implement a functionality to snap the DesignerItems of the WPF Diagram Designer to the DesignerCanvas Grid. It is designed to work with single DesignerItems and multiple selected items too! Everything is kept generic, so that you should be able to adapt this to other WPF projects as well if needed.

To implement, please follow these steps:

  1. Open the file Shared.xaml in the folder Resources\Styles and insert the following piece of code inside the element ResourceDictionary. It is a tile-brush, which will draw the grid-points onto the DesignerCanvas.
    <VisualBrush x:Key="SnappingGridBrush_Ponits"
    	TileMode="Tile"
    	Viewport="0,0,16,16"
    	ViewportUnits="Absolute"
    	Viewbox="0,0,16,16"
    	ViewboxUnits="Absolute">
    	<VisualBrush.Visual>
    		<Ellipse Fill="#FF000000"
    			Width="2"
    			 Height="2" />
    	</VisualBrush.Visual>
    </VisualBrush>
    
  2. Open the Window1.xaml and search for the DesignerCanvas. Replace the Background-Attribute Background="{StaticResource WindowBackgroundBrush}" with this Background="{StaticResource SnappingGridBrush_Ponits}". If you now start the Diagram Designer, you can already see (as on the screenshot below) the points to which the DesignerItems will snap.
    WPF Diagram Designer with grid snap points
  3. As we have a beautiful grid, we need to snap the DesignerItems to it. To be able to do this, we first need to know the size of our grid-rectangles we’d like to snap to. Open the file DesignerCanvas.cs and add the following method into it, which get’s the rectangle-size out of our VisualBrush.
    private Size GetSnapGridSize()
    {
    	System.Windows.Media.VisualBrush pointBrush = this.Background as System.Windows.Media.VisualBrush;
    
    	return new Size(pointBrush.Viewbox.Width, pointBrush.Viewbox.Height);
    }
    
  4. Next we need a method, which does the snapping of the DesignerItems. We’ll name it SnapToGrid. This method will first call the GetSnapGridSize-method above to get the size of our grid and then calculates where to snap to. Finally it snaps the DesignerItems on the grid.The method itself is kept generically to snap any UIElement, in case you’d like to snap any other elements than DesignerItems. As DesignerItems derives from a UIElement, it is no problem to pass them to this method.As we can have multiple DesignerItems selected at the same time which we’d like to move, we need one, that defines where to snap to (masterItem), and a list of all selected items which must be moved (itemsToMove) as well.Here’s the implementation, you can paste it right below the GetSnapGridSize-method:
    private void SnapToGrid(UIElement masterItem, List<UIElement> itemsToMove)
    {
    	Size gridSize = GetSnapGridSize();
    
    	int gridSizeX = Convert.ToInt32(gridSize.Width);
    	int gridSizeY = Convert.ToInt32(gridSize.Height);
    
    	double xSnap = Canvas.GetLeft(masterItem) % gridSizeX;
    	double ySnap = Canvas.GetTop(masterItem) % gridSizeY;
    
    	// If it's less than half the grid size, snap left/up
    	// (by subtracting the remainder),
    	// otherwise move it the remaining distance of the grid size right/down
    	// (by adding the remaining distance to the next grid point).
    	if (xSnap <= gridSizeX / 2.0)
    		xSnap *= -1;
    	else
    		xSnap = gridSizeX - xSnap;
    	if (ySnap <= gridSizeY / 2.0)
    		ySnap *= -1;
    	else
    		ySnap = gridSizeY - ySnap;
    
    	if (itemsToMove.Contains(masterItem) == false)
    		itemsToMove.Add(masterItem);
    
    	foreach (UIElement itemToMove in itemsToMove)
    	{
    		double xSnapPosition = xSnap + Canvas.GetLeft(itemToMove);
    		double ySnapPosition = ySnap + Canvas.GetTop(itemToMove);
    
    		Canvas.SetLeft(itemToMove, xSnapPosition);
    		Canvas.SetTop(itemToMove, ySnapPosition);
    	}
    }
    
  5. Great, but the code does nothing so far! Now we need to snap, when the user drops a DesignerItem onto the DesignerCanvas. Find the method
    protected override void OnDrop(DragEventArgs e)
    

    in the DesignerCanvas.cs. Right above the line e.Handled = true; paste the following piece of code:

    SnapToGrid(newItem, new List<UIElement>());
    

    If you’d now start the Diagram Designer, the items will already snap after dropping onto the DesignerCanvas!

  6. The final step we need to take is to snap, when the user stops moving the items. This is, when the MouseLeftButtonUp-Event fires. In this case we get the masterItem from the MouseButtonEventArgs-Source and all the DesignerItems to be moved from the selection-service and pass them to the SnapToGrid-method. You can paste the code right below the code of the SnapToGrid-method.
    protected override void OnPreviewMouseLeftButtonUp(MouseButtonEventArgs e)
    {
    	base.OnPreviewMouseLeftButtonUp(e);
    
    	if (e.Source as UIElement != null)
    	{
    		List<UIElement> itemsToMove = new List<UIElement>();
    		foreach (ISelectable selectable in SelectionService.CurrentSelection.Where(t => t as UIElement != null))
    		{
    			itemsToMove.Add(selectable as UIElement);
    		}
    
    		SnapToGrid((UIElement)e.Source, itemsToMove);
    	}
    }
    

You can now start the Diagram Designer and will have a fully working snapping grid!