One question that I've seen come up countless times in various forms is "why is my drawing so flickery?" Simple -- you made it that way. ;-) Some operating systems, such as OS X and Linux, automatically double buffer screen redraws for you. This means that you have to work very hard to cause an application to have flickery drawing on it. However, other operating systems such as Windows or Classic do not automatically double buffer for you. So if you're drawing something yourself, it's quite easy to make it flickery if you take the naive approach.
The way to solve this issue is with a technique called double buffering. In practice, it sounds just like its name -- you're using a second buffer (with the screen being the first buffer) to do all your drawing to. The reason this solves the issue is simple. As you are drawing to the screen, it updates in real time. That means that if you are drawing more than one thing onto the screen at a time, it looks very choppy because you can see each part drawing. Furthermore, since you're most likely erasing the background of the Canvas object, this means there's a period in time when there's nothing drawn on the screen at all -- and this is what causes the flickering. However, when you double buffer, all your drawing happens to an offscreen Graphics object, so each change is not seen by the user. Once all your changes have been made, then you take the offscreen buffer and draw it in one operation onto the screen. This provides you with a smooth animation without flickering.
Here's the basic steps you need to take to achieve double buffering:
1) Create a back buffer that all drawing operations are done on.
2) Make sure the back buffer is "erased" to the proper color.
3) Do all your drawing to the back buffer.
4) Draw the back buffer onto the screen buffer.
That's it! So let's take a look at creating our own DoubleBufferCanvas.
Here's a first try at implementing the steps above.
[rbcode]
Class DoubleBufferCanvas Inherits Canvas
// Define a new event definition so that the implementor
// has a Paint event to implement.
Event Paint( g as Graphics )
Sub Paint( g as Graphics ) Handles Event
// Make a back buffer to use
dim buffer as new Picture( me.Width, me.Height, 32 )
// Erase the back buffer
buffer.Graphics.ForeColor = FillColor
buffer.Graphics.FillRect( 0, 0, me.Width, me.Height )
// Call the implementor's Paint event, but
// pass in our buffer
Paint( buffer.Graphics )
// Now paint the buffer to the screen
g.DrawPicture( buffer, 0, 0, me.Width, me.Height )
End Sub
End Class[/rbcode]
As you can see, the code does exactly what we set out to have it do. We make a new offscreen buffer (in this case, a Picture). Then we erase it to the background color of the window. Then we do all our painting to it by calling the user's Paint event. Finally, we flip the buffer to the main screen.
But, this code is very brute force -- it constantly makes a new buffer which chews up a lot of memory. It also does double buffering on platforms which don't need it, such as Linux or OS X. And it still lets the user do bad things by drawing directly to the screen via the Canvas.Graphics property.
So let's try making a more robust class that fills all these holes.
[rbcode]
Class DoubleBufferCanvas Inherits Canvas
// We need a property to hold our back buffer
Private Dim mBackBuffer as Picture
// And another one to flag whether we're
// double buffering or not
Private Dim mDoubleBuffer as Boolean
// Define a new event definition so that the implementor
// has a Paint event to implement.
Event Paint( g as Graphics )
// Also, define an Open event definition so that we
// can implement the Open event and re-expose it
Event Open()
Sub Open() Handles Event
// Check to see whether the user is on a system that
// requires double buffering
#if TargetWin32
// On Windows, we always want to double buffer
mDoubleBuffer = true
#elseif TargetMacOS
// We only want to double buffer on the Mac if
// we're running on OS 9. OS X automatically
// double buffers for us
dim vers as Integer
if System.Gestalt( "sysv", vers ) then
mDoubleBuffer = vers < &h1000
end if
#endif
if mDoubleBuffer then
// We don't want the background to be erased every time
// since we're doing our own buffering. This would cause
// a lot of flickering.
me.EraseBackground = false
end if
// Call the user's Open event
Open
End Sub
Sub Paint(g As Graphics) Handles Event
// We're going to implement the paint event ourselves
// so that we can automatically do the double buffering
if mDoubleBuffer then
// Check our back buffer to make sure we've got
// one that we can draw to
CheckBackBuffer
// Erase the back buffer ourselves
mBackBuffer.Graphics.ForeColor = FillColor
mBackBuffer.Graphics.FillRect( 0, 0, me.Width, me.Height )
// Set the background color to be black, like
// a normal canvas would be
mBackBuffer.Graphics.ForeColor = &c000000
// Now call the user's paint event, but pass in
// our back buffer
Paint( mBackBuffer.Graphics )
// Now we can draw the back buffer to the screen
g.DrawPicture( mBackBuffer, 0, 0, me.Width, me.Height )
else
// We're not double buffering, so just pass the Graphics
// object on to the user's Paint event.
Paint( g )
end if
End Sub
Sub CheckBackBuffer()
// We want to make sure our back buffer is the proper size
// and ready for drawing to
dim create as Boolean
// If we don't have a back buffer, then we need to create one
// If our size has gotten larger than our
// back buffer, then we need to create a new one. We don't
// care if the canvas is smaller than our back buffer since
// we're only going to be drawing to the canvas' size on the
// screen.
if mBackBuffer = nil or me.Width > mBackBuffer.Width or _
me.Height > mBackBuffer.Height then create = true
if create then
// We only need to make the picture to the Canvas' current
// size. Anything more than that is a waste.
mBackBuffer = new Picture( me.Width, me.Height, 32 )
end if
End Sub
Function Graphics() As Graphics
// We're going to be mean and tell users that
// there *is* no Graphics object. This forces users
// to use the class correctly. However, it's also a total
// hack since we're shadowing a property of the Canvas
// class. This causes a problem since we can't call
// super.PropertyName to return the Canvas' true
// Graphics handle in the case we're not double buffering.
if mDoubleBuffer then
return nil
end if
End Function
Sub Redraw()
// We force the user to call Redraw instead of Refresh because
// we don't want the user to be able to accidentally cause an
// entire screen erase. So we override Refresh by making it
// a private function.
super.Refresh( false )
End Sub
Sub Refresh(eraseBackground as Boolean = true)
// We don't do anything here because we don't want the user
// to be able to call refresh
End Sub
End Class[/rbcode]
And there we have it! That's the entire DoubleBufferCanvas class. As you can see, it neatly solves all of our issues. We check to see whether we need to double buffer in the Open event by checking what platform the user is running on. This way we don't do extra work on platforms that don't need it. We're also not making a new picture every time we paint to the screen. Instead, we just keep reusing the same picture object until it's no longer the proper size. It's important that you pay attention to the size of the canvas since the user may be changing it at runtime, or have it locked to the window. So if the canvas gets bigger, the back buffer needs to get bigger as well. Finally, we disallow the things which we don't like. We default to not erasing the background on every screen refresh, we don't let the user access the internal Graphics object by shadowing the property with a method, and we don't let the user call Refresh directly -- they must call Redraw instead.
So how do you use one of these nifty things? Simple -- almost exactly like you'd use a regular Canvas. This class is meant to be a drop in replacement for the Canvas class (with a bit more limited functionality, but that's for your own safety).
So put one of these on a Window and size it to fit the entire screen. Then, set it's lock properties so that when the window resizes, the DoubleBufferCanvas resizes with it. Don't forget to make the window resizeable!
So let's see what we want the window and double buffering canvas to look like.
[rbcode]
Class Window1 Inherits Window
Dim Private mX as Integer
Dim Private mY as Integer
Sub DoubleBufferCanvas1.Paint( g as Graphics ) Handles Event
g.ForeColor = HSV( rnd, rnd, rnd )
g.FillRect( mX, mY, 25, 25 )
End Sub
Function DoubleBufferCanvas.MouseDown( x as Integer, y as Integer ) _
as Boolean Handles Event
mX = x
mY = y
me.Redraw
return true
End Function
Sub DoubleBufferCanvas.MouseDrag( x as Integer, y as Integer ) Handles Event
mX = x
mY = y
me.Redraw
End Sub
End Class[/rbcode]
The window has two propeties -- they keep track of the current drawing position. The real interesting stuff happens inside of the DoubleBufferCanvas events. In MouseDown, we track where the x and y location of the click happened. Then we tell ourselves to Redraw. We return true so that we can follow the mouse drag events. That event does much of the same -- it tracks the x and the y, and it redraws. The Paint event simply sets the forecolor to be a random color (a neat trick of using HSV instead of RGB) and fills a rectangle at the current track positions.
When you run the application, click inside of the canvas and drag the mouse around. No matter what platform you're on, you'll see that there's no flicker! That's the magic of double buffering. I've made this example project (with the DoubleBufferCanvas included) available for download here. Enjoy!
I believe you could avoid using Gestalt and use the TargetMacOSClassic conditional instead, correct?
Very nice. I'll have to use this class in my own projects...
How about adding this to RB's core functionality?
Using TargetMacOSClassic will only tell you if you compiled a Classic application -- not whether the app is a Carbon application running on OS 9. So that's why I used gestalt.
And I'm on the fence about this being core functionality. I can see it being a good thing, so I'm more leaning towards making it a property of the Canvas class itself. Canvas.DoubleBuffered as Boolean, which is false by default (since double buffering does take up more memory and is slower -- and we don't want to break anything relying on the old behavior).
My vote is... include it!
~joe
Well, it's a little more involved than that since I couldn't just chicken out and make .Graphics return nil if it was a framework feature. And once you try to make .Graphics return the back buffer, you run into all sorts of nagging questions like "when is the right time to clear the back buffer", "when should you flip the changes made to .Graphics to the screen" and things like that. It's a much harder problem to solve if you want to keep the functionality the same as what you'd get on OS X or Linux.
Also, if you have DoubleBuffered as Boolean and it was default as False, surely this would be incorrect for Mac OS X or Linux because they double buffer as standard. Could you make a call to turn it off at a system level? Then you could have it work as a multi-option property (enum: Off, System, Manual). Obviously only 'System' would work on OS X/Linux. 'Manual' control can then enable special methods like ReBuffer() and ClearBuffer() which do the work.
Sorry, I meant to imply that this setting would have no affect on OSes which automatically double buffer. Setting it to true or false would do nothing on OS X.
I'm not certain whether you can turn it off at the OS level to be honest.
I've been trying your code. There's one annoying flaw: I have to manually tell it when to refresh on things like mouse-clicks etc. When simply swapping it in an existing project for a normal canvas, everywhere where I have explicitly said 'refresh', it now doesn't. Is there a way round this without defeating the purpose of double-buffering? Why not just redefine 'Refresh'?
I changed it to redefine the refresh method - now it's working fine. Windows is running as smooth as a baby's bottom. Nice! What is the purpose of having Redraw separate from Refresh?
Because I wasn't certain whether I could override Refresh since it has an optional parameter. If I redefine the optional parameter, then the method signatures may be considered different and who knows what I'd get.
Mostly laziness on my part -- feel free to modify the project as you see fit!
re: "add it"
Let's just say that my kneejerk response (or 'response from a jerk' - you decide) is to include any useful technology in RB. I don't mean to bloat that baby up - it's just that I see all this useful WFS stuff and other sites with cool additional features and find it a hassle to include them in my project (you caught me... I'm lazy). It's not so much "adding them in" that's the issue. It's remembering where I saw the feature, if I downloaded it, if I am remembering what I think I saw correctly, etc...
For example I currrently have no use for this neat little code snippet. When I do I can only hope that I remember that it exists, remember where it is, and remember why I need it.
~joe
@Joe -- I agree with you that adding features to make people's lives easier isn't always a bad thing (sometimes it is though). Heck, that's what RB is all about -- making it easier to write x-platform applications!
The only reason I hesitate to add it is because it's a tough problem to solve. One that's usually easier handled by the application programmer instead of the framework. But tough problems aren't that bad, I still think it'd make a very neat feature. :-)
How did Charlie Boisseau "redefine the refresh method"? I am running Windows XP. The program I downloaded does not show my .bmp picture and when I drag the square the picture shows but flickers very much. When I release the mouse button the pictures disappears but the square is a different color each time.
Thanks
Neal
Instead of making Refresh be private, he made it public and forced the value to be false when calling the super's Refresh method (or something similar to that).
There's no bmp picture to be shown -- it simply draws randomly colored rectangles without flicker. Are you, by any chance, setting a background picture for the canvas? If you are -- don't. Draw it yourself in the Paint event.
It works great in your project. I go to use it in mine and it doesn't :-/. I don't get it, my project's very similar to your example one......
It flickers on redraws :-/.
-- sirG3
I found the culprit -- in my project it was on a tab panel. Putting it on a tab panel in your project also causes flickering. Any ideas on how to fix that?
-- SirG3
@SirG3 -- The only reason why it should be flickering with Refresh(False) is if there is another control on top of the DoubleBufferCanvas. When your DoubleBufferCanvas redraws, it forces the control above to refresh and if that control has Refresh(True) then it would cause your DoubleBufferCanvas to redraw again... causing an infinite loop of refreshes.
I have been working with PagePanels (not TabPanels) but I haven't seen this flickering you are describing. See if you can reproduce it in a simple project with just the DoubleBufferCanvas on the TabPanel.
With the help of SirG3, I tracked down what's going on with tab panels. The problem turns out to be an implementation details that's a hold-over from trying to force RB apps to work like Classic apps. Refreshes happen from the bottom up. So when you refresh the Canvas, it first refreshes its parent, which then refreshes its parent, and so-on. However, TabPanels must erase their background (a limitation of the OS) and that's why you get flicker. The tab panel is erasing the entire canvas.
So to fix the issue, I changed my redraw code to be this:
[rbcode]
#if TargetWin32
Declare Sub InvalidateRect Lib "User32" ( hwnd as Integer, lpRect as Ptr, erase as Boolean )
dim r as new MemoryBlock( 16 )
r.Long( 0 ) = 0
r.Long( 4 ) = 0
r.Long( 8 ) = me.Width
r.Long( 12 ) = me.Height
InvalidateRect( me.Handle, r, false )
Declare Sub UpdateWindow Lib "User32" ( hwnd as Integer )
UpdateWindow( me.Window.Handle )
#else
super.Refresh( false )
#endif
[/rbcode]
The flicker disappears and your framerates skyrocket.
The post before mine makes Aarons first super tip, to a perfect tip! I have spend so many hours getting this to work, and now it just works :-)
Thank you so much...