views:

192

answers:

5

I'm new to c#, and trying to learn by writing some simple apps to get familiar with the syntax and .NET library. The most recent miniproject I took on is a polar clock like the one found here.

One of the problems I noticed early on was that the app would constantly "flicker", which really took away from the presentation, so I read online about how to implement a double buffer, which eliminated this problem, but may or may not have something to do with the problem. Here is my onPaint method; it is called every 33ms (~30 FPS) by a timer control. Most of the rest of the app is simply handlers for dragging the app (since it is frameless and has a transparent background), exiting on double-click, etc.

    protected override void OnPaint(PaintEventArgs e) {
        DateTime now = DateTime.Now;

        float secondAngle = now.Second / 60F;
        secondAngle += (now.Millisecond / 1000F) * (1F / 60F);

        float minuteAngle = now.Minute / 60F;
        minuteAngle += secondAngle / 60F;

        float hourAngle = now.Hour / 24F;
        hourAngle += minuteAngle / 60F;

        float dayOfYearAngle = now.DayOfYear / (365F + (now.Year % 4 == 0 ? 1F : 0F));
        dayOfYearAngle += hourAngle / 24F;

        float dayOfWeekAngle = (float)(now.DayOfWeek + 1) / 7F;
        dayOfWeekAngle += hourAngle / 24F;

        float dayOfMonthAngle = (float)now.Day / (float)DateTime.DaysInMonth(now.Year, now.Month);
        dayOfMonthAngle += hourAngle / 24F;

        float monthAngle = now.Month / 12F;
        monthAngle += dayOfMonthAngle / (float)DateTime.DaysInMonth(now.Year, now.Month);

        float currentPos = brushWidth / 2F;

        float[] angles = {
            secondAngle, minuteAngle,
            hourAngle, dayOfYearAngle,
            dayOfWeekAngle, dayOfMonthAngle,
            monthAngle
        };

        SolidBrush DateInfo = new SolidBrush(Color.Black);
        SolidBrush background = new SolidBrush(Color.Gray);
        Pen lineColor = new Pen(Color.Blue, brushWidth);
        Font DateFont = new Font("Arial", 12);

        if (_backBuffer == null) {
            _backBuffer = new Bitmap(this.Width, this.Height);
        }

        Graphics g = Graphics.FromImage(_backBuffer);
        g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;

        try {                
            g.Clear(Color.White);
            if (_mouseIsOver) {
                g.FillEllipse(background, new Rectangle(0, 0, this.Width, this.Height));
            }
            foreach (float angle in angles) {
                g.DrawArc(
                    lineColor,
                    currentPos, currentPos,
                    this.Height - currentPos * 2, this.Width - currentPos * 2,
                    startAngle, angle * 360F
                );

                currentPos += brushWidth + spaceStep;
            }

            // Text - Seconds

            g.DrawString(String.Format("{0:D2} s", now.Second), DateFont, DateInfo, new PointF(115F, 0F));
            g.DrawString(String.Format("{0:D2} m", now.Minute), DateFont, DateInfo, new PointF(115F, 20F));
            g.DrawString(String.Format("{0:D2} h", now.Hour), DateFont, DateInfo, new PointF(115F, 40F));
            g.DrawString(String.Format("{0:D3}", now.DayOfYear), DateFont, DateInfo, new PointF(115F, 60F));
            g.DrawString(now.ToString("ddd"), DateFont, DateInfo, new PointF(115F, 80F));
            g.DrawString(String.Format("{0:D2} d", now.Day), DateFont, DateInfo, new PointF(115F, 100F));
            g.DrawString(now.ToString("MMM"), DateFont, DateInfo, new PointF(115F, 120F));
            g.DrawString(now.ToString("yyyy"), DateFont, DateInfo, new PointF(115F, 140F));

            e.Graphics.DrawImageUnscaled(_backBuffer, 0, 0);
        }
        finally {
            g.Dispose();
            DateInfo.Dispose();
            background.Dispose();
            DateFont.Dispose();
            lineColor.Dispose();
        }
        //base.OnPaint(e);
    }

    protected override void OnPaintBackground(PaintEventArgs e) {
        //base.OnPaintBackground(e);
    }

    protected override void OnResize(EventArgs e) {
        if (_backBuffer != null) {
            _backBuffer.Dispose();
            _backBuffer = null;
        }
        base.OnResize(e);
    }

I thought by disposing of everything at the end of the method I'd be safe, but it doesn't seem to help. Furthermore, the interval between run-time and the OutOfMemoryException isn't constant; once it happened only a few seconds in, but usually it takes a minute or two. Here are some class-wide variable declarations.

    private Bitmap _backBuffer;

    private float startAngle = -91F;
    private float brushWidth = 14;
    private float spaceStep = 6;

And a screenshot (edit: screenshot links to a view with some code present):

Screenshot

EDIT: Stacktrace!

System.OutOfMemoryException: Out of memory.
   at System.Drawing.Graphics.CheckErrorStatus(Int32 status)
   at System.Drawing.Graphics.DrawArc(Pen pen, Single x, Single y, Single width, Single height, Single startAngle, Single sweepAngle)
   at PolarClock.clockActual.OnPaint(PaintEventArgs e) in C:\Redacted\PolarClock\clockActual.cs:line 111
   at System.Windows.Forms.Control.PaintWithErrorHandling(PaintEventArgs e, Int16 layer, Boolean disposeEventArgs)
   at System.Windows.Forms.Control.WmPaint(Message& m)
   at System.Windows.Forms.Control.WndProc(Message& m)
   at System.Windows.Forms.Control.ControlNativeWindow.WndProc(Message& m)
   at System.Windows.Forms.NativeWindow.Callback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)

