The easiest way to perform something after a specific interval of time has passed is to use the TTimer component (System category). If the TTimer's Enabled property is set to True, the TTimer fires the OnTimer event after a specific amount of time has passed. The time interval (in milliseconds) is determined by the Interval property.
The following example uses the TTimer component to continuously display a series of colored rectangles.
The Interval property of the timer is set to 100 to have the OnTimer event occur every 100 milliseconds. Listing 22-17 shows the code that displays the rectangles displayed in Figure 22-17.
Figure 22-17: TTimer animation
Listing 22-17: Randomly drawing colored rectangles
procedure TMainForm.AnimatedDraw(Sender: TObject); var i,j: Integer; begin for i := 0 to (ClientWidth div 50) do for j := 0 to (ClientHeight div 50) do begin Canvas.Brush.Color := RGB(Random(255), Random(255), Random(255)); Canvas.Rectangle(i * 50, j * 50, (i + 1) * 50, (j + 1) * 50); end; end;
The TTimer component is not suitable for high-quality, accurately timed animation because it has a resolution of ~50 milliseconds. One way to measure time more accurately is to use the Windows API GetTickCount function (declared in the Windows unit).
The GetTickCount function retrieves the number of milliseconds that have elapsed since the system was started. So all you have to do to determine a specific interval is to save the beginning time in a variable and simply wait until a desired amount of time has passed. In the following example, the FTick variable (of type Cardinal) holds the start value:
if GetTickCount > FTick + 40 {25 fps} then begin { your code here } FTick := GetTickCount; end; // if GetTickCount
Now we're going to create a simple application that simulates snow falling (see Figure 22-18). Unfortunately, I didn't have a snowflake image, so I used a smiley.
Figure 22-18: "Snow"
The application that we're going to create should work at 25 frames per second and use double-buffering to remove flickering. Double-buffering is a drawing technique that is often used, especially when drawing animated graphics, to eliminate flicker. Double-buffering involves drawing on two different surfaces: the back buffer and the screen. Double-buffering is implemented by:
Drawing everything on the back buffer (an off-screen image)
Displaying the back buffer onscreen when you're finished drawing
The first thing we have to do is declare the variables that we're going to use in this application. We need a back buffer bitmap, a snowflake bitmap, and an integer variable that will be used for timing. We also need a TPoint array to contain the coordinates of the snowflakes. Here are the variable declarations:
type TMainForm = class(TForm) private { Private declarations } Flake: TBitmap; { the snowflake (smiley bitmap) } FBuffer: TBitmap; { the back buffer } FTick: Cardinal; { used for animation } Snow: array[1..255] of TPoint; { let's work with 255 snowflakes } public { Public declarations } end;
Now we have to create the back buffer and the snowflake images in the OnCreate event handler and also write an OnDestroy event handler to remove these images from memory when we're done.
Listing 22-18: Preparing the back buffer and randomizing the flakes
procedure TMainForm.FormCreate(Sender: TObject); var i: Integer; begin { create and set up the back buffer bitmap } FBuffer := TBitmap.Create; { make the back buffer the same size as the form - very important} FBuffer.Width := Self.ClientWidth; FBuffer.Height := Self.ClientHeight; { create and load the snowflake image } Flake := TBitmap.Create; Flake.LoadFromFile('flake.bmp'); Flake.Transparent := True; { set random flake coordinates; Y coordinates have to be negative to have the flakes appear at the top of the form at startup } for i := Low(Snow) to High(Snow) do begin Snow[i].Y := -Random(Self.ClientHeight); Snow[i].X := Random(Self.ClientWidth) + Flake.Width; end; { start timing } FTick := GetTickCount; end; procedure TMainForm.FormDestroy(Sender: TObject); begin { remove the sprite and the back buffer from memory } Flake.Free; FBuffer.Free; end;
All we have to do now is create one procedure that will draw the snowflakes and another that will call this procedure every 40 milliseconds to achieve the desired 25 FPS animation.
Animating a snowflake is not hard at all. You only have to do the following:
Make the snowflake fall down by constantly increasing its Y coordinate by a couple of pixels (randomly is best).
Have the snowflake randomly move left or right.
When the snowflake reaches the ground, set its Y coordinate to 0 or less to make it fall again (if you want to, of course).
Listing 22-19 contains the DrawFlakes method that draws all flakes on the back buffer and the back buffer on screen. The DrawFlakes method uses a constant array of values to move the flakes, rather than the Random or RandomRange functions, because using the constant array results in better movement.
Listing 22-19: Drawing the snowflakes
procedure TMainForm.DrawFlakes; const RANDOM_MOVES: array[0..5, 0..2] of Integer = ((1, -2, -1), (-1, 0, 1), (-1, 2, 1), (2, 1, -2), (-3, 1, 3), (2, 0, -2)); var i: Integer; begin { clear the back buffer } FBuffer.Canvas.Brush.Color := Color; FBuffer.Canvas.FillRect(FBuffer.Canvas.ClipRect); Randomize; for i := Low(Snow) to High(Snow) do begin { select a new horizontal position for each snowflake } Snow[i].X := Snow[i].X + RANDOM_MOVES[Random(5), Random(2)]; { use Abs to get positive values and to make the flakes fall } Snow[i].Y := Snow[i].Y + Abs(RANDOM_MOVES[Random(5), Random(2)]); { if a flake reaches the end, recycle it } if Snow[i].Y > Self.ClientHeight then Snow[i].Y := -(Random(ClientHeight) div 2); { don't let the snowflake leave the screen horizontally } if Snow[i].X < 0 then Snow[i].X := 0 else if (Snow[i].X + Flake.Width) > Self.ClientWidth then Snow[i].X := Self.ClientWidth - Flake.Width; { draw the flake on the back buffer } FBuffer.Canvas.Draw(Snow[i].X, Snow[i].Y, Flake); end; { finally, display the back buffer } Canvas.Draw(0, 0, FBuffer); end;
Now it's time to write the animation code. The best place for this code is in the application's OnIdle event handler. The OnIdle event is perfect because it allows us, by passing False to the Done parameter, to get as much CPU time as possible. Inside the OnIndle event handler, we can call the GetTickCount function to determine how much time has passed and call DrawFlakes to display the flakes.
First, drop a TApplicationEvents component on the Designer Surface and then write the code in Listing 22-20.
Listing 22-20: Animating the snowflakes
procedure TMainForm.ApplicationEventsIdle(Sender: TObject; var Done: Boolean); begin { set Done to False to keep OnIdle working constantly } Done := False; { if enough time has passed, call DrawFlakes to update the screen } if GetTickCount > FTick + 40 {25 fps} then begin DrawFlakes; { remember the time the frame was drawn } FTick := GetTickCount; end; // if GetTickCount end;
If you want to draw on the desktop, you'll have to use several GDI functions, especially if you want to do what we are about to do: update the previous example and draw the snowflakes on the desktop. In addition, we are going to use the snowflakes to draw an image on the desktop (see Figure 22-19).
Figure 22-19: Drawing on the desktop
To successfully update the previous example, you have to know how do to the following:
How to use the GetDC function to retrieve the desktop's device context
How to use the BitBlt function to draw images on the screen
How to use the TransparentBlt function to draw images transparently (works only on Win98, Win2K, and later versions)
How to use the InvalidateRect function to refresh a window
To retrieve the desktop's device context, call the GetDC function and pass 0 as the hWnd parameter. Don't forget to call ReleaseDC to free the device context when you're done using it:
var desktopDC: HDC; begin desktopDC := GetDC(0); try finally ReleaseDC(0, desktopDC); end; // try end;
If you want to display a bitmap as fast as possible, you should use the BitBlt function. The BitBlt function requires you to pass a lot of parameters: handles for the destination and the source device contexts, four destination rectangle coordinates, two source coordinates, and the raster operation code:
function BitBlt(DestDC: HDC; X, Y, Width, Height: Integer; SrcDC: HDC; XSrc, YSrc: Integer; Rop: DWORD): BOOL; stdcall;
The Rop (raster operation code) parameter defines how the image pixels are combined with the source pixels (inverted, merged, etc.). There are several values that you can pass as the Rop parameter, but we're only going to use the SRCCOPY constant. The SRCCOPY constant is used when you want to copy the source pixels directly to the destination rectangle.
Here's a simple example that illustrates how to use the BitBlt function to display a bitmap image on the form.
Listing 22-21: Using the BitBlt function to draw bitmaps
type TMainForm = class(TForm) procedure FormPaint(Sender: TObject); procedure FormDestroy(Sender: TObject); procedure FormCreate(Sender: TObject); private { Private declarations } public { Public declarations } B: TBitmap; end; var MainForm: TMainForm; implementation {$R *.dfm} procedure TMainForm.FormCreate(Sender: TObject); begin B := TBitmap.Create; B.LoadFromFile(ExtractFilePath(Application.ExeName) + 'image.bmp'); end; procedure TMainForm.FormDestroy(Sender: TObject); begin B.Free; end; procedure TMainForm.FormPaint(Sender: TObject); begin { draw the entire image in the top-left corner of the form } BitBlt(Canvas.Handle, 0, 0, B.Width, B.Height, B.Canvas.Handle, 0, 0, SRCCOPY); end; end.
The parameter list of the TransparentBlt function is very similar to the parameter list of the BitBlt function. The only big difference is in the last parameter. The TransparentBlt function requires you to pass the color that will be treated as transparent rather than the raster operation code. Here's the declaration of the TransparentBlt function:
function TransparentBlt(DC: HDC; p2, p3, p4, p5: Integer; DC6: HDC; p7, p8, p9, p10: Integer; p11: UINT): BOOL; stdcall;
The InvalidateRect function can be used to invalidate a portion of or the entire window. This function accepts three parameters: the window's handle, a rectangle that defines the portion of the window that needs to be refreshed (pass nil to refresh the entire window), and a Boolean value that specifies whether or not the window's background should be erased. Here's the declaration of the InvalidateRect function:
function InvalidateRect(hWnd: HWND; lpRect: PRect; bErase: BOOL): BOOL; stdcall;
Now that you know how these functions work, you can take a look at Listing 22-22 to see how to draw snowflakes on the desktop.
Listing 22-22: Drawing snowflakes on the desktop
unit Unit1; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, ExtCtrls, AppEvnts, XPMan, StdCtrls; type TMainForm = class(TForm) ApplicationEvents: TApplicationEvents; RefreshButton: TButton; XPManifest: TXPManifest; procedure RefreshButtonClick(Sender: TObject); procedure ApplicationEventsIdle(Sender: TObject; var Done: Boolean); procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); private { Private declarations } Flake: TBitmap; FBuffer: TBitmap; public { Public declarations } FTick: Cardinal; Snow: array[1..255] of TPoint; procedure DrawFlakes; end; var MainForm: TMainForm; implementation {$R *.dfm} procedure TMainForm.FormCreate(Sender: TObject); var i: Integer; begin { create the back buffer and use it to store the image the flakes will draw on the desktop } FBuffer := TBitmap.Create; FBuffer.LoadFromFile(ExtractFilePath(Application.ExeName) + 'back.bmp'); { create and load the snowflake } Flake := TBitmap.Create; Flake.LoadFromFile('flake.bmp'); for i := Low(Snow) to High(Snow) do begin Snow[i].Y := -Random(Screen.Height); Snow[i].X := Random(Screen.Width) + Flake.Width; end; { start timing } FTick := GetTickCount; end; procedure TMainForm.FormDestroy(Sender: TObject); begin Flake.Free; FBuffer.Free; { refresh the desktop } InvalidateRect(0, nil, True); end; procedure TMainForm.DrawFlakes; const RANDOM_MOVES: array[0..5, 0..2] of Integer = ((1, -2, -1), (-1, 0, 1), (-1, 2, 1), (2, 1, -2), (-3, 1, 3), (2, 0, -2)); var i: Integer; desktopDC: HDC; begin { retrieve the desktop device context } desktopDC := GetDC(0); try for i := Low(Snow) to High(Snow) do begin { modify the portion of the desktop the flake passed } BitBlt(desktopDC, Snow[i].X, Snow[i].Y, Flake.Width, Flake.Height, FBuffer.Canvas.Handle, Snow[i].X, Snow[i].Y, SRCCOPY); { select a new horizontal position for the snowflake } Snow[i].X := Snow[i].X + RANDOM_MOVES[Random(5), Random(2)]; { make the snowflake fall down } Snow[i].Y := Snow[i].Y + Abs(RANDOM_MOVES[Random(5), Random(2)]); { if flake reaches the end, recycle it } if Snow[i].Y > Screen.Height then Snow[i].Y := -(Random(Screen.Height) div 2); { don't let the snowflake leave the screen } if Snow[i].X < 0 then Snow[i].X := 0 else if (Snow[i].X + Flake.Width) > Screen.Width then Snow[i].X := Screen.Width - Flake.Width; { draw the flake } TransparentBlt(desktopDC, Snow[i].X, Snow[i].Y, Flake.Width, Flake.Height, Flake.Canvas.Handle, 0, 0, Flake.Width, Flake.Height, clWhite); end; // for finally ReleaseDC(0, desktopDC); end; // try end; procedure TMainForm.ApplicationEventsIdle(Sender: TObject; var Done: Boolean); begin Done := False; if GetTickCount > FTick + 40 {25 fps} then begin DrawFlakes; FTick := GetTickCount; end; // if GetTickCount end; procedure TMainForm.RefreshButtonClick(Sender: TObject); begin InvalidateRect(0, nil, False); end; end.