Search code examples
c#.netwinformsdrawinggdi+

Draw Shapes and Strings with undo and redo feature


Is there a way to drawstring and then remove it?

I've used following classes to Undo/Redo Rectangle, Circle, Line, Arrow type shapes but cant figure how i can remove drawn string.

https://github.com/Muhammad-Khalifa/Free-Snipping-Tool/blob/master/Free%20Snipping%20Tool/Operations/UndoRedo.cs

https://github.com/Muhammad-Khalifa/Free-Snipping-Tool/blob/master/Free%20Snipping%20Tool/Operations/Shape.cs

https://github.com/Muhammad-Khalifa/Free-Snipping-Tool/blob/master/Free%20Snipping%20Tool/Operations/ShapesTypes.cs

Here is how i'm adding Rectangle in shape list: This works well when i undo or redo from the list.

DrawString

Shape shape = new Shape();
shape.shape = ShapesTypes.ShapeTypes.Rectangle;
shape.CopyTuplePoints(points);
shape.X = StartPoint.X;
shape.Y = StartPoint.Y;
shape.Width = EndPoint.X;
shape.Height = EndPoint.Y;

Pen pen = new Pen(new SolidBrush(penColor), 2);
shape.pen = pen;
undoactions.AddShape(shape);

This is how i'm drawing text:

var fontFamily = new FontFamily("Calibri");
var font = new Font(fontFamily, 12, FontStyle.Regular, GraphicsUnit.Point);

Size proposedSize = new Size(int.MaxValue, int.MaxValue);
TextFormatFlags flags = TextFormatFlags.WordEllipsis | TextFormatFlags.NoPadding | TextFormatFlags.PreserveGraphicsClipping | TextFormatFlags.WordBreak;

Size size = TextRenderer.MeasureText(e.Graphics, textAreaValue, font, proposedSize, flags);

Shape shape = new Shape();
shape.shape = ShapesTypes.ShapeTypes.Text;
shape.X = ta.Location.X;
shape.Y = ta.Location.Y;
shape.Width = size.Width;
shape.Height = size.Height;
shape.Value = textAreaValue;

Pen pen = new Pen(new SolidBrush(penColor), 2);
shape.pen = pen;
undoactions.AddShape(shape);

But this does not work with undo-redo list. Maybe problem is with pen and font-size but i cant figure it out how to use pen with DrawString.

Edit: Here's how i'm drawing in paint event

protected override void OnPaint(PaintEventArgs e)
{
    e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;

    foreach (var item in undoactions.lstShape)
    {
        if (item.shape == ShapesTypes.ShapeTypes.Line)
        {
            e.Graphics.DrawLine(item.pen, item.X, item.Y, item.Width, item.Height);
        }
        else if (item.shape == ShapesTypes.ShapeTypes.Pen)
        {
            if (item.Points.Count > 1)
            {
                e.Graphics.DrawCurve(item.pen, item.Points.ToArray());
            }
        }

        else if (item.shape == ShapesTypes.ShapeTypes.Text)
        {
            var fontFamily = new FontFamily("Calibri");
            var font = new Font(fontFamily, 12, FontStyle.Regular, GraphicsUnit.Point);

            e.Graphics.TextRenderingHint = TextRenderingHint.AntiAlias;
            e.Graphics.DrawString(item.Value, font, new SolidBrush(item.pen.Color), new PointF(item.X, item.Y));
        }
    }
}

Shape.cs

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Drawing
{
    public class Shape : ICloneable
    {
        public ShapesTypes.ShapeTypes shape { get; set; }
        public List<Point> Points { get; }
        public int X { get; set; }
        public int Y { get; set; }
        public int Width { get; set; }
        public int Height { get; set; }
        public Pen pen { get; set; }

        public String Value { get; set; }

        public Shape()
        {
            Points = new List<Point>();
        }

        public void CopyPoints(List<Point> points)
        {
            for (int i = 0; i < points.Count; i++)
            {
                Point p = new Point();
                p.X = points[i].X;
                p.Y = points[i].Y;

                Points.Add(p);
            }
        }

        public void CopyCopyPoints(List<List<Point>> points)
        {
            for (int j = 0; j < points.Count; j++)
            {
                List<Point> current = points[j];

                for (int i = 0; i < current.Count; i++)
                {
                    Point p = new Point();
                    p.X = current[i].X;
                    p.Y = current[i].Y;

                    Points.Add(p);
                }
            }
        }

        public void CopyTuplePoints(List<Tuple<Point, Point>> points)
        {
            foreach (var line in points)
            {
                Point p = new Point();
                p.X = line.Item1.X;
                p.Y = line.Item1.Y;
                Points.Add(p);

                p.X = line.Item2.X;
                p.Y = line.Item2.Y;
                Points.Add(p);
            }
        }


        public object Clone()
        {
            Shape shp = new Shape();
            shp.X = X;
            shp.Y = Y;
            shp.Width = Width;
            shp.Height = Height;
            shp.pen = pen;
            shp.shape = shape;
            shp.Value = Value;

            for (int i = 0; i < Points.Count; i++)
            {
                shp.Points.Add(new Point(Points[i].X, Points[i].Y));
            }

            return shp;
        }
    }
}

DrawCircle

if (currentshape == ShapesTypes.ShapeTypes.Circle)
{
    Shape shape = new Shape();
    shape.shape = ShapesTypes.ShapeTypes.Circle;
    shape.CopyTuplePoints(cLines);
    shape.X = StartPoint.X;
    shape.Y = StartPoint.Y;
    shape.Width = EndPoint.X;
    shape.Height = EndPoint.Y;

    Pen pen = new Pen(new SolidBrush(penColor), 2);
    shape.pen = pen;
    undoactions.AddShape(shape);
}

Undo

if (currentshape != ShapesTypes.ShapeTypes.Undo)
{
    oldshape = currentshape;
    currentshape = ShapesTypes.ShapeTypes.Undo;
}
if (undoactions.lstShape.Count > 0)
{
    undoactions.Undo();
    this.Invalidate();
}
if (undoactions.redoShape.Count > 0)
{
    btnRedo.Enabled = true;
}

UndoRedo

public class UndoRedo
{
    public List<Shape> lstShape = new List<Shape>();
    public List<Shape> redoShape = new List<Shape>();

    public void AddShape(Shape shape)
    {
        lstShape.Add(shape);
    }

    public void Undo()
    {
        redoShape.Add((Shape)lstShape[lstShape.Count - 1].Clone());
        lstShape.RemoveAt(lstShape.Count - 1);
    }

    public void Redo()
    {
        lstShape.Add((Shape)redoShape[redoShape.Count - 1].Clone());
        redoShape.RemoveAt(redoShape.Count - 1);
    }
}

Solution

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