Painting issue, when to draw/erase snap lines
Hi,
I have started to work some more on my 'shape editor'. If you're not familiar with it, just imagine the Visual Studio form designer: a 'canvas' (the form) with a bunch of 'shapes' (the controls) that can be selected, moved, resized, etc.
I have just finished implementing snapping between shapes and the canvas boundaries. But now I am having some slight issues with painting the snap lines.
Just FYI: this is what I mean by snap lines:
http://i52.tinypic.com/2mi42yx.png
The blue lines that indicate that the 'buttons' are snapped to each other, and a purple line indicating that button1 is snapped to the boundary.
My shape editor is based loosely on jmcilhinney's 'manipulating GDI+ drawings' codebank entry, and a central idea in that post is to only draw what is necessary. In other words, when I move a shape I could re-draw the entire canvas, but it is much faster and smoother to only re-draw the old and the new location of the shape. Since nothing else has changed, I don't need to draw the rest. So, I simply invalidate the old bounds and the new bounds, and even with a lot of shapes all moving simultaneously it still feels very smooth.
My snaplines however provide a problem in this approach. I can determine when I need to draw a snap line of course, that is no problem. The problem is when to 'erase' them again.
In code, I simply hold a List(Of SnapLine), and in the Paint event I loop through that list and draw all the snap lines, if any. The list will usually be empty, unless I am snapping a control in which case a few snap lines will be present.
Now, in order to draw a snapline of course I need to invalidate a rectangle around that snapline. This is also not much of a problem. The problem is that, once I do not need to snap, then I do need to draw the entire canvas (once!) to clear all the snaplines. If I don't do this and only draw the shapes then the snaplines will persist until windows decides to repaint my form, which can take a long time.
So basically what I'm doing is this:
- In the MouseMove event of the Canvas, I move the selected shape (there can be multiple but let's assume a single selection).
- In the Move event of the shape, I call a SnapShape method.
- The SnapShape method loops through all other shapes and checks if it should snap to them, and to the boundary.
- If it should snap, it snaps, and one or more snaplines are added to the List(Of SnapLine).
- The parts of the Canvas that show the snaplines are invalidated and repainted.
This all sounds about right, yeah? Well it's not. What if in the previous 'iteration' I have snapped a shape, but now the user released the mouse and the shape is just sitting there. The snaplines should disappear! So I should at least invalidate the entire Canvas once.
I can do that of course, but the question I guess is: when?
I can determine when I need to draw the snaplines (when I snap the shape), but I cannot determine when not to draw it.
You might think something like this (the SnapShape method returns a boolean whether it snapped or not):
Code:
Private Sub Shape_Moved(ByVal sender As Object, ByVal e As EventArgs)
Dim shape = TryCast(sender, Shape)
Dim snapped = Me.SnapShape(shape)
If snapped Then
Me.Invalidate() ' draw the new snaplines
Else
'...?
Me.Invalidate() ' clear the old snaplines
End If
End Sub
See what I mean? I need to Invalidate the canvas in both cases: if it snapped I need to draw the lines, and if it didn't snap I need to erase them.
But this way I am invalidating the canvas EVERY TIME the shape moves! This defeats the entire purpose of invalidating only the regions that changed. Even though nothing changed except the shape location, I am still drawing the entire canvas including all other shapes, which haven't changed at all.
Does anyone know a clever way to handle this? I may be overlooking something obvious, but all I come up with is horribly complicated schemes that probably won't work anyway lol...
Re: Painting issue, when to draw/erase snap lines
I hope I'm not misunderstanding the problem, but can't you do it in the MouseUp event? You could loop throught the snaplines list and invalidate a 1-pixel high or wide invalidation rectangle for each line. And then clear the list of whatever isn't needed.
Still, I wonder whether you would see any performance difference if you did simply invalidate the whole control instead.
BB
1 Attachment(s)
Re: Painting issue, when to draw/erase snap lines
No, I can't use the MouseUp event. Suppose I move a shape close to another shape. The snap lines appear because the control is snapping. Then, without releasing the mouse, I drag the shape away again. Snap lines should disappear, but they won't because I haven't released the mouse.
And yes, there is a VERY big performance difference if I invalidate the entire canvas every time or only invalidate the required parts. I've uploaded the source so you can take a look if you want to.
In order to see the difference clearly:
- Create a new document
- Add a bunch of buttons (5, 6, 7 or something)
- Make a few of them quite large (at least 30-40% of the 'form')
- Select most of them (use ctrl + click to select multiple)
- Select a big button (just click it while having multiple shapes selected so that it gets the white grab handles instead of the black)
- Now drag them around quickly. Try it with the Shapes Snapping checkbox checked and unchecked. I can see a very large difference. For some reason the difference is much less pronounced if you select one of the smaller buttons, but it's definitely an improvement to disable the snapping. With snapping disabled it doesn't draw the entire canvas every time. With snapping enabled, it does.
Re: Painting issue, when to draw/erase snap lines
Thanks for letting me see the code Nick. Unfortunately I can't run it until I get round to downloading and installing VS2010Cs (Express). But I had a look at the Canvas.cs file.
I think you could invalidate the whole list of snaplines at the start of OnMouseMove, to make sure they will all be erased when the list is renewed. Then invalidate each new snapline when you add it to the list in the Snap subs, to make sure it will be drawn. Of course it won't matter if you invalidate the same pixels twice because it's Updating that takes time, not Invalidating.
Hope it helps, BB
Edit: Something to think about tomorrow -- what if invalidating controls like buttons involves a lot more than invalidating a rectangular area of the form?
Re: Painting issue, when to draw/erase snap lines
I would think that you should treat each snapline just like a shape. Whenever a shape moves you invalidate the area it used to occupy and the area it now occupies. Do the same with the snaplines. Invalidate the area that each old snapline occupied and the area that each new snapline occupies. Do this every time a shape moves and when the user releases a shape.
Re: Painting issue, when to draw/erase snap lines
Quote:
Originally Posted by
boops boops
Thanks for letting me see the code Nick. Unfortunately I can't run it until I get round to downloading and installing VS2010Cs (Express). But I had a look at the Canvas.cs file.
I think you could invalidate the whole list of snaplines at the start of OnMouseMove, to make sure they will all be erased when the list is renewed. Then invalidate each new snapline when you add it to the list in the Snap subs, to make sure it will be drawn. Of course it won't matter if you invalidate the same pixels twice because it's Updating that takes time, not Invalidating.
Hope it helps, BB
Edit: Something to think about tomorrow -- what if invalidating controls like buttons involves a lot more than invalidating a rectangular area of the form?
Ah yes, I completely forgot I was using C#, sorry 'bout that. Well the issue isn't a code issue so I don't think it matters much where I would have posted it.
If you have Visual Studio 2008 (not VB Express) then you can still run it easily though, just downgrade the solution file to VS2008 and you should be able to open it.
Quote:
Originally Posted by
jmcilhinney
I would think that you should treat each snapline just like a shape. Whenever a shape moves you invalidate the area it used to occupy and the area it now occupies. Do the same with the snaplines. Invalidate the area that each old snapline occupied and the area that each new snapline occupies. Do this every time a shape moves and when the user releases a shape.
That seems appropriate. I'm going to see if I can implement that this afternoon.
Thanks for the help so far.
Re: Painting issue, when to draw/erase snap lines
Hi Nick,
Yesterday I was puzzled why you were having problems simply invalidating the whole form in the MouseMove sub. After all, you see GDI+ demos with hundreds of shapes flitting around on a (maximized) form and they successfully refresh the form quickly enough to do it in time with an animation clock. So why should dragging a control or shape be different?
Looking at your Canvas.cs code again, I see that you are using Refresh and Update in several places. I think that is a mistake.
The reason is that Invalidate just involves putting some numbers on a queue and takes a few microseconds. The corresponding pixels will be not be redrawn until the processor has some idle time, and the then area refreshed will be the union of all the invalidated regions.
Refresh on the other hand takes several milliseconds. What is more it has to happen straight away, before the next line of code is executed. So it must surely be a mistake to loop through a list of shapes with This.Refresh in each pass. I am even doubtful about putting Update in the Shape move handler.
Here's a little demo of the difference:
Code:
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
Dim sw As New Stopwatch
sw.Start()
For i As Integer = 0 To 100
Me.Invalidate()
Next
MessageBox.Show("Invalidate: " & (sw.ElapsedTicks / Stopwatch.Frequency * 1000).ToString("0.000") & " milliseconds")
sw = Stopwatch.StartNew
For i As Integer = 0 To 100
Me.Refresh()
Next
MessageBox.Show("Refresh: " & (sw.ElapsedTicks / Stopwatch.Frequency * 1000).ToString("0.000") & " milliseconds")
End Sub
(I've used a 100x loop to eliminate the Stopwatch overhead which I believe is a couple of microseconds.) For me, Refreshing appears to take about 1,500 times as long as Invalidating.
So I wonder how your app would behave if you just deleted all the lines with Refresh or Update, and put This.Invalidate at the end of the MouseMove sub. I suspect it will work smoothly and you will not have to worry about invalidating the snap lines. I'm not a fluent Cs reader and not having a working project makes it hard to try it out, so I may be wrong about this.
BB
Re: Painting issue, when to draw/erase snap lines
I know the difference between Invalidate, Update and Refresh, but I don't think the error is there.
I am using Update in the InvalidateShapes method. If you're not fluent in C# it might look as if I'm calling it for every shape, but I'm not.
Code:
foreach (Shape s in shapes) this.InvalidateShape(s);
this.Update();
This is the same as
Code:
foreach (Shape s in shapes)
{
this.InvalidateShape(s);
}
this.Update();
So the Update is outside the for loop.
And I am only calling Refresh in the SnapShapes method. I admit this was a mistake, I was doing this for a very different issue with the snap lines: for some reason they are only drawn when a shape snaps to the right or bottom, but not to the top or left. If I use Refresh the lines are drawn, but they keep flickering horribly. I think there's a stupid mistake somewhere that removes the snapline immediately after drawing them again but I haven't found it yet.
Anyway, taking out Refresh and replacing by Invalidate doesn't really help. I can still see a difference between snapping on (Invalidate after every move) and off (Invalidate only the required parts).
Re: Painting issue, when to draw/erase snap lines
The way I understand it is that the MouseMove code can change the locations/sizes of one or more Shapes. So the LocationChanged event will fire for each shape involved. You handle that in the Shape_LocationChanged sub, which calls SnapShape. But SnapShape clears the SnapLines list every time (i.e. once for each Shape affected in MouseMove), which may account for the disappearing snaplines. Putting Refresh in SnapShape could be very messy; calling Me.Refresh even once from MouseMove tends to cause flickering, because it often takes longer to process than the interval between MouseMove events.
I still don't see why you need to use Update or Refresh at all. It seems that the only processing required at the moment consists of integer arithmetic, inter-assembly references and iterating a couple of quite small collections. If it isn't processor-bound, why not let the application update the invalidated pixels in its own good time? There will be plenty of idle time between the MouseMove events.
BB
Re: Painting issue, when to draw/erase snap lines
Yeah something like that is going on, but that doesn't explain why the snaplines show up for the bottom and right boundaries, but not for top and left. The code is exactly the same (except location of the snaplines) so I'm a little puzzled.
I only call SnapShape for the main selected shape though (I check if the IndexOf the shape is 0), the rest of the selected shapes aren't snapped obviously.
Of course, the snapping itself is merely changing the location of the shape. This would cause the LocationChanged event to fire again, snap again, etc. So I use a 'isSnapping' flag. I set it to true right before I do the snapping, and to false right afterwards. If it is true, I do not snap.
With this logic I think it should work, and it does for two of the four sides, strangely.
Anyway that wasn't the purpose of this thread, I'll probably make another one on that later if I can't figure it out :p
I'm not actually using Update or Refresh anymore. The Refresh was a mistake and I've replaced it by Invalidate. The Update well yeah maybe that isn't required, but I'm not calling the method ever. It was created in a very early stage where I thought I might sometimes need to invalidate all shapes. Turns out I've never needed it yet, so I never call it.
Re: Painting issue, when to draw/erase snap lines
Quote:
Originally Posted by
NickThissen
Yeah something like that is going on, but that doesn't explain why the snaplines show up for the bottom and right boundaries, but not for top and left. The code is exactly the same (except location of the snaplines) so I'm a little puzzled.
I'd guess they are the last ones to be drawn, after the rest have been erased. Anyway, it's time I stopped pretending I understand how it all works;).
I'm impressed by how the app is structured -- small files, little routines sometimes with just 1 line of code but always with 4 lines of documentation, all fitting neatly together. Is that a Csharp thing? Or is it good design practice anyway?
BB
Re: Painting issue, when to draw/erase snap lines
Quote:
Originally Posted by
NickThissen
Yeah something like that is going on, but that doesn't explain why the snaplines show up for the bottom and right boundaries, but not for top and left. The code is exactly the same (except location of the snaplines) so I'm a little puzzled.
....
I'm not sure if this is relevant to this issue, but drawing rectangles and filling rectanges actually covers 2 different areas - that is, a drawn rectangle is 1 pixel wider and taller than the fill region. so, that may account for the fact that the bottom-right lines show and the top-left ones don't?
(I'm currently working on my own designer, and haven't implemented snapping to lines, but have implemented aligning multiple selected shapes, grab handles)
Re: Painting issue, when to draw/erase snap lines
Quote:
Originally Posted by
boops boops
I'm impressed by how the app is structured -- small files, little routines sometimes with just 1 line of code but always with 4 lines of documentation, all fitting neatly together. Is that a Csharp thing? Or is it good design practice anyway?
It's just good design practice I suppose. I try to keep as much code in separate methods so that each method has only the relevant code. The only place where I haven't done that yet is in the shape moving/resizing and snapping, because that's the area I'm working on now. It was very neat before, but then I introduced multiple selections and it all became rather messy. Still have to refactor that.
Quote:
Originally Posted by
SJWhiteley
I'm not sure if this is relevant to this issue, but drawing rectangles and filling rectanges actually covers 2 different areas - that is, a drawn rectangle is 1 pixel wider and taller than the fill region. so, that may account for the fact that the bottom-right lines show and the top-left ones don't?
(I'm currently working on my own designer, and haven't implemented snapping to lines, but have implemented aligning multiple selected shapes, grab handles)
The snap lines are just that: lines. I have a class SnapLine with a Draw method, but all it does is draw the line using Graphics.DrawLine. Nothing special going on there. The lines are drawn correctly, and in the correct spot. They just are removed from the collection right afterwards and the canvas is invalidated a second time, so they disappear again. If I use Refresh instead of Invalidate, then I can see them flickering. By debugging I've determined that the lines are drawn, and then the entire 'line drawing method' is run a second time, this time with the lines collection empty (so they are removed again). I just can't figure out why it's being called again. This seems easy to debug but it's not because it is called when I move a shape, and I cannot move a shape and be in the debugger at the same time :p
Re: Painting issue, when to draw/erase snap lines
Quote:
Originally Posted by
NickThissen
The snap lines are just that: lines. I have a class SnapLine with a Draw method, but all it does is draw the line using Graphics.DrawLine. Nothing special going on there. The lines are drawn correctly, and in the correct spot. They just are removed from the collection right afterwards and the canvas is invalidated a second time, so they disappear again. If I use Refresh instead of Invalidate, then I can see them flickering. By debugging I've determined that the lines are drawn, and then the entire 'line drawing method' is run a second time, this time with the lines collection empty (so they are removed again). I just can't figure out why it's being called again. This seems easy to debug but it's not because it is called when I move a shape, and I cannot move a shape and be in the debugger at the same time :p
The way I'd do it, essentially, in an invalidate (paint) event of your canvas it'd loop through each and every snap line object, determine if it's 'region' is within the passed clip region, and if so, go ahead and paint it. effectively, you isolate the logic associated with the controls, and the required painting of those controls. It doesn't matter whether a 'snap' was in progress or not.
By using the event args clip region you can quite readily determine what needs painting. Even with hundreds of controls, looping through each and every one, determine if an intersect between the control and the clip region, is quite fast and doesn't bog down the paint routine (painting is magnitudes slower than the math involved).
Having said all that, I'm not sure how relevant or useful it is to your design.
Edit: Just downloaded your application; I see what you mean - I wasn't thinking of those lines as 'snap' lines; I was thinking of the Visio-style snap lines which are vertical and horizontal lines that the edges of objects snap to...my comments might just be useless in this case...
Edit2: on a quick scan over it, is the fact that you may perform a canvas invalidate on multiple shapes independently an issue? Wouldn't it be a better bet to loop through all the shapes that need invalidating and build a region that contains all those rectangles, then call invalidate once? It may be that the form is invalidated once, with the snaplines list populated, then re-invalidated with an empty snaplines list.
As an additional note, I'm not sure how the snap-lines actually become visible if you only ever invalidate a shape region - unless the shape region includes the region for the snap lines (The only Canvas.Invalidate I can see is cascaded through the InvalidateShapes() method of the canvas). Again, not looked in any great detail so may possibly be missing something...don't want to put you on a path of frustration trying to explain something to an ignoramus... ;)