Snap to grid effects

 

Isn't it annoying when the mouse is too sensitive for placing objects on screen in nice neat rows? The snap-to-grid feature is much requested and fairly simple to accomplish.

The trick in this feature is to use some simple math trickery to get the mouse position to go to the nearest neighboring point in the grid. There is, as usual, more than one way to do this but my favourite is to override the mouse handling methods, condition the mouse position and then allow the control events to broadcast the modified mouse positions to code which is wired to them.

Before looking at the code proper just examine in detail the method that's used to fool the mouse events into broadcasting what we want it to. Basically, every mouse event has a MouseEventArgs object which reports the position of the mouse. By creating new mouse event arguments containing massaged data we can fool anything attached to the mouse events that the mouse is somewhere else. The following routine takes a mouse position, modifies it according to the grid-snap criteria and sends back a modified MouseEventArgs object for use elsewhere.

    protected MouseEventArgs MouseSnap(MouseEventArgs e)

    {

      int px, py;

 

      if(_snap)

      {

        px=(int)(((float)e.X/_snapX)+0.5f)*_snapX;

        py=(int)(((float)e.Y/_snapY)+0.5f)*_snapY;

      }

      else

      {

        px=e.X;

        py=e.Y;

      }

 

      MouseEventArgs t=new MouseEventArgs(e.Button,e.Clicks,px,py,e.Delta);

 

      return t;

    }

 

    Protected Function MouseSnap(e As MouseEventArgs) As MouseEventArgs

      Dim px, py As Integer

      Dim ox, oy As Integer

 

      If _snap Then

        ox = e.X Mod _snapX

        oy = e.Y Mod _snapY

        px = e.X \ _snapX

        py = e.Y \ _snapY

        If ox > _snapX / 2 Then

          px += 1

        End If

        If oy > _snapY / 2 Then

          py += 1

        End If

        px *= _snapX

        py *= _snapY

      Else

        px = e.X

        py = e.Y

      End If

 

      Dim t As New MouseEventArgs(e.Button, e.Clicks, px, py, e.Delta)

 

      Return t

    End Function 'MouseSnap

 

As you can see, the snap distance and indeed, whether snap is applied at-all, is controlled by properties in a control. The following listing shows that control in it's entirety. You can control the X and Y snap settings, using SnapX and SnapY and turn the effect on or off using the Snap property.

 

  public class GridSnap : ScrollableControl

  {

    int _snapX=8;

    public int SnapX

    {

      get{return _snapX;}

      set{_snapX=value;}

    }

 

    int _snapY=8;

    public int SnapY

    {

      get{return _snapY;}

      set{_snapY=value;}

    }

 

    bool _snap;

    public bool Snap

    {

      get{return _snap;}

      set{_snap=value;}

    }

 

 

 

    public GridSnap()

    {

    }

 

    protected MouseEventArgs MouseSnap(MouseEventArgs e)

    {

      int px, py;

 

      if(_snap)

      {

        px=(int)(((float)e.X/_snapX)+0.5f)*_snapX;

        py=(int)(((float)e.Y/_snapY)+0.5f)*_snapY;

      }

      else

      {

        px=e.X;

        py=e.Y;

      }

 

      MouseEventArgs t=new MouseEventArgs(e.Button,e.Clicks,px,py,e.Delta);

 

      return t;

    }

 

    protected override void OnMouseMove(MouseEventArgs e)

    {

      base.OnMouseMove(this.MouseSnap(e));

    }

 

    protected override void OnMouseDown(MouseEventArgs e)

    {

      base.OnMouseDown(this.MouseSnap(e));

    }

 

    protected override void OnMouseUp(MouseEventArgs e)

    {

      base.OnMouseUp(this.MouseSnap(e));

    }

 

  }

 

   Public Class GridSnap

    Inherits ScrollableControl

    Private _snapX As Integer = 8

    

    Public Property SnapX() As Integer

     Get

      Return _snapX

     End Get

     Set

      _snapX = value

     End Set

    End Property

    Private _snapY As Integer = 8

    

    Public Property SnapY() As Integer

     Get

      Return _snapY

     End Get

     Set

      _snapY = value

     End Set

    End Property

    Private _snap As Boolean

    

    Public Property Snap() As Boolean

     Get

      Return _snap

     End Get

     Set

      _snap = value

     End Set

    End Property

     

    

    

    Public Sub New()

    End Sub 'New

    

    

    Protected Function MouseSnap(e As MouseEventArgs) As MouseEventArgs

      Dim px, py As Integer

      Dim ox, oy As Integer

 

      If _snap Then

        ox = e.X Mod _snapX

        oy = e.Y Mod _snapY

        px = e.X \ _snapX

        py = e.Y \ _snapY

        If ox > _snapX / 2 Then

          px += 1

        End If

        If oy > _snapY / 2 Then

          py += 1

        End If

        px *= _snapX

        py *= _snapY

      Else

        px = e.X

        py = e.Y

      End If

 

      Dim t As New MouseEventArgs(e.Button, e.Clicks, px, py, e.Delta)

 

      Return t

    End Function 'MouseSnap

    

    

    Protected Overrides Sub OnMouseMove(e As MouseEventArgs)

     MyBase.OnMouseMove(Me.MouseSnap(e))

    End Sub 'OnMouseMove

    

    

    Protected Overrides Sub OnMouseDown(e As MouseEventArgs)

     MyBase.OnMouseDown(Me.MouseSnap(e))

    End Sub 'OnMouseDown

    

    

    Protected Overrides Sub OnMouseUp(e As MouseEventArgs)

     MyBase.OnMouseUp(Me.MouseSnap(e))

    End Sub 'OnMouseUp

   End Class 'GridSnap

 

