Flicker Free API Drawing
Improve the quality of controls using GDI drawing commands with this easy to use class.
VB offers a way to reduce flicker when drawing a control through its AutoRedraw property. When AutoRedraw
is set, VB creates an Off-Screen buffer to draw into, and only transfers this to the drawing surface when the Refresh
method is called. Whilst this works well (ish), it doesn't help if you need to draw graphics into some other drawing surface,
particularly ones passed to you by the Windows API. This article provides a simple class which allows you to create an
Off-Screen buffer you can use for any drawing purposes.
About Flicker Free Drawing and Off-Screen Buffers
The concept of an off-screen buffer is simple: whenever something gets drawn to the screen in
Windows it gets there by virtue of being drawn into a Device Context which contains some sort
of bitmap to hold the data. Everything that you draw onto a device context that is part of the
display is immediately visible on screen: whilst that's good for immediacy it can very easily result
in flicker depending on how you draw to it. For example, generally its a whole lot easier to clear the
background of a complex graphic display and then redraw everything than it is to work out what all the
changes are and then only redraw the changes. If you do this directly onto a device context that's
visible to the screen, flicker occurs because you see the background of all the items being
cleared prior to them being refreshed. You will see this happen if you try the sample project
from the download with the "Use Mem DC" CheckBox unchecked: it flickers a lot. If you
check the box, then the drawing suddenly becomes smooth, because all of the disruptive drawing
(such as clearing the background) is done offscreen and only transferred at the last moment.
To prevent this flickering, what you'd like to do is to do all of the drawing somewhere off-screen
and then transfer the whole lot to the screen more quickly than the screen refreshes. Windows allows
you to do both of these things: you can create a device context which isn't part of the screen to
draw on and you can use the BitBlt GDI API call to transfer the contents of one device context
to another extremely quickly.
This is how the VB AutoRedraw property operates behind the scenes for Forms, PictureBoxes
and UserControls. However, as is usually the way with VB the implementation of AutoRedraw is
hidden and can't be reused for your own purposes. So if you want to implement this feature for
anything that isn't a VB Form, PictureBox or UserControl then you need to write some custom code.
Techno City
So on to writing an equivalent off-screen buffer. The basic requirements are:
- Ability to create a suitable Device Context.
- Ability to create a bitmap of the right width and height to draw onto.
- Ability to draw to the buffer.
- Ability to copy the contents of the buffer to the screen DC quickly.
The download provides a class which provides all of these facilities in an easy-to-use
object called pcMemDC. Before we look at how it works, first we'll look at the handful
of methods and properties required to use it.
Ingredients: Water-Malt-Hops-Yeast
-
Width
Gets or Sets the width of the offscreen buffer.
-
Height
Gets or Sets the height of the offscreen buffer.
-
hDC
Gets the device context of the offscreen buffer to draw onto using GDI methods.
-
Draw
Transfers the contents of the offscreen buffer to another device context using BitBlt
-
CreateFromPicture
Helper method: if you have a VB StdPicture object then an offscreen buffer of the same
dimensions is created containing the same image.
Sex In The City
The class itself is simple enough, so into how it works. There are various ways to create
an offscreen DC, but the simplest is to use the Desktop as a basis, since otherwise you need
to work out what the colour depth of the display is. By this method, we get a handle to the
desktop device context and then create a Compatible device context:
Private Declare Function CreateDC Lib "gdi32" _
Alias "CreateDCA" _
(ByVal lpDriverName As String, ByVal lpDeviceName As String, _
ByVal lpOutput As String, lpInitData As Any) As Long
Private Declare Function CreateCompatibleDC Lib "gdi32" _
(ByVal hdc As Long) As Long
...
lhDCC = CreateDC("DISPLAY", "", "", ByVal 0&)
If Not (lhDCC = 0) Then
m_hDC = CreateCompatibleDC(lhDCC)
If Not (m_hDC = 0) Then
..
End If
End If
Once you have a DC, then you need a bitmap selected into the device context to actually
draw into. Again, we can create an object which is compatible with the desktop and simplify
the calls:
Private Declare Function CreateCompatibleBitmap Lib "gdi32" ( _
ByVal hdc As Long, ByVal nWidth As Long, ByVal nHeight As Long _
) As Long
...
m_hBmp = CreateCompatibleBitmap(lhDCC, Width, Height)
If Not (m_hBmp = 0) Then
m_hBmpOld = SelectObject(m_hDC, m_hBmp)
If Not (m_hBmpOld = 0) Then
m_lWidth = Width
m_lHeight = Height
DeleteDC lhDCC
Exit Sub
End If
Else
DeleteDC lhDCC
End If
That is about it for creating the offscreen DC, but you may have noticed the calls
to DeleteDC. One of the things that's most important to using GDI successfully
is that whenever you get a handle to an object from the API, you must always delete it
again. So wherever a GDI object is created, the class always has a corresponding call
to delete it. Here's the destroy call which is performed whenever you change the size
of the offscreen buffer, or the object terminates:
If Not m_hBmpOld = 0 Then
SelectObject m_hDC, m_hBmpOld
m_hBmpOld = 0
End If
If Not m_hBmp = 0 Then
DeleteObject m_hBmp
m_hBmp = 0
End If
If Not m_hDC = 0 Then
DeleteDC m_hDC
m_hDC = 0
End If
m_lWidth = 0
m_lHeight = 0
For performance, the class is set up to only recreate the internal bitmap when you
make the offscreen buffer larger than it was previously. This also makes it a lot easier
to use the class, since you simply set the Width and Height properties
regardless of whether they have been set before.
Sheep In The City
The sample application provided with the download aims to show how you can easily prevent
the most basic GDI code from flickering when drawing. The code itself creates a number
of Rectangular objects which move around the screen in response to a timer in an old-school
BreakOut fashion (I admit it is fairly unlikely that a real control will draw this way, but anyway).
Since as usual it is difficult to determine the overlaps between the objects and which areas
of the background will be revealed by moving any object, the drawing is accomplished by first
clearing the background and then drawing each of the rectangles in turn. When this is done
without using the pcMemDC class, the drawing flickers a lot, both from the background
being repainted and from the overlap between the object rectangles. Once the class is turned
on however, you should see that the flicker is completely eliminated.
|