-
Jan 1st, 2012, 09:24 AM
#1
Thread Starter
Addicted Member
[RESOLVED] What happened to simple speed?
#1. Write code
#2. Debug
#3. Make twice as fast
#4. Make even faster...
Anyway that's the way it usally is for me when i'm drawing stuff on a form. Now i've come to quite a different problem - that of drawing a rectangle at a reasonable speed, this time i have no idea how to make it faster. For my speed experimenting i'm covering a form with recangles but it's REALLY SLOW. I'm hoping that i'm missing a major concept in graphics drawing in vb2010 so here's my code so that you can tell me what's wrong:
Code:
Public Class Form1
Private Sub Form1_Paint(ByVal sender As Object, ByVal e As System.Windows.Forms.PaintEventArgs) Handles Me.Paint
Dim s = Microsoft.VisualBasic.Timer
Dim x As Integer, y As Integer, r As Rectangle, g As Graphics = Me.CreateGraphics
g.Clear(Me.BackColor)
For x = 0 To Me.Width Step 30
For y = 0 To Me.Height Step 15
r = New Rectangle(x, y, 30, 15)
g.DrawRectangle(Pens.Black, r)
Next
Next
Debug.Print(Microsoft.VisualBasic.Timer - s)
End Sub
End Class
Guess what? That takes about .15 seconds to draw each time!
I did a direct comparison to vb6 using this code:
Code:
Private Sub Form_Paint()
Dim start
start = Timer
Cls
For X = 0 To Me.ScaleWidth Step 30
For Y = 0 To Me.ScaleHeight Step 15
Me.Line (X, Y)-(X + 30, Y + 30), vbBlack, B
Next
Next
Debug.Print Timer - start
End Sub
Guess what? That takes about .015625 seconds to draw each time!
Something has got to be really wrong with my method in .net. I'm just waiting to kick myself
-
Jan 1st, 2012, 10:05 AM
#2
Re: What happened to simple speed?
First things first, if you ever create a Graphics object, always make sure you dispose it. Secondly, never call CreateGraphics. Have you read the documentation for the Paint event? I can only assume not if you don't know that it provides its own Graphics object.
As for the question, the fact that you are calling DrawRectangle means that you are drawing four times as many lines as you need to. What you're doing is basically drawing grid lines on the form, so why not do just that: draw lines? If you simply draw each vertical line with a single DrawLine call and the same for each horizontal line then I would expect things to be faster.
That said, if you're not changing any of the colours or dimensions then I would think that it would be more efficient to simply create an Image, set that as the form's BackgroundImage and tile it.
-
Jan 1st, 2012, 10:50 AM
#3
Re: What happened to simple speed?
It's one thing to teach a man to fish, it's another to gesture at a lake and a fishing pole Personally I remember trying to learn the ins and outs of System.Graphics from documentation and it was overwhelming and bewildering. I learned tons more from blog and forum posts like this.
Here's some snippets incorporating JMC's advice and some of my own. Let's start with a scratch form:
Code:
Public Class Form1
Public Sub New()
InitializeComponent()
Me.Width = 800
Me.Height = 600
End Sub
Protected Overrides Sub OnPaint(ByVal e As System.Windows.Forms.PaintEventArgs)
Dim sw = Stopwatch.StartNew()
Text = String.Format("{0:N}", sw.ElapsedMilliseconds)
End Sub
Protected Overrides Sub OnClick(ByVal e As System.EventArgs)
MyBase.OnClick(e)
Me.Invalidate()
End Sub
End Class
This lets us redraw by clicking so we can try multiple times and places the number of milliseconds per draw cycle in the caption bar. I set the width and height to a constant number because the bigger the form is the more we draw and the slower it will get. Run this and click a few times to get a feel for the baseline delay for painting a form. Shows up as 0ms for me.
Now, a direct translation of your attempt:
Code:
Protected Overrides Sub OnPaint(ByVal e As System.Windows.Forms.PaintEventArgs)
Dim sw = Stopwatch.StartNew()
Dim g = e.Graphics
g.Clear(BackColor)
For x As Integer = 0 To Me.ClientRectangle.Width Step 30
For y = 0 To Me.ClientRectangle.Height Step 15
e.Graphics.DrawRectangle(Pens.Black, x, y, 30, 15)
Next
Next
Text = String.Format("{0:N}", sw.ElapsedMilliseconds)
End Sub
PaintEventArgs already provides a Graphics object for the form. In GDI terms, this is like an HDC. They're expensive, and limited by the system. Worse, if you create one yourself you're responsible for disposing it, but your code doesn't do that. This code doesn't worry about it because the Graphics provided by PaintEventArgs isn't yours to dispose. It hovers around 300ms on my machine, 0.3s. But as JMC pointed out, you're drawing many, many redundant lines.
I don't understand how the VB6 Line function worked, but here's how I would do gridlines in .NET if I wanted to do them by hand:
Code:
Dim sw = Stopwatch.StartNew()
Dim g = e.Graphics
g.Clear(BackColor)
For x As Integer = 0 To Me.ClientRectangle.Width Step 30
g.DrawLine(Pens.Black, x, 0, x, Me.ClientRectangle.Height)
Next
For y = 0 To Me.ClientRectangle.Height Step 15
g.DrawLine(Pens.Black, 0, y, Me.ClientRectangle.Width, y)
Next
Text = String.Format("{0:N}", sw.ElapsedMilliseconds)
This hovers around 30ms, 0.03s. I can't compare with the VB6 because your machine is different, the form size might be different, and I don't fully understand the Line() function, but this is a hundredfold improvement over using DrawRectangle(). If your numbers are correct it is on par with VB6 assuming the ratio is constant.
All I can say to learn more is read. A lot. It took me the better part of two years to understand enough about graphics in .NET to be dangerous. Don't read just the documentation, find tutorials and samples. Not all of them use good practices, but if you read enough of them you'll figure out which ones are bad.
*edit*
And if you want to make things really complicated, set your form's DoubleBuffered property to true. I get times ~2ms at this point. I'm not sure if offscreen drawing is faster or if it's excluding the screen drawing, but I'm sure someone else can explain.
This answer is wrong. You should be using TableAdapter and Dictionaries instead.
-
Jan 1st, 2012, 03:13 PM
#4
Thread Starter
Addicted Member
Re: What happened to simple speed?
I've reworked my code with a buffer, using a graphics path(i found this slightly faster than drawing rectangles), and with proper disposal and came up with this:
Code:
Private Sub Form1_Paint(ByVal sender As Object, ByVal e As System.Windows.Forms.PaintEventArgs) Handles Me.Paint
REM Declare variables
Dim s = Microsoft.VisualBasic.Timer
Dim x As Integer, y As Integer
Dim Buffer As Bitmap = New Bitmap(Me.Width, Me.Height)
Dim g As Graphics = Graphics.FromImage(Buffer)
Dim p As Drawing2D.GraphicsPath = New Drawing2D.GraphicsPath
REM Build the path
For x = 0 To Me.Width Step 30
For y = 0 To Me.Height Step 15
p.AddRectangle(New Rectangle(x, y, 30, 15))
Next
Next
REM Draw everything
g.Clear(Me.BackColor)
g.DrawPath(Pens.Black, p)
REM Put the buffer onto the form
e.Graphics.DrawImageUnscaled(Buffer, 0, 0)
REM Clean up
Buffer.Dispose()
g.Dispose()
p.Dispose()
REM Print the time taken
Debug.Print(Microsoft.VisualBasic.Timer - s)
End Sub
I've got an entirely new question (of course i expect you to criticize the code just seen). This works a lot faster with a buffer than without one - fast enough to satisfy myself. This is a really weird problem - in my program I need to call my paint event quite often in the mouse event. No, i wouldn't call it every time the mouse moves but yes, it does need to be snappy enough to update immediately. For testing purposes I call it like this:
Code:
Private Sub Form1_MouseMove(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles Me.MouseMove
Refresh()
End Sub
For some reason this will make the drawing process to take twice as long as it would when the paint event is fired as a result of resizing the form. This is a noticeable delay and can't exist. Really weird!!! Please help.
-
Jan 1st, 2012, 05:10 PM
#5
Re: What happened to simple speed?
The fact that it works faster with a buffer seems very odd, as it's just unnecessary overhead. Anyway, the reason for the speed improvement is that, when resizing, Windows repaints only the areas that actually need to be repainted (i.e. the side/s that was/were resized) whereas you're repainting the entire form. You can have the same behaviour by using Me.Invalidate() and passing a Rectangle or Region (for example) representing the area that needs repainting.
Here's a rewrite of your code that may or may not be faster, too:
Code:
Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs)
' Start timing
Dim s As New Stopwatch()
s.Start()
' Get a copy of the graphics object for convenience
Dim g As Graphics = e.Graphics
' Draw everything
g.Clear(Me.BackColor)
For x As Integer = 0 To Me.Width Step 30
For y As Integer = 0 To Me.Height Step 15
g.DrawRectangle(Pens.Black, x, y, 30, 15)
Next
Next
' Stop timing
s.Stop()
' Print the time taken
Debug.Print(s.Elapsed.ToString())
End Sub
And, since you're drawing over the entire graphics surface, you can add this code into your constructor:
Code:
Public Sub New()
Me.InitializeComponent()
Me.SetStyle(ControlStyles.AllPaintingInWmPaint Or
ControlStyles.UserPaint Or
ControlStyles.Opaque Or
ControlStyles.OptimizedDoubleBuffer, True)
End Sub
What that does is it tells the form not to paint its background because you're already doing it, plus it enables double-buffering.
-
Jan 1st, 2012, 05:34 PM
#6
Re: What happened to simple speed?
Wow, that first example looks zeta familiar, minitech. I wonder why
Of course it gets slower when you "use a buffer", cheesebrother. Your code snippet is doing way too much work. The concept of a buffer isn't "create a new bitmap and draw to it every frame", it's "draw to a bitmap and display that bitmap." There's quite a few things in that code that could be improved. For example, the bitmap's going to be the same each time; why draw on it every time? Or maybe you have plans to change it but the path will be the same each time; why not cache that? Still, since it's a static image, it'd be smarter to draw the grid once in the constructor/load event and set that to the form's BackgroundImage property. Then you don't have to worry about drawing it. There's always lots of options; don't reach for the most complicated tools!
This answer is wrong. You should be using TableAdapter and Dictionaries instead.
-
Jan 1st, 2012, 05:36 PM
#7
Re: What happened to simple speed?
-
Jan 1st, 2012, 08:26 PM
#8
Re: What happened to simple speed?
Code:
' Draw everything
g.Clear(Me.BackColor)
This is superfluous and probably reduces performance. The form client area has already been cleared to the Backcolor in MyBase.OnPaintBackground. There is no need to do it again in the OnPaint sub or in the Paint event handler. It forces the entire client area to be repainted instead of just the lines to be drawn.
Code:
Public Sub New()
Me.InitializeComponent()
Me.SetStyle(ControlStyles.AllPaintingInWmPaint Or
ControlStyles.UserPaint Or
ControlStyles.Opaque Or
ControlStyles.OptimizedDoubleBuffer, True)
End Sub
This is obsolete and possibly superfluous. The SetStyles statements have been replaced by the DoubleBuffered property since VB2005. You can set it in the Designer or for example in the Load event sub. Besides, it doesn't affect the speed of painting but prevents flickering if the image changes rapidly.
Replacing the Paint event handler by an OnPaint sub obscures matters. Normally you would use OnPaint in a class definition but handle the Paint Event in an instance. Both of these are possible for a form added in Visual Studio because it is both a class and a default instance of that class. MyBase.Paint in the OnPaint sub raises the Paint event, so it is not going to make the least difference to performance.
BB
Last edited by boops boops; Jan 1st, 2012 at 08:39 PM.
-
Jan 1st, 2012, 08:30 PM
#9
Re: What happened to simple speed?
Originally Posted by boops boops
Code:
' Draw everything
g.Clear(Me.BackColor)
This is superfluous and probably reduces performance. The form client area has already been cleared to the Backcolor in MyBase.OnPaintBackground. There is no need to do it again in the OnPaint sub or in the Paint event handler. It forces the entire client area to be repainted instead of just the lines to be drawn.
Code:
Public Sub New()
Me.InitializeComponent()
Me.SetStyle(ControlStyles.AllPaintingInWmPaint Or
ControlStyles.UserPaint Or
ControlStyles.Opaque Or
ControlStyles.OptimizedDoubleBuffer, True)
End Sub
This is obsolete and possibly superfluous. The SetStyles statements have been replaced by the DoubleBuffered property since VB2005. You can set it in the Designer or for example in the Load event sub. Besides, it doesn't affect the speed of painting but prevents flickering if the image changes rapidly.
Me.SetStyle isn't entirely obsolete. Setting DoubleBuffered doesn't also set Opaque, for example, and that's the important part of that. I just wanted to set the rest of the styles through SetStyle because it's cleaner (IMO). So the Opaque was the important part of that.
Since the OP used Color.Black as an example and changed to Me.BackColor, anything is possible
Originally Posted by Sitten Spynne
Wow, that first example looks zeta familiar, minitech. I wonder why
I wonder, too. Maybe it's because it's a rewrite of the code above?
-
Jan 1st, 2012, 09:47 PM
#10
Re: What happened to simple speed?
Minitech, have you tried that combination of SetStyles? On my system setting ControlStyle.Opaque possibly gives a slight performance gain, but it clashes with DoubleBuffered (black background only). I did a grid-drawing test with the following code using a maximized form (1680*1080 screen) on my rather average twin-core PC.
Code:
Private Sub Form1_Paint(sender As Object, e As System.Windows.Forms.PaintEventArgs) Handles Me.Paint
Dim sw As Stopwatch = Stopwatch.StartNew
For x As Integer = 0 To Me.ClientSize.Width Step 30
e.Graphics.DrawLine(Pens.Black, x, 0, x, Me.ClientSize.Height)
Next
For y As Integer = 0 To Me.ClientSize.Height Step 15
e.Graphics.DrawLine(Pens.Black, 0, y, Me.ClientSize.Width, y)
Next
Console.WriteLine(sw.ElapsedMilliseconds.ToString)
End Sub
The first run of a piece of code always produces deviant timing (maybe it's something to do with visual studio, or with JIT compiling). So I added a button to the form with Me.Refresh in its click event, in order to do repeated tests. The successive timings without ControlStyle.Opaque were:
62, 26, 25, 26 milliseconds.
With ControStyle.Opaque the result appeared to be very slightly quicker:
60, 25, 25, 25 milliseconds.
I also tried playing with the graphics smoothingmode, which in theory should affect line drawing. Adding e.Graphics.SmoothingMode = Drawing2D.SmoothingMode.HighQuality to the above code gave the following successive timings:
71, 38, 37, 37 milliseconds.
Paradoxically, setting the SmoothingMode to HighSpeed gave slightly slower times than the default!
70, 27, 29, 27
HighSpeed is supposed to be default anyway, so I wonder if just setting the SmoothingMode consumes a couple of milliseconds.
The upshot is that I think the above code (excluding the stopwatch lines) is as fast as you are going to get in GDI+. I didn't test drawing the lines with GraphicsPath.Addline followed by Graphics.DrawPath, but I don't think that should make any difference. All this is a quicker than the OP's original 150 ms., but of course it depends on the speed of the hardware and above all on the size of the form.
So cheesebrother, how big is your form client area (in pixels)? And did you test the timing just once or a number of times in succession?
BB
-
Jan 1st, 2012, 09:59 PM
#11
Re: What happened to simple speed?
Aren't those timings faster?
-
Jan 1st, 2012, 10:08 PM
#12
Re: What happened to simple speed?
Originally Posted by minitech
Aren't those timings faster?
Yes, that's what i said -- especially if you discount the first measurement. But I think cheesebrother needs to do a similar test on his own machine and with the same sized form to see how much it actually helps. It would also be interesting to compare the results with VB6 again; it does seem to have a distinct speed advantage over dotnet/GDI+ for drawing a simple grid. BB
-
Jan 2nd, 2012, 04:12 AM
#13
Re: What happened to simple speed?
Without reading the other posts ... i would do it this way:
vb Code:
Private Sub Form1_Paint(ByVal sender As Object, ByVal e As System.Windows.Forms.PaintEventArgs) Handles Me.Paint
Using b As New Bitmap(30, 15) 'Create a bitmap for 1 cell of the grid
Using g = Graphics.FromImage(b) 'Create graphics so we can draw on our bitmap
g.DrawLine(Pens.Black, 0, 0, b.Width, 0) 'Draw the x Line on it
g.DrawLine(Pens.Black, 0, 0, 0, b.Height) 'Draw the y Line on it
End Using
Using tb As New TextureBrush(b) 'Create a texture brush to fill our form bg from the cell that we drew on b
e.Graphics.FillRectangle(tb, Me.ClientRectangle) 'Fill the form with this brush
End Using
End Using
End Sub
EDIT: Also on another note it may be even faster if you cache the texture brush:
vb Code:
Private Sub Form1_Paint(ByVal sender As Object, ByVal e As System.Windows.Forms.PaintEventArgs) Handles Me.Paint
Static tb As TextureBrush 'Create a texture brush to fill our form bg from - note this is static so we do not need to create it each time...
If tb Is Nothing Then 'If we haven't created the texture brush yet...
Using b As New Bitmap(30, 15) 'Create a bitmap for 1 cell of the grid
Using g = Graphics.FromImage(b) 'Create graphics so we can draw on our bitmap
g.DrawLine(Pens.Black, 0, 0, b.Width, 0) 'Draw the x Line on it
g.DrawLine(Pens.Black, 0, 0, 0, b.Height) 'Draw the y Line on it
End Using
tb = New TextureBrush(b) 'Create the texturebrush from b
End Using
End If
e.Graphics.FillRectangle(tb, Me.ClientRectangle) 'Fill the form with this brush
End Sub
Kris
Last edited by i00; Jan 2nd, 2012 at 04:18 AM.
-
Jan 2nd, 2012, 04:46 AM
#14
Thread Starter
Addicted Member
Re: What happened to simple speed?
Without reading the other posts ... i would do it this way
Unfortunately the cells need to be different sizes.
Ideally I'd like to be able to draw on an area that's 1000*800 pixels big.
It would also be interesting to compare the results with VB6 again; it does seem to have a distinct speed advantage over dotnet/GDI+ for drawing a simple grid.
Yes it does, I'm surprised because .net uses gdi. Perhaps if i need to i could switch to directx for drawing these lines . Bit of an overkill on speed but it would certainly do the job.
Don't get me wrong, i won't go to dx unless i absolutely have to. I'm not a speed thirsty - unreasonable person. To see why this isn't fast enough for me try this code:
Code:
Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs)
' Get a copy of the graphics object for convenience
Dim g As Graphics = e.Graphics
' Draw everything
g.Clear(Me.BackColor)
Dim SelectBounds As Point = New Point(Cursor.Position.X - Me.Left - 15, Cursor.Position.Y - Me.Top - 45)
For x As Integer = 0 To Me.Width Step 30
For y As Integer = 0 To Me.Height Step 15
If x > SelectBounds.X Or y > SelectBounds.Y Then
g.DrawRectangle(Pens.Black, x, y, 30, 15)
End If
Next
Next
For x As Integer = 0 To SelectBounds.X Step 30
For y As Integer = 0 To SelectBounds.Y Step 15
g.DrawRectangle(Pens.Orange, x, y, 30, 15)
Next
Next
End Sub
Private Sub Form1_MouseMove(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles Me.MouseMove
Static PX, PY
If Int(e.X / 30) <> PX Or Int(e.Y / 15) <> PY Then Me.Invalidate()
PX = Int(e.X / 30) : PY = Int(e.Y / 15)
End Sub
I took out the timing code, one doesn't need to even think about speed. All the evidence for speed is right before you - flash, flash, flash, flash, flash... and all that on a form whose size is 300*300!
-
Jan 2nd, 2012, 05:56 AM
#15
Re: What happened to simple speed?
Hi cheesbro, I just tried your code without looking at it too much. After a few minor corrections (declare PX and PY as Integer, use Cint or integer division instead of Int -- probably required by Option Strict) it ran OK except for flickering. The flickering was very strong when I maximized the form. But then I set the form's DoubleBuffered property to true. The flickering was immediately cured.
You can set DoubleBuffered in the designer or e.g. in the Load event.
BB
Last edited by boops boops; Jan 2nd, 2012 at 05:59 AM.
-
Jan 2nd, 2012, 06:34 AM
#16
Thread Starter
Addicted Member
Re: What happened to simple speed?
Wow, sorry for being stupid everyone. (that i mean by not trying double buffering when you talked about it) Double buffering fixes everything! Now it not only doesn't flicker but it draws way faster. This is totally awesome! I just plugged the new code into my other project (my control) and it's drawing about 10 times as fast as it was before! I am really set to go now.
-
Jan 2nd, 2012, 06:53 AM
#17
Re: [RESOLVED] What happened to simple speed?
Well it turns out that all the alternative techniques suggested in this thread, including my own suggestions, don't make a significant difference. Drawing rectangles, drawing lines, using a graphics path or using a texture brush are all good alternatives and worth knowing about. But in the end the time to execute a Refresh depends almost entirely on the number of pixels whose colour has to be changed. Even re-clearing the background, while unnecessary, doesn't make much difference to performance because the pixels are the same. On top of that, timing code for performance remains a tricky business in DotNet and can easily lead to wrong conclusions.
BB
Posting Permissions
- You may not post new threads
- You may not post replies
- You may not post attachments
- You may not edit your posts
-
Forum Rules
|
Click Here to Expand Forum to Full Width
|