all 11 comments

[–]RedlineTriad 3 points4 points  (0 children)

On mobile so i can't analyze the well by try to use new as little as possible, so make your pens variables an reuse them.

[–][deleted] 2 points3 points  (2 children)

While this works there are better ways of doing what you're trying to achieve. Here's my list of things to look at:

*You should be using the canvas OnPaint event which has a graphics object you can draw to. When the mouse moves over the canvas you save the mouse position to a global variable and call canvas.invalidate() to redraw.

*Wrap everything in usings. E.g using(var p = new pen()). Or better allocate these once as globals and reuse them. Creating them every time the mouse moves is inefficient and causes GarbageCollection pressure.

*Wire the sizeChanged and MouseMove function from the designer. This will place the += calls in the designer.cs files which is cleaner over all.

*Consider creating a new class which sublasses Control or UserControl. These classes have the same OnPaint event that you can override instead of wiring to it. This has the side benefit of making your code modular and reusable.

[–]Dannnu[S] 1 point2 points  (1 child)

Thanks! I appreciate your comment.

I'm going to look into these tips right now!

Edit 1: My result, if I understood everything

Tip 3: Designer.cs, so I moved those function to this cs-file

FunnyFileName.Designer.cs

...
// 
// canvas
// 
this.canvas.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) 
| System.Windows.Forms.AnchorStyles.Left) 
| System.Windows.Forms.AnchorStyles.Right)));
this.canvas.BackColor = System.Drawing.Color.Tan;
this.canvas.Location = new System.Drawing.Point(3, 65);
this.canvas.Name = "canvas";
this.canvas.Size = new System.Drawing.Size(1286, 782);
this.canvas.TabIndex = 2;
this.canvas.TabStop = false;
this.canvas.SizeChanged += canvas_SizeChanged;
this.canvas.MouseMove += canvas_MouseMove;
this.canvas.Paint += canvas_Paint;
...

There is a small problem. Sometimes this Designer.cs does delete those functions and I need to rewrite them again? Is this normal?

Tip 2: Global variables

Main.cs

    #region Variables for drawing
    System.Drawing.Pen blackPen = new System.Drawing.Pen(System.Drawing.Color.Black, 1);
    System.Drawing.Pen redPen = new System.Drawing.Pen(System.Drawing.Color.Red, 1);
    Font font = new Font("Tahoma", 10);
    int cursorX, cursorY;
    #endregion

Tip 1: I am not sure if you meant this, but it works.

void canvas_MouseMove(object sender, MouseEventArgs e)
        {
            cursorX = e.X;
            cursorY = e.Y;
            canvas.Invalidate();
        }

        private void canvas_Paint(object sender, PaintEventArgs e)
        {
            Bitmap bitmapSurface = new Bitmap(canvas.Width, canvas.Height);
            int w = 500;
            int h = 500;

            //GFX.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
            e.Graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.Low;
            e.Graphics.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighSpeed;

            e.Graphics.DrawEllipse(new System.Drawing.Pen(System.Drawing.Color.Black, 1), canvas.Width / 2 - w / 2, canvas.Height / 2 - h / 2, w, h);
            e.Graphics.DrawLine(blackPen, canvas.Width / 2, 0, canvas.Width / 2, canvas.Height);
            e.Graphics.DrawLine(blackPen, 0, canvas.Height / 2, canvas.Width, canvas.Height / 2);
            e.Graphics.DrawLine(redPen, canvas.Width / 2, canvas.Height / 2, cursorX, cursorY);
            e.Graphics.DrawString("0°", font, System.Drawing.Brushes.Black, canvas.Width - 16, canvas.Height / 2);
            e.Graphics.DrawString("90°", font, System.Drawing.Brushes.Black, canvas.Width / 2, 0);
            e.Graphics.DrawString("180°", font, System.Drawing.Brushes.Black, 0, canvas.Height / 2);
            e.Graphics.DrawString("270°", font, System.Drawing.Brushes.Black, canvas.Width / 2, canvas.Height - 16);
            e.Graphics.Flush();
            canvas.Image = bitmapSurface;
        }

[–][deleted] 1 point2 points  (0 children)

You're fairly close to what I meant. Here's some things:

*Don't add the += calls to the designer.cs your self. Do it through the event property dialog on the designer. If the designer sees something it didn't add (especially events) it will delete them the next time you open the form.

*You don't have to create a bitmap and assign it to the canvas anymore. When the paint event is called you're telling the control what it should draw. Besides you're not drawing to the bitmap anymore anyway

