5

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);
    }
}
Reza Aghaei
  • 120,393
  • 18
  • 203
  • 398
m.qayyum
  • 401
  • 2
  • 15
  • 44
  • _i cant figure it out how to use pen with DrawString_ - __Text is rendered not by a pen but by filling it with a Brush.__ - You can easily create a Brush from a Pen.Color, though. - I don't see where and how you draw. You will not get many folks to look into __off-site sources__. You are supposed to present us with a reproducible example __here.__ – TaW Jun 05 '18 at 08:40
  • I have edited the question and added paint event. All other shapes works well with undo-redo except DrawString method. – m.qayyum Jun 05 '18 at 15:03
  • To close/down voters IDC, as I haven't got an answer on this site without starting a bounty. – m.qayyum Jun 05 '18 at 15:05
  • So, what exactly is your problem with un/re-do?? _does not work_ is not a helpful problem description! - Looks like you maybe do not store the font info in your `item` class. could that be the issue? - ((Wrt close/down-votes: They are always and only about the question; so you sholud take care if you can improve the question..)) – TaW Jun 05 '18 at 15:12
  • Deleting drawn text after Drawing it, is the problem. – m.qayyum Jun 06 '18 at 16:35
  • But why would that work any different from deleting shapes??? You remove it from the doActions list and add it to the UndoAction list. Then Invalidate and you should be good. If it still shows maybe it was added twice? Do use the debugger to look into the lists! – TaW Jun 06 '18 at 16:48
  • I have added `DrawCircle` and `Shapes.cs`, you see we need to copy each point in shape to make it work. I just cant understand how i create similar method for DrawString that i have created for DrawCircle `CopyTuplePoints()` – m.qayyum Jun 06 '18 at 16:58
  • So is drawing the text or undoing it the issue? Do you have two list/stacks/queues or only one? Is `undoactions` that list? Or is there a `doactions` as well? If it is about undoing? what is the undo code? Is drawing text in some way ralated to rectangles? – TaW Jun 06 '18 at 17:08
  • Btw: To create a copy of a `List points` you can simply write: `List points2 = point.ToList()`. – TaW Jun 06 '18 at 17:22
  • Undoing it is the issue, hmm we can say that its rectangle but we need to find out its width/height with `TextRenderer.MeasureText` but rectangle involves pen with width to draw. – m.qayyum Jun 06 '18 at 17:25
  • Do where is the undo code? – TaW Jun 06 '18 at 17:26
  • Edited the question – m.qayyum Jun 06 '18 at 17:30
  • 1
    OK, now I still don't see `undoactions.Undo();`, right? – TaW Jun 06 '18 at 17:31
  • oops sorry, added now – m.qayyum Jun 06 '18 at 17:34
  • 1
    Ok, no errors I could see right away. You really will ahve to go to the debugger and step through the undoing code alway watching the lstShape list. Much bettzer than my speculating.. – TaW Jun 06 '18 at 17:37
  • I will try, i know where the problem is but cant think beyond that. Is there a way to convert DrawnString to points? – m.qayyum Jun 06 '18 at 17:41
  • 1
    _Is there a way to convert DrawnString to points?_ yes and I know that __you do not want that__. - Get to know the debugger, it is __by far the best friend__ you have in the world of coding. – TaW Jun 06 '18 at 17:42
  • Sorry didnt get, you mean converting it into ponits is wrong way to do it? – m.qayyum Jun 06 '18 at 17:45
  • ok i will try.Thanks for your time. Good day :) – m.qayyum Jun 06 '18 at 17:46
  • 1
    Text is text. converting/rendering it to pixels/points is only for display. Neither for storing nor editing. It would be expensive and still lose the flexibilty to scale or change the font. Or, of course the text itself.. – TaW Jun 06 '18 at 17:48
  • 1
    [Draw multiple freehand Polyline or Curve drawing - Adding Undo Feature](https://stackoverflow.com/a/38297293/3110834) – Reza Aghaei Jun 07 '18 at 18:01
  • 1
    `Text` can be kind of shape as well. Take a look at [this post](https://stackoverflow.com/a/38347945/3110834) for example. – Reza Aghaei Jun 07 '18 at 18:07
  • Interesting, will check them for sure and update. Thanks Reza – m.qayyum Jun 07 '18 at 23:06
  • 1
    I took your code fragments and put them together how I think you're using them and Undo/redo for strings works fine for me. Could you post your complete code? I don't think you need to store the string as a series of points since you're storing the Value and X, Y and that's all you're using to draw it. – gunnerone Jun 08 '18 at 02:05

3 Answers3

2

In the future, please follow the guidelines for a Minimal, Complete, and Verifiable example. This will help us to help you. For example, you could have excluded all of the code related to cloning, since it's not related to your problem.

I refactored your code a little and created a small, reproducible example. This example works with the general approach you outlined, so I can't tell you exactly why your code doesn't work unless you could also post a similar example that I can copy / paste into my environment. Please do not link to external code - it must be hosted here.

I refactored it to highlight some language features which could help to make your code more maintainable. Please let me know if you have any questions about what I put here. Please let me know if this helps. If not, please use it as a template and replace my code with yours so I can assist you.

  public partial class Form1 : Form
  {
    private EntityBuffer _buffer = new EntityBuffer();
    private System.Windows.Forms.Button btnUndo;
    private System.Windows.Forms.Button btnRedo;

    public Form1()
    {
      this.btnUndo = new System.Windows.Forms.Button();
      this.btnRedo = new System.Windows.Forms.Button();
      this.SuspendLayout();

      this.btnUndo.Location = new System.Drawing.Point(563, 44);
      this.btnUndo.Name = "btnUndo";
      this.btnUndo.Size = new System.Drawing.Size(116, 29);
      this.btnUndo.TabIndex = 0;
      this.btnUndo.Text = "Undo";
      this.btnUndo.UseVisualStyleBackColor = true;
      this.btnUndo.Click += new System.EventHandler(this.btnUndo_Click);

      this.btnRedo.Location = new System.Drawing.Point(563, 79);
      this.btnRedo.Name = "btnRedo";
      this.btnRedo.Size = new System.Drawing.Size(116, 29);
      this.btnRedo.TabIndex = 0;
      this.btnRedo.Text = "Redo";
      this.btnRedo.UseVisualStyleBackColor = true;
      this.btnRedo.Click += new System.EventHandler(this.btnRedo_Click);

      this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
      this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
      this.ClientSize = new System.Drawing.Size(800, 450);
      this.Controls.Add(this.btnRedo);
      this.Controls.Add(this.btnUndo);
      this.Name = "Form1";
      this.Text = "Form1";
      this.ResumeLayout(false);
    }

    protected override void OnLoad(EventArgs e)
    {
      _buffer.Add(new Rectangle(10, 10, 10, 10, Color.Red));
      _buffer.Add(new Rectangle(20, 20, 10, 10, Color.Red));
      _buffer.Add(new Rectangle(30, 30, 10, 10, Color.Red));
      _buffer.Add(new Text(40, 40, "Test", Color.Black));
      _buffer.Add(new Rectangle(50, 50, 10, 10, Color.Red));
      _buffer.Add(new Text(60, 60, "Test", Color.Black));
      base.OnLoad(e);
    }

    protected override void OnPaint(PaintEventArgs e)
    {
      foreach (var entity in _buffer.Entities)
        entity.Draw(e.Graphics);

      base.OnPaint(e);
    }

    private void btnUndo_Click(object sender, EventArgs e)
    {
      if (!_buffer.CanUndo)
        return;
      _buffer.Undo();
      Invalidate();
    }

    private void btnRedo_Click(object sender, EventArgs e)
    {
      if (!_buffer.CanRedo)
        return;
      _buffer.Redo();
      Invalidate();
    }
  }

  public abstract class Entity
  {
    public int X { get; set; }
    public int Y { get; set; }
    public Color Color { get; set; }

    public abstract void Draw(Graphics g);

    public Entity(int x, int y, Color color)
    {
      X = x;
      Y = y;
      Color = color;
    }
  }

  public class Text : Entity
  {
    private static Font _font = new Font(new FontFamily("Calibri"), 12, FontStyle.Regular, GraphicsUnit.Point);
    public string Value { get; set; }

    public Text(int x, int y, string value, Color color) : base(x,y,color) => Value = value;

    public override void Draw(Graphics g) => g.DrawString(Value, _font, new SolidBrush(Color), new PointF(X, Y));
  }

  public abstract class Shape : Entity
  {
    public int Width { get; set; }
    public int Height { get; set; }
    public Pen Pen { get; set; }

    public Shape(int x, int y, int width, int height, Color color) : base(x, y, color)
    {
      Width = width;
      Height = height;
    }
  }

  public class Rectangle : Shape
  {
    public Rectangle(Point start, Point end, Color color) : this(start.X, start.Y, end.X, end.Y, color) { }
    public Rectangle(int x, int y, int width, int height, Color color) : base(x, y, width, height, color) { }

    public override void Draw(Graphics g) => g.DrawRectangle(new Pen(new SolidBrush(Color)), X, Y, Width, Height);
  }

  public class EntityBuffer
  {
    public Stack<Entity> Entities { get; set; } = new Stack<Entity>();
    public Stack<Entity> RedoBuffer { get; set; } = new Stack<Entity>();
    public bool CanRedo => RedoBuffer.Count > 0;
    public bool CanUndo => Entities.Count > 0;

    public void Add(Entity entity)
    {
      Entities.Push(entity);
      RedoBuffer.Clear();
    }

    public void Undo() => RedoBuffer.Push(Entities.Pop());
    public void Redo() => Entities.Push(RedoBuffer.Pop());
  }
Robear
  • 986
  • 1
  • 11
  • 15
2

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);
    }
}
Reza Aghaei
  • 120,393
  • 18
  • 203
  • 398
0

I have done a similar kind of project, after drawing shapes and writing it's dimension as a string on images; after pressing Ctrl-Z/Ctrl-Y it does undo/redo the operations performed on images.

Here is a link to my Github project, a C# win-form soln. After running the soln, tool usage instruction will get appear on the tool itself.

Hope this helps you...

Sachin Patel
  • 499
  • 2
  • 12