Seems to be the same line it crashed on last time, the main drawArc inside the loop.

+1  A: 

I didn't find anything horribly wrong with your code. Can you provide the exact line on which the OutOfMemoryException is happening?

Just so you know, that really took me a few months to understand: OutOfMemoryException doesn't mean out of memory. ;-) It happens in GDI+ when something simply went wrong (shows a bad coding style inside of the GDI, IMHO), like you tried to load an invalid image, or an image with invalid pixel format, etc.

Michael
I'll run it until it crashes and post a stack trace.
Dereleased
Seems strange. It either really has something to do with the fact that you create new object every 33ms, or some calculations of the arc yield invalid results (like 0). Try debugging the project, waiting for the crash again and inspect the values you pass to DrawArc! Apart from that I can't give you any advice :-(
Michael
For some reason, when it crashes while running in the IDE, I don't get the option to view the details of the exception, I just get the invalid bitmap rendered and that's that, nothing stops, and my other methods (like drag and double click) work fine.
Dereleased
Very interesting. As soon as I get home, if the issue is not fixed, I'll check if I can reproduce this error!
Michael
Any idea why I don't get notified of the exception in the IDE? I'd love to check the values when it crashes...
Dereleased
From my experience, it sometimes *just happens* that exceptions are treated very differently in the IDE. Not sure why though... Normally you should be able to check the values. You might try and try-catch the exceptions and/or trace the values yourself.
Michael
I put a breakpoint inside the catch, that worked.
Dereleased
Nice, congratulations on solving the issue!
Michael
+2  A: 

Not really an answer to why, but a possible solution:

You shouldn't create a new bitmap every time. Just clear it everytime you draw a new frame.

You should however create a new bitmap when your size changes.

Stormenet
The size should, at this point, never change, but thanks for the tip!
Dereleased
This is not the problem, but is still good advice.
Ed Swangren
Oh, and if you change it to work that way, watch out with minimising your application, your control might receive a size of 0x0 and a bitmap doesn't likes to be created with that size ;)
Stormenet
+7  A: 

Make sure that you dispose Pen and Brush objects as well and use using blocks to make sure that you dispose objects even if there are exceptions.

As a side note: avoid recreating and disposing _backBuffer each time you draw. Either catch resize event and dispose _backBuffer there or just check if _backBuffer has correct dimensions on each Paint event and dispose and recreate if dimensions does not match.

Andreas Paulsson
+1 - also don't forget the `Font`.
Fredrik Mörk
The size should, at this point, never change, but thanks for the tip! I'll go back and start killing pens, brushes, fonts, etc.
Dereleased
As Fredrik Mörk says, do not forget the Font as well. Windows (and .NET) are really picky when it comes to GDI objects. Always dispose all GDI objects that can bedDisposed.
Andreas Paulsson
And don't dispose `e`. That's the responsibility of the code calling the method.
Guffa
I've implemented all these suggestions, and updated the code in the original question to reflect this. I'm just letting it run to see if it's better now. In five minutes I'll call it a tentative success, and if it lasts all night I'll accept this answer.
Dereleased
just crashed again, hooray.
Dereleased
What if you add the check that I mention in a comment above: do not draw arcs that are smaller than 3.5 degrees? There is also two Connect reports of this bug: http://connect.microsoft.com/VisualStudio/feedback/details/253886/graphics-drawarc-throws-outofmemoryexception-for-no-good-reason and http://connect.microsoft.com/VisualStudio/feedback/details/121532/drawarc-out-of-memory-exception-on-small-arcs .
Andreas Paulsson
I will try implementing that check next, after I get some info from the exception.
Dereleased
I added a check inside the foreach that makes sure the sweepAngle is not < 1.0F, and if so, increases it to 1.0F; with this simple conditional, it ran all night, so I'll tentatively declare it a success. Hooray!
Dereleased
+1  A: 

Why do you need a new bitmap every time you want something drawn with OnPaint?! You need exactly 1. Try something like this:

private Bitmap _backBuffer = new Bitmap(this.Width, this.Height);

protected override void OnPaint(PaintEventArgs e) { 

    Graphics g = Graphics.FromImage(_backBuffer);

    //Clear back buffer with white color...
    g.Clear(Color.White);

    //Draw all new stuff...
}
Cipi
I initially did that to make sure I wasn't using up anything unnecessary by keeping it around, although the overwhelming consensus seems to be that it was a bad idea; on the other hand, it can be recreated if the canvas changes size, which is a possibility down the road once I had some customization in; in that case, it'll need to be remade any time it's resized.
Dereleased
A: 

Not a answer to your question and maybe there is a good reason why you do it this way (I'd might learn something), but why create a bitmap first, draw on the bitmap and afterwards draw the bitmap on the form? Wouldn't it be more efficient to draw directly on the form? Something along the line of this:

protected override void OnPaint(PaintEventArgs e) {
    base.OnPaint(e);
    //_backBuffer = new Bitmap(this.Width, this.Height);
    Graphics g = Graphics.FromImage(_backBuffer);

    //Rest of your code
    //e.Graphics.DrawImageUnscaled(_backBuffer, 0, 0);

    //g.Dispose();
    //e.Dispose();
    //base.OnPaint(e);

    //_backBuffer.Dispose();
    //_backBuffer = null;
}

Also according to MSDN

When overriding OnPaint in a derived class, be sure to call the base class's OnPaint method so that registered delegates receive the event.

Grif
To answer your question, http://www.bobpowell.net/doublebuffer.htm
Dereleased
Thanks a lot! Interesting material.
Grif