How to draw a rectangle that could move to the edge of the PictureBox and when it reached the edge, it would unfold. And there could be several such rectangles
Here is the code of the class with the Draw function (Pike.cs):
using System.Drawing;
namespace course_project
{
internal class Pike : Fish
{
private Point coordinates;
private Size size = new Size(40, 40);
private Size speed;
public Pike(Point data, AquariumForm aquariumForm) : base(Color.Green, aquariumForm)
{
Data = data;
speed = new Size();
}
public Pike Next { get; set; }
public override void Draw(Graphics graphics)
{
graphics.FillEllipse(brush, coordinates.X, coordinates.Y, size.Width, size.Height);
}
public void UpdateLocation(Rectangle bounds)
{
if (!bounds.Contains(coordinates + speed))
{
if (coordinates.X + speed.Width < bounds.Left || coordinates.X + speed.Width > bounds.Right - size.Width)
{
speed.Width *= -1;
}
if (coordinates.Y + speed.Height < bounds.Top || coordinates.Y + speed.Height > bounds.Bottom - size.Height)
{
speed.Height *= -1;
}
}
coordinates += speed;
}
}
}
Linked List (PikeFlock.cs):
using System.Collections;
using System.Collections.Generic;
using System.Drawing;
namespace course_project
{
internal class PikeFlock : IEnumerable<Pike>
{
Pike head;
Pike tail;
int count;
public void Add(Point data, AquariumForm aquariumForm)
{
Pike pike = new Pike(data, aquariumForm);
if (head == null)
head = pike;
else
tail.Next = pike;
tail = pike;
count++;
}
public bool Remove(Point data)
{
Pike current = head;
Pike previous = null;
while (current != null)
{
if (current.Data.Equals(data))
{
if (previous != null)
{
previous.Next = current.Next;
if (current.Next == null)
{
tail = previous;
}
}
else
{
head = head.Next;
if (head == null)
tail = null;
}
count--;
return true;
}
previous = current;
current = current.Next;
}
return false;
}
public int Count { get { return count; } }
public bool IsEmpty { get { return count == 0; } }
public void Clear()
{
head = null;
tail = null;
count = 0;
}
public bool Contains(Point data)
{
Pike current = head;
while (current != null)
{
if (current.Data.Equals(data))
return true;
current = current.Next;
}
return false;
}
public void AppendFirst(Point data, AquariumForm aquariumForm)
{
Pike pike = new Pike(data, aquariumForm)
{
Next = head
};
head = pike;
if (count == 0)
{
tail = head;
}
count++;
}
public IEnumerator<Pike> GetEnumerator()
{
Pike current = head;
while (current != null)
{
yield return current;
current = current.Next;
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable)this).GetEnumerator();
}
}
}
AquariumForm.cs (The form itself):
using System;
using System.Drawing;
using System.Windows.Forms;
namespace course_project
{
public partial class AquariumForm : Form
{
readonly Aquarium aquarium;
public AquariumForm()
{
InitializeComponent();
DoubleBuffered = true;
aquarium = new Aquarium(ClientRectangle);
aquarium_timer.Interval = 100;
aquarium_timer.Tick += Timer_Tick;
aquarium_timer.Enabled = true;
}
private void Timer_Tick(object sender, EventArgs e)
{
foreach (Pike pike in aquarium.pikeFlock)
{
pike.UpdateLocation(ClientRectangle);
}
foreach (Carp carp in aquarium.carpFlock)
{
carp.UpdateLocation(ClientRectangle);
}
Invalidate();
}
private void Add_carp_button_Click(object sender, EventArgs e)
{
aquarium.carpFlock.Add(new Point(), this);
}
private void Add_pike_button_Click(object sender, EventArgs e)
{
aquarium.pikeFlock.Add(new Point(), this);
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
aquarium.Paint(e.Graphics);
}
}
}
Aquarium.cs:
using System.Drawing;
namespace course_project
{
internal class Aquarium
{
readonly public PikeFlock pikeFlock;
readonly public CarpFlock carpFlock;
readonly Color waterColor;
public Aquarium(Rectangle clientRectangle)
{
waterColor = Color.LightSkyBlue;
pikeFlock = new PikeFlock();
carpFlock = new CarpFlock();
}
public void Paint(Graphics graphics)
{
graphics.Clear(waterColor);
foreach (Pike pike in pikeFlock)
pike.Draw(graphics);
foreach (Carp carp in carpFlock)
carp.Draw(graphics);
}
}
}
Fish.cs
using System.Drawing;
namespace course_project
{
internal class Fish
{
private protected Brush brush;
private protected AquariumForm aquarium_form;
public Fish(Color color, AquariumForm aquariumForm)
{
aquarium_form = aquariumForm;
brush = new SolidBrush(color);
}
public virtual void Draw(Graphics graphics) { }
public Point Data { get; set; }
}
}
Confused just at the point where you need to animate it all. By clicking the button everything is created as it should be, but animation does not work, even I do not know how :(
You didn't post fish.cs so without testing I would suggest the following changes:
Do not use PictureBox, it just complicates things if you perform custom animations this way. So instead of extracting the Graphics from its assigned Image property, which is backed by a Bitmap that has the same size as your form (btw. what happens if you resize your form?) simply just use the Graphics that is already there when your form is redrawn.
Unlike PictureBox, a Form does not use double buffering by default so you might want to set DoubleBuffered = true in the form constructor to avoid flickering.
Override OnPaint in the form and initiate the whole repaint session from there:
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
aquarium.Paint(e.Graphics); // instead of your Init() method
}
It means you don't need bitmap, graphics and aquarium_form fields in your Aquarium class. Instead, you can pass the form's Graphics to a Paint method:
// in Aquarium.cs:
public void Paint(Graphics graphics)
{
// Please note that graphics can have any size now so it works even if you
// resize the form. Please also note that I treat pikeFlock as a Pike
// enumeration instead of just coordinates
g.Clear(waterColor);
foreach (Pike pike in pikeFlock) // instead of int[] items
pike.Draw(graphics); // instead of Draw(int[])
}
Your PikeFlock is just IEnumerable, I would change it to IEnumerable<Pike> to make it strongly typed.
It also means that PikeFlock.GetEnumerator should yield Pike instances instead of int arrays: yield return current instead of current.Data
Pike has now int[] as coordinates. I would change it to Point. And do not pass new random coordinates in every drawing iteration because every fish will just randomly 'teleport' here and there. Instead, maintain the current horizontal and vertical speed.
// in Pike.cs:
private Point coordinates;
private Size size = new Size(40, 40);
// speed is declared as Size because there is an operator overload for Point + Size
// Do not forget to initialize speed in constructor
private Size speed;
public override void Draw(Graphics graphics)
{
// no Refresh is needed because graphics comes from the form's OnPaint now
graphics.FillEllipse(brush, coordinates.X, coordinates.Y, size.Width, size.Height);
}
public void UpdateLocation(Rectangle bounds)
{
// if a fish would go out of bounds invert its vertical or horizontal speed
// TODO: if you shrink the form an excluded fish will never come back
// because its direction will oscillate until you enlarge the form again.
if (!bounds.Contains(coordinates + speed))
{
if (coordinates.X + speed.Width < bounds.Left
|| coordinates.X + speed.Width > bounds.Right - size.Width)
{
speed.Width *= -1;
}
if (coordinates.Y + speed.Height < bounds.Top
|| coordinates.Y + speed.Height > bounds.Bottom - size.Height)
{
speed.Height *= -1;
}
}
coordinates += speed;
}
Now the only thing is missing is the animation itself. You can add a Timer component to the form from the Toolbox.
// form:
public AquariumForm()
{
InitializeComponent();
DoubleBuffered = true;
// No need to pass the Form anymore. Pass the bounds instead to
// generate the initial coordinates within the correct range.
// No Random field is needed here, use one in Aquarium constructor only.
aquarium = new Aquarium(ClientRectangle);
timer.Interval = 100;
timer.Tick += Timer_Tick;
timer.Enabled = true;
}
private void Timer_Tick(object sender, EventArgs e)
{
// Updating coordinates of all fish. We just pass the current bounds.
foreach (Pike pike in aquarium.pikeFlock)
pike.UpdateLocation(ClientRectangle);
// Invalidating the form's graphics so a repaint will be automatically
// called after processing the pending events.
Invalidate();
}
I use this program: https://www.codeproject.com/Articles/24681/WPF-Diagram-Designer-Part-4 and change it a bit to make a new program to draw graph(mathmatical graph) but not completed.
for each DesignerItem I add a Text as Adorner(TextAdorner class), you can see this at: https://i.stack.imgur.com/VDvGQ.png
but when two or more DesignerItems overlaped all text mixed together, because Adorner is top of all DesignerItems in canvas.
There is a way to make an adorner below of a DesignerItem in canvas?
You can see TextAdorner class below.
class TextAdorner : Adorner
{
VisualCollection _visualChildren;
Path _textGeometry;
TexFormula formula;
TexFormulaParser parser;
public Brush ForeColor
{ get; set; }
public double FontSize
{ get; set; }
public TextAdorner(UIElement adornedElement)
: base(adornedElement)
{
_visualChildren = new VisualCollection(this);
this.Tag = #"v^{b_i}_{c_j}";
parser = new TexFormulaParser();
formula = parser.Parse(this.Tag.ToString());
_textGeometry=new Path();
this.ForeColor = Brushes.Black;
this.FontSize = 32;
_textGeometry.Fill = this.ForeColor;
_textGeometry.Stroke = this.ForeColor;
_textGeometry.StrokeThickness = 1;
_visualChildren.Add(_textGeometry);
this.HorizontalAlignment = System.Windows.HorizontalAlignment.Center;
}
protected override Size ArrangeOverride(Size finalSize)
{
double _left;
double _top;
_textGeometry.Data = formula.GetRenderer(TexStyle.Display, this.FontSize, "Times New Roman").RenderToGeometry(0, 0);
switch (this.HorizontalAlignment)
{
case System.Windows.HorizontalAlignment.Left:
{
_left = 0;
break;
}
case System.Windows.HorizontalAlignment.Right:
{
_left = AdornedElement.RenderSize.Width - _textGeometry.RenderedGeometry.Bounds.Width;
break;
}
default:
{
_left = AdornedElement.RenderSize.Width / 2 - _textGeometry.RenderedGeometry.Bounds.Width / 2;
break;
}
}
switch (this.VerticalAlignment)
{
case System.Windows.VerticalAlignment.Top:
{
_top = 0;
break;
}
case System.Windows.VerticalAlignment.Bottom:
{
_top = AdornedElement.RenderSize.Height- _textGeometry.RenderedGeometry.Bounds.Height;
break;
}
default:
{
_top = AdornedElement.RenderSize.Height / 2 - _textGeometry.RenderedGeometry.Bounds.Height/ 2;
break;
}
}
Rect _leftTopRect = new Rect(_left,_top,AdornedElement.RenderSize.Width,AdornedElement.RenderSize.Height);
_textGeometry.Arrange(_leftTopRect);
return finalSize;
}
protected override int VisualChildrenCount
{
get { return _visualChildren.Count; }
}
protected override Visual GetVisualChild(int index)
{
return _visualChildren[index];
}
}
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);
}
}
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);
}
}
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());
}
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...
I want to make an application in which the user could add rectangle with customizable text inside it. The rectangle also can have another rectangles inside. Just as you can see on these picture:
I read about DrawingVisual, Shapes etc. So far I did it using DrawingVisual + Host, which derivies from FrameworkElement. DrawingVisual has FormattedText field, and list of Children elements; Host maintain drawing all elements.
The main problem is that, everytime user changes text in any child element I need to calculate new coordinates, width, height of all child elements. Maybe there is any method to do that automatically?
Also, DrawingVisual doesn't have any mouse events. So how to make all elements selectable / hoverable? Or should I derive from some other class?
Later I will post some code...
EDIT:
public class VisualHost: FrameworkElement
{
private VisualCollection _children;
private List<MyElement> _list;
public VisualHost(List<MyElement> list)
{
_children = new VisualCollection(this);
_list = list;
}
protected override int VisualChildrenCount
{
get { return _children.Count; }
}
protected override Visual GetVisualChild(int index)
{
if (index < 0 || index >= _children.Count)
{
throw new ArgumentOutOfRangeException();
}
return _children[index];
}
private void CheckSize(MyElement element)
{
double sw = 0;
double mh = 0;
if (element.GetChildCount() > 0)
{
for (int i = 0; i < element.GetChildCount(); i++)
{
CheckSize(element.GetChild(i));
sw += element.GetChild(i).Width;
mh = Math.Max(mh, element.GetChild(i).Height);
}
}
element.Width = Math.Max(element.Formatted.Width, sw);
element.Height = element.Formatted.Height + mh;
}
private void DrawElement(double top, double left, MyElement element)
{
CheckSize(element);
var context = element.RenderOpen();
context.DrawRectangle( null, new Pen(Brushes.Black, 2d), new Rect(new Point(left, top), new Size(element.Width, element.Height)));
context.DrawText(element.Formatted, new Point(left, top));
top += element.Formatted.Height;
if (element.GetChildCount() > 0)
{
for (int i = 0; i < element.GetChildCount(); i++)
{
context.DrawRectangle(null, new Pen(Brushes.Black, 2d), new Rect(new Point(left, top), new Size(element.GetChild(i).Width, element.GetChild(i).Height)));
context.DrawText(element.GetChild(i).Formatted, new Point(left, top));
left += element.GetChild(i).Width;
}
}
context.Close();
_children.Add(element);
}
public void Redraw()
{
if (_list != null)
{
double top = 0, left = 0;
foreach (MyElement element in _list)
{
DrawElement(top, left, element);
top += element.Height + 10d;
}
}
}
}
public class MyElement: DrawingVisual
{
private string _text;
public string Text
{
get { return _text; }
set {
if (_text != value)
{
Typeface typeface = new Typeface(new FontFamily("Arial"), FontStyles.Normal, FontWeights.Normal, FontStretches.Normal);
Formatted = new FormattedText(value, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, typeface, 12, Brushes.Red);
_text = value;
}
}
}
public FormattedText Formatted { get; private set; }
public double Height { get; set; }
public double Width { get; set; }
private List<MyElement> _children;
public MyElement GetChild(int i)
{
if (i < 0 || i >= _children.Count)
{
throw new ArgumentOutOfRangeException();
}
return _children[i];
}
public int GetChildCount()
{
return _children.Count;
}
public void AddChild(MyElement child)
{
_children.Add(child);
}
public MyElement(string Text)
{
this.Text = Text;
this._children = new List<MyElement>();
}
}
MainWindow.xaml.cs
public MainWindow()
{
InitializeComponent();
_list = new List<MyElement>();
_list.Add(new MyElement("text"));
var e = new MyElement("text 2");
e.AddChild(new MyElement("a"));
e.AddChild(new MyElement("b"));
e.AddChild(new MyElement("c"));
_list.Add(e);
_host = new VisualHost(_list);
MyCanvas.Children.Add(_host);
_host.Redraw();
}
This is my code for now. I wrote it only to check if idea is correct.
well I'm not sure if you would like this approach but you can actually do it very simple... I'm thinking maybe you can use blend to create a user control and design a label and a listbox in a stackpanel and set 'em all on autosizing.
or design 2 stack panels set 1 to do vertical orientation and the other one to do horizontal and add textblocks or something to the horizontal one.
I have a custom element class that is a subclass of FrameworkElement.
public class MyCustomElement : FrameworkElement
{
private VisualCollection children;
public MyCustomElement()
{
this.children = new VisualCollection(this);
this.children.Add(MyDrawingRoutines());
}
private DrawingVisual MyDrawingRoutines()
{
//...
}
protected override int VisualChildrenCount
{
get { return children.Count; }
}
protected override Visual GetVisualChild(int index)
{
if (index < 0 || index >= children.Count)
{
throw new ArgumentOutOfRangeException();
}
return children[index];
}
}
The UI holds a canvas in which these custom drawing elements are added and hit testing is performed.
public partial class MainWindow : Window
{
private MyCustomElement myCustomElement;
public MainWindow()
{
InitializeComponent();
myCustomElement = new MyCustomElement();
myCanvas.Children.Add(myCustomElement);
}
private void myCanvas_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
System.Windows.Point pt = e.GetPosition((UIElement)sender);
VisualTreeHelper.HitTest(this, null, new HitTestResultCallback(leftClickCallback), new PointHitTestParameters(pt));
}
public HitTestResultBehavior leftClickCallback(HitTestResult result)
{
if (result.VisualHit.GetType() == typeof(DrawingVisual))
{
if (((DrawingVisual)result.VisualHit).Opacity == 1.0)
{
((DrawingVisual)result.VisualHit).Opacity = 0.4;
}
else
{
((DrawingVisual)result.VisualHit).Opacity = 1.0;
}
}
return HitTestResultBehavior.Stop;
}
}
}
This code works as expected, but I cannot find a way to determine which MyCustomElement the detected DrawingVisual belongs. Right now, the opacity adjustment is done only superficially correct? I would like to change the opacity property on MyCustomElement, have the MyDrawingRoutines() method apply it, and have only the finished DrawingVisual drawn on the Canvas.
You should be able to cast the Parent property of the DrawingVisual to your MyCustomElement class:
public HitTestResultBehavior leftClickCallback(HitTestResult result)
{
var visual = result.VisualHit as DrawingVisual;
if (visual != null)
{
var element = visual.Parent as MyCustomElement;
if (element != null)
{
if (element.Opacity == 1.0)
{
element.Opacity = 0.4;
}
else
{
element.Opacity = 1.0;
}
}
}
return HitTestResultBehavior.Stop;
}
In case you need to get the parent of any visual (not just a ContainerVisual, which has the Parent property as shown above), you may use VisualTreeHelper.GetParent:
var visual = result.VisualHit;
var element = VisualTreeHelper.GetParent(visual) as MyCustomElement;