Draw Shapes and Strings with undo and redo feature - c#

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...

Related

Make a CustomListBox to add BorderColor then can't use ListBox normal properties/methods

I'm trying to change the border color of a ListBox.
I made this custom control where i have a function that makes the border by drawing.
After had it working, noticed that can't use ListBox.Items anymore, methods such as .Add() or .Clear().
Code of the Custom ListBox:
class CustomListBox : UserControl
{
//Fields
private Color borderColor = Color.MediumSlateBlue;
private int borderSize = 1;
//Items
private ListBox Listb;
//Properties
[Category("Custom")]
public Color BorderColor
{
get { return borderColor; }
set
{
borderColor = value;
}
}
[Category("Custom")]
public int BorderSize
{
get { return borderSize; }
set
{
borderSize = value;
this.Padding = new Padding(borderSize);//Border Size
AdjustListBoxDimensions();
}
}
public CustomListBox()
{
Listb = new ListBox();
this.SuspendLayout();
// ListBox
Listb.BorderStyle = BorderStyle.None;
Listb.DrawMode = DrawMode.OwnerDrawFixed;
Listb.ForeColor = Color.FromArgb(((int)(((byte)(249)))), ((int)(((byte)(249)))), ((int)(((byte)(249)))));
Listb.FormattingEnabled = true;
Listb.ItemHeight = 24;
Listb.Location = new Point(567, 64);
Listb.Name = "CustomListBox";
Listb.Size = new Size(235, 936);
Listb.DrawItem += new DrawItemEventHandler(Listb_DrawItem);
this.MinimumSize = new Size(200, 30);
this.Size = new Size(200, 30);
this.Padding = new Padding(borderSize);//Border Size
this.Font = new Font(this.Font.Name, 12F);
this.ResumeLayout();
AdjustListBoxDimensions();
}
// Highlight event
private void Listb_DrawItem(object sender, DrawItemEventArgs e)
{
Color backgroundColor = Color.FromArgb(50, 50, 50);
Color horizontalColor = Color.FromArgb(100, 100, 100);
if (e.Index >= 0)
{
SolidBrush sb = new SolidBrush(((e.State & DrawItemState.Selected) == DrawItemState.Selected) ? horizontalColor : backgroundColor);
e.Graphics.FillRectangle(sb, e.Bounds);
string text = Listb.Items[e.Index].ToString();
SolidBrush tb = new SolidBrush(e.ForeColor);
e.Graphics.DrawString(text, e.Font, tb, Listb.GetItemRectangle(e.Index).Location);
}
}
//Adjust Dimension (Still in test)
private void AdjustListBoxDimensions()
{
Listb.Location = new Point()
{
X = this.Width - this.Padding.Right - Listb.Width,
Y = Listb.Height
};
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
Graphics graph = e.Graphics;
//Draw border
using (Pen penBorder = new Pen(borderColor, borderSize))
{
penBorder.Alignment = PenAlignment.Inset;
graph.DrawRectangle(penBorder, 0, 0, this.Width - 0.5F, this.Height - 0.5F);
}
}
}
My problem is that i can't use the ListBox properties/methods, is there a way to inherit them?
Since you've made your own control, you have to make your own properties and methods, calling Listbox's methods and getters/setters
something like this:
public void Add(object item)
{
this.Listb.Items.Add(item);
}
public ObjectCollection ListItems
{
get
{
return this.Listb.Items;
}
set
{
this.Listb.Items.AddRange(value);
}
}
//etc...
public void Add(object item)
{
this.Listb.BeginUpdate();
this.Listb.Items.Add(item);
this.Listb.EndUpdate();
}
public void Clear()
{
this.Listb.BeginUpdate();
this.Listb.Items.Clear();
this.Listb.EndUpdate();
}
public int SelectedIndex()
{
return this.Listb.SelectedIndex;
}
public object Item(int index)
{
return this.Listb.Items[index];
}
Add to code:
Dock=Fill
This.control.add(listb)

How do I associate a mouse click with a drawn object in C#?