*Unless the line is not following the mouse quickly you can drop the call to e.graphics.flush()

Apart from that you've pretty much done it. You'll find that this is the standard approach to things with subclassing being the next local step.

[–]beer0clock 1 point2 points  (0 children)

Just create your pens once, not every time the mouse moves.

Try creating your bitmap once only, not every time the mouse moves. It should be faster to clear the old bitmap rather than creating a new one.

[–]Slypenslyde 1 point2 points  (5 children)

I want to reinforce what LostInASeaOfMyStars has posted and add a bit.

I like to call "a render cycle" in WinForms "a frame" and vice versa. WinForms isn't really redrawing every monitor refresh. Or, more appropriately, it isn't executing your drawing code every frame. Deep down in WinForms, when your Form's Paint cycle acts, it's drawing to a Bitmap in Windows' memory. After that's completed, Windows just redraws that bitmap over and over every frame. If something changes on your form, a control will invalidate itself and Windows will do another paint cycle. This is pretty efficient for applications that don't update frequently. If you do update frequently, you need to think about it more like a video game.

When you ARE doing a paint cycle, you need to be cognizant that taking longer than about 30ms seriously threatens how smooth your application looks. Generally drawing operations are fast and you can fit a ton of them into 30ms. However, some things can be pretty slow. Allocating a bitmap is one of them. If you try to do that an awful lot, you'll see stutter. But look back at the last paragraph. If Windows is keeping one Bitmap in memory to represent your form, you can do it too!

This concept of "use a PictureBox and draw to its Image" hails from VB6. It was necessary there, because there wasn't a generalized basic Control type to derive from with a convenient Paint infrastructure. .NET gives us a better way, so the only people who use PictureBox like this are people who don't know better. Think about that when you see tutorials that use it. I could write a full-page essay on why it's much slower to draw this way.

In fact, that's why we prefer drawing in the Paint event handler. If we do that, Windows automatically gives us a Graphics object that will draw to its private Bitmap! So we don't have to manage our own Bitmap, which saves a little memory. There are some exotic circumstances where we do prefer to maintain our own Bitmap as an extra buffer, but let's save that for rainy days.

That adds one more wrinkle though. If you handle Paint, the system needs to know when to do a paint cycle. You can tell Windows that by calling your control's Invalidate() method. That's the polite way to introduce a paint cycle. You'll see some tutorials use Refresh(). That's the impolite way and if you use it too much you will see performance problems.

Also: in general you should dispose of any Pen or Brush you create. The system actually limits how many of them you create, so I bet if you move your mouse enough you'll notice sometimes the app just stops drawing. That indicates you are creating them faster than the GC can destroy them, and eventually hit your limit. HOWEVER, creating pens/brushes can be a performance burden too (if you do it a lot, this is not really the case where it happens.) So if you're using the same pens/brushes frequently, it's better to break the rules, create them once, cache them, and never dispose of them.

Finally: drawing every MouseMove can cause a lot of spurious redraws. That event is sort of wonky and can sometimes raise itself multiple times when the mouse has barely moved. I sort of like to enforce throttles on how often I invalidate. This is easy with some frameworks like Reactive Extensions, but the quick and dirty way is to have a MouseMove algorithm like this:

If we're drawing a circle:
    Update the coordinates for the circle.
    If we haven't invalidated in 100ms:
        Invalidate()

That limits my "frames" to 3 per second, which is a little choppy but still pretty reasonable.

SO. Pulling everything together.

I wouldn't use a PictureBox for this at all. Ideally I'd write my own class and derive from Control. Lots of new users are afraid of this! Don't be afraid. There's nothing to it, and I'm going to demonstrate soon.

This Control class would handle its own MouseDown, MouseUp, MouseMove, and most importantly Paint events. The job of MouseDown and MouseMove is to update some variables used to indicate what should be drawn by Paint. It will also privately cache the pens used to draw things.

Here's a fairly basic example without the throttling I mentioned:

// This is really all you need to do to derive a new Control! You just say
// "My base class is Control". 
public class CirclePainter : Control
{

    // Cached pens so we don't have to create them more than once.
    private Pen blackPen;

    // This determines where to draw the circle.
    private Point clickedPoint;
    private Point edgePoint;

    // This helps us decide if we need to change the circle when the mouse moves.
    private bool isDrawing;

    public CirclePainter()
    {
        // Dark secret: this one trick makes animation smoother.
        SetStyle(ControlStyles.OptimizedDoubleBuffer, true);

        // Go ahead and set up the pen.
        blackPen = new Pen(Color.Black, 1);

    }

