Copying Free Hand Drawing from Panel in Visual Studio 2013

copying free hand drawing from panel in visual studio 2013

Looking at your code I'm afraid I have to say: This is all wrong.

Sorry to be so blunt, but you must never use control.CreateGraphics!!

  • The first thing to do is to throw away the Graphics objgraphics object.

It is (almost) always wrong to store a Graphics object!

Instead, you have to use the one you get from the e.Graphics parameter in the Paint events of your controls.

Note that Graphics doesn't contain any graphics, it is a tool used to draw onto an associated Bitmap or a control's surface.

  • The next thing is to do is to understand about drawing freehand lines. Often one can see the code you have; but it is useless and only an example of how many stupid things you find in introductions. Forget it. It will always look like crap as the circles simply never look smooth and as soon as you move the mouse faster the pseudo-lines completely fall apart.

There is a nice method DrawCurve that will draw smooth lines. You feed it a Pen and an array of Points.

This is what we will use.

Let's return to the basics: How to create a graphic? Now we know that you need to call DrawCurve in the Paint event:

e.Graphics.DrawCurve(somePen, somePointsArray);

This brings up the next questions:

  • what's somePen
  • what's somePointsArray

There is a hidden third question:

  • what about drawing more lines?

The first one is simple; you create a Pen with a stroke width of 5.5 pixels as

Pen somePen = new Pen(Color.Blue, 5.5f); 

If you want to you can give it a linestyle (dashes), too.

Now for the array: In its simplest form this is easy as well.

In the MouseMove event you store the current Location in a list of points. First, we declare it at class level:

List<Point> currentLine = new List<Point>();

Then we start filling it as long as the left button is pressed:

private void panel1_MouseMove_1(object sender, MouseEventArgs e)
{
if (e.Button == System.Windows.Forms.MouseButtons.Left)
{
currentLine.Add(e.Location);
panel1.Invalidate();
}
}

Note the last line: Calling Invalidate on a control triggers the system to invoke the Paint event. It may look complicated but this is the only correct way as it guarantees that the very same drawing will also happen when some other reason makes it necessary.

We need to draw because we have changes the data that should be drawn. But there are many outside reasons, most notoriously the Minimize/maximize sequence that will clear the drawing and trigger the Paint event, too. So we need to cooperate with the way windows draws its controls! Only this way the graphics will persist.

Also, note we don't use an array as we don't know how many Points we will need. Instead, we use a List and later cast it to Array.

Let's code the Paint event for our simple case:

private void panel1_Paint(object sender, PaintEventArgs e)
{
using (Pen somePen = new Pen(Color.Blue, 5.5f) )
if (currentLine.Count > 1) e.Graphics.DrawCurve(yourPen , currentLine.ToArray());
}

Note that I have created the Pen in a using clause. This is a cheap and safe way to ensure that the Pen is disposed of properly.

Also note how we cast the List to an Array!

The above code alone would work and allow you free-hand drawing a line.

But what about the next line? It should not connect to the first one so we can't just add more points!

So we need not just one list of points but more than that, in fact, a list of lists of points is called for:

List<List<Point>> curves = new List<List<Point>>();

And we add the current curve to it whenever the mouse is released:

private void panel1_MouseUp(object sender, MouseEventArgs e)
{
if (currentLine.Count > 1) curves.Add(currentLine.ToList()); // copy!!
currentLine.Clear();
panel1.Invalidate();
}

Note how I use a cast from List to List to enforce a copy or else only the reference would be assigned and then, in the next line cleared..

Again we trigger the Paint event as the final thing to do.

We now should change the Paint event to display all the lines, both the one currently being drawn and all the earlier ones..:

private void panel1_Paint(object sender, PaintEventArgs e)
{
using (Pen somePen = new Pen(Color.Blue, 5.5f) )
{
if (currentLine.Count > 1) e.Graphics.DrawCurve(somePen, currentLine.ToArray());
foreach (List<Point> lp in curves)
if (lp.Count > 1) e.Graphics.DrawCurve(somePen, lp.ToArray());
}
}

Now we are basically done with the free-hand drawing part.

So we return to your original question: How can you copy the drawing to a second Panel?

Well, you have stored everything in the curves data structure.

So you have two options: Either simply use the same data in the panel2_Paint event or if you need to copy and change the data to, maybe adapt to a different size.

Here are the things still missing:

  • Saving the data, saving the drawing (Serialize, DrawToBitMap)
  • Drawing with varying pens & colors (create a drawAction class to store all you need)
  • Using other tools like Line or Rectangle (create a drawAction class)
  • Clearing the drawing (see below)
  • Undo and Redo (look into Stacks an Queues)
  • Caching when there are a great number of lines ( draw the first portion into a BackgroundImage Bitmap)

