How to Treat the Circle as a Control After Drawing It? - Moving and Selecting Shapes

How can I treat the circle as a control after drawing it? - Moving and selecting shapes

You need to perform a hit-test to check if a point is in a circle. As an option you can add a circle to a GraphicsPath and the use IsVisible method of the path to check if the point is in circle.

For example having a ponit p as top-left location of a circle with diameter d, you can check if current clicked point is in the circle or con this way:

var result = false;
using (var path = new GraphicsPath())
{
path.AddEllipse(p.X, p.Y, d, d);
result = path.IsVisible(e.Location);
}

Sample Code

I see you have asked multiple questions in this topic. So here I share some code to help you to be in right direction.

define variables for fill color, selected fill color, circle size, border width and etc, to be able to change them simply if you need.

List<Rectangle> Shapes = new List<Rectangle>();
int selectedIndex = -1;
Size size = new Size(25, 25);
Color fillColor = Color.White;
Color selectedfillCOlor = Color.Red;
Color borderColor = Color.Gray;
int borderWidth = 2;

DoubleClick

Here add circles to the Shapes list. It's enough to add the bounding rectangle of a circle to the list.

private void pic_MouseDoubleClick(object sender, MouseEventArgs e)
{
var p = e.Location;
p.Offset(-size.Width / 2, -size.Height / 2);
Shapes.Add(new Rectangle(p, size));
pic.Invalidate();
}

Click

Here perform hit-test to check if the point is in one of circles.Check if the Ctrl key is down when click, to make selection, then set the found index as selectedIndex to use it when painting.

private void pic_MouseClick(object sender, MouseEventArgs e)
{
if (ModifierKeys != Keys.Control)
return;
selectedIndex = -1;
for (int i = 0; i < Shapes.Count; i++)
{
using (var path = new GraphicsPath())
{
path.AddEllipse(Shapes[i]);
if (path.IsVisible(e.Location))
selectedIndex = i;
}
}
pic.Invalidate();
}

Paint

Set SmoothingMode of graphics object to AntiAlias to have a more smooth drawing. Then draw shapes in a for loop and pay attention to selectedIndex to use a different fill color for selected shape.

To draw the text, yo don't need to use a label and you can simply draw text using TextRenderer class.

private void pic_Paint(object sender, PaintEventArgs e)
{
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
for (int i = 0; i < Shapes.Count; i++)
{
var selected = (selectedIndex == i);
using (var brush = new SolidBrush(selected ? selectedfillCOlor : fillColor))
e.Graphics.FillEllipse(brush, Shapes[i]);
using (var pen = new Pen(borderColor, borderWidth))
e.Graphics.DrawEllipse(pen, Shapes[i]);
TextRenderer.DrawText(e.Graphics, (i + 1).ToString(),
this.Font, Shapes[i], Color.Black,
TextFormatFlags.VerticalCenter | TextFormatFlags.HorizontalCenter);
}
}

Some Notes

  • It's better to encapsulate codes in a new control derived from PictureBox or derived Controland set DoubleBuffered to true.

  • It's a good option to encapsulate Circle in a Circle class which performs hit testing and rendering of a circle. Specially if you want to move them later or perform some other interactions or let each circle has it's own properties like color ,etc.

Sample Circle Class

Here is a sample circle class which can be a good start point.

public class Circle
{
private Color selectedFillColor = Color.Red;
private Color normalFillColor = Color.Red;
private Color borderColor = Color.Red;
private int borderWidth = 2;
public Point Location { get; set; }
public int Diameter { get; set; }
public Rectangle Bounds
{
get
{
return new Rectangle(Location, new Size(Diameter, Diameter));
}
}
public bool HitTest(Point p)
{
var result = false;
using (var path = new GraphicsPath())
{
path.AddEllipse(Bounds);
result = path.IsVisible(p);
}
return result;
}
public bool Selected { get; set; }
public void Draw(Graphics g)
{
using (var brush = new SolidBrush(
Selected ? selectedFillColor : normalFillColor))
g.FillEllipse(brush, Bounds);
using (var pen = new Pen(borderColor, 2))
g.DrawEllipse(pen, Bounds);
}
}

How to control size and location of circle when I draw it

Okay here is an example, I tried to apply the same way as you did, I have created my own picture box,

