4

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
(source: ggot.org)

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.

Glorfindel
  • 21,988
  • 13
  • 81
  • 109
Dereleased
  • 9,939
  • 3
  • 35
  • 51
  • 1
    Please crop your screen shots so they are no larger than necessary. – Gabe Sep 08 '10 at 06:45
  • @Gabe: Done; I used the larger one to reveal some additional code, so I have made the new screenshot a link to the older one. – Dereleased Sep 08 '10 at 06:51
  • According to comments in http://msdn.microsoft.com/en-us/library/system.drawing.graphics.drawarc(VS.90).aspx there is a bug in drawArc when arc i smaller than 2 pixels or 3.5 sweep degrees. – Andreas Paulsson Sep 08 '10 at 07:10
  • I've just added a provision for 1.5 sweep degrees, as crashes seem to be on extremely small values, such as .02; we'll see how this goes – Dereleased Sep 08 '10 at 07:31
  • No leakage whatsoever in the provided code, runs stable on my machine. – Hans Passant Sep 08 '10 at 11:04

6 Answers6

8

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
  • 7,745
  • 3
  • 25
  • 31
  • 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 Sep 08 '10 at 06:53
  • 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 Sep 08 '10 at 06:56
  • 2
    And don't dispose `e`. That's the responsibility of the code calling the method. – Guffa Sep 08 '10 at 07:00
  • 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 Sep 08 '10 at 07:09
  • 1
    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 Sep 08 '10 at 07:18
  • I will try implementing that check next, after I get some info from the exception. – Dereleased Sep 08 '10 at 07:22
  • 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 Sep 08 '10 at 15:50
8

Just for anyone else, finding this page via Google:

A possible cause of System.OutOfMemoryException if you use System.Drawing.DrawArc could also be a bug if you try to print small angles.

For angles < 1 this bug occured several times in my code.

See also:

http://connect.microsoft.com/VisualStudio/feedback/details/121532/drawarc-out-of-memory-exception-on-small-arcs

buhlara
  • 96
  • 1
  • 1
3

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.

Joey
  • 344,408
  • 85
  • 689
  • 683
Michael
  • 365
  • 1
  • 11
  • I'll run it until it crashes and post a stack trace. – Dereleased Sep 08 '10 at 06:47
  • 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 Sep 08 '10 at 07:02
  • 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 Sep 08 '10 at 07:05
  • Very interesting. As soon as I get home, if the issue is not fixed, I'll check if I can reproduce this error! – Michael Sep 08 '10 at 07:09
  • 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 Sep 08 '10 at 07:18
  • 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 Sep 08 '10 at 07:22
  • Nice, congratulations on solving the issue! – Michael Sep 09 '10 at 08:10
  • This answer just ended a couple hours debugging for me. In my case, I was using FillRectangle() with a Rectangle having screwed-up coords. Had no clue "Out of memory" could be the result of that. – DarenW Mar 26 '15 at 22:15
2

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
  • 25,926
  • 9
  • 53
  • 65
  • The size should, at this point, never change, but thanks for the tip! – Dereleased Sep 08 '10 at 06:52
  • 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 Sep 08 '10 at 17:06
1

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
  • 11,055
  • 9
  • 47
  • 60
  • 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 Sep 08 '10 at 07:07
0

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
  • 666
  • 5
  • 5