
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.
|