I'm trying to make a program to draw on the a Panel (a square, circle, etc...) by clicking on a button.
I have not done much so far, just tried the code drawing directly to the panel but don't know how to move it to the button. Here is the code I have so far.
If you know a better method to draw than the one I'm using please let me know.
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void mainPanel_Paint(object sender, PaintEventArgs e)
{
Graphics g;
g = CreateGraphics();
Pen pen = new Pen(Color.Black);
Rectangle r = new Rectangle(10, 10, 100, 100);
g.DrawRectangle(pen, r);
}
private void circleButton_Click(object sender, EventArgs e)
{
}
private void drawButton_Click(object sender, EventArgs e)
{
}
}
}
Using this extremely simplified example class..:
class DrawAction
{
public char type { get; set; }
public Rectangle rect { get; set; }
public Color color { get; set; }
//.....
public DrawAction(char type_, Rectangle rect_, Color color_)
{ type = type_; rect = rect_; color = color_; }
}
Have a class level List<T>:
List<DrawAction> actions = new List<DrawAction>();
you would code a few buttons like this:
private void RectangleButton_Click(object sender, EventArgs e)
{
actions.Add(new DrawAction('R', new Rectangle(11, 22, 66, 88), Color.DarkGoldenrod));
mainPanel.Invalidate(); // this triggers the Paint event!
}
private void circleButton_Click(object sender, EventArgs e)
{
actions.Add(new DrawAction('E', new Rectangle(33, 44, 66, 88), Color.DarkGoldenrod));
mainPanel.Invalidate(); // this triggers the Paint event!
}
And in the Paint event:
private void mainPanel_Paint(object sender, PaintEventArgs e)
{
foreach (DrawAction da in actions)
{
if (da.type == 'R') e.Graphics.DrawRectangle(new Pen(da.color), da.rect);
else if (da.type == 'E') e.Graphics.DrawEllipse(new Pen(da.color), da.rect);
//..
}
}
Also use a double-buffered Panel subclass:
class DrawPanel : Panel
{
public DrawPanel()
{ this.DoubleBuffered = true; BackColor = Color.Transparent; }
}
The next steps will be to add more types, like lines, curve, text; also colors, pen widths and styles. Also make it dynamic, so that you pick a tool and then click at the Panel..
For freehand drawing you need to collect a List<Point> in the MouseMove etc..
Lots of work, lots of fun.
Updates from comments:
Note: This is, as I wrote extremely simplified. I have character to draw shapes like Rectangle and Ellipse. With a little more code you can add more characters for a filled rectangle and a filledEllipse. But a) the shapes really ought to be in an Enum and b) more complicated shapes like lines, polygons, text, or shapes with rotation will need more data that just a Rectangle. .
The restriction to rectangle coodinates was a simplification, not so much of the shape but of the data structure. Your other shapes could be reduced to fit in a rectangle (four triangles and two hexagons come to mind); just add charcters and new drawxxx calls.. But evetually adding a List for complex shapes and maybe a string and a font will allow for more complex results..
Related
I have a WinForms program where the user clicks on a PictureBox control.
I want a small red dot (a few pixels across) every time the user clicks.
I also don't want any of the previous dots to go away.
I know I will need a generic list of ellipses and rectangles, but am not sure how to execute this. How would I go about doing this?
In my program a pictureBox1_Click method handles mouse click events and returns the position of the clicks.
A pictureBox1_Paint method handles the graphics to be drawn on these points.
You have to create a container that can reference the Points collection and add one point to the collection each time you click on a paint-able Control.
Maybe, you want to also create different kinds of drawing points, based on some conditions or requirements.
Thus, you need to store also these extra properties, not just a point coordinate.
If so, you need a specialized object that can expose these properties when needed.
So, here's a custom Class object with some simple properties, that lets you define a Point's Color and Size. For each of its Points collection.
It also implements the IDisposable interface, because we need to create a Pen object for each Point we draw. And a Pen object needs to be disposed (implements IDisposable).
To perform the drawing, you just need to call Control.Invalidate() (pictureBox1.Invalidate() in the example). This causes the repainting of the invalidated parts of the control, raising the OnPaint() event.
Each point (that needs to be repainted) is drawn using e.Graphics.DrawEllipse().
You can test it this way:
With predefined properties, using just a Mouse pointer coordinate:
myPoints.Add(new MyPoints.DrawingPoint(e.Location));
With specific properties when something different is needed:
With a size of 8x8 pixels
newPoint.Dot = new Rectangle(e.Location, new Size(8, 8)));.
With an orange Pen of 2 pixels in size
newPoint.DrawingPen = new Pen(Color.Orange, 2);
Add this new Point to the collection
myPoints.DrawingPoints.Add(newPoint);
The Clear() method is used to Dispose() the current list of Points and create a new, empty, List:
MyPoints.Clear();
Sample implementation:
MyPoints myPoints = new MyPoints();
private void pictureBox1_MouseUp(object sender, MouseEventArgs e)
{
//Use default property values
//myPoints.Add(new MyPoints.DrawingPoint(e.Location));
MyPoints.DrawingPoint newPoint = new MyPoints.DrawingPoint();
newPoint.Dot = new Rectangle(e.Location, new Size(4, 4));
newPoint.DrawingPen = new Pen(Color.Red, 2);
myPoints.DrawingPoints.Add(newPoint);
(sender as Control).Invalidate();
}
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
foreach (MyPoints.DrawingPoint mypoint in myPoints.DrawingPoints) {
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
e.Graphics.DrawEllipse(mypoint.DrawingPen, mypoint.Dot);
}
}
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
myPoints.Dispose();
}
The Point objects collection Class container:
internal class MyPoints : IDisposable
{
bool IsDisposed = false;
public MyPoints() => DrawingPoints = new List<DrawingPoint>();
public List<DrawingPoint> DrawingPoints { get; set; }
public void Add(DrawingPoint NewPoint)
{
if (NewPoint.Dot.Size.Width > 1 && NewPoint.Dot.Size.Height > 1) {
DrawingPoints.Add(NewPoint);
}
}
public void Clear()
{
this.Dispose();
this.DrawingPoints.Clear();
this.DrawingPoints = new List<DrawingPoint>();
}
public void Remove(Point point)
{
Remove(this.DrawingPoints.Select((p, i) => { if (p.Dot.Contains(point)) return i; return -1; }).First());
}
public void Remove(int Index)
{
if (Index > -1) {
this.DrawingPoints[Index].Delete();
this.DrawingPoints.RemoveAt(Index);
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected void Dispose(bool IsSafeDisposing)
{
if (IsSafeDisposing && (!this.IsDisposed) && (this.DrawingPoints.Count > 0)) {
foreach (DrawingPoint dp in this.DrawingPoints)
if (dp != null) dp.Delete();
}
}
public class DrawingPoint
{
Pen m_Pen = null;
Rectangle m_Dot = Rectangle.Empty;
public DrawingPoint() : this(Point.Empty) { }
public DrawingPoint(Point newPoint)
{
this.m_Pen = new Pen(Color.Red, 1);
this.m_Dot = new Rectangle(newPoint, new Size(2, 2));
}
public Pen DrawingPen { get => this.m_Pen; set => this.m_Pen = value; }
public Rectangle Dot { get => this.m_Dot; set => this.m_Dot = value; }
public void Delete()
{
if (this.m_Pen != null) this.m_Pen.Dispose();
}
}
}
This is how it can work, changing its properties when required:
Basically you must use GDI+
Check the code bellow:
private void pictureBox1_Click(object sender, EventArgs e)
{
Graphics g = Graphics.FromImage(pictureBox1.Image);
Pen p = new Pen(Color.Red, 3);
var cursorPosition = pictureBox1.PointToClient(Cursor.Position);
g.DrawEllipse(p, cursorPosition.X, cursorPosition.Y, 15, 15);
MyCircles.Add(cursorPosition);
pictureBox1.Refresh();
}
List<Point> MyCircles = new List<Point>();
private void pictureBox1_DoubleClick(object sender, EventArgs e)
{
foreach (var item in MyCircles)
{
MessageBox.Show($"One circle was created: X:{item.X}, Y:{item.Y}");
}
}
The MyCircles List is only if you want to store the circles to reshow the image with the circles later, and the doubleclick is a sample of using it (i mean persistency purposes, because the circles will not leave the image until you close your app). Check other g methods to draw lines, rectangles or whatever. (Uses System.Drawing namespace)
I came up with a simple solution for my problem
I made a list of rectangles:
List<Rectangle> rects = new List<Rectangle>();
And added rectangles with ever click:
private void pictureBox1_Click(object sender, MouseEventArgs e)
{
rects.Add(new Rectangle(e.Location, new Size(4,4)));
}
And painted the points:
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
foreach (var rect in rects)
{
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
e.Graphics.DrawEllipse(magenta,rect);
}
}
And added a method for clearing drawings with rects.Clear()
I am having trouble with removing DrawReversibleFrame from the screen. Docs say to enter the function twice to remove it but it seems like it does not work with my logic layout.
using System.Drawing;
using System.Windows.Forms;
namespace WindowsFormsApp6
{
public partial class Form1 : Form
{
private Point? _start;
private Rectangle _previousBounds;
public Form1()
{
InitializeComponent();
}
private void OnMouseDown(object sender,MouseEventArgs e)
{
_start = e.Location;
}
private void OnMouseMove(object sender,MouseEventArgs e)
{
if (_start.HasValue)
{
ReverseFrame();
DrawFrame(e.Location);
}
}
private void OnMouseUp(object sender,MouseEventArgs e)
{
ReverseFrame();
_start = null;
}
private void ReverseFrame()
{
ControlPaint.DrawReversibleFrame(_previousBounds, Color.Red, FrameStyle.Dashed);
}
private void DrawFrame(Point end)
{
ReverseFrame();
var size = new Size(end.X - _start.Value.X, end.Y - _start.Value.Y);
_previousBounds = new Rectangle(_start.Value, size);
_previousBounds = RectangleToScreen(_previousBounds);
ControlPaint.DrawReversibleFrame(_previousBounds, Color.Red, FrameStyle.Dashed);
}
}
}
This is my result when I drag my mouse on the form. http://imgur.com/a/Kh4h7
The pesudo code should work like this which I feel like I am doing.
MouseDown:
Draw rectangle.
Save rectangle bounds.
--
MouseMove (w/ left button down):
Redraw previous rectangle to erase.
Calculate next rectangle.
Draw next rectangle.
Save rectangle bounds.
Rinse/Repeat (indefinitely)
--
MouseUp:
Redraw last rectangle to erase.
Take a look at this example
MouseUp handler in the example is point of interest.
private void Form1_MouseUp(object sender, System.Windows.Forms.MouseEventArgs e)
{
// If the MouseUp event occurs, the user is not dragging.
isDrag = false;
// Draw the rectangle to be evaluated. Set a dashed frame style
// using the FrameStyle enumeration.
ControlPaint.DrawReversibleFrame(theRectangle, this.BackColor, FrameStyle.Dashed);
// Find out which controls intersect the rectangle and
// change their color. The method uses the RectangleToScreen
// method to convert the Control's client coordinates
// to screen coordinates.
Rectangle controlRectangle;
for (int i = 0; i < Controls.Count; i++)
{
controlRectangle = Controls[i].RectangleToScreen(Controls[i].ClientRectangle);
if (controlRectangle.IntersectsWith(theRectangle))
{
Controls[i].BackColor = Color.BurlyWood;
}
}
// Reset the rectangle.
theRectangle = new Rectangle(0, 0, 0, 0);
}
Specifically, I put following line under comment when tested it and got the same result as yours.
// ControlPaint.DrawReversibleFrame(theRectangle, this.BackColor, FrameStyle.Dashed);
Update
I tested your code. I think your rectangle _previousBounds is not getting drawn for the second time.
I am getting a list of pictures from a directory and storing the filenames in a List<String>. I then loop through each of these and create a PictureBox for each of them, I then add the same click event to each. The controls are in a FlowLayoutPanel
foreach(String file in this._files){
PictureBox box = new PictureBox();
box.Height = 50;
box.Width = 50;
box.ImageLocation = file;
box.SizeMode = PictureBoxSizeMode.Zoom;
box.Click += this.PictureClick;
this.flowLayoutPanel1.Controls.Add(box);
}
private void PictureClick(object sender, EventArgs e){
// how do I get the one that has been clicked and set its border color
}
How do I get the one that has been clicked and set its border color?
sender is the PictureBox that was clicked:
private void PictureClick(object sender, EventArgs e) {
PictureBox oPictureBox = (PictureBox)sender;
// add border, do whatever else you want.
}
The sender parameter is indeed your PictureBox, downcast to object. Access it this way:
var pictureBox = sender as PictureBox;
Drawing a border around it could not be so easy as you will have to either override the OnPaint method of the PictureBox, either handle the Paint event.
You can use this class to draw a black thin border around your image.
public class CustomBorderPictureBox : PictureBox
{
public bool BorderDrawn { get; private set; }
public void ToggleBorder()
{
BorderDrawn = !BorderDrawn;
Invalidate();
}
protected override void OnPaint(PaintEventArgs pe)
{
base.OnPaint(pe);
if (BorderDrawn)
using (var pen = new Pen(Color.Black))
pe.Graphics.DrawRectangle(pen, 0, 0, Width - 1, Height - 1);
}
}
I am getting a list of pictures from a directory and storing the filenames in a List<String>. I then loop through each of these and create a PictureBox for each of them, I then add the same click event to each. The controls are in a FlowLayoutPanel
foreach(String file in this._files){
PictureBox box = new PictureBox();
box.Height = 50;
box.Width = 50;
box.ImageLocation = file;
box.SizeMode = PictureBoxSizeMode.Zoom;
box.Click += this.PictureClick;
this.flowLayoutPanel1.Controls.Add(box);
}
private void PictureClick(object sender, EventArgs e){
// how do I get the one that has been clicked and set its border color
}
How do I get the one that has been clicked and set its border color?
sender is the PictureBox that was clicked:
private void PictureClick(object sender, EventArgs e) {
PictureBox oPictureBox = (PictureBox)sender;
// add border, do whatever else you want.
}
The sender parameter is indeed your PictureBox, downcast to object. Access it this way:
var pictureBox = sender as PictureBox;
Drawing a border around it could not be so easy as you will have to either override the OnPaint method of the PictureBox, either handle the Paint event.
You can use this class to draw a black thin border around your image.
public class CustomBorderPictureBox : PictureBox
{
public bool BorderDrawn { get; private set; }
public void ToggleBorder()
{
BorderDrawn = !BorderDrawn;
Invalidate();
}
protected override void OnPaint(PaintEventArgs pe)
{
base.OnPaint(pe);
if (BorderDrawn)
using (var pen = new Pen(Color.Black))
pe.Graphics.DrawRectangle(pen, 0, 0, Width - 1, Height - 1);
}
}
I am developing a very rudimentary drawing program: A 2D grid comprised of multiple RectangleShapes, around 20x30 pixels each, which when clicked change color based on user RGB input, which works just fine:
Color SelectedColor = new Color();
private void Pixel_1_1_Click(object sender, EventArgs e) // on Rectangle click
{
Pixel_1_1.FillColor = SelectedColor; // change to currently desired color.
}
Since the number of squares is rising dramatically, I'm looking for a way to arrange the "pixel" rectangles into a 2D array. (I really don't want to have to make a Pixel_Click method for every single Rectangle on the screen!) Hoping eventually to be able to call something like:
private void Pixel_[x]_[y]_Click(object sender, EventArgs e)
{
Pixel_[x]_[y].FillColor = SelectedColor;
}
My friends suggest the use of an anonymous delegate, but I don't understand how to fully use one to solve my problem.
What would be the best way to generate a 2D array of rectangles in a C# Windows Form? And once generated, how can I access them with a single method for variant values of x and y?
While you are probably correct in thinking of each rectangle as an object, it probably isn't correct to think of each rectangle as a windows control, especially since you have so many of them.
So try creating your own rectangle object:
public class MyRect {
public Color FillColor { get; set; }
public Rectangle Rectangle { get; set; }
public MyRect(Rectangle r, Color c) {
this.Rectangle = r;
this.FillColor = c;
}
}
Now you just need to keep a list of your objects and paint on a single Panel control (or PictureBox) all of your rectangles:
private List<MyRect> myRectangles = new List<MyRect>();
public Form1() {
InitializeComponent();
myRectangles.Add(new MyRect(new Rectangle(10, 10, 64, 16), Color.Blue));
myRectangles.Add(new MyRect(new Rectangle(20, 48, 16, 64), Color.Red));
}
private void panel1_Paint(object sender, PaintEventArgs e) {
foreach (MyRect mr in myRectangles) {
using (SolidBrush sb = new SolidBrush(mr.FillColor)) {
e.Graphics.FillRectangle(sb, mr.Rectangle);
}
}
}
To handle the "click" event of the rectangles, you just handle the MouseDown or MouseClick event of your container control and determine yourself which rectangle is being clicked on:
void panel1_MouseDown(object sender, MouseEventArgs e) {
if (e.Button == MouseButtons.Left) {
foreach (MyRect mr in myRectangles) {
if (mr.Rectangle.Contains(e.Location)) {
ChangeColor(mr, Color.Green);
}
}
panel1.Invalidate();
}
}
private void ChangeColor(MyRect mr, Color newColor) {
mr.FillColor = newColor;
}
If you want to maintain the rectangles as components on screen then you can assign all of them the same click event, the click event will have a little dropdown to pick an existing event. To know which recantangle was clicked use the sender parameter ((Pixel)sender).FillColor = SelectedColor;
For ease I would recommend using something like a panel and drawing rectangles on it, That means you only have a single click event to deal with. So now your question becomes "How do I draw a grid of rectangles on a panel" and "How do I know which rectangle was clicked"
So for the first part you could use this not the very efficient way.
Create a class which stores the information about your pixels
class MyPixel
{
public Color PixelColour;
public Rectangle Bounds;
}
Keep a list of them in memory
List<MyPixels> MyGrid = new List<MyPixels>();
then in the onpaint event of the panel Draw the pixels on the panel
foreach(MyPixel Pixel in MyGrid)
{
using(Brush B = new SolidBrush(Pixel.PixelColor))
{
e.Graphics.DrawRectangle(B, Pixel.Bounds);
}
}
Now in the click event you'll need to know which pixel was clicked
foreach(MyPixel Pixel in MyGrid)
{
if (Pixel.Bounds.Contains(e.Location))
{
PixelClicked(Pixel);
}
}
I believe you're going about this the wrong way. What you want to do is to draw directly into a bitmap. Here is some code that uses a PictureBox to allow the user to draw into it.
using System;
using System.Drawing;
using System.Windows.Forms;
namespace WindowsFormsApplication1
{
public partial class Form1 : Form
{
private Pen _pen;
private bool _mouseDown;
private int _startX;
private int _startY;
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
pictureBox1.Image = new Bitmap(pictureBox1.Width, pictureBox1.Height);
_pen = new Pen(Color.Black);
}
private void pictureBox1_MouseDown(object sender, MouseEventArgs e)
{
_mouseDown = true;
_startX = e.X;
_startY = e.Y;
}
private void pictureBox1_MouseUp(object sender, MouseEventArgs e)
{
_mouseDown = false;
}
private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
{
if (_mouseDown)
{
using (var graphics = Graphics.FromImage(pictureBox1.Image))
{
graphics.DrawLine(_pen, _startX, _startY, e.X, e.Y);
_startX = e.X;
_startY = e.Y;
}
pictureBox1.Invalidate();
}
}
}
}
This is the normal method to write a painting app and is quite performant as well. You can also easily save, write new tools or manipulate images better in this way.