class MyPBox : PictureBox
{
public MyPBox()
{
this.BackColor = Color.Red; // for see better
this.Location = new Point(50, 50); // set location at form

}
protected override void OnPaint(PaintEventArgs pe)
{
if (this.Parent != null)
{
this.Parent.Paint += Parent_Paint; // picturebox's paint means it added to parent so we need to trigger parent's paint event
}
base.OnPaint(pe);

}
bool clickPerformed = false; // to catch control has mouse down
protected override void OnMouseDown(MouseEventArgs e)
{
base.OnMouseDown(e);
clickPerformed = true; // set mouse down
Control tempSender = this.Parent; // get sender
tempSender.Invalidate(); // invalidate to trigger paint event

}

private void Parent_Paint(object sender, PaintEventArgs e)
{
if (clickPerformed)
{
using (Graphics g = e.Graphics)
{
using (Pen pen = new Pen(Color.Black, 2))
{
float locationX = this.Location.X + this.Size.Width / 2;
float locationY = this.Location.Y + this.Size.Height / 2;
float radius = (this.Size.Height + this.Size.Width) / 2;

float[] dashValues = { 5, 2, 15, 4 };
pen.DashPattern = dashValues;
DrawCircle(g, pen, locationX
, locationY
, radius); // draw circle
clickPerformed = false; // process done so set it to false
}
}
}
base.OnPaint(e);

}

protected override void OnMouseUp(MouseEventArgs e)
{
this.Parent.Invalidate(); // mouse up circle should be erased, so invalidate again to trigger paint, but this time clickPerformed is false
// so it won't draw circle again
base.OnMouseDown(e);
}
public void DrawCircle(Graphics g, Pen pen, float centerX, float centerY, float radius)
{
g.DrawEllipse(pen, centerX - radius, centerY - radius, radius + radius, radius + radius);
}

}

Result(I am clicking on picturebox btw :) );

Sample Image

Hope helps,

why there are duplicate values in list of circles in c#?

Here's some example code that 1) creates a new circle each double-click (your original code used one circle object over and over), and 2) refactors the Circle class into CircleManager and Circle classes, so that you can separate logic dealing with the collection of circles while still having each circle be it's own individual object.

This compiled and ran fine for me, but you may still want to follow the code execution to see how the CircleManager is used.

using System.Windows.Forms;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;

namespace CircleTest
{
public partial class Form1 : Form
{
private CircleManager circleManager = new CircleManager();

private Font font = new Font("Tahoma", 8, FontStyle.Bold);
public Color normalFillColor = Color.White;
public Color selectedFillColor = Color.Red;
public Color borderColor = Color.Gray;
public int borderWith = 2;

public Form1()
{
InitializeComponent();
pictureBox1.Paint += new PaintEventHandler(pic_Paint);
}

private void pic_Paint(object sender, PaintEventArgs e)
{
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
Graphics g = e.Graphics;
DrawCircles(g);
}

public void DrawCircles(Graphics g)
{
for (int i = 0; i < circleManager.CircleShapes.Count; i++)
{
using (var brush = new SolidBrush(circleManager.Circles[i].Selected ? selectedFillColor : normalFillColor))
g.FillEllipse(brush, circleManager.CircleShapes[i]);
using (var pen = new Pen(borderColor, 2))
g.DrawEllipse(pen, circleManager.CircleShapes[i]);
TextRenderer.DrawText(g, i.ToString(), font,
circleManager.CircleShapes[i], Color.Black,
TextFormatFlags.VerticalCenter | TextFormatFlags.HorizontalCenter);
}
}

private void pictureBox1_MouseDoubleClick(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
Circle newCircle = new Circle();
newCircle.Name = (circleManager.Circles.Count + 1).ToString();
Location.Offset(-newCircle.size.Width / 2, -newCircle.size.Height / 2);
newCircle.Location = e.Location;
circleManager.Circles.Add(newCircle);
circleManager.CircleShapes.Add(new Rectangle(newCircle.Location, newCircle.size));
pictureBox1.Invalidate();
}
}

private void pictureBox1_MouseClick(object sender, MouseEventArgs e)
{
if (ModifierKeys != Keys.Control && e.Button != MouseButtons.Right)
{
return;
}
else
{
circleManager.HitTest(e.Location);
}
pictureBox1.Invalidate();
}
}