I have a picturebox with a bunch of rectangles drawn over it (highlighting some features of the image). I want to determine if my user clicked within a given rectangle, and add an action specific to that rectangle (i.e. show additional information). How do I do this?
I can provide more information if desired, I'm just not sure what information would be useful at this point.
Current code to draw rectangles. rectX, rectY, rectRot, rectColor are all currently arrays. rectW and rectH are constants.
private void pbPicture_Paint(object sender, PaintEventArgs e)
{
for(int i = 0; i < rectX.Length; i++)
{
e.Graphics.ResetTransform();
e.Graphics.TranslateTransform(rectX[i], rectY[i]);
e.Graphics.RotateTransform(rectRot[i]);
e.Graphics.DrawRectangle(new Pen(rectColor[i], penWidth), 0, 0, rectW, rectH);
}
e.Graphics.ResetTransform();
}
Edit: added link to picture, additional code.
It'll be easier if you apply and keep the transform data for each shape, then you can use that in your implementation to draw the shapes, interact with the mouse inputs...etc. without doing any additional transform calls to draw the main shapes, nor math routines to find out whether a shape/rectangle contains a given Point.
Consider the Shape class here which encapsulates the relevant data and functionalities that you'll need in your implementation. Using the GraphicsPath class to keep the shape and apply the transform, as well as to use the GraphicsPath.IsVisible method to determine whether the shape contains a given point so you can take an action accordingly. Keeping and exposing the Matrix instance is to use it to transform the graphics in case you need to do more drawings over the shape like drawing text, image...etc.
using System;
using System.Drawing;
using System.Drawing.Text;
using System.Windows.Forms;
using System.Drawing.Drawing2D;
using System.Collections.Generic;
public class Shape : IDisposable
{
private bool disposedValue;
private Matrix mx;
private GraphicsPath gp;
private Size sz;
private Point loc;
private float rot;
public string Text { get; set; }
public Size Size
{
get => sz;
set
{
if (sz != value)
{
sz = value;
CleanUp();
}
}
}
public Point Location
{
get => loc;
set
{
if (loc != value)
{
loc = value;
CleanUp();
}
}
}
public float Rotation
{
get => rot;
set
{
if (rot != value)
{
rot = value;
CleanUp();
}
}
}
public Matrix Matrix
{
get
{
if (mx == null)
{
mx = new Matrix();
// According to your code snippet, you don't need to offset here.
// mx.Translate(Location.X, Location.Y);
mx.RotateAt(Rotation, Center);
}
return mx;
}
}
public GraphicsPath GraphicsPath
{
get
{
if (gp == null)
{
gp = new GraphicsPath();
gp.AddRectangle(Rectangle);
gp.Transform(Matrix);
}
return gp;
}
}
public Point Center
{
get
{
var r = Rectangle;
return new Point(r.X + r.Width / 2, r.Y + r.Height / 2);
}
}
public Rectangle Rectangle => new Rectangle(Location, Size);
public bool Selected { get; set; }
public Color BorderColor { get; set; } = Color.Black;
// Add more, ForeColor, BackColor ...etc.
public bool Contains(Point point) => GraphicsPath.IsVisible(point);
private void CleanUp()
{
gp?.Dispose();
gp = null;
mx?.Dispose();
mx = null;
}
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing) CleanUp();
disposedValue = true;
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
Having that, your implementation should be as simple as:
private readonly List<Shape> shapes = new List<Shape>();
private const int recW = 100;
private const int recH = 20;
// A method to create the list...
private void SomeMethod()
{
shapes.ForEach(s => s.Dispose());
shapes.Clear();
// In some loop...
var shape = new Shape
{
Text = "Shape...",
Size = new Size(recW, recH),
Location = new Point(some.X, some.Y),
Rotation = someAngle
};
shapes.Add(shape);
// Add the reset...
pbox.Invalidate();
}
// And to dispose of them...
protected override void OnFormClosed(FormClosedEventArgs e)
{
base.OnFormClosed(e);
shapes.ForEach(x => x.Dispose());
}
The drawing part:
private void pbox_Paint(object sender, PaintEventArgs e)
{
var g = e.Graphics;
using (var sf = new StringFormat(StringFormat.GenericTypographic))
{
sf.Alignment = sf.LineAlignment = StringAlignment.Center;
shapes.ForEach(s =>
{
using (var pnBorder = new Pen(s.BorderColor))
{
g.SmoothingMode = SmoothingMode.AntiAlias;
g.PixelOffsetMode = PixelOffsetMode.Half;
if (s.Selected) g.FillPath(Brushes.DarkOrange, s.GraphicsPath);
g.DrawPath(pnBorder, s.GraphicsPath);
if (!string.IsNullOrEmpty(s.Text))
{
g.SmoothingMode = SmoothingMode.None;
g.PixelOffsetMode = PixelOffsetMode.Default;
g.TextRenderingHint = TextRenderingHint.ClearTypeGridFit;
g.Transform = s.Matrix;
g.DrawString(s.Text, Font, Brushes.Black, s.Rectangle, sf);
g.ResetTransform();
}
}
});
}
}
Interacting with the mouse events:
private void pbox_MouseDown(object sender, MouseEventArgs e)
{
foreach (var shape in shapes)
shape.Selected = shape.Contains(e.Location);
pbox.Invalidate();
}
I've created rectangles (Shape objects) with random values to demo.
Note: Some offset (Matrix.Translate(...)) also applied here to have some space between the shapes.
\You can use the event argument e to get the mouse co ordinates
private void pictureBox1_Click(object sender, EventArgs e)
{
MouseEventArgs me = (MouseEventArgs)e;
Point coordinates = me.Location;
}
There is one more link to help you identify the click events on the shape -
https://learn.microsoft.com/en-us/previous-versions/visualstudio/visual-studio-2015/ide/tutorial-3-create-a-matching-game?view=vs-2015&redirectedfrom=MSDN

