Introduction
Flicker free animated drawing had been a very hot issue with Win32 and MFC.
Many excellent articles are available to explain the techniques to get a flicker
free animated effect. As many of the reader know that most popular technique has
been to use off-screen DC (device context) to do the entire complex drawing and
then copying this off-screen DC to the screen DC directly. This technique is
also known as double buffering.
C# is projected by Microsoft as the future for C++ programmers. So like many
other C++ programmers, I used some of my spare time to play around with C# to
have a feel of it. A few days back I was trying to write an application in C# to
simulate an analog clock. After establishing a base frame work and seeing my
clock work (with flicker of course) I was excited to use the old double
buffering technique to let my clock animate smoothly. But my first dilemma was
when I could not find functions like CreateCompatibleDC
,
CreateCompatibleBitmap
and SelectObject
etc. So I
started to search around MSDN and studied the Graphics
class. After
some research I was able to find two ways to produce smooth animated effects and
these techniques I will be explaining below.
Double-buffering technique the old way
I was glad to know that there was a way in C# to use the old Win32 techniques
for smooth animation. Although one cannot find direct implementation for
functions like CreateCompatibleDC
,
CreateCompatibleBitmap
and SelectObject
, but there is
an indirect way to use these functions for your GDI+ device context. The idea is
to let C# know that you will be using some functions from an unmanaged dll. You
can import a function that is exported by a dll using the DllImport
attribute. The detailed documentation for DllImport
can be found in
.NET documentation. In short with the help of DllImport
we can tell
the compiler that we will be using the specified function from the specified
dll. For example,
[DllImport("msvcrt.dll")]
public static extern int puts(string c);
The above declaration will declare the function named puts
with
static and extern attributes and the actual implementation of this function will
be imported from msvcrt.dll. I used DllImport
to import all
the necessary functions from gdi32.dll. To keep things managed, I
declared a separate class to import all such functions. The code below shows the
actual implementation for this class
public class Win32Support
{
public enum Bool
{
False = 0,
True
};
public enum TernaryRasterOperations
{
SRCCOPY = 0x00CC0020,
SRCPAINT = 0x00EE0086,
SRCAND = 0x008800C6,
SRCINVERT = 0x00660046,
SRCERASE = 0x00440328,
NOTSRCCOPY = 0x00330008,
NOTSRCERASE = 0x001100A6,
MERGECOPY = 0x00C000CA,
MERGEPAINT = 0x00BB0226,
PATCOPY = 0x00F00021,
PATPAINT = 0x00FB0A09,
PATINVERT = 0x005A0049,
DSTINVERT = 0x00550009,
BLACKNESS = 0x00000042,
WHITENESS = 0x00FF0062,
};
[DllImport("gdi32.dll", ExactSpelling=true,
SetLastError=true)]
public static extern IntPtr CreateCompatibleDC(IntPtr hDC);
[DllImport("gdi32.dll", ExactSpelling=true,
SetLastError=true)]
public static extern Bool DeleteDC(IntPtr hdc);
[DllImport("gdi32.dll", ExactSpelling=true)]
public static extern IntPtr SelectObject(IntPtr hDC,
IntPtr hObject);
[DllImport("gdi32.dll", ExactSpelling=true,
SetLastError=true)]
public static extern Bool DeleteObject(IntPtr hObject);
[DllImport("gdi32.dll", ExactSpelling=true,
SetLastError=true)]
public static extern IntPtr CreateCompatibleBitmap(
IntPtr hObject, int width, int height);
[DllImport("gdi32.dll", ExactSpelling=true,
SetLastError=true)]
public static extern Bool BitBlt(
IntPtr hObject,
int nXDest, int nYDest,
int nWidth, int nHeight,
IntPtr hObjSource, int nXSrc, int nYSrc,
TernaryRasterOperations dwRop);
}
Now I can use this Win32Support
class to use my old techniques.
The code snippet below shows how to create a memory DC with help of
Win32Support
from within your Form
class.
Graphics memDC;
Bitmap memBmp;
memBmp = new Bitmap(this.Width, this.Height);
Graphics clientDC = this.CreateGraphics();
IntPtr hdc = clientDC.GetHdc();
IntPtr memdc = Win32Support.CreateCompatibleDC(hdc);
Win32Support.SelectObject(memdc, memBmp.GetHbitmap());
memDC = Graphics.FromHdc(memdc);
clientDC.ReleaseHdc(hdc);
One important point to note here is that every call to the function
Graphics.GetHdc
on some DC must be paired with the call to
Graphics.ReleaseHdc
. this is what MSDN has to say about this issue
"Calls to the GetHdc and ReleaseHdc methods must appear in pairs. During the
scope of a GetHdc- ReleaseHdc method pair, you usually make only calls to GDI
functions. Calls in that scope made to GDI+ methods of the Graphics object that
produced the hdc parameter fail with an ObjectBusy error. Also, GDI+ ignores any
state changes made to the Graphics object of the hdc parameter in subsequent
operations."
Once you have memDC
, you can use it for off screen drawing and
then we will use BitBlt
to copy the contents of memDC to actual
screen DC.
Graphics clientDC = this.CreateGraphics();
IntPtr hdc = clientDC.GetHdc();
IntPtr hMemdc = memDC.GetHdc();
Win32Support.BitBlt(hdc, 0, 0, this.Width, this.Height,
hMemdc, 0, 0, Win32Support.TernaryRasterOperations.SRCCOPY);
clientDC.ReleaseHdc(hdc);
memDC.ReleaseHdc(hMemdc);
This will have dramatic effect on the animation that you have been trying to
produce.
The sample application uses this technique when you click the "Offscreen
Drawing Using BitBlt" radio button.
Double-buffering technique the .NET way
Luckily we can achieve the same goal without any direct help from Win32 API.
Image rendering in .NET is very simple and efficient compared to MFC. There are
two functions in Graphics
class to render your Image
object on screen, these are DrawImage
and
DrawImageUnscaled
. What makes these functions important is the fact
that .NET always uses BitBlt
in background to render the image on
DC. So if we are able to do our off-screen drawing in an Image object, we can
use these functions to render this object directly to DC and have the smooth
animated effects.
The technique is same, but the way to implement it differs a little bit. In
the code fragment below, we are using a Bitmap
object to do our
off-screen drawing. In order to draw on some Image
object, it must
be attached to a Graphics
object. We can create a new
Graphics
object from an Image
object using the static
member function, of Graphics
, named FromImage
. Once we
get a Graphics
object from some Image
object, any
drawing done on this Graphics
object will actually be changing the
Image
.
Bitmap offScreenBmp;
Graphics offScreenDC;
offScreenBmp = new Bitmap(this.Width, this.Height);
offScreenDC = Graphics.FromImage(offScreenBmp);
Provided that we have created offScreenDC
as per shown in
example above, we can implement the double buffering technique as per the code
fragment below.
Graphics clientDC = this.CreateGraphics();
clientDC.DrawImage(offScreenBmp, 0, 0);
I will recommend this technique as it does not involve any call to
unmanaged code and is simpler in nature.
The sample application uses this technique when you click the "Offscreen
Drawing Using Image" radio button.