public class CircleManager
{
public List<Circle> Circles = new List<Circle>();
public List<Rectangle> CircleShapes = new List<Rectangle>();

public void HitTest(Point p)
{
for (int i = 0; i < CircleShapes.Count; i++)
{
using (var path = new GraphicsPath())
{
path.AddEllipse(CircleShapes[i]);
if (path.IsVisible(p))
{
Circles[i].Selected = true;
}
}
}
}
}

public class Circle
{
public string Name { get; set; }

public Point Location { get; set; }
public Size size = new Size(25, 25);
public bool Selected { get; set; }

public Rectangle Bounds
{
get
{
return new Rectangle(Location, size);
}
}

}
}

How to handle click event on shapes of System.Drawing

Special Thanks for Reza Aghaei for the help ,
i used in my solution the following code

EDIT: IMPROVED ANSWER

Disposing the graphics path , and making code neater

Imports System.Drawing.Drawing2D
Public Class SurfaceSelection

Private Sub SurfaceSelection_Click(sender As Object, e As MouseEventArgs) Handles Me.Click
Dim hitSurface As String = String.Empty
If GetPath(EnumsClass.SurfacesEnum.L).IsVisible(e.Location) Then
hitSurface = "L"
ElseIf GetPath(EnumsClass.SurfacesEnum.M).IsVisible(e.Location) Then
hitSurface = "M"
ElseIf GetPath(EnumsClass.SurfacesEnum.F).IsVisible(e.Location) Then
hitSurface = "F"
ElseIf GetPath(EnumsClass.SurfacesEnum.D).IsVisible(e.Location) Then
hitSurface = "D"
Else
hitSurface = "Missed"
End If

MsgBox(hitSurface)

End Sub

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

Me.DrawPath(EnumsClass.SurfacesEnum.L, e)
Me.DrawPath(EnumsClass.SurfacesEnum.M, e)
Me.DrawPath(EnumsClass.SurfacesEnum.F, e)
Me.DrawPath(EnumsClass.SurfacesEnum.D, e)

End Sub

Private Sub DrawPath(ByVal v_bytSurface As EnumsClass.SurfacesEnum, ByVal e As System.Windows.Forms.PaintEventArgs)
Using p As GraphicsPath = GetPath(v_bytSurface)
e.Graphics.FillPath(Brushes.Green, p)
e.Graphics.DrawPath(Pens.Black, p)
End Using
End Sub

Private Function GetPath(ByVal v_bytSurface As EnumsClass.SurfacesEnum) As GraphicsPath
Dim path As New GraphicsPath
Dim center = New Point(100, 100)
Dim innerR = 70
Dim thickness = 20
Dim startAngle = getGraphicsPathAngle(v_bytSurface)
Dim arcLength = 70
Dim outerR = innerR + thickness
Dim outerRect = New Rectangle(center.X - outerR, center.Y - outerR, 2 * outerR, 2 * outerR)
Dim innerRect = New Rectangle(center.X - innerR, center.Y - innerR, 2 * innerR, 2 * innerR)
path.AddArc(outerRect, startAngle, arcLength)
path.AddArc(innerRect, startAngle + arcLength, -arcLength)
path.CloseFigure()
Return path
End Function

Private Function getGraphicsPathAngle(ByVal v_bytSurface As EnumsClass.SurfacesEnum) As Integer
Select Case v_bytSurface
Case EnumsClass.SurfacesEnum.F
Return 235
Case EnumsClass.SurfacesEnum.O
Return 0
Case EnumsClass.SurfacesEnum.L
Return 55
Case EnumsClass.SurfacesEnum.M
Return 145
Case EnumsClass.SurfacesEnum.D
Return 325
Case EnumsClass.SurfacesEnum.Unspecified
Return -1

End Select

End Function

End Class

Public Class EnumsClass
Public Enum SurfacesEnum As Byte
Unspecified = 0
F = 1
O = 2
L = 3
M = 4
D = 5
End Enum
End Class

i used Reza's answer in the following stackoverflow question :

How to draw a circular progressbar pie using GraphicsPath in WinForm?

How can I treat the circle as a control after drawing it? - Moving and selecting shapes

How to drag and move shapes in C#

How to drag and move shapes in C#

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);
}
}


Related Topics



Leave a reply



Submit