How to drag and move shapes in C#
In a C# WindoeFormsApplication, is it possible to select, hence to move or delete a plotted shape with mouse? Like the windows paint program.
The shape plotting works totally fine, all points are stored in some array. As this line drawing example
Point Latest { get; set; }
List<Point> _points = new List<Point>();
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
// Save the mouse coordinates
Latest = new Point(e.X, e.Y);
// Force to invalidate the form client area and immediately redraw itself.
Refresh();
}
protected override void OnPaint(PaintEventArgs e)
{
var g = e.Graphics;
base.OnPaint(e);
if (_points.Count > 0)
{
var pen = new Pen(Color.Navy);
var pt = _points[0];
for(var i=1; _points.Count > i; i++)
{
var next = _points[i];
g.DrawLine(pen, pt, next);
pt = next;
}
g.DrawLine(pen, pt, Latest);
}
}
private void Form1_MouseClick(object sender, MouseEventArgs e)
{
Latest = new Point(e.X, e.Y);
_points.Add(Latest);
Refresh();
}
I can let it to calculate the shortest distance between mouse position and each line by basic linear algebra, and set a threshold distance, if it's shorter than the threshold, make this line selected, and can be dragged or edited by mouse. But, just wondering, is there any way that's more manageable for such task? Mainly the selection part. Any suggestion will be appreciated, thank you!
To hit test shapes you don't need linear algebra. You can create GraphicsPath
for your shapes and then using GraphicsPath.IsVisible
method or GraphicsPath.IsOutlineVisible
method perform hit-testing.
-
To check if a point is in the area of your path, for example a filled shape, use
IsVisible
. -
To hit-test for lines or curves or empty shapes, you can use
IsOutlineVisible
.
Example
As an example, you can create a base IShape
interface that contains methods for hit-testing, drawing and moving. Then in classes implement those methods. Also you can create a DrawingSurface
control which can handle hit-testing, drawing and moving IShape
objects.
In the below example, we create IShape
interface, Line
and Circle
classes. Also we create a DrawingSurface
control. To test the example, its enough to put a DrawingSurface
control on a Form
and handle Load
event of form and add some shapes, then run application and try to move shapes.
IShape
This interface contains some useful methods which if any class implements them, can be used for drawing, hit-testing and moving. At the end of this example, you can see a DrawingSurface
control which can work with IShape
implementations simply:
public interface IShape
{
GraphicsPath GetPath();
bool HitTest(Point p);
void Draw(Graphics g);
void Move(Point d);
}
Line
Here is a line class which implements IShape
interface. When hit-testing if you click on line, the HitTest
returns true. Also to let you choose line more simply, I added 2 points for hit-testing:
public class Line : IShape
{
public Line() { LineWidth = 2; LineColor = Color.Black; }
public int LineWidth { get; set; }
public Color LineColor { get; set; }
public Point Point1 { get; set; }
public Point Point2 { get; set; }
public GraphicsPath GetPath()
{
var path = new GraphicsPath();
path.AddLine(Point1, Point2);
return path;
}
public bool HitTest(Point p)
{
var result = false;
using (var path = GetPath())
using (var pen = new Pen(LineColor, LineWidth + 2))
result = path.IsOutlineVisible(p, pen);
return result;
}
public void Draw(Graphics g)
{
using (var path = GetPath())
using (var pen = new Pen(LineColor, LineWidth))
g.DrawPath(pen, path);
}
public void Move(Point d)
{
Point1 = new Point(Point1.X + d.X, Point1.Y + d.Y);
Point2 = new Point(Point2.X + d.X, Point2.Y + d.Y);
}
}
Circle
Here is a circle class which implements IShape
interface. When hit-testing if you click in circle, the HitTest
returns true:
public class Circle : IShape
{
public Circle() { FillColor = Color.Black; }
public Color FillColor { get; set; }
public Point Center { get; set; }
public int Radious { get; set; }
public GraphicsPath GetPath()
{
var path = new GraphicsPath();
var p = Center;
p.Offset(-Radious, -Radious);
path.AddEllipse(p.X, p.Y, 2 * Radious, 2 * Radious);
return path;
}
public bool HitTest(Point p)
{
var result = false;
using (var path = GetPath())
result = path.IsVisible(p);
return result;
}
public void Draw(Graphics g)
{
using (var path = GetPath())
using (var brush = new SolidBrush(FillColor))
g.FillPath(brush, path);
}
public void Move(Point d)
{
Center = new Point(Center.X + d.X, Center.Y + d.Y);
}
}
DrawingSurface
The control, draws a list of shapes. Also it performs hit-testing in MouseDown
and moves the shape if you drag it. You should add some shapes like Line
or Circle
to Shapes
collection of the control.
public class DrawingSurface : Control
{
public List<IShape> Shapes { get; private set; }
IShape selectedShape;
bool moving;
Point previousPoint = Point.Empty;
public DrawingSurface() { DoubleBuffered = true; Shapes = new List<IShape>(); }
protected override void OnMouseDown(MouseEventArgs e)
{
for (var i = Shapes.Count - 1; i >= 0; i--)
if (Shapes[i].HitTest(e.Location)) { selectedShape = Shapes[i]; break; }
if (selectedShape != null) { moving = true; previousPoint = e.Location; }
base.OnMouseDown(e);
}
protected override void OnMouseMove(MouseEventArgs e)
{
if (moving) {
var d = new Point(e.X - previousPoint.X, e.Y - previousPoint.Y);
selectedShape.Move(d);
previousPoint = e.Location;
this.Invalidate();
}
base.OnMouseMove(e);
}
protected override void OnMouseUp(MouseEventArgs e)
{
if (moving) { selectedShape = null; moving = false; }
base.OnMouseUp(e);
}
protected override void OnPaint(PaintEventArgs e)
{
e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
foreach (var shape in Shapes)
shape.Draw(e.Graphics);
}
}