How to drag and move shapes in C#

In a C# WindoeFormsApplication, is it possible to select, hence to move or delete a plotted shape with mouse? Like the windows paint program.
The shape plotting works totally fine, all points are stored in some array. As this line drawing example
Point Latest { get; set; }
List<Point> _points = new List<Point>();
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
// Save the mouse coordinates
Latest = new Point(e.X, e.Y);
// Force to invalidate the form client area and immediately redraw itself.
Refresh();
}
protected override void OnPaint(PaintEventArgs e)
{
var g = e.Graphics;
base.OnPaint(e);
if (_points.Count > 0)
{
var pen = new Pen(Color.Navy);
var pt = _points[0];
for(var i=1; _points.Count > i; i++)
{
var next = _points[i];
g.DrawLine(pen, pt, next);
pt = next;
}
g.DrawLine(pen, pt, Latest);
}
}
private void Form1_MouseClick(object sender, MouseEventArgs e)
{
Latest = new Point(e.X, e.Y);
_points.Add(Latest);
Refresh();
}
I can let it to calculate the shortest distance between mouse position and each line by basic linear algebra, and set a threshold distance, if it's shorter than the threshold, make this line selected, and can be dragged or edited by mouse. But, just wondering, is there any way that's more manageable for such task? Mainly the selection part.
Any suggestion will be appreciated, thank you!
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);
}
}

Draw rectangle with text inside

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.

Round shaped buttons