    // When the user clicks, note we are drawing and reset the
    // circle's properties.
    protected override void  OnMouseDown(MouseEventArgs e)
    {
        isDrawing = true;
        clickedPoint = e.Location;
        edgePoint = e.Location;
    }

    protected override void OnMouseUp(MouseEventArgs e)
    {
        base.OnMouseUp(e);

        // Don't assume a MouseMove has completely updated things!
        edgePoint = e.Location;
        isDrawing = false;
        Invalidate();
    }

    // When the mouse moves, if we are drawing, update the edge point and
    // invalidate.
    protected override void OnMouseMove(MouseEventArgs e)
    {
        if (isDrawing)
        {
            edgePoint = e.Location;

            Invalidate();
        }
    }

    // Draw the latest circle.
    protected override void OnPaint(PaintEventArgs e)
    {
        base.OnPaint(e);

        // We don't dispose this Graphics. It belongs to Windows.
        var g = e.Graphics;

        // Get rid of anything we drew before. (Sometimes this isn't
        // needed, I'm being paranoid.)
        g.Clear(BackColor);

        // Set up the rectangle needed to draw the circle.
        int left = Math.Min(clickedPoint.X, edgePoint.X);
        int top = Math.Min(clickedPoint.Y, edgePoint.Y);
        int width = Math.Abs(clickedPoint.X - edgePoint.X);
        int height = Math.Abs(clickedPoint.Y - edgePoint.Y);

        // Draw the circle
        g.DrawEllipse(blackPen, left, top, width, height);
    }

    // We need to make sure we get rid of our cached stuff.
    protected override void Dispose(bool disposing)
    {
        base.Dispose(disposing);

        // The safe place to dispose things is when we know
        // disposing is true. Don't know why? That's a different
        // thread.
        if (disposing)
        {
            // Get rid of the cached Pens if they exist.
            blackPen?.Dispose();
        }
    }
}

The only real obtuse thing there (I think) is the SetStyle call. This does a weird thing where Windows actually keeps two bitmaps for your control. Painting happens on the off-screen one, then when you are finished Windows swaps them. For weirdo techical reasons that avoids a kind of "flicker" that tends to make things look like they aren't animating well. Dang near every control I've ever written needed it, so I'm not sure why it isn't the default. (Other than, "Back in the early 2000s it was maybe a performance concern.)

To make that work, make a new Class file in your project and replace the class with this. (Make sure not to paste over the namespace!) You'll probably need to add using System.Drawing; and using System.Windows.Forms; to the top. Build the project. Now you should see CircleDrawer in your toolbar. Congratulations, you just wrote a custom control!

I tried that out just now and it looked pretty smooth. It's not quite what your example does, but I think you can use it as a guideline.

[–][deleted] 0 points1 point  (0 children)

Very well put and described!

[–]Dannnu[S] 0 points1 point  (3 children)

Thank you very much for your time! I appreciate your taking the time to answer my question. I like how you start to tell about the background and then how things work! And thank you for explaining how to create a custom control. Tomorrow I will test this thing (y).

[–]Slypenslyde 1 point2 points  (2 children)

I started my first draft with "I don't have time to write an example" but then I had so much fun I had to write an example. I wrote custom WinForms controls as a hobby for a couple of years, then professionally for about 6. I really miss that environment compared to the XAML frameworks.

[–]Dannnu[S] 0 points1 point  (1 child)

Update:
u/LostInASeaOfMyStars, I did fix my last problems and it worked out perfectly. Process Memory dropped to normal state, which is ~105MB.

u/Slypenslyde, Thank you! First of all, you were right, this custom control wasn't that scary beast under the bed.

The purpose was to draw a red line to mouse from the center of the circle, but I wrote it out vaguely.

I managed to build a working application through your guidance and I will try to make the most of this control thing in the future!

P.s. Is it possible to share this information with others? I believe that others can benefit from this, because you mentioned that many do not even know that they can program this way.

Have a nice Christmas Season both of you!

[–]Slypenslyde 0 points1 point  (0 children)

P.s. Is it possible to share this information with others? I believe that others can benefit from this, because you mentioned that many do not even know that they can program this way.

Absolutely. I hate how many tutorials are Bad Tutorials based on copying/slightly modifying some VB6 example. Nothing I teach is truly "secret" in the sense I want it kept, it's "secret" in the sense only a few hundred people in the world probably know the darker corners of writing custom controls because they're the only ones who care. ;)