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 derivedControl
and setDoubleBuffered
to true.It's a good option to encapsulate
Circle
in aCircle
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 :) );
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
Is There Any Simple Way to Convert .Xls File to .CSV File? (Excel)
Scope of Static Variable in Multi-User ASP.NET Web Application
Why Is the "F" Required When Declaring Floats
The Input Is Not a Valid Base-64 String as It Contains a Non-Base 64 Character
Fast Way to Convert a Two Dimensional Array to a List ( One Dimensional )
Async Void, ASP.NET, and Count of Outstanding Operations
How to Distinguish Between Multiple Input Devices in C#
Is There a Lower Bound Function on a Sortedlist<K ,V>
Getting Day Suffix When Using Datetime.Tostring()
What's the How to Minimize to Tray a C# Winforms App
Generics - Where T Is a Number
How to Determine for Which Platform an Executable Is Compiled