How do I make a button in a round shape rather than the conventional rectangle.
I am using winforms(2.0)
First make a class. Give it name: "RoundButton".
Then write the code directly as this:
using System;
using System.Collections.Generic;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
using System.Linq;
using System.Text;
namespace WindowsFormsApplication1
{
public class RoundButton : Button
{
protected override void OnPaint(System.Windows.Forms.PaintEventArgs e)
{
GraphicsPath grPath = new GraphicsPath();
grPath.AddEllipse(0, 0, ClientSize.Width, ClientSize.Height);
this.Region = new System.Drawing.Region(grPath);
base.OnPaint(e);
}
}
}
Then, build your application and close this.
Now go to the toolbox and you will see a control named RoundButton.
Then drag and drop this on your Windows form and test it.
GraphicsPath p = new GraphicsPath();
p.AddEllipse(1, 1, button1.Width - 4, button1.Height - 4);
button1.Region = new Region(p);
Code project has many articles about these kinds of things, especially the article RoundButton Windows Control - Ever Decreasing Circles might be of interest since it shows you have to do different kinds of round buttons.
This or this could help if you need to implement your own Button class based on the default Windows Forms button. You can also search online for more examples.
What about 'GDI'?
Implementation Example:
#region <Round Corners> : (Properties)
// [Use Round Corners]
private bool useRoundCorners = false;
[Category("Control Corners"), DisplayName("Round Corners")]
[Description("Set Round Corners.")]
[Browsable(true)]
public bool UseRoundBorders
{
get { return useRoundCorners; }
set { if (useRoundCorners != value) { useRoundCorners = value; Invalidate(); } }
}
// [Ellipse Radius]
private int ellipseRadius = 20;
[Category("Control Corners"), DisplayName("Radius")]
[Description("Set Corner (Ellipse) Radius")]
[Browsable(true)]
public int EllipseRadius
{
get { return ellipseRadius.FixedValue(0, 90); }
set { if (ellipseRadius != value.FixedValue(0, 90)) { ellipseRadius = value; Invalidate(); } }
}
#endregion
#region <Round Corners> : (Draw)
[DllImport("Gdi32.dll", EntryPoint = "CreateRoundRectRgn")]
private static extern IntPtr CreateRoundRectRgn
(
int nLeftRect, // x-coordinate of upper-left corner
int nTopRect, // y-coordinate of upper-left corner
int nRightRect, // x-coordinate of lower-right corner-
int nBottomRect, // y-coordinate of lower-right corner
int nWidthEllipse, // width of ellipse
int nHeightEllipse // height of ellipse
);
/// <summary> Draw Corners (Round or Square). </summary>
/// <param name="e"></param>
private void DrawCorners()
{
if (useRoundCorners) { this.Region = Region.FromHrgn(CreateRoundRectRgn(0, 0, Width, Height, ellipseRadius, ellipseRadius)); }
else { this.Region = Region.FromHrgn(CreateRoundRectRgn(0, 0, Width, Height, 0, 0)); }
}
#endregion
/// <summary> Redraw (Update) the Control. </summary>
/// <param name="e"></param>
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
DrawCorners();
}
Extension to Limit Round Corner Radius (*Range: 0-90)
Author: Salvador
// Extension to Set Min. & Max. Values to Properties
public static class Extensions
{
public static int FixedValue(this int value, int min, int max)
{
if (value >= min && value <= max) { return value; }
else if (value > max) { return max; }
else if (value < min) { return min; }
else { return 1; }
}
}
This is what you want
public class RoundButton : Control
{
private readonly Label lbl;
public RoundButton() : base()
{
lbl = new Label
{
Text = Text,
ForeColor = ForeColor,
BackColor = BackColor,
Font = Font
};
CenterInParent();
}
private void CenterInParent()
{
lbl.Left = (Width - lbl.Width) / 2;
lbl.Top = (Height - lbl.Height) / 2;
}
protected override void OnPaint(PaintEventArgs e)
{
GraphicsPath grPath = new GraphicsPath();
grPath.AddEllipse(0, 0, ClientSize.Width, ClientSize.Height);
Region = new Region(grPath);
base.OnPaint(e);
}
protected override void OnMove(EventArgs e)
{
CenterInParent();
base.OnMove(e);
}
protected override void OnTextChanged(EventArgs e)
{
lbl.Text = Text;
base.OnTextChanged(e);
}
protected override void OnForeColorChanged(EventArgs e)
{
lbl.ForeColor = ForeColor;
base.OnForeColorChanged(e);
}
protected override void OnBackColorChanged(EventArgs e)
{
lbl.BackColor = BackColor;
base.OnBackColorChanged(e);
}
protected override void OnFontChanged(EventArgs e)
{
lbl.Font = Font;
base.OnFontChanged(e);
}
}
Pros:
No cuts
Round
Cons:
Wrong designer loader for round button
public class OptionsMenu : Button
{
public OptionsMenu(int NoOfOptions, Point Location, ControlCollection controls,
Size ButtonSize, int DistanceBetweenOptions)
{
Button[] buttons = new Button[NoOfOptions];
for (int i = 0; i < NoOfOptions; i++)
{
buttons[i] = new Button()
{
Size = ButtonSize,
};
GraphicsPath p = new GraphicsPath();
p.AddEllipse(1, 1, buttons[i].Width - 4, buttons[i].Height - 4);
buttons[i].Region = new Region(p);
buttons[i].Location = new Point(Location.X, Location.Y + DistanceBetweenOptions * i);
controls.Add(buttons[i]);
}
}
}
You can call it like this:
OptionsMenu menu = new OptionsMenu(4, new Point(50, 50), Controls, new Size(20, 20), 40);
Controls.AddRange(new Control[]
{
menu
});

Categories