Draw multiple freehand Polyline or Curve drawing - Adding Undo Feature
You need to store lines in a List<List<Point>>
. Each element of the list contains points of a drawing which you draw using a down, move and up. The next line which you draw, will store in the next element of list. Each undo, will remove the last drawing.
Put an instance of this control on your form and it will handle the drawing for you. Also to perform undo, call its Undo
method.
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
public class DrawingSurface : Control
{
public DrawingSurface() { this.DoubleBuffered = true; }
List<List<Point>> Lines = new List<List<Point>>();
bool drawing = false;
protected override void OnMouseDown(MouseEventArgs e) {
Lines.Add(new List<Point>());
Lines.Last().Add(e.Location);
drawing = true;
base.OnMouseDown(e);
}
protected override void OnMouseMove(MouseEventArgs e) {
if (drawing) { Lines.Last().Add(e.Location); this.Invalidate(); }
base.OnMouseMove(e);
}
protected override void OnMouseUp(MouseEventArgs e) {
if (drawing) {
this.drawing = false;
Lines.Last().Add(e.Location);
this.Invalidate();
}
base.OnMouseUp(e);
}
protected override void OnPaint(PaintEventArgs e) {
e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
foreach (var item in Lines)
e.Graphics.DrawLines(Pens.Black, item.ToArray()); /*or DrawCurve*/
}
public void Undo() {
if (Lines.Count > 0) { this.Lines.RemoveAt(Lines.Count - 1); this.Invalidate(); }
}
}
Note
- Using this logic, you can simply implement redo using an other
List<List<Point>>
. It's enough to copy the last item before undo to redo list usingRedoBuffer.Add(Lines.Last());
. Then for each redo command, it's enough to add the last item of redo buffer toLines
and remove it from redo buffer. You should also clear the redo buffer after each mouse down. You can use either of
DrawLines
orDrawCurve
based on your requirement.DrawLines
draws a poly-line, whileDrawCurve
draws a more smooth curve.I prefer to encapsulate
Lines.Count > 0
in a property likebool CanUndo
and make it accessible from outside of control.It's just an example and you can simply extend the solution. For example, instead of
List<List<Point>>
you can create aShape
class containingList<Point>
,LineWidth
,LineColor
, etc and perform task usingList<Shape>
.
Draw Shapes and Strings with undo and redo feature
you can create a TextShape
deriving from Shape
, having Text
, Font
, Location
and Color
properties and treat it like other shapes, so redo and undo will not be a problem.
Here are some tips which will help you to solve the problem:
- Create a base
Shape
class or interface containing basic methods likeDraw
,Clone
,HitTest
, etc. - All shapes, including
TextShape
should derive fromShape
.TextShape
is also a shape, havingText
,Font
,Location
andColor
properties. - Each implementation of
Shape
has its implementation of base methods. - Implement
INotifyPropertyChanged
in all your shapes, then you can listen to changes of properties and for example, add something to undo buffer after change of color, border width, etc. - Implement
IClonable
or base classClone
method. All shapes should be clonable when adding to undo buffer. - Do dispose GDI objects like
Pen
andBrush
. It's not optional. - Instead of adding a single shape to undo buffer, create a class like drawing context containing List of shapes, Background color of drawing surface and so on. Also in this class implement
INotifyPropertyChanged
, then by each change in the shapes or this class properties, you can add a clone of this class to undo buffer.
Shape
Here is an example of Shape
class:
public abstract class Shape : INotifyPropertyChanged {
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string name = "") {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
public abstract void Draw(Graphics g);
public abstract Shape Clone();
}
TextShape
Pay attention to the implementation of properties to raise PropertyChanged
event and also Clone
method to clone the object for undo buffer, also the way that GDI object have been used in Draw
:
public class TextShape : Shape {
private string text;
public string Text {
get { return text; }
set {
if (text != value) {
text = value;
OnPropertyChanged();
}
}
}
private Point location;
public Point Location {
get { return location; }
set {
if (!location.Equals(value)) {
location = value;
OnPropertyChanged();
}
}
}
private Font font;
public Font Font {
get { return font; }
set {
if (font!=value) {
font = value;
OnPropertyChanged();
}
}
}
private Color color;
public Color Color {
get { return color; }
set {
if (color!=value) {
color = value;
OnPropertyChanged();
}
}
}
public override void Draw(Graphics g) {
using (var brush = new SolidBrush(Color))
g.DrawString(Text, Font, brush, Location);
}
public override Shape Clone() {
return new TextShape() {
Text = Text,
Location = Location,
Font = (Font)Font.Clone(),
Color = Color
};
}
}
DrawingContext
This class in fact contains all shapes and some other properties like back color of drawing surface. This is the class which you need to add its clone to undo buffer:
public class DrawingContext : INotifyPropertyChanged {
public DrawingContext() {
BackColor = Color.White;
Shapes = new BindingList<Shape>();
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string name = "") {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
private Color backColor;
public Color BackColor {
get { return backColor; }
set {
if (!backColor.Equals(value)) {
backColor = value;
OnPropertyChanged();
}
}
}
private BindingList<Shape> shapes;
public BindingList<Shape> Shapes {
get { return shapes; }
set {
if (shapes != null)
shapes.ListChanged -= Shapes_ListChanged;
shapes = value;
OnPropertyChanged();
shapes.ListChanged += Shapes_ListChanged;
}
}
private void Shapes_ListChanged(object sender, ListChangedEventArgs e) {
OnPropertyChanged("Shapes");
}
public DrawingContext Clone() {
return new DrawingContext() {
BackColor = this.BackColor,
Shapes = new BindingList<Shape>(this.Shapes.Select(x => x.Clone()).ToList())
};
}
}
DrawingSurface
This class is in fact the control which has undo and redo functionality and also draws the current drawing context on its surface:
public class DrawingSurface : Control {
private Stack<DrawingContext> UndoBuffer = new Stack<DrawingContext>();
private Stack<DrawingContext> RedoBuffer = new Stack<DrawingContext>();
public DrawingSurface() {
DoubleBuffered = true;
CurrentDrawingContext = new DrawingContext();
UndoBuffer.Push(currentDrawingContext.Clone());
}
DrawingContext currentDrawingContext;
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[Browsable(false)]
public DrawingContext CurrentDrawingContext {
get {
return currentDrawingContext;
}
set {
if (currentDrawingContext != null)
currentDrawingContext.PropertyChanged -= CurrentDrawingContext_PropertyChanged;
currentDrawingContext = value;
Invalidate();
currentDrawingContext.PropertyChanged += CurrentDrawingContext_PropertyChanged;
}
}
private void CurrentDrawingContext_PropertyChanged(object sender, PropertyChangedEventArgs e) {
UndoBuffer.Push(CurrentDrawingContext.Clone());
RedoBuffer.Clear();
Invalidate();
}
public void Undo() {
if (CanUndo) {
RedoBuffer.Push(UndoBuffer.Pop());
CurrentDrawingContext = UndoBuffer.Peek().Clone();
}
}
public void Redo() {
if (CanRedo) {
CurrentDrawingContext = RedoBuffer.Pop();
UndoBuffer.Push(CurrentDrawingContext.Clone());
}
}
public bool CanUndo {
get { return UndoBuffer.Count > 1; }
}
public bool CanRedo {
get { return RedoBuffer.Count > 0; }
}
protected override void OnPaint(PaintEventArgs e) {
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
using (var brush = new SolidBrush(CurrentDrawingContext.BackColor))
e.Graphics.FillRectangle(brush, ClientRectangle);
foreach (var shape in CurrentDrawingContext.Shapes)
shape.Draw(e.Graphics);
}
}
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);
}
}
Retrieving Correct PictureBox Images From Array
Change the following line
ImageCollection.Add(pictureBox1.Image);
To
ImageCollection.Add((Image)pictureBox1.Image.Clone())
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);
}
}
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);
}
}
}
}
Related Topics
Regular Expression to Get the Src of Images in C#
Reading Fromuri and Frombody at the Same Time
Httpwebrequest Times Out on Second Call
Why Does the Async Keyword Exist
Set Dllimport Attribute Dynamically
How to Configure Many to Many Relationship Using Entity Framework Fluent API
Improving/Fixing a Regex for C Style Block Comments
Unity 5.5 Obsolete Particle System Code
Selecting the Size of a System.Drawing.Icon
Converting Long String of Binary to Hex C#
Selectively Coloring Text in Richtextbox