Ef4 Cause Circular Reference in Web Service

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 using RedoBuffer.Add(Lines.Last());. Then for each redo command, it's enough to add the last item of redo buffer to Lines and remove it from redo buffer. You should also clear the redo buffer after each mouse down.
  • You can use either of DrawLines or DrawCurve based on your requirement. DrawLines draws a poly-line, while DrawCurve draws a more smooth curve.

  • I prefer to encapsulate Lines.Count > 0 in a property like bool 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 a Shape class containing List<Point>, LineWidth, LineColor, etc and perform task using List<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 like Draw, Clone, HitTest, etc.
  • All shapes, including TextShape should derive from Shape. TextShape is also a shape, having Text, Font, Location and Color 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 class Clone method. All shapes should be clonable when adding to undo buffer.
  • Do dispose GDI objects like Pen and Brush. 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 Shapeclass:

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

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



Leave a reply



Submit