Using this control is demonstrated in the following application. This lets you draw blobs on a form with the grid-snap set.

  /// <summary>

  /// Summary description for Form1.

  /// </summary>

  public class Form1 : System.Windows.Forms.Form

  {

    private WellFormed.GridSnap gridSnap1;

    /// <summary>

    /// Required designer variable.

    /// </summary>

    private System.ComponentModel.Container components = null;

 

    public Form1()

    {

      //

      // Required for Windows Form Designer support

      //

      InitializeComponent();

 

      //

      // TODO: Add any constructor code after InitializeComponent call

      //

    }

 

    /// <summary>

    /// Clean up any resources being used.

    /// </summary>

    protected override void Dispose( bool disposing )

    {

      if( disposing )

      {

        if (components != null)

        {

          components.Dispose();

        }

      }

      base.Dispose( disposing );

    }

 

    #region Windows Form Designer generated code

    /// <summary>

    /// Required method for Designer support - do not modify

    /// the contents of this method with the code editor.

    /// </summary>

    private void InitializeComponent()

    {

      this.gridSnap1 = new WellFormed.GridSnap();

      this.SuspendLayout();

      //

      // gridSnap1

      //

      this.gridSnap1.Dock = System.Windows.Forms.DockStyle.Fill;

      this.gridSnap1.Location = new System.Drawing.Point(0, 0);

      this.gridSnap1.Name = "gridSnap1";

      this.gridSnap1.Size = new System.Drawing.Size(432, 318);

      this.gridSnap1.Snap = true;

      this.gridSnap1.SnapX = 32;

      this.gridSnap1.SnapY = 32;

      this.gridSnap1.TabIndex = 0;

      this.gridSnap1.Text = "gridSnap1";

      this.gridSnap1.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.gridSnap1_KeyPress);

      this.gridSnap1.Click += new System.EventHandler(this.gridSnap1_Click);

      this.gridSnap1.Paint += new System.Windows.Forms.PaintEventHandler(this.gridSnap1_Paint);

      this.gridSnap1.KeyDown += new System.Windows.Forms.KeyEventHandler(this.gridSnap1_KeyDown);

      this.gridSnap1.MouseMove += new System.Windows.Forms.MouseEventHandler(this.gridSnap1_MouseMove);

      //

      // Form1

      //

      this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);

      this.ClientSize = new System.Drawing.Size(432, 318);

      this.Controls.Add(this.gridSnap1);

      this.Name = "Form1";

      this.Text = "Form1";

      this.ResumeLayout(false);

 

    }

    #endregion

 

    /// <summary>

    /// The main entry point for the application.

    /// </summary>

    [STAThread]

    static void Main()

    {

      Application.Run(new Form1());

    }

 

    ArrayList points=new ArrayList();

 

    private void gridSnap1_Click(object sender, System.EventArgs e)

    {

      points.Add(pos);

      this.gridSnap1.Invalidate();

    }

 

    private void gridSnap1_Paint(object sender, System.Windows.Forms.PaintEventArgs e)

    {

      foreach(Point p in points)

      {

        e.Graphics.FillEllipse(Brushes.Red, p.X-2,p.Y-2,4,4);

      }

 

      e.Graphics.DrawEllipse(Pens.Teal,pos.X-5,pos.Y-5,10,10);

    }

 

    Point pos;

 

    private void gridSnap1_MouseMove(object sender, System.Windows.Forms.MouseEventArgs e)

    {

      pos=new Point(e.X,e.Y);

      this.gridSnap1.Invalidate();

    }

 

    private void gridSnap1_KeyPress(object sender, System.Windows.Forms.KeyPressEventArgs e)

    {

      MessageBox.Show(e.KeyChar.ToString());

    }

 

    private void gridSnap1_KeyDown(object sender, System.Windows.Forms.KeyEventArgs e)

    {

      MessageBox.Show(e.KeyData.ToString());

    

    }

  }

 

   '/ <summary>

   '/ Summary description for Form1.

   '/ </summary>

  

   Public Class Form1

    Inherits System.Windows.Forms.Form

    Private WithEvents gridSnap1 As GridSnapVB.WellFormed.GridSnap

    '/ <summary>

    '/ Required designer variable.

    '/ </summary>

    Private components As System.ComponentModel.Container = Nothing

    

    

    Public Sub New()

     '

     ' Required for Windows Form Designer support

     '

     InitializeComponent()

    End Sub 'New

     

    '

    ' TODO: Add any constructor code after InitializeComponent call

    '

    

    '/ <summary>

    '/ Clean up any resources being used.

    '/ </summary>

    Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)

      If disposing Then

        If Not (components Is Nothing) Then

          components.Dispose()

        End If

      End If

      MyBase.Dispose(disposing)

    End Sub 'Dispose

 

