I'm drawing a lot of lines on a panel. As I move the mouse, I move my own cursor over the lines. When there is a lot of data my cursor is slow to respond - I assume it's because I'm drawing everything and my cursor.
I am using double buffering.
Can you suggest how I can make my cursor more responsive?
Below are code snippets to show what I'm doing.
Thanks
Code:
Private Sub MyPanel_MouseMove(sender As Object, e As System.Windows.Forms.MouseEventArgs) Handles Me.MouseMove
Dim x As Single = e.X
Dim y As Single = e.Y
MyCursor.X = x
MyCursor.Y = y
Me.Refresh()
End Sub
Code:
Private Sub MyPanel_Paint(sender As Object, e As System.Windows.Forms.PaintEventArgs) Handles Me.Paint
eGraphics = e.Graphics
' draw lots of lines
Call DrawCursor()
End Sub
Code:
Public Sub DrawCursor()
eGraphics.DrawLine(Pens.Black, MyCursor.X, 0, MyCursor.X, frmMain.MyPanel1.Height)
eGraphics.DrawLine(Pens.Black, 0, MyCursor.Y, frmMain.MyPanel1.Width, MyCursor.Y)
End Sub
The first (and simplest) thing to try is to replace Refresh by Invalidate.
Explanation: Refresh forces an immediate repaint. Since the Mouse events may be trying to fire at 64 hz. or so, there will be far more repainting taking place than necessary, and this will slow everything else down. Invalidate, on the other hand, places repainting on a queue so repainting may take place less often; it's a bit like using a separate thread to do the repainting.
What is more, you are Refreshing the whole form instead of just the panel, and that will mean repainting all the other controls on the form too. So use Panel1.Invalidate instead. In some situations you can further improve repainting performance by specifying only the "dirty" rectangle of the control to be repainted.
Code:
control.Invalidate(rectangle)
Occasionally I have found Refresh working better than Invalidate without understanding why. So if you are getting slow response to mouse moves, I suggest you try one then the other to see if it makes any difference.
BB
Last edited by boops boops; May 12th, 2017 at 06:27 AM.
It may help if you "trigger" the DrawCursor sub less frequently.
Instead to triggering on every pixel (ie, X and Y), could you live with doing so
every, say, 10 pixels? This could be accomplished by the following:
1. save the X and Y coords each time you do DrawCursor
2. compare "actual" cursor new X and Y coords with the "saved" ones
3. if the "actual" coords are too close to saved ones, don't redraw.
4. If the "actual" coords are greater that your "minimum" move, the do redraw, and resave the "used" coords.
Thanks, I tried replacing Refresh with Invalidate but it has made no apparent difference.
That's possible. You are drawing dynamically, so your drawing surface needs to be Double Buffered. If it's not, there will be a lot of flashing and jerking as you draw. I wonder if that's what you identify as slowness?
Not that setting the form's DoubleBuffered property to True (or doing the same thing with SetStyle) doesn't help because it only applies to the form background, not to the controls. But there are several ways to get a double buffered control.
The simplest way is to use a PictureBox instead of a Panel, bacause that is double buffered by default.
Alternative ways to get a DoubleBuffered control are:
- draw directly on the Form and set its DoubleBuffered property to True.
- create a UserControl (VS menu Project/Add User Control), set its DoubleBuffered property to True, build the project, and use the UC instead of a Panel.
- add a double buffered panel Class (VS menu Project/Add Class) to the project, code it like this, and build the project:
Code:
Public Class DBPanel
Inherits Panel
Public Sub New()
Me.DoubleBuffered = True
End Sub
End Class
Once you build the project, will find the UC or custom Panel in the Forms Toolbox, and you can add it in the designer and/or in code just like a normal panel.
If this doesn't help, we'll need to see more of your code.
I have attached a screen shot to show what is happening.
I was using double buffering by inheriting a panel but I have changed to just use a PictureBox.
I have tried to redraw only if the cursor moves by a few pixels but it has made no significant improvement.
I am moving the cursor and the arrow cursor moves. On the paint event I'm drawing the graphics and my own cursor. If there is a lot of graphics to be drawn them my cursor is slow to follow the arrow cursor. If there is not of graphics to be drawn then it works fine.
Thanks for the screen shot, it makes the situation clearer. I suspect that the "lots of graphics" may be the problem. If you were drawing that as a static image, there should be no problem in dragging your cursor around smoothly. So does "lots of graphics" have to change in some way, or is there anything else that must happen while you are moving the cursor?
It's clear from your code that the Graphics object eGraphics is declared at a higher level than the Paint sub. It would be safer to pass the PaintEventArgs or e.Graphics as a parameter to the DrawCursor sub, for example:
Code:
'in the Paint event handler:
DrawCursor(e) '(you don't need Call in VB.Net)
'in the DrawCursor sub:
Private Sub DrawCursor(e As PaintEventArgs)
e.Graphics.DrawLine '... etc.
However, if you are doing something else with eGraphics besides the code you have shown, it could be the cause of the problem.
I saw this thread a few days ago and didn't get a chance to post, now I remember it.
You definitely do need to use Invalidate() instead of Refresh(). Invalidate() is a polite, "Please repaint when you can." Refresh() is more like, "Repaint NOW, I'm not going ANYWHERE until you do." There is almost never a good reason to use Refresh() instead of Invalidate(), which is why it's popular in internet tutorials.
But that's not going to solve the problem outright.
"Draw lots of lines" is always going to take "lots of time", where "time" is how long it takes to draw one line. So long as you're doing that every time, it's going to take a long time. So what can you do?
I'm assuming the user draws these lines less frequently than the "lots of lines" updates. I'm assuming it only takes them a second or two. That means you can cheat. The reason you're redrawing now is you want to hide the old line, then display the new line, and the only way to do that is to redraw all of the old lines. There's normally two ways to draw a line like this quickly, but because your 'background' is complicated you can't use the second.
1) Draw "lots of lines" to a bitmap and redraw that.
In this technique, you draw the "lots of lines" to a bitmap if and only if you need to draw them. The paint logic ends up looking like:
Code:
If lines need to be updated:
Clear the bitmap.
Redraw the lines to the bitmap.
End If
Draw the bitmap.
Draw the cursor.
Copying a bitmap to the screen's faster than drawing a lot of lines, so long as "a lot" is a fairly large number. The details of figuring out when you need to update the lines might get complex, and you know more about your app than I do.
This answer is wrong. You should be using TableAdapter and Dictionaries instead.
The "thing" you labelled as "MY Cursor" .. does it consist of the small square,
the large square, and the "cross-hairs"?
Further, does is also include the segment of the colored image, and if so, is this
segment of the colored image being "dragged" along with the "My Cursor"?
1. As the amount of detail is variable and the user can zoom in, I think that drawing to a bitmap may add too much complexity to the program. I will try changing the level of graphics detail depending on the zoom level. When I'm zoomed in (and there is a lot of graphics) I will try to simplify what is drawn (i.e. instead of drawing a complex symbol just draw a circle) and when zoomed out (and there is less graphics and the cursor works as expected) draw the complex symbol in full. I will still consider drawing to a bitmap.
2. "My cursor" is the two squares and the "cross hair". It moves over the coloured image "below".
Sitten is absolutely right that drawing a bitmap can be much faster than drawing lines. Create the bitmap in memory and then drawing it takes about the same amount of time, but if you can re-use the bitmap it's considerably quicker.
I did some testing and found that drawing a bitmap of about 1000 x 800 pixels with 2 thousand random straight lines was about 6 times as fast as drawing the lines separately. The smaller the image rendered, the greater the difference. The actual timing depend on the hardware and OS, but this indicates how much difference a pre-rendered bitmap can make.
You didn't mention before that you were zooming the view of "lots of lines". I've noticed that a scaled-up image can cause a considerable slow-down when you draw over it. I haven't checked, but maybe the same applies when you draw lines separately using a scaled Graphics. The bigger the scaling factor, the worse the slowdown and it wouldn't surprise me if that is the cause of your problem.
In that case it would make a lot of sense to pre-generate a LotsOfLines bitmap whenever the actual drawing changes. Then make a scaled/cropped copy to match the current PictureBox (or other target control) client size. Use that for painting at 1:1 size. I doubt if it would add much complexity to your present code, and I can offer some more detailed suggestions if you like.
BB
Last edited by boops boops; May 17th, 2017 at 09:08 AM.
There's not a lot of choices. Your drawing code is what takes a lot of time, so if you want to reduce the time you spend you have to find a way to minimize the amount of drawing you do.
There are levels of sophistication, and it's important you don't try to skip a level. Doing too much at once risks breaking something that works. But you're going to need a fairly high degree of sophistication if you want this to be fast. If you don't want to deal with bitmaps or sophisticated techniques, your app is going to be slow and there's nothing that can be done. There's no magic bullet to make "draw a lot of lines" take less than (time to draw 1 line) * (a lot), other than "buy faster machines".
Level 1 is where you're at. At level 1 we just naively draw whatever we need every frame. If it takes you less than about 300ms to draw a frame, most users won't notice a delay. Somewhere around 500-700ms is when users start to notice a delay, and most people consider >= 2s annoying if frequent. So when we hit those thresholds it's time to consider adding sophistication. But it's stupid to ever start at a level that isn't Level 1, because it's easy to implement and if it's "fast enough" we won't waste time on sophistication.
One way to next-level your code is to consider using invalidation rectangles. Invalidate() can take a parameter that indicates a rectangle that needs to be redrawn. Windows respects this, and there are properties of PaintEventArgs that will pass that information along to you. If you invalidate multiple rectangles before the Paint event happens, Windows is smart enough to resize the rectangle to include all "invalid" areas. You can check this and opt to ignore any lines that don't fall within the rectangle. This can dramatically decrease the amount of time spent drawing. But it also requires you to think about restructuring your data structures to help you figure out what to draw in a hurry.
You want to be able to zoom, and it turns out that's a natural evolution of invalidation rectangles. One way to think about zoom is to imagine you are mapping a small rectangle of a large image to a larger rectangle. So you have to do some coordinate space re-mapping, but the logic's very much like using an invalidation rectangle. Let's say you zoom in on a 50-pixel square at (10, 10) and your window is 100 pixels square. That means the window's bounds { (0, 0), (100, 100) } now map to the image's { (10, 10), (60, 60) }. So you're only drawing lines within that rectangle, and you have to apply some scaling/translation to the coordinate systems. In other words, "It's just an extra bit of logic on top of using validation rectangles".
So at this point, you're able to draw specific rectangles from the image and also remap the coordinates to the screen. It's super trivial to do this to an off-screen bitmap when needed. The only times you need to draw to the bitmap are:
The lines have changed within the current region the bitmap represents.
The zoom/pan has changed.
To be clear, the scenarios in increasing complexity are:
There is no zoom, the entirety of the image is being displayed. You draw all lines to a bitmap only when the lines change.
The ability to redraw just part of the image is implemented. You can now draw some lines to the bitmap when they change.
Zoom is implemented. When the user zooms, the bitmap has to be redrawn to accomodate the new viewport.
Pan is implemented. When the user pans, the bitmap has to be redrawn to accomodate the new viewport.
At each of these levels, the complexity of maintaining a bitmap of the last rendered results is trivial compared to the complexity of implementing the feature. It's not always easy to get from one level to the next, but I don't believe it's hard to switch to a bitmap at any of these levels.
There are further levels of sophistication. You can cache bitmaps for certain zoom/pan levels. You can subdivide the image into tiles to assist with panning performance. Tiles can allow progressive quality increases to make zooming look faster (Google Maps does this.) You have to do anything you can to make "I need to redraw this one line" not require "I have to redraw every line."
This answer is wrong. You should be using TableAdapter and Dictionaries instead.
I must confess to being confused as to OP's actual issue.
I thought the issue involved moving the "My Cursor" (2 squares + cross-hairs) as the
MousePointer moves. Where does "lots of lines" enter the (ahem) picture?
"My Cursor" consists of only 12 lines.
I must confess to being confused as to OP's actual issue.
I thought the issue involved moving the "My Cursor" (2 squares + cross-hairs) as the
MousePointer moves. Where does "lots of lines" enter the (ahem) picture?
"My Cursor" consists of only 12 lines.
Spoo
To draw the cursor in a new location, you have to repaint the form.
The original "repaint the form" logic is:
Code:
* Draw all of the lines.
* Draw the cursor.
You can't skip "draw all of the lines" because the old image of the form still has the old cursor on it. So you have to at least erase the part of the form the cursor was covering, then redraw it. That's the "invalidation rectangle" approach I suggested, something like:
Code:
* Figure out where the cursor used to be.
* Clear that region.
* Figure out which lines were in that region and redraw only those.
* Draw the cursor.
But ultimately I'm proposing something more like this:
Code:
* If the lines have changed:
* Redraw the cached bitmap.
* Draw the cached bitmap.
* Draw the cursor.
This answer is wrong. You should be using TableAdapter and Dictionaries instead.
To draw the cursor in a new location, you have to repaint the form.
Hmmm..
Here is a potential alternative. Note that it is written in VB6 (not .Net)
It loads the image into a PictureBox and "draws" the "cursor" with 6 Line objects.
As the mouse is moved, only the 6 lines move.
No delay or need to repaint the form.
Basics:
1. Form
2. CommandButton
3. PictureBox named PB1
4. Line named Line1 with Index set to 1
Here is the code
Code:
Public curTop, curLeft
Private Sub Command1_Click()
v = 6
'
If v = 6 Then
With PB1
.Width = 8000
.Height = 3000
.Top = 1000
.ZOrder 0
End With
fname = "D:\VBForums\DG1.bmp"
Set PB1.Picture = LoadPicture(fname)
'
Load Line1(2)
Load Line1(3)
Load Line1(4)
Load Line1(5)
Load Line1(6)
oo = 1500
For ii = 1 To 6
With Line1(ii)
.Visible = True
.BorderColor = vbRed
.BorderWidth = 2
' 1. left vert
If ii = 1 Then
.X1 = 500
.X2 = 500
.Y1 = 500
.Y2 = 500 + oo
' 2. right vert
ElseIf ii = 2 Then
.X1 = 500 + oo
.X2 = 500 + oo
.Y1 = 500
.Y2 = 500 + oo
' 3. top horiz
ElseIf ii = 3 Then
.X1 = 500
.X2 = 500 + oo
.Y1 = 500
.Y2 = 500
' 4. bot horiz
ElseIf ii = 4 Then
.X1 = 500
.X2 = 500 + oo
.Y1 = 500 + oo
.Y2 = 500 + oo
' 5. center vert
ElseIf ii = 5 Then
.X1 = 500 + oo / 2
.X2 = 500 + oo / 2
.Y1 = 500
.Y2 = 500 + oo
' 6. center horiz
ElseIf ii = 6 Then
.X1 = 500
.X2 = 500 + oo
.Y1 = 500 + oo / 2
.Y2 = 500 + oo / 2
End If
End With
Next ii
curTop = 500
curLeft = 500
End If
End Sub
Private Sub PB1_MouseMove(button As Integer, shift As Integer, x As Single, y As Single)
'
oo = 1500
pp = 750
'
zzx = curLeft - x + pp
zzy = curTop - y + pp
'
For ii = 1 To 6
With Line1(ii)
.Visible = True
.BorderColor = vbRed
.BorderWidth = 2
If ii = 1 Then
.X1 = 500 - zzx
.X2 = 500 - zzx
.Y1 = 500 - zzy
.Y2 = 500 + oo - zzy
ElseIf ii = 2 Then
.X1 = 500 + oo - zzx
.X2 = 500 + oo - zzx
.Y1 = 500 - zzy
.Y2 = 500 + oo - zzy
ElseIf ii = 3 Then
.X1 = 500 - zzx
.X2 = 500 + oo - zzx
.Y1 = 500 - zzy
.Y2 = 500 - zzy
ElseIf ii = 4 Then
.X1 = 500 - zzx
.X2 = 500 + oo - zzx
.Y1 = 500 + oo - zzy
.Y2 = 500 + oo - zzy
' 5. center vert
ElseIf ii = 5 Then
.X1 = 500 + oo / 2 - zzx
.X2 = 500 + oo / 2 - zzx
.Y1 = 500 - zzy
.Y2 = 500 + oo - zzy
' 6. center horiz
ElseIf ii = 6 Then
.X1 = 500 - zzx
.X2 = 500 + oo - zzx
.Y1 = 500 + oo / 2 - zzy
.Y2 = 500 + oo / 2 - zzy
End If
End With
Next ii
End Sub
Here is a snap.
Note that "my" cursor is only 6 lines, shown in red
The mouse arrow does not appear (dunno why), but it is located at the
intersection of the "cross-hairs"
Can this be accomplished in .Net?
Dave
If so, does this accomplish what the you wanted?
Spoo
Last edited by Spooman; May 17th, 2017 at 05:25 PM.
I tried simplifying the graphics by drawing a circle instead of a drawing a complex symbol and reducing the number of points in the line - but it did not make a significant difference - I would have had to reduce the amount of drawing to say 1% of the original.
If the image has changed I save the PictureBox image to a bitmap and then if the image has not changed recalling the save image and drawing the cursor on top. The cursor is now very responsive.
The problems are:
1. Saving the bitmap is slow.
2. It is saving the form and not just the PictureBox - see the attached image - I think the Rectangle parameters need changing.
3. When saving the image a second time (i.e. after a zoom or pan) I get a generic error occurred in GDI+ error message - this occurs on the bmp.save statement - I don't know why.
The code I'm using to save the bitmap is:
Code:
Dim bmp As New Bitmap(PicBox.Width, PicBox.Height)
DrawToBitmap(bmp, New Rectangle(0, 0, PicBox.Width, PicBox.Height))
bmp.Save(ImageFile, Imaging.ImageFormat.Png)
bmp.Dispose()
I think that the bitmap method will not work in practice. Saving the bitmap takes time - I assume it's the same time for images with a few or a lot of lines, and this will occur every time the user zooms or pans. Moving 'My cursor' is only slow if there are a lot of lines. The case I'm testing there are a very large number of line, normally there are significantly less.
I'm trying to rewrite a Visual Fortran program - there was not this problem, I did not have to redraw the whole image, I could just draw the cursor using XOR draw mode - I don't think this is possible in VB.net
Yes. I suggested "draw to an image, draw the cursor over it". Your code draws an image, then draws the cursor over it. It can be accomplished in .NET, that's why I suggested it.
@DavidGraham167: There is no reason to save the image. I can't see what DrawToBitmap() does, so I can't comment on why it might be not working. On the other hand, because there's no reason to save the image to a file, chasing that error's kind of a waste of time.
Yes. I suggested "draw to an image, draw the cursor over it". Your code draws an image, then draws the cursor over it. It can be accomplished in .NET, that's why I suggested it.
Thanks for confirmation.
Sorry I missed the fact that you'd already suggested it ..
It is just Step 1 and 2 that are wrong.
Don't draw the lines on the picture box. Draw the lines in a bitmap so it resides in memory.
You then draw this bitmap on the picture box as needed and draw your cursor on top.
When you need to redraw the "lots of lines", you redraw it in the bitmap, so you cache your drawing, and then use the bitmap to update the picturebox.
There's no need to save the bitmap to a file every time you zoom or pan, is there? I think it will be more efficient to use the stored memory bitmap (bmp) and regenerate it from the original image when zooming or panning. Saving to a file should be a separate function.
BB
edit: I think this amounts to the same as Passel is recommending.
Yes. I suggested "draw to an image, draw the cursor over it". Your code draws an image, then draws the cursor over it. It can be accomplished in .NET, that's why I suggested it.
Thanks for confirmation.
Sorry I missed the fact that you'd already suggested it ..
Spoo
FYI, that isn't really the same thing as Spoo was suggesting from VB6
VB6 allows drawing using different Raster OPs, so you can draw a line using an XOR raster op to show a line, then redraw the same line again using the XOR raster op to erase it.
You can't do that in .Net unless you switch your drawing over to using GDI and use the older API to draw (which is a little tricky).
The DrawReversibleLine method may be wrapping that old XOR drawing capability and providing it to .Net users. Of course it could be using a different method (not XOR), but doing it in some fairly efficient manner to provide the same, or perhaps better looking, result.
Edit:!!!
I just looked back and realized that spoo was suggesting using Line Shape controls, not XOR drawing.
That capability is provided in .Net through the Microsoft.VisualBasic.PowerPacks namespace, but how well it would work I don't know. It may be a bit flashy. I don't know as I normally avoid using any of the PowerPacks namespace which exists to emulate some VB6 capabilities, i.e. the Shape controls, which you were referring to from VB6. I generally avoided using the shape controls in VB6, just did the drawing myself.
Last edited by passel; May 18th, 2017 at 10:58 AM.
Unfortunately DrawReversibleLine is fairly useless because all it does is draw a line in the inverse of a single specified colour. So you might as well just choose a suitable system pen. For what it's worth, I posted a rather roundabout way of drawing anything in "XOR mode" in .Net here. Even that is of limited use because the inverse of anything around middle gray will be invisible.
I've never heard of a way of drawing a cursor or other contrasting image which can be relied on to stand out against any conceivable background. Suppose you made a cursor consisting of alternating black, red and white dots, someone would come along with an image which consists exclusively of black, red and white dots.
Unfortunately DrawReversibleLine is fairly useless because all it does is draw a line in the inverse of a single specified colour. So you might as well just choose a suitable system pen. For what it's worth, I posted a rather roundabout way of drawing anything in "XOR mode" in .Net here. Even that is of limited use because the inverse of anything around middle gray will be invisible.
I've never heard of a way of drawing a cursor or other contrasting image which can be relied on to stand out against any conceivable background. Suppose you made a cursor consisting of alternating black, red and white dots, someone would come along with an image which consists exclusively of black, red and white dots.
BB
Oh. Duh. You're right. I thought about this the other day, then promptly forgot why I didn't mention DrawReversibleLine(). It only works vs. a solid background color.
This answer is wrong. You should be using TableAdapter and Dictionaries instead.
Oh. Duh. You're right. I thought about this the other day, then promptly forgot why I didn't mention DrawReversibleLine(). It only works vs. a solid background color.
That isn't quite true, and what boops boops said isn't fully correct either.
As you noted in the other post, DrawReversibleLine does use the XOR raster OP to draw the line, so it works as well as any XOR raster op drawn line does.
The color passed is used to generate/select a high contrast value to that color, which is then used in the XOR operation when the line is drawn.
For instance if you pass in Black or White to the method, the highest contrasting value to use for the XOR operation would be White, so White will be used in the Raster OP.
When the line draws over a multicolored background, the colors will be inverted, i.e. the line pixels will be Cyan over the Red pixels, yellow over blue, and magneta over green.
The line over a gray background would be a very close gray, so wouldn't be discernible to the human eye so would appear not drawn.
Since the method chooses a contrasting color, then perhaps passing gray to the method would be the best choice (if you were to use the method on a multi-colored image) as the line would show up as black on a gray background and various shades of gray tinted with color on the other colors, but should always be visible, I think. This is assuming you have patches of gray that are large enough to be an issue. If not, then passing White to the method would be my default.
p.s. That said, I don't know that I would use it in this case since it is defined as being usable outside the paint event of the control, so seems that it might be suitable for a dynamic short lived indication, i.e. a rubberband line, but not something that may be expected to always be present, like a cursor.
Last edited by passel; May 18th, 2017 at 01:49 PM.
Well, either way I'm stuck on the example for now.
I have it drawing 200 lines to a bitmap with a 500ms delay, and only redrawing the bitmap when I ask. I have it drawing a square around the mouse to represent the cursor. I can see some hiccups in rendering even in this state. So I started working on invalidation regions.
I screwed up in a lot of different ways trying to implement those invalidation regions. I think what happened was I got order of operations wrong. I mean, it's working, but I'm not always fully erasing the cursor, especially if you move it quickly. What I want is something like:
Code:
When mouse moves:
* Calculate where the new cursor will be.
* Invalidate a region that encloses both the new and old cursors.
When painting:
* Clear the entire invalidation rectangle.
* Figure out what parts of the bitmap overlap the cleared region.
* Redraw those parts of the bitmap.
* Draw the new cursor.
I think what I screwed up and did wsa:
Code:
When mouse moves:
* SAME
When painting:
* Try to clear where the old cursor was, BUT
* The only thing I am storing is where the new cursor is
* Jeez I'm stupid
I'm going to try to fix that but can't spend more than about another half-hour on it today
This answer is wrong. You should be using TableAdapter and Dictionaries instead.
{edit: perhaps your example is not using the DrawReversibleLine, in which case my comment below is not valid. You are probably just trying a method of invalidating smaller regions for the paint event.}
It seems like it might be unreliable because windows will sometimes choose to cause a paint event for some reason which will mess up the XOR two step dance. If a paint event came along, your "old" line will already be erased, so if you "erase" it, you'll end up drawing it instead.
I think you would need to coordinate with the paint event, so that you know not to "erase" the line if a paint event occurred.
Last edited by passel; May 18th, 2017 at 02:02 PM.
@Sitten:
It helps to inflate the dirty rectangle by a few pixels before invalidating it. Don't worry about combining rectangles to form the region. The framework does that automatically when clearing the invalid queue, so it doesn't matter if you invalidate the same area repeatedly.
I use a pattern like this:
Code:
Dim r as Rectangle = cursor bounds
r.Inflate(2, 2)
control.Invalidate(r) 'invalidate old cursor bounds
cursor bounds.Offset(dx, dy) 'draw cursor in new position
r.Offset(dx, dy)
control.Invalidate(r) 'invalidate new cursor bounds
BB
Last edited by boops boops; May 19th, 2017 at 03:37 AM.
Huh. I'm incorporating that. It's a lot more elegant than my "try to remember the last position but also keep the current position in mind and oh crud what if multiple MouseMove events happen before Paint" approach.
This answer is wrong. You should be using TableAdapter and Dictionaries instead.
Below is a Form file, it sort of sets itself up so you can paste the code into a new project, push play, then go. Let's talk about parts of it.
The application draws 200 lines in a grid pattern in a 400x400 pixel square. That didn't really introduce a delay on my system, so there's a Thread.Sleep() to make it wait 500ms for some visible delay when it happens. This is a protip for evaluating if you invalidate more than you should when prototyping. There is a "Redraw in 3" button that waits 3 seconds, then initiates a redrawing of the lines. This obviously causes the 500ms delay. A 3-pixel thick green square surrounds the cursor. I made it 3 pixels because if it's 1 pixel it creates the illusion of a lot of flicker as it passes over the grid.
So if you run it and poke around, it should look pretty smooth. When the mouse moves, a rectangle surrounding the old cursor and the new cursor is invalidated. When the form draws, it checks if it needs to redraw all of the lines. If it does, it draws the lines to a Bitmap that's stored in the field _lastLines. If it doesn't, it checks if any of the lines are inside the invalid rectangle. If so, it redraws that chunk of the bitmap, then redraws the cursor. It's pretty smooth on my machine.
One glitch: when the mouse moves over the button, the button gets the MouseMove event. This causes some artifacts from the old cursor border to not always get erased. I can think of a few ways to handle it, but left it because example code. There's a lot of other problems, like "I create 200 pens despite only needing 7 total". Example code.
Here's the tricky bits.
The code needs to understand how to translate between two coordinate spaces. The bitmap's top-left corner is (0, 0) in its coordinate space, but that is located at (10, 10) in the Form's coordinate space. So when GetBitmapAreaToDraw() does some translation. The intersectingRegion variable is in Form coordinates. So when calculating the return value, the X and Y coordinate are shifted left by 10.
Code:
Return New Rectangle(intersectingRegion.X - 10, intersectingRegion.Y - 10, intersectingRegion.Width, intersectingRegion.Height)
The next most important part is the MouseMove handler. I based it on boops boops' suggestion, with some tweaks:
Code:
' Invalidate the area around the cursor.
Dim cursorPosition = Cursor.Position
Dim clientCursorPosition = Me.PointToClient(cursorPosition)
' Thanks to boops boops for this snippet, it's better than anything I came up with.
_cursorBounds.Inflate(5, 5)
Invalidate(_cursorBounds)
_cursorBounds = GetCursorBounds(clientCursorPosition)
Invalidate(_cursorBounds)
The field _cursorBounds stores the rectangle used to draw the cursor on the last rendering pass. The border is 3 pixels wide. So I invalidate a 5-pixel region around it to account for the 2-3 extra pixels of height and width the Pen's thickness represents. The double-Invalidate() looks wasteful to the untrained eye, but it's important to tell Windows both rectangles are invalid. Behind the scenes, Windows creates a larger rectangle that includes both of these rectangles and only one call to OnPaint is made.
I feel like everything else is self-exlpanatory. I added the "Redraw" button to help highlight you can't get around "repainting the entire thing is slow". The purpose of this demonstration is to showcase, "You don't have to repaint everything."
Code:
Public Class Form1
Private Const DrawDelay As Integer = 500
Private _shouldRedrawLines As Boolean = True
Private ReadOnly _lines As New List(Of LineInfo)()
Private _lastLines As Bitmap
Private _cursorBounds As Rectangle
Private _rng As New Random()
Private ReadOnly _colors() As Color = {Color.Red, Color.Orange, Color.Yellow, Color.Green, Color.Blue, Color.Indigo, Color.Violet}
Public Sub New()
' This call is required by the designer.
InitializeComponent()
' Add any initialization after the InitializeComponent() call.
Me.DoubleBuffered = True
End Sub
Private Sub Form1_Shown(sender As Object, e As EventArgs) Handles MyBase.Shown
Me.ClientSize = New Size(600, 450)
Dim redrawButton As New Button() With {
.Text = "Redraw in 3 seconds",
.Location = New Point(430, 10)
}
' This is ugly as heck and would look nicer with some features of VS2013. Consider upgrading!
' Also, this is NEVER how I'd write this in a real application.
AddHandler redrawButton.Click, Sub(s, args)
Threading.ThreadPool.QueueUserWorkItem(
Sub(state)
Threading.Thread.Sleep(3000)
Me.Invoke(Sub()
_shouldRedrawLines = True
Me.Invalidate()
End Sub)
End Sub
)
End Sub
Me.Controls.Add(redrawButton)
End Sub
Private Sub Form1_Paint(sender As Object, e As PaintEventArgs) Handles MyBase.Paint
e.Graphics.DrawRectangle(Pens.Black, New Rectangle(10, 10, 400, 400))
'GetBitmapAreaToDraw(e.ClipRectangle)
If _shouldRedrawLines Then
_shouldRedrawLines = False
RedrawLines()
End If
DrawImage(e.Graphics)
DrawCursor(e.Graphics)
End Sub
Private Sub DrawCursor(ByVal g As Graphics)
Using stroke As New Pen(Color.Green, 3)
g.DrawRectangle(stroke, _cursorBounds)
End Using
End Sub
Private Sub DrawImage(ByVal g As Graphics)
Dim imageArea = GetBitmapAreaToDraw(Rectangle.Round(g.ClipBounds))
If Not imageArea.HasValue Then
Return
Else
Dim redrawBounds = imageArea.Value
Dim clientBounds = New Rectangle(redrawBounds.X + 10, redrawBounds.Y + 10, redrawBounds.Width, redrawBounds.Height)
g.DrawImage(_lastLines, clientBounds, redrawBounds, GraphicsUnit.Pixel)
End If
End Sub
' Determines which parts, if any, of the bitmap need to be redrawn.
Private Function GetBitmapAreaToDraw(ByVal invalidBounds As Rectangle) As Nullable(Of Rectangle)
' This is the area of the bitmap in terms of the Form's coordinates.
Dim clientBounds As New Rectangle(10, 10, 400, 400)
' This is the area of the bitmap in terms of its own coordinates.
Dim bitmapBounds As New Rectangle(10, 10, 400, 400)
' The intersection of the two is what part of the Bitmap needs to be drawn.
Dim intersectingRegion = clientBounds
intersectingRegion.Intersect(invalidBounds)
If intersectingRegion.Height = 0 OrElse intersectingRegion.Width = 0 Then
Return Nothing
Else
Return New Rectangle(intersectingRegion.X - 10, intersectingRegion.Y - 10, intersectingRegion.Width, intersectingRegion.Height)
End If
End Function
Private Sub RedrawLines()
GenerateLines()
Dim newBitmap As New Bitmap(400, 400)
Using g As Graphics = Graphics.FromImage(newBitmap)
For Each line As LineInfo In _lines
Using linePen As New Pen(line.Color, 1)
g.DrawLine(linePen, line.Start, line.End)
End Using
Next
End Using
If _lastLines IsNot Nothing Then
_lastLines.Dispose()
End If
_lastLines = newBitmap
Threading.Thread.CurrentThread.Sleep(DrawDelay)
End Sub
Private Sub GenerateLines()
_lines.Clear()
' Horizontal
For row = 1 To 99
Dim thisLine = New LineInfo
thisLine.Start = New Point(0, row * 4)
thisLine.End = New Point(399, row * 4)
Dim colorIndex As Integer = _rng.Next(0, _colors.Length)
thisLine.Color = _colors(colorIndex)
_lines.Add(thisLine)
Next
' Vertical
For column = 1 To 99
Dim thisLine = New LineInfo
thisLine.Start = New Point(column * 4, 0)
thisLine.End = New Point(column * 4, 400)
Dim colorIndex As Integer = _rng.Next(0, _colors.Length)
thisLine.Color = _colors(colorIndex)
_lines.Add(thisLine)
Next
End Sub
Private Class LineInfo
Public Property Start As Point
Public Property [End] As Point
Public Property Color As Color
End Class
Private Sub Form1_MouseMove(sender As Object, e As MouseEventArgs) Handles MyBase.MouseMove
' Invalidate the area around the cursor.
Dim cursorPosition = Cursor.Position
Dim clientCursorPosition = Me.PointToClient(cursorPosition)
' Thanks to boops boops for this snippet, it's better than anything I came up with.
_cursorBounds.Inflate(5, 5)
Invalidate(_cursorBounds)
_cursorBounds = GetCursorBounds(clientCursorPosition)
Invalidate(_cursorBounds)
End Sub
Private Function GetCursorBounds(ByVal position As Point) As Rectangle
Return New Rectangle(position.X - 20, position.Y - 20, 40, 40)
End Function
End Class
This answer is wrong. You should be using TableAdapter and Dictionaries instead.
I don't think images are going to demonstrate "see how the animation is smooth". But if you can imagine a green, square cursor moving smoothly over a field of plaid, that's what the images would look like.
This answer is wrong. You should be using TableAdapter and Dictionaries instead.
Thanks to everybody for their help, especially to 'Sitten Spynne' for posting the code. I couldn't have managed without you.
My example has over 17,000 being drawn and My cursor now moves smoothly over the image. I couldn't get the MouseMove part of invalidating the two rectangles to work, maybe I've got my coordinates wrong - I have just invalidated the whole area. My cursor is slightly behind the arrow but it doesn't jerk and the user wouldn't complain.