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):
(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.
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:
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.
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.
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.
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...
}
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.
来源:https://stackoverflow.com/questions/3665206/why-is-my-c-sharp-paint-method-running-out-of-memory