#Region "Windows Form Designer generated code"

 

    '/ <summary>

    '/ Required method for Designer support - do not modify

    '/ the contents of this method with the code editor.

    '/ </summary>

    Private Sub InitializeComponent()

      Me.gridSnap1 = New GridSnapVB.WellFormed.GridSnap()

      Me.SuspendLayout()

      '

      ' gridSnap1

      '

      Me.gridSnap1.Dock = System.Windows.Forms.DockStyle.Fill

      Me.gridSnap1.Location = New System.Drawing.Point(0, 0)

      Me.gridSnap1.Name = "gridSnap1"

      Me.gridSnap1.Size = New System.Drawing.Size(292, 273)

      Me.gridSnap1.Snap = True

      Me.gridSnap1.SnapX = 32

      Me.gridSnap1.SnapY = 32

      Me.gridSnap1.TabIndex = 0

      Me.gridSnap1.Text = "gridSnap1"

      '

      ' Form1

      '

      Me.AutoScaleBaseSize = New System.Drawing.Size(5, 13)

      Me.ClientSize = New System.Drawing.Size(292, 273)

      Me.Controls.Add(gridSnap1)

      Me.Name = "Form1"

      Me.Text = "Form1"

      Me.ResumeLayout(False)

    End Sub 'InitializeComponent

#End Region

 

 

    '/ <summary>

    '/ The main entry point for the application.

    '/ </summary>

    <STAThread()> _

    Shared Sub Main()

      Application.Run(New Form1)

    End Sub 'Main

 

    Private points As New ArrayList

 

 

    Private Sub gridSnap1_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles gridSnap1.Click

      points.Add(pos)

      Me.gridSnap1.Invalidate()

    End Sub 'gridSnap1_Click

 

 

    Private Sub gridSnap1_Paint(ByVal sender As Object, ByVal e As System.Windows.Forms.PaintEventArgs) Handles gridSnap1.Paint

      Dim p As Point

      For Each p In points

        e.Graphics.FillEllipse(Brushes.Red, p.X - 2, p.Y - 2, 4, 4)

      Next p

 

      e.Graphics.DrawEllipse(Pens.Teal, pos.X - 5, pos.Y - 5, 10, 10)

    End Sub 'gridSnap1_Paint

 

    Private pos As Point

 

 

    Private Sub gridSnap1_MouseMove(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles gridSnap1.MouseMove

      pos = New Point(e.X, e.Y)

      Me.gridSnap1.Invalidate()

    End Sub 'gridSnap1_MouseMove

  End Class 'Form1

Return to Windows Forms Tips and Tricks

Visit the GDI+ FAQ

Copyright Ramuseco Limited 2004. All rights reserved.