CS-360 Fall, 2000 Class 4 R. Eckert BITMAPS, ANIMATION, AND TIMERS Windows graphics--up to now we have used simple GDI drawing functions (Ellipse(), etc.) We want to be able to work with more complex images and animation. Bitmap: An Off-screen Canvas-- -A rectangular image that can be created with painting programs. -A data structure that stores a matrix of pixel values in memory. -The pixel value stored determines the color of a pixel in the image. -Windows supports 4-bit, 8-bit (indirect), & 16/24-bit (direct) pixel values. -Bitmaps can be stored as .bmp files (static resource data). -They can be edited with paint programs. -But a bitmap takes up lots of space. -It is a GDI object that must be selected into a Device Context to be used. -It can be thought of as the canvas of a DC upon which drawing takes place. -A bitmap must be compatible with a video display or printer. -It can be manipulated invisibly and apart from a physical display device. -Can be transfered to/from physical a device ==> flicker-free animation. -A bitmap doesn't store information on drawing commands (metafiles do that). -Icons and cursors are small bitmaps. Using Bitmaps-- -Create and save a bitmap using a paint editor --> image.bmp -Add it to the program's resource script file, e.g.: Img BITMAP image.bmp -To display it, we must first load it from the program's resources: HBITMAP hBitmap; HINSTANCE hInstance; hInstance = (HINSTANCE)GetWindowLong (hWnd, GWL_HINSTANCE); hBitmap = LoadBitmap (hInstance, Img); -Note the use of GetWindowLong() to get the program's instance handle which identifies the program's resource data. This is one possible way of getting hInstance. (The type cast is necessary to avoid a compiler warning since GetWindowLong() is typed as a pointer to a void--which is not the same as HINSTANCE.) Once we have the instance handle, we can call any of the Windows functions that load in program resources. Here LoadBitmap() is used to get our bitmap. Steps in displaying the bitmap-- 0. Get a DC (with GetDC() or BeginPaint() as usual). 1. Create a memory device context with CreateCompatibleDC(). 2. Load the bitmap with LoadBitmap() [or create it with CreateCompatibleBitmap()]. 3. Select bitmap into the memory DC with SelectObject(). 4. Copy bitmap from the memory DC to the device DC with BitBlt() or StretchBit().We can say: Bitmap Window Client Area ---------- = -------------------- Memory DC DC Memory DC-- Like a DC for a physical device, but it is not tied to the device. A memory DC is used to access a bitmap rather than a device. A bitmap must be selected into a memory DC before it can be displayed on the physical device. hMemDC=CreateCompatibleDC(hDC)-- Creates a memory DC with the same physical attributes as the DC of the given device. A subsequent call to SelectObject() to select a bitmap into this DC sets up the bitmap data with the exact sequence of bytes needed to display it on the physical device, so copying from a Memory DC to a device DC is fast BitBlt (hDestinationDC, x, y, w, h, hSrcDC, xsrc, ysrc, dwRop) -- Copies pixels from bitmap selected into the source DC to the destination DC. x,y -- specify the upper lefthand corner of the destination rectangle. w,h -- specify the width, height in pixels of the rectangle to be copied. xsrc, ysrc -- specify the upper lefthand corner of the source bitmap. dwRop -- specify the raster operation for the copy. Raster Ops-- How to combine source pixel colors with current pixel colors on the destination screen--Boolean logic combinations (AND, NOT, OR, XOR, etc.) The currently-selected brush pattern also can be combined==> 256 different possible combintations, 15 of which are named. (In the following table: S=source bitmap, D=destination bitmap, P=currently-selected brush, i.e., the current Pattern.) BLACKNESS 0 (all black) DSTINVERT ~D MERGECOPY P & S MERGEPAINT ~S | D NOTSRCCOPY ~S NOTSRCERASE ~(S | D) PATCOPY P PATINVERT P ^ D PATPAINT (~S | P) | D SRCAND S & D SRCCOPY S SRCERASE S & ~D SRCINVERT S ^ D SRCPAINT S | D WHITENESS 1 (all white) StretchBlt()-- Same as BitBlt() except the size of the copied bitmap can be changed. Both the source and destination width and height are specified. StretchBlt(hDestinationDC,x,y,w,h,hSrcDC, xsrc,ysrc,wsrc,hsrc,RasterOp) BITMAP1: An Example program that uses BitBlt() and StretchBlt(). PatBlt(hDC, x, y, w, h, dwRop) -- Paints a bit pattern on the specified DC. The pattern is a combination of the currently-selected brush and the pattern already on the destination device. x,y,w,h determine the rectangular area and dwRop (raster op) specifies how the pattern will be combined with the destination pixels. Possible dwRops are: BLACKNESS (0), DSTINVERT (~D), PATCOPY (P), PATINVERT (P^D), and WHITENESS (1). The pattern is tiled across the specified area. BITMAP3 Example program-- Experimenting with different raster ops: Source--COLORS.BMP (white, red, yellow, blue, green bands: see SRCCOPY image). Destination--a pattern of black and white tiles: BRIKBRSH.BMP (look at DSTINVERT image for reversed destination pattern). Brush pattern--a blue diagonal cross pattern (look at PATCOPY image). Note that the "INVERT" raster ops disappear and appear (for animation). The program uses BitBlt() to copy the bitmap described in COLORS.BMP to different regions of the application window using each of the named raster ops. PatBlt() is used to paint the client area with the pattern described in BRIKBRSH.BMP; a brush is created from this bitmap with CreatePatternBrush(). The names of the various raster ops are displayed (using ANSI_VAR_FONT) below each bitmap. A string table is used to store the raster op names with the program's resource data. This is an alternative to defining the strings as static data in the program. Storing strings in a string table with the resource data means it's easy to make changes without searching through the program for the strings. Defining a string table resource-- STRINGTABLE [load option] [memory options] BEGIN idNumber "Text String 1" idNumber "Text String 2" ... END Developer Studio also has a string table editor that facilitates the preparation of a string table with the other program resources. Loading a string from the string table--like loading other resources from the program's resources: LoadString(hInstance, idNumber, lpszBuf, cbSizeBuf); where lpszBuf is the address of a buffer to receive the string.
ANIMATED GRAPHICS-- To create a moving picture-- We want to give the illusion of motion by continual draw/erase/redraw. But how do we do it without taking over Windows? We shouldn't allow disruption of other applications. In a DOS application, you could do the following: while (TRUE) { /* exit loop if a key is pressed */ /* erase old object image */ /* compute new location of object */ /* draw object at new location */ } In Windows, other programs won't be able to run while this loop is executing. We need to keep giving control back to Windows so other programs can operate. This is especially true under the cooperative multitasking used in Windows 3.xx. One method: Use a PeekMessage() loop instead of a GetMessage() loop. GetMessage() only returns control if a message is waiting for the calling program. PeekMessage() returns (with 0 value) if no active messages are in the system. -i.e., when no other programs are doing anything and there's no message for our program. When this occurs, our application can use the opportunity to redraw the image: -i.e., take action if PeekMessage() doesn't find a pending message. -This is different from the GetMessage() loop structure. Detail--PeekMessage() doesn't return zero for WM_QUIT message (like Getmessage()) So application must explicitly check for a WM_QUIT message to exit the program. PeekMessage(lpMsg, hWnd, uFilterFirst, uFilterLast, wRemove); -The first 4 parameters are same as GetMessage. -Last one: specifies whether or not the message should be removed from the Queue. Our PeekMessage() message loop will look like: while (TRUE) { if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { /* non-zero means we must handle the message */ if (msg.message == WM_QUIT) return msg.wParam; /* return to Windows if msg is a WM_QUIT */ else { TranslateMessage (&msg); DispatchMessage (&msg); /* dispatch the msg to our WndProc */ } } else /* zero means there's dead time that can be used */ { do some other stuff like drawing the next animation frame } } THE "BALL" ANIMATION APPLICATION (Ball bouncing off walls)-- BALL.H-- Define menu item constants. Define ball constants: (VELOCITY, BALLRAD, MINRAD--how close to wall). BALL.CPP-- global variables-- _dDrawOn: on/off switch to toggle animation on or off. _nXSize,_nYSize: window width, height--to determine if ball is inside (if window is resized, we need to change these). WM_CREATE message--Set initial animation toggle switch to off. WM_SIZE message--sent by Windows anytime window is resized by the user. -vertical/horizontal size of client area is encoded in lParam. -least significant two bytes = horizontal size in pixels. -most significant two bytes = vertical size. -macros HIWORD() and LOWORD() get these values. -store in globals _nXSize, _nYSize (used by WndProc(), WinMain() & our function DrawBall()). Drawing the ball with helper function DrawBall()-- Use a broad white pen to outline the ball-- -has the effect of erasing the visible part of the "old" ball. Use a solid red brush for the ball's interior. Draw the ball in its new position with Ellipse(). Use Sleep() to slow down the animation (for fast machines). Get rid of pen, brush, and DC. Motion of the ball in DrawBall()-- Keep track of current position in nX,nY. Each time function is called, add nVelX, nVelY velocity values to previous x,y. When ball is within MINRAD pixels of a wall... -reverse the velocity in the direction of the collision; -this is an elastic collision in physics. If part of the animation window is obscured by another program's wndow, the ball continues to bounce underneath the hidden parts of the window--it will show up again when exposed. We don't have to worry about whether or not the ball is hidden by another window; we just send our output to the window's device context; Windows takes care of figuring out which portions of our window are visible and which are not. DRAWING ON A MEMORY BITMAP--IMPROVING AN ANIMATION-- The BALL application is fairly flicker free, but if we were drawing many objects during each frame of an animation, we would notice an annoying flicker. This is because of the multiple accesses to the frame buffer during each new frame. The best way of getting around this problem is to just make ONE ACCESS to the frame buffer during each new frame. Under Windows this can be done by using offscreen memory bitmaps. We can use GDI graphics functions to "draw" on a bitmap selected into a memory DC just as with a "real" DC. So we can do many drawing operations, and, when done, BitBlt() the result to the real DC-- which is very fast, so there will be no flicker in animation applications. Getting a Bitmap to draw on-- Up to now we've used LoadBitmap() to get a bitmap defined in a resource description (.rc) file and then selected it into a memory DC. An alternative: create a blank bitmap in memory with: hBitmap = CreateCompatibleBitmap (hDC, w, h); hDC--a handle to the device (hDC) the Bitmap is to be compatible with. w,h--the width and height of the bitmap. When this bitmap is selected into a memory DC, we can use all the GDI graphics functions to draw on it without affecting the real device screen. (All the GDI drawing operations are now invisible to the user.) When drawing is all done, BitBlt() it to the real device==>just one screen access, as opposed to drawing directly to the screen device context. If the image is fairly complex, many accesses would be made to the screen during each animation frame, which could cause flicker. Animation of a moving object over a stationary background-- Set up an offscreen bitmap and select it into a memory DC. For Each Frame (each time PeekMessage() returns): Calculate the new position of the object(s). Erase entire off-screen bitmap (or BitBlt() the background bitmap to it). Redraw the object(s) (in new position) on the off-screen bitmap. BitBlt() the entire off-screen bitmap to the screen. For a large image field, this BitBlt() covers a large area, so it could be slow. A better method would be to calculate the affected area (the rectangle encompassing the old and new object position) and BitBlt() to that area only. See the BALLBLT example program-- Sprite--a little bitmap that moves around on the screen. We could restore the background and just BitBlt() the sprite over it. But there's a problem--The sprite consists of the object we want enclosed in a rectangle. So when the blitting is done, the background color inside the enclosing rectangle will wipe out the background area on the destination bitmap. So our moving object will have a "halo" around it. It will also always have a rectangular shape. Solution (Sprite Animation)-- 1. Set up a "mask bitmap" in which the sprite pixels are black and the rest of the enclosing rectangle is white. 2. BitBlt() this over the background using the SRCAND (AND) raster op. 3. Set up an "image bitmap" in which the sprite pixels are set to the color they should be (whatever colors are in the sprite object) and the rest of the enclosing rectange pixels are black. 4. BitBlt() this to the result of step 2 using the SRCINVERT (XOR) raster op. The result will make the sprite move to its new location with the background around it intact. DIB (Device Independent Bitmaps)-- The bitmaps we've looked at until now are Device Dependent Bitmaps(DDB). These have the same color organization as the real graphics output device. What's stored are the dimensions of the display and the pixel values--no information showing how the pixel values are mapped to actual RGB colors. So DDBs can't be used without loss of color fidelity on a device with different color organization (e.g., a different video card). Windows 3.0 initiated the Device Independent Bitmap (DIB)--includes its own color table showing how pixel values correspond to RGB colors. It can be displayed on any raster output device; the color mapping info in the DIB can be used to convert to the nearest colors the device can render. DDB storage format DIB storage format ------------------------------------------------------- Bitmap Info Header Bitmap Info Header Pixel values Color Table Pixel Values Using a DIB-- It Can't be selected into a DC ==> no BitBlt(), so how do we display it? 1. Convert it to a DDB using CreateDIBitmap(); -creates a DDB and returns a handle to it. -can be used as any other DDB (BitBlt(), etc.). -memory intensive since it requires twice the memory. or 2. Use StretchDIBits(); -copies bits directly from a DIB to the target DC. -used like StretchBlt(), but with differences (search "StretchDIBits"). Check out the online help and the course references for more information on DIBs. The Direct-X Graphics Library-- Although BitBlt() and StretchBlt() are relatively fast, they are no match to direct accesses to the computer's frame buffer (video RAM memory). Until fairly recently, that meant that the best fast-action game programs for PCs had to be written as DOS applications. In 1995 Microsoft created a library of routines that permit direct access to the frame buffer and to any acceleration hardware that might be built into the system's video card. This library is called DirectX, and comes with the Windows operating system. DirectX will be introduced in the next set of notes. It is also covered in detail in several of the references. THE WINDOWS TIMER-- An input device that periodically notifies an application when a specified time interval has elapsed. The program tells Windows the interval; Windows sends WM_TIMER message to signal the interval has elapsed. SOME TIMER APPLICATIONS-- Keeping time--In clock programs, timer messages tell a program when to update the time. But this is not precise. Waking up--a timer message is used to trigger a preset alarm. Multitasking--Since Windows 3.xx was a nonpre-emptive multitasking system, programs must return control to Windows quickly. But what if the program has to do a lot of processing? We can divide job up into smaller pieces and process each piece on receipt of timer message. Even in pre-emptive multitasking (Windows 95/NT), it's more efficient to return control to Windows as soon as possible. Maintaining an updated status report--realtime updates of continuously- changing information. Autosave feature--a timer message can prompt a program to periodically save the user's work. Pacing movement--If game objects must move at a certain rate, timer messages can trigger that movement ==> no inconsistencies from variations in processor speed. Activation of a screen saver after a certain period of time. Terminating demonstration versions of programs--a timer message signals when the time is up. Multimedia--Programs that play CD audio, sound, music, often let audio data play in the background. A program can use a timer to periodically determine how much of the audio has played and to coordinate on-screen visual information. USING A TIMER-- Allocate and set a timer with: SetTimer(hWnd, timerID, Timeout, f_address); Parameters: -hWnd of the window to receive the timer messages (if the last parameter is NULL, this window's WndProc() will receive the messages). -the timer id (UINT); -the timeout duration (UINT); interval in milliseconds between WM_TIMER messages. -the TimerProc--the address of the procedure that will receive/process the timer messages--a "callback" function. Possible Return Values (UINT): -the identifier of the new timer if 2nd argument was NULL & call was OK. -nonzero if 1st argument was a valid window handle and the call was successful. -zero if unsuccessful. From that point on, the timer will repeatedly generate WM_TIMER messages and reset itself each time it times out. WM_TIMER message: wParam = Timer ID; lParam = 0. When an application is done using a timer, stop timer messages and remove the timer from the system with-- KillTimer(hWnd, timerID); Make sure all timers have been killed in response to WM_DESTROY message prior to program termination. There can only be a finite number of timers running at once. If all have been allocated, SetTimer() returns NULL, so be sure to check if the timer was allocated successfully. If no timer is available, a program could put up a Message Box advising the user to close another application that may be using one or more timers: while (!SetTimer(hWnd, 1, 1000, NULL)) if (IDCANCEL == MessageBox (hwnd, "Too many timers!", "Pgm Name", MB_ICONEXCLAMATION | MB_RETRYCANCEL)) return FALSE; /* return to Windows if user hits Cancel Button */ How does a timer work?-- It uses the 8253 hardware timer interrupt (INT 8)--every 54.925 msec. (18.2 times a second). The SYSTEM.DRV program intercepts INT 8 and sets a new vector. This points to a routine within the USER module of Windows that decrements counters for each timer set by any Windows application. When any of the counters reaches 0, USER puts a WM_TIMER message in that application's message queue and resets the counter to its original value. ==> the resolution is that of the PC timer--18.2 times per second. So WM_TIMER messages can't be generated any faster than that. Also the time interval specified is rounded down to an integral number of clock ticks. WM_TIMER messages are handled like WM_PAINT messages--low priority; i.e. if a program has only WM_TIMER and WM_PAINT messages on its queue, and other programs have other messages on theirs, Windows will pass control to them. If other applications can be busy, your program may not get WM_TIMER messages at the specified interval. They are put on your program's queue, but until your program regains control, it won't receive them. This means that you should not use timers for precision timekeeping. BEEPER1 Program (See Petzold)--Beeps and changes colors once every second. Moving/resizing window --> causes a program to enter a "modal message loop"-- i.e., Windows prevents interference with resize/move operations by trapping all messages with a message loop inside Windows. They are disgarded and don't make it to the program's message loop ==> the program stops beeping! Using a Timer as an alternative to the PeekMessage() loop in animation-- A timer message can be used to generate the next frame in the animation ==> the animation will run at the same speed on all machines. BALLTIME Program (like BALL, but uses a Timer instead of PeekMessage())-- AN ALTERNATIVE WAY OF USING TIMER MESSAGES (use of callbacks)-- A callback function-- A function in your program called by Windows. You give Windows information on the location of the callback function, and then Windows calls it when, for example, a timer message comes along. It's sort of like the WndProc(). A callback function must be type CALLBACK, just as WndProc(), since it's called from outside our program (by Windows). The parameters and value returned by a callback function depend on the purpose of the callback function. For the call-back function associated with the timer, the parameters are the same as for the WndProc(). VOID CALLBACK TimerProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); -hWnd is the handle to the window specified when you call SetTimer(). -message is always a WM_TIMER message. -wParam is the timer ID. -lParam is the current system time. This function should do the processing of the WM_TIMER message. But you must tell Windows where this function is--in the 4th parameter to SetTimer(). Under Windows 95 this is just the address of the timer callback function. (Under Windows 3.1 it was much more complicated--you had to specify the "instance address" of a chunk of code--a thunk--that would generate the correct segment/offset address of the timer procedure.) Then when you set the timer, you do it with: SetTimer (hWnd, ID_TIMER, interval, (TIMERPROC) TimerProc); /* setting a timer with callback function TimerProc() under Windows 95 */ BEEPER2 (Petzold)--An example of using a timer callback function.
ANOTHER TIMER FUNCTION-- GetCurrentTime()--returns the number of milliseconds from Windows startup; not very precise. PERFORMANCE MONITOR COUNTERS-- High-resolution timers that provide the most accurate measurement of time possible in the system. Can be used in applications for which precise time measurement is important--e.g., scientific measurements, musical interval timings, game interactions, and performance monitoring of code execution. QueryPerformanceCounter(*pmilliseconds)--the current value of the high- resolution performance counter is returned in *pmilliseconds with 64-bit precision. The function returns FALSE if there is a high-resolution timer in the system, TRUE if not. [See Visual C++ 5.0 online help for details] GETTING ABSOLUTE TIME INFORMATION--use C library functions time() and localtime(). (See on-line help for details.)