Here is a clearing code:

curves.Clear(); currentLine .Clear(); panel1.Invalidate();

I noted that your original code lets you draw with two different stroke widths using the left and right button. This alone shows that this code is not very good. Who would a) think of that and b) be satisfied with only two stroke widths.

Please read this post where I explain a little about creating a class that can store a pen width, a color etc so that you can change then between lines you draw.

How to use the paint event to draw freehand on a panel?

There are two ways to do it.

  1. Instead of drawing to the panel directly have a Bitmap and use Graphics.FromImage( to get the graphics object, then just set the background of the panel to the bitmap.

  2. Have all the drawing be recorded as a series of steps then re-draw the steps every paint event.

Update a drawing without deleting the previous one

The way to do it is to create a DrawAction class which holds all the data needed for the things you want to draw: The Point data, the Pen or Brush etc.

Then you create and manage a List<DrawAction> drawActions and then you have a choice:

  • You either do all the drawing 'live' in the Paint event of the PictureBox or a Panel (or any control with a Paint event) by looping over the list..

  • ..Or you add the new Actions into the Bitmap Image you are building up.

What is better really depends: do you expect to do dynamic drawing, say by user actions? Do you want an undo/redo option? Then live drawing onto the control surface is a little better suited.

Or is the list of things to do fixed or derive from a fixed set of data and meant to be eventually saved to disk. That sounds more like drawing into the bitmap.

Both can also be combined, maybe collect a number of actions while keeping the option of undoing (by removing the last list item) and offering an Apply button to pump them into the bitmap..

Note: The key to drawing stuff is to keep the drawing data ready in a list so you can use it when you need it again, expand and delete the list and even change it: It would be a simple two-liner to go over all the actions and to change the Color or the Width or the LineStyle of the Pen or shifting Points a little etc etc!

When you create the DrawAction class it helps if you can decide which actions you will need. If you can't you still can go for a more extended class with enough members to work with all the many options the Graphics class offers: Drawxx, Fillxxx, Pen properties, Colors maybe even zoom..

For starters a Type, a List<Point> a float PenWidth and a Color will do..

Here is an example with a simple class, the Paint event, code to add a few test actions and both:

  • A button to do live drawing and..
  • ..one to apply the action to the apply the actions to the bitmap Image of a PictureBox.

The test data are one Line and one set of Polylines.

You should start improving on it by defining an Enum with all the types of drawing actions you want to use! This will be much better and easier to understand the the cheapo character type I have coded ;-) The types could include Rectangle, FilledRectangle, Ellipse, FilledEllipse, Line, Lines, Polygon, FilledPolygon, Text, Curve, Curves and then some. With a little extra you can also work with Image, GraphicsPath, Spline.. And other data could control Rotation, Scaling, Gradients, Transparency etc..

List<DrawAction> actions = new List<DrawAction>();

public class DrawAction
{
public char type { get; set; } // this should be an Enum!
public Color color { get; set; }
public float penWidth { get; set; } // only one of many Pen properties!
public List<Point> points { get; set; } // use PointF for more precision

public DrawAction(char type_, Color color_, float penwidth_)
{
type = type_; color = color_; penWidth = penwidth_;
points = new List<Point>();
}
}

private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
Draw(e.Graphics, actions);
}

public void Draw(Graphics G, List<DrawAction> actions)
{
foreach (DrawAction da in actions)
if (da.type == 'L' && da.points.Count > 1)
using (Pen pen = new Pen(da.color, da.penWidth))
G.DrawLine(pen, da.points[0], da.points[1]);
else if (da.type == 'P' && da.points.Count > 1)
using (Pen pen = new Pen(da.color, da.penWidth))
G.DrawLines(pen, da.points.ToArray());
// else..

}

private void button1_Click(object sender, EventArgs e)
{
AddTestActions();
pictureBox1.Invalidate();
}

private void button2_Click(object sender, EventArgs e)
{
AddTestActions();
Bitmap bmp = new Bitmap(pictureBox1.ClientSize.Width,
pictureBox1.ClientSize.Height);
using (Graphics G = Graphics.FromImage(bmp)) Draw(G, actions);
pictureBox1.Image = bmp;
}

void AddTestActions()
{
actions.Add(new DrawAction('L', Color.Blue, 3.3f));
actions[0].points.Add(new Point(23, 34));
actions[0].points.Add(new Point(23, 134));
actions.Add(new DrawAction('P', Color.Red, 1.3f));
actions[1].points.Add(new Point(11, 11));
actions[1].points.Add(new Point(55, 11));
actions[1].points.Add(new Point(55, 77));
actions[1].points.Add(new Point(11, 77));
}

The result for both looks the same:

Sample Image

Update Another, maybe more appropriate design would be to have a Draw (Graphics g) method in the DrawAction class. Then in a Paint event this would do: foreach (DrawAction da in drawActionList) da.Draw(e.Graphics);

Why isn't my application drawing anything?

private void panel1_Paint(object sender, PaintEventArgs e)
{
Graphics graphics = panel1.CreateGraphics();

This is nonsense! Always and only use the e.Graphics object from the Paint param!!

Also: To trigger the Paint event do a panel1.Invalidate(); whenever your drawing data have changed!

Also: Make sure you understand just what your mousePaint flag is supposed to control: the mouse painting (i.e. adding new shapes to draw) or the regular painting (i. all shape previously drawn)!? Note that all drawing, current and previous needs to be done from the Paint event, whenever necessary i.e. over and over again!

To be able to do so: Collect all the shpes' data in a List<T>..

To Doublebuffer a Panel you need to subclass it. Your code turns on DoubleBuffering for the Form, which fine but won't help the Panel..

Instead simply use a PictureBox, which is control meant for drawing on!

A DoubleBuffered Panel subclass is as simple as this:

class DrawPanel : Panel 
{
public DrawPanel()
{
DoubleBuffered = true;
}
}

Update: Instead you can also use a Label (with Autosize=false); it also has the DoubleBuffered property turned on out of the box and supports drawing better than Panels do.

Use picturebox as a canvas and draw text

Writing a Paint program is a lot of fun, but you need to plan ahead for all or most of the features you want.

So far you have these:

  • A background you can change
  • A way to modify an image by drawing text on it
  • The need to save it all to a file

Here are a few more things you will need:

  • Other tools than just text, like lines, rectangles etc..
  • A choice of colors and pens with widths
  • A way to undo one or more steps

Here are few thing that are nice to have:

  • A way to help with drawing and positioning with the mouse
  • Other type backgrounds like a canvas or pergament paper
  • The ability to draw with some level of tranparency
  • A redo feature (*)
  • Rotation and scaling (***)
  • Levels (*****)

Some things are harder (*) or a lot harder (***) than others, but all get hard when you decide to patch them on too late..

Do read this post (starting at 'actually') about PictureBoxes, which explain how it is the ideal choice for a Paint program.

Your original piece of code and your question have these problems:

  • You seem to think that repeating anything, like redrawing the text is wrong. It is not. Windows redraws huge numbers of things all the time..
  • You have mixed two of the tasks which really should be separate.
  • You have not parametrizied anything, most notably the drawing of the text should use several variables:

    • Font
    • Brush
    • Position
    • the text itself

The same will be true once you draw lines or rectangles..

So here are the hints how do get it right:

  • Use the BackgroundColor and/or the BackgroundImage of the Picturebox to dynamically change the background!

  • Collect all things to draw in a List<someDrawActionclass>

  • Combine all drawings by drawing it into he Picturebox's Image

  • Use the Paint event to draw supporting things like the temporary rectangle or line while moving the mouse. On MouseUp you add it to the list..

So, coming to the end, let's fix your code..:

You set the backgound with a function like this:

void setBackground(Color col, string paperFile)
{
if (paperFile == "") pictureBox1.BackColor = col;
else pictureBox1.BackgroundImage = Image.FromFile(paperFile);
}

you can call it like this: setBackground(Color.White, "");

To draw a piece of text into the Image of the PictureBox, first make sure you have one:

void newCanvas()
{
Bitmap bmp = new Bitmap(pictureBox1.ClientSize.Width, pictureBox1.ClientSize.Height);
pictureBox1.Image = bmp;
}

Now you can write a function to write text. You really should not hard-code any of the settings, let alone the text! This is just a quick and very dirty example..:

void drawText()
{
using (Font font = new Font("Arial", 24f))
using (Graphics G = Graphics.FromImage(pictureBox1.Image))
{
// no anti-aliasing, please
G.TextRenderingHint = System.Drawing.Text.TextRenderingHint.SingleBitPerPixel;
G.DrawString("Hello World", font, Brushes.Orange, 123f, 234f);
}
pictureBox1.Invalidate();
}

See here and here for a few remarks on how to create a drawAction class to store all the things your drawing is made up from..!

The last point is how to save all layers of the PictureBox:

void saveImage(string filename)
{
using (Bitmap bmp = new Bitmap(pictureBox1.ClientSize.Width,
pictureBox1.ClientSize.Height))
{
pictureBox1.DrawToBitmap(bmp, pictureBox1.ClientRectangle);
bmp.Save("yourFileName.png", ImageFormat.Png);
}
}


Related Topics



Leave a reply



Submit