Animation with the Windows GDI
Saving and restoring bitmapped images within a window
Joe Sam is the owner of Autumn Software, which provides software and services for the AS/400, DOS, and Windows. He has been involved in consulting and commercial software for over 15 years and currently specializes in C, C++, and RPG. He can be contacted at P.O. Box 261646, Tampa, FL 33685.
Animation with Windows presents some special challenges, as I discovered in a recent project that involved an animation sequence in a multiprogram-database application. The animation was similar to the classic bouncing ball, moving a nonrectangular bitmap over a previously painted background. A requirement of the project was that the animation should work even if the background changed, and it had be independent and reusable enough to easily integrate into other programs. I soon found that my standard library let me down, and I was off on one of those deductive journeys for which Windows so freely offers a ticket to ride.
This article describes some of the quirks_er, features in Windows that, if ignored, can cause graphical graffiti to erupt all over your beautiful displays. Some of the hurdles include performing bit-block transfers (bitblts) in a window and overcoming Windows' optimization procedures when repainting the screen. I'll also address smoothness in larger animations and present a program, MOVBMP, that shows you how to put it all together.
Animating Bitmaps
Most paint programs as well as the BitBlt function, which is our primary tool for manipulating images, require rectangular bitmaps. In general, to display nonrectangular images, a program combines a portion of the screen, a bit mask and the image itself using AND and XOR operations. This allows background graphics to appear in the areas of the rectangle outside of the actual image ("transparent bitmaps" in Microsoft lingo). For this to work, the bit mask must be a duplicate of the original, except that the image is totally black on a white background. The image bitmap must be enclosed in black. This can be done in code at run time, but it is both more convenient and faster to use predrawn images, usually as BITMAPs in the resource file. The animated image in MOVBMP (see Listing One, page 50, and Listing Two, page 51) is an iron cross with three layers of color on each arm and a mostly transparent center. Figure 1 shows the six bitmaps that are used. Background graphics will appear in the white areas of the mask. The top, right, bottom, and left bitmaps are used for rotating the animation sample. The "true image" bitmap shows the iron cross as it appears on screen.
In any graphics system, this type of animation is accomplished by saving the display area where the image will appear, ANDing the mask with the screen, and XORing the image with the ANDed result. New positions are then calculated, the saved image is restored, and the process is repeated. Depending on the bitmap size, the preceding steps may be enough. Windows is no speed demon, however, and display memory is relatively slow, so most programs require the use of memory device contexts (DCs). Memory DCs created with CreateCompatibleDC can be treated exactly like a display or other DC, depending on which type of DC is passed as an argument to this function. For larger bitmaps, additional efficiency measures are necessary.
The Trouble with BitBlt
The BitBlt function provides a means to quickly get and put predrawn screen images. Unfortunately, when copying a displayed image to memory, BitBlt is more interested in what appears on the screen, and not necessarily what appears in your window. This means that if another window overlaps your client area, or even if one of your own menus is displayed over the animation, BitBlt copies the image from the overlapping display or menu. When your window regains focus, you may suddenly have a piece of another program's screen embedded in your window, which is generally unacceptable in a professional application. Also, due to the low priority and imprecision of timer messages, which are used to determine when to move the images, this state of events occurs more frequently than you might expect. This is a known problem that has occasionally popped up in question-and-answer columns. Microsoft is surely aware of the problem, but who knows when, or if, they will fix it, and, more importantly, if your users will have the fix. Fortunately, BitBlts to the screen are confined to a window's client area.
An Optimization that Creates Work
The second feature to overcome involves a Windows 3.1 optimization. When BeginPaint is called, a structure is returned that contains, among other things, what Windows considers the invalid portion of your client area expressed as a rectangle. For the example in Figure 2, your program will get coordinates corresponding to rect A, C, G, F. However, Windows appears to clip to the region F, D, E, B, C; and the area A, B, E, D will not be repainted. This is good for Windows since less work and less time is necessary, but bad for you under certain circumstances. There's no fast, reliable method to determine if another window covers the image or if Windows will clean up the screen for you. In the end, you're pretty much on your own since most animations operate outside of the paint routine.
And there you have it: BitBlt may end up saving and restoring pieces of a menu or another program's display on your client area. The paint routine would seem to be a good place to get a clean copy of the background to avoid that situation, but part or all of the image can be inside the invalid rectangle yet outside of the clip region. In that case, if you resave the screen area, your image gets embedded in the screen in
interesting ways.
Getting a Clean Image
My solution appears in DoEraseBkgnd (see Listing Two) which is called in response to the WM_ERASEBKGND message. The bypass timer flag is set (reset in DoPaint) to temporarily stop the animation, and the window DC is obtained to avoid clipping problems. The currently saved image is then restored to the screen. At that point, the DC is released and DefWindowProc is called to perform normal processing. If the image was in the invalid region, the screen is cleaned up. If not, the proper background is restored. Either way, you're then able to get a certifiably clean background image in DoPaint. This may seem inefficient, but the available alternatives take more code and more time, and are still imprecise (rectangles vs. regions again).
To automate the animation, a timer is used to send messages at approximately correct intervals and to allow the animation to proceed even when other windows have the focus. GetNewTimer illustrates obtaining a timer and allows the user, via a message box, to retry or cancel on failure (courtesy Charles Petzold).
Now that you can ensure clean images and know when to display them, how can you do so smoothly? Figure 3 illustrates a typical situation: The image is moving to the southeast. Rectangle 1 has just been displayed. Rectangle 2 will be displayed after restoring the screen. There is a large area of overlap and, if possible, you'd prefer to paint this area only once. This is actually what happens in MOVBMP. When the animation begins, a clean image of the screen is saved in the global HBITMAP hbmOrg (see SelectBmp) and maintained throughout the animation. The Animate routine handles all save, display, and restore operations using two additional bitmaps, the window DC, and two memory DCs.
In Animate, the old positions are saved and new ones calculated. hbmOrg is copied to hbmSave. The old and new positions are passed to DiffORects, which returns the client coordinates of the incremental area to be covered by the next display (the L-shaped area marked "2" in Figure 3) as two rectangles. These clean areas are copied from the screen to the proper positions in hbmOrg, as is the overlap portion from hbmSave. hbmOrg now has a complete clean image of the area to be used by the next display. hbmOrg is then copied to hbmDest. The bit mask is ANDed with hbmDest and the selected image is XORed with hbmDest. BitBlt accomplishes these tasks using the raster operation (ROP) codes SRCAND and SRCINVERT instead of the usual SRCCOPY. At this point, the new "transparent" image is complete. DiffORects is called again to determine the area to be restored. The same two rectangles are used, but the arguments are reversed, since we are now concerned with the L-shaped area marked "1" in Figure 3. With the values for these two differential rectangles in hand, the restore area is BitBlted to the screen from hbmSave and the new image is put to the screen from hbmDest. While there is a lot going on here, the work is done with very fast memory bitmaps and DCs. No redundant data is written to the display. DiffORects is the mainstay of this differential approach. The routine was developed empirically and has been (very) thoroughly tested. Because it works with client coordinates, the returned values are immediately usable. For smaller images, routines like DiffORects can be eliminated, but after about 20x20 pixels,
flash and jerkiness become increasingly noticeable. The iron
-cross images are 66x66 pixels.
Unless the Single Image menu option has been chosen, a different image is selected every other cycle. The bitmaps were drawn and loaded as top, right, bottom, and left images. By displaying the images in order, the illusion of rotation is generated in addition to the diagonal movement of the image. These could have been different colors, sunbursts, gradual fills, and so on, and are intended to demonstrate that animation does not have to involve moving to another area of the display. MOVBMP supports zero displacements so you can see pure rotation and even what appears to be a single stationary image.
Lastly, Animate cleans up the working DCs and bitmaps. Pay close attention to the reselection of the original or old bitmaps before deleting the DCs and work bitmaps. If this is omitted or done incorrectly, Windows will experience severe technical difficulties, as may your reputation, because the resulting loss in system resources continues for the duration of the session. Animation routines may be invoked thousands of times during a program run, so any leakage can totally deplete available system resources.
Additions for Your Toolbox
MOVBMP also illustrates both unusual and commonly needed functionality for your toolbox. Probably the most obvious is the elimination of the switch statement in WndProc via an array of message id/function pointer pairs (courtesy Ray Duncan). This is efficient for programmers because, among other benefits, new messages are easy to add, each message is related to a function (which helps avoid side effects and increases modularity), size boundaries can be calculated for you at compile time, and the basic skeleton can be used in every program. An extra function call is generated, but in practice this has been a small price to pay. Priority messages or those requiring special operations can be handled outside the array loop, as are WM_TIMER and WM_DESTROY. The message-associated functions take the standard hwnd, msg, wParam, and lParam arguments, which are #defined as STDWINARGS in the header file for use in function declarations and definitions. I've defined STDWINARGS in a base header that is included in every program.
Illustrations of font common-dialog usage are available, but little has been written about nondefault font initialization, as shown in Listing Two. Probably the best way to determine appropriate values is to write a small program using the font common dialog and use your debugger to view the returned LOGFONT structure. The font common dialog is also demonstrated in MOVBMP and used as a device to allow the
background to be changed in order to verify that the animation is working properly.
Partial invalidation of the client area is used when displacement or timer values are changed. Although all of the code in DoPaint is invoked, only the affected area of the screen is redrawn. I cheated in calculating the area. You should determine the longest string using GetTextExtent for actual sizes.
Conclusion
I've tried to avoid "exercises for the reader," but any program can be improved. You might prefer to use a larger bitmap and one BitBlt to blast both the restore area and new image in one operation. I chose lesser memory usage and fewer calculations, and on my 386 20-MHz test machine, that is good enough. Modifying SelectBmp to allow loading any bitmap would be a more rewarding improvement. MOVBMP could then be used as a tool to determine optimal timer and displacement values for a given animation. The remainder of the program is not tied to specific bitmaps and may be used as is. MOVBMP is written as a single C-code module for simplicity; however, the animation functionality could easily be placed in a DLL or encapsulated in an animator class. The complete project, including resource, definition, and executable files is available electronically; see "Availability," page 2.
References
Duncan, Ray. "The Hazards of Exploring Evolving Environments." PC Magazine (April 13, 1993).
Petzold, Charles. Programming Windows. Redmond, WA: Microsoft Press, 1990.
Figure 1: Bitmaps used in the MOVBMP program. The animated image is an iron cross with three layers of color on each arm. The center is transparent.
Figure 2: The invalid portion of your client area expressed as a rectangle. Given the coordinates corresponding to rect A, C, G, F, Windows clips to the region F, D, E, B, C, and the area A, B, E, D is not repainted.
Figure 3: Typical situation where the image is moving to the southeast, rectangle 1 has just been displayed, and rectangle 2 will be displayed after restoring the screen.
ANIMATION
[LISTING ONE]
// movbmp.h #ifndef MOVBMP_H #define MOVBMP_H #define STRICT // INCLUDES // -------- #include <windows.h> #include <windowsx.h> #include <commdlg.h> // DEFINES // ------- // define standard windows arguments #define STDWINARGS HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam #define IDM_EXIT 501 #define IDM_FONT 505 #define IDM_SELECTBMP 510 #define IDM_UNSELECTBMP 511 #define IDM_DDEC 515 #define IDM_DINC 520 #define IDM_TDEC 525 #define IDM_TINC 530 #define IDM_SINGLEIMAGE 535 // Timer ID #define IMAGE_TIMER 1 // VARIABLE/USER-DEFINED TYPES // --------------------------- typedef struct { UINT msgid; // message id // pointer to a funtion taking std winproc parms LONG PASCAL (*pfnProc)( STDWINARGS ); } WINMSGFN; // FUNCTION PROTOTYPES // ------------------- // Window and Exported procedures extern "C" { LRESULT WINAPI WndProc( STDWINARGS ); } // end extern statement // message function procs LONG PASCAL DoCmd( STDWINARGS ); LONG PASCAL DoEraseBkgnd( STDWINARGS ); LONG PASCAL DoPaint ( STDWINARGS ); LONG PASCAL DoSize ( STDWINARGS ); // Command message function procs LONG PASCAL ChangeDisplacement( STDWINARGS ); LONG PASCAL ChangeTimer ( STDWINARGS ); LONG PASCAL DoFont ( STDWINARGS ); LONG PASCAL SelectBmp ( STDWINARGS ); LONG PASCAL SingleImage ( STDWINARGS ); LONG PASCAL UnSelectBmp ( STDWINARGS ); // other functions void Animate(HWND hwnd); void DiffORects(RECT *rBmp, RECT *rRst, RECT *rUpper, RECT *rLower); BOOL GetNewTimer(HWND hwnd); #endif /* movbmp.h */
[LISTING TWO]
// MOVBMP Main Module #include "movbmp.h" // pgm globals --------------------------- WINMSGFN msgfn[] = { WM_COMMAND, DoCmd, WM_ERASEBKGND, DoEraseBkgnd, WM_PAINT, DoPaint, WM_SIZE, DoSize, }; WINMSGFN cmdmsgfn[] = { IDM_DDEC, ChangeDisplacement, IDM_DINC, ChangeDisplacement, IDM_TDEC, ChangeTimer, IDM_TINC, ChangeTimer, IDM_FONT, DoFont, IDM_SELECTBMP, SelectBmp, IDM_SINGLEIMAGE, SingleImage, IDM_UNSELECTBMP, UnSelectBmp, }; // keep number of msgfn and cmdmsgfn elements int imsgfndim = (sizeof(msgfn) / sizeof(msgfn[0])); int icmdmsgfndim = (sizeof(cmdmsgfn) / sizeof(cmdmsgfn[0])); HINSTANCE hInstGBL; // this instance COLORREF crrgbColors = RGB(0,0,255); // blue RECT rWndGBL = {0, 0, 0, 0}, rWndInv = {5, 0, 255, 0}; HDC hdcOrg; HBITMAP hbmOrg, hbmOldOrg; HBITMAP hbm[5]; // top, right, bottom, left, background // init the font to TrueType Arial, 72 points LOGFONT lfFontSel = { -96, // Height 0, // Width 250, // Escapement 0, // Orientation 400, // Weight '\0', // Italic '\0', // Underline '\0', // StrikeOut '\0', // CharSet '\x03', // OutPrecision '\x02', // ClipPrecision '\x01', // Quality '\x22', // PitchAndFamily "Arial" // FaceName }; // flag variables BYTE btrblFlag = 0; // top/right/bottom/left flag BYTE bBypassTimer = 255; // 255 indicates first pass BYTE bBmpIsSelected = 0, bSingleImage = 0; char szAppName[] = "Move Bitmap", szClassName[] = "MOVBMP"; int ndxGBL; // work index int imageWidth, imageHeight, nTimerValue = 50; int nXDsp = 1, nYDsp = 1; // displacement values int nbmpX = 1, nDeltaX = 1, nbmpY = 1, nDeltaY = 1; // begin ------------------------------------- #pragma argsused int pascal WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdLine, int cmdShow) { HWND hWnd; MSG msg; WNDCLASS wndclass; char szString[] = "Move Bitmap is ending..."; hInstGBL = hInstance; // save instance info // handle class registration and window creation if( !hPrevInstance ) { wndclass.lpszClassName = szClassName; wndclass.hInstance = hInstance; wndclass.lpfnWndProc = WndProc; wndclass.hCursor = LoadCursor(NULL, IDC_ARROW); wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION); wndclass.lpszMenuName = "Main"; wndclass.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); wndclass.style = NULL; wndclass.cbClsExtra = 0; wndclass.cbWndExtra = 0; if(!RegisterClass( &wndclass )) //error { MessageBox(NULL, "Can't create window class.", szString, MB_ICONEXCLAMATION | MB_OK); return 1; } } hWnd = CreateWindow( szClassName, szAppName, WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN, CW_USEDEFAULT, 0, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL ); if(hWnd == NULL) { MessageBox(NULL, "Can't create window.", szString, MB_ICONEXCLAMATION | MB_OK); return 1; } ShowWindow (hWnd, cmdShow); UpdateWindow(hWnd); // pgm loop while ( GetMessage(&msg, NULL, 0, 0) ) { TranslateMessage (&msg); DispatchMessage (&msg); } return 0; } // end WNDMAIN LRESULT WINAPI WndProc( STDWINARGS ) { // handle Windows messages if( msg == WM_TIMER ) // here for faster handling { // animate if appropriate timer; if(wParam == IMAGE_TIMER && !bBypassTimer) Animate(hwnd); return(0L); } for(ndxGBL = 0; ndxGBL < imsgfndim; ndxGBL++) { // walk messages array if(msg == msgfn[ndxGBL].msgid) // if match, call Fn return ((*msgfn[ndxGBL].pfnProc) (hwnd, msg, wParam, lParam)); } if( msg == WM_DESTROY ) // if not in array { // happens once; don't waste searches or fn if(bBmpIsSelected) // clean up if bitmap was moving UnSelectBmp(hwnd, msg, wParam, lParam); PostQuitMessage (0); return(0L); } else return ( DefWindowProc(hwnd, msg, wParam, lParam) ); } // end WndProc void Animate(HWND hwnd) // Handle Bitmap Movement { static int ncounter = 0; // when to inc btrblFlag int nToX = 0, nToY = 0, nFromX = 0, nFromY = 0, nPrevX = nbmpX, nPrevY = nbmpY; RECT rBmp, rRst, rUpper, rLower; HDC hdcClient, hdcDest, hdcSave; HBITMAP hbmDest, hbmOldDest, hbmSave, hbmOldSave; hdcClient = GetDC( hwnd); hdcDest = CreateCompatibleDC( hdcClient); hdcSave = CreateCompatibleDC( hdcClient); hbmSave = CreateCompatibleBitmap(hdcClient, imageWidth, imageHeight); hbmOldSave = (HBITMAP)SelectObject(hdcSave, hbmSave); // create destination bitmap hbmDest = CreateCompatibleBitmap(hdcClient, imageWidth, imageHeight); hbmOldDest = (HBITMAP)SelectObject(hdcDest, hbmDest); // determine border/direction changes, if any if( nbmpX < 1) nDeltaX = nXDsp; else if( nbmpX > (rWndGBL.right-imageWidth) ) nDeltaX = -(nXDsp); if( nbmpY < 1) nDeltaY = nYDsp; else if( nbmpY > (rWndGBL.bottom-imageHeight) ) nDeltaY = -(nYDsp); nbmpX += nDeltaX; nbmpY += nDeltaY; // get new positions // copy clean image of saved screen BitBlt(hdcSave, 0, 0, imageWidth, imageHeight, hdcOrg, 0, 0, SRCCOPY); // set up incremental new screen image rBmp.left = nbmpX; rBmp.top = nbmpY; rBmp.right = nbmpX + imageWidth - 1; rBmp.bottom = nbmpY + imageHeight - 1; rRst.left = nPrevX; rRst.top = nPrevY; rRst.right = nPrevX + imageWidth - 1; rRst.bottom = nPrevY + imageHeight - 1; // get overlapping rect differentials DiffORects(&rRst, &rBmp, &rUpper, &rLower); // get clean incremental rects from screen BitBlt(hdcOrg, rUpper.left - nbmpX, 0, (rUpper.right - rUpper.left + 1), (rUpper.bottom - rUpper.top + 1), hdcClient, rUpper.left, rUpper.top, SRCCOPY); BitBlt(hdcOrg, rLower.left - nbmpX, rLower.top - nbmpY, (rLower.right - rLower.left + 1), (rLower.bottom - rLower.top + 1), hdcClient, rLower.left, rLower.top, SRCCOPY); // copy overlapping portion from previously saved screen image if(nbmpX < nPrevX) nToX = nXDsp; // if moving left else nFromX = nXDsp; if(nbmpY < nPrevY) nToY = nYDsp; // if moving up else nFromY = nYDsp; BitBlt(hdcOrg, nToX, nToY, imageWidth - nXDsp, imageHeight - nYDsp, hdcSave, nFromX, nFromY, SRCCOPY); // set up initial destination image BitBlt(hdcDest, 0, 0, imageWidth, imageHeight, hdcOrg, 0, 0, SRCCOPY); // set up mask reusing hdcSave SelectObject(hdcSave, hbm[4]); // AND the mask BitBlt(hdcDest, 0, 0, imageWidth, imageHeight, hdcSave, 0, 0, SRCAND); // set up to put the bitmap SelectObject(hdcSave, hbm[btrblFlag]); // XOR the bitmap BitBlt(hdcDest, 0, 0, imageWidth, imageHeight, hdcSave, 0, 0, SRCINVERT); // restore incremental rects from old screen image // use same rects with reverse order in fn call DiffORects(&rBmp, &rRst, &rUpper, &rLower); // put clean rects to screen SelectObject(hdcSave, hbmSave); // reselect hbmSave BitBlt(hdcClient, rUpper.left, rUpper.top, (rUpper.right - rUpper.left + 1), (rUpper.bottom - rUpper.top + 1), hdcSave, rUpper.left - nPrevX, 0, SRCCOPY); BitBlt(hdcClient, rLower.left, rLower.top, (rLower.right - rLower.left + 1), (rLower.bottom - rLower.top + 1), hdcSave, rLower.left - nPrevX, (rLower.top - nPrevY), SRCCOPY); // PUT image to the screen BitBlt(hdcClient, nbmpX, nbmpY, imageWidth, imageHeight, hdcDest, 0, 0, SRCCOPY); if(!bSingleImage) { // change top, right, bottom, left every other display if(++ncounter >= 2) { // set bitmap index btrblFlag++; ncounter = 0; if(btrblFlag > 3) btrblFlag = 0; // reset flag/index } } // clean up memory DC's, work bitmaps SelectObject(hdcDest, hbmOldDest); DeleteDC(hdcDest); DeleteObject(hbmDest); SelectObject( hdcSave, hbmOldSave); DeleteDC( hdcSave); DeleteObject( hbmSave ); ReleaseDC( hwnd, hdcClient); } // end Animate #pragma argsused LONG PASCAL ChangeDisplacement( STDWINARGS ) { // inc/dec x & y displacement by 1 to min/max if(wParam == IDM_DDEC) // decrement rqs { if(nXDsp < 1 || nYDsp < 1) // 0 pix min nXDsp = nYDsp = 1; else nXDsp -= 1, nYDsp -= 1; } else // must have been increment rqs { if(nXDsp > 49 || nYDsp > 49) // 50 pix max nXDsp = nYDsp = 50; else nXDsp += 1, nYDsp += 1; } // reset delta values nDeltaX = (nDeltaX < 1) ? -nXDsp : nXDsp; nDeltaY = (nDeltaY < 1) ? -nYDsp : nYDsp; InvalidateRect(hwnd, &rWndInv, TRUE); // redraw new values return (0L); } // end ChangeDisplacement #pragma argsused LONG PASCAL ChangeTimer( STDWINARGS ) { // inc or dec timer interval by 50 if(bBmpIsSelected) { KillTimer(hwnd, IMAGE_TIMER); bBypassTimer = 1; // set bypass flag } if(wParam == IDM_TDEC) // decrement rqs { // 50 ms min if(nTimerValue < 100) nTimerValue = 50; else nTimerValue -= 50; } else // must have been increment rqs { // 1000 ms max if(nTimerValue > 950) nTimerValue = 1000; else nTimerValue += 50; } InvalidateRect(hwnd, &rWndInv, TRUE); // redraw new values if(bBmpIsSelected) { if( !GetNewTimer(hwnd) ) // if can't get timer, end movement UnSelectBmp(hwnd, msg, wParam, lParam); bBypassTimer = 0; // reset bypass flag } return (0L); } // end ChangeTimer void DiffORects(RECT *rBmp, RECT *rRst, RECT *rUpper, RECT *rLower) { // finds 2 rects portion of rRST outside overlap in // client coords, returns rects in rUpper and rLower rUpper->top = rRst->top; rLower->bottom = rRst->bottom; // always if(rBmp->top > rRst->top) rUpper->bottom = rBmp->top - 1, rLower->top = rBmp->top; else rUpper->bottom = rBmp->bottom, rLower->top = rBmp->bottom + 1; if(rBmp->left > rRst->left) { rUpper->left = rLower->left = rRst->left; if(rBmp->top > rRst->top) rUpper->right = rRst->right, rLower->right = rBmp->left - 1; else // rBmp->top < rRst->top rUpper->right = rBmp->left - 1, rLower->right = rRst->right; } else // rBmp->left is same or less than rRst->left { rUpper->right = rLower->right = rRst->right; if(rBmp->top > rRst->top) rUpper->left = rRst->left, rLower->left = rBmp->right + 1; else rUpper->left = rBmp->right + 1, rLower->left = rRst->left; } return; } // end DiffORects #pragma argsused LONG PASCAL DoCmd( STDWINARGS ) { // Handle WM_COMMAND msg, check wParam contents for(ndxGBL = 0; ndxGBL < icmdmsgfndim; ndxGBL++) { if(wParam == cmdmsgfn[ndxGBL].msgid) // if match, call Fn return ((*cmdmsgfn[ndxGBL].pfnProc)(hwnd, msg, wParam, lParam)); } if( wParam == IDM_EXIT ) // if not in array { // happens once; don't waste searches or fn SendMessage(hwnd, WM_CLOSE, 0, 0L); return(0L); } else return ( DefWindowProc(hwnd, msg, wParam, lParam) ); } // end DoCmd #pragma argsused LONG PASCAL // Handle WM_ERASEBKGND message DoEraseBkgnd( STDWINARGS ) { // chk bypass - SOMETIMES get this msg before AND during // WM_PAINT/BeginPaint. Set flag here, DoPaint clears it. if(bBmpIsSelected && !bBypassTimer) { HDC hdc; // WGO-- BitBlt captures image from overlapping screen // when bmp is underneath. So work around... If this // restore is erroneous, ERASE gets it in invalidated // region. If valid, the restoration remains. // Done here due to timing and because BeginPaint // CLIPS to the invalid rectangle (or smaller) // so our work would be for nothing in that routine. bBypassTimer = 1; // set bypass timer routine flag. hdc = GetDC( hwnd ); // restore saved screen BitBlt(hdc, nbmpX, nbmpY, imageWidth, imageHeight, hdcOrg, 0, 0, SRCCOPY); ReleaseDC( hwnd, hdc); } return ( DefWindowProc(hwnd, msg, wParam, lParam) ); } // end DoEraseBkgnd #pragma argsused LONG PASCAL // Handle IDM_FONT Menu message DoFont( STDWINARGS ) { CHOOSEFONT cfStruct; LOGFONT lfStruct = lfFontSel; cfStruct.lStructSize = sizeof(CHOOSEFONT); cfStruct.hwndOwner = hwnd; cfStruct.lpLogFont = &lfStruct; cfStruct.Flags = CF_SCREENFONTS | CF_EFFECTS | CF_INITTOLOGFONTSTRUCT; cfStruct.rgbColors = crrgbColors; if( ChooseFont(&cfStruct) ) { lfFontSel = lfStruct; lfFontSel.lfEscapement = 250; crrgbColors = cfStruct.rgbColors; // set selected color // redraw with selected font InvalidateRect(hwnd, &rWndGBL, TRUE); } return (0L); } // end DoFont #pragma argsused LONG PASCAL // Handle WM_PAINT message DoPaint( STDWINARGS ) { HDC hdc; HFONT hFont = NULL, hFontPrev = NULL; PAINTSTRUCT ps; char szOutString[40]; int nlength; hdc = BeginPaint(hwnd, &ps); // draw timer and displacement values nlength = wsprintf(szOutString, "Timer Value is %d.", nTimerValue); ExtTextOut(hdc, 5, rWndGBL.bottom - 60, 0, NULL, szOutString, nlength, NULL); nlength = wsprintf(szOutString, "X Displacement is %d.", nXDsp); ExtTextOut(hdc, 5, rWndGBL.bottom - 40, 0, NULL, szOutString, nlength, NULL); nlength = wsprintf(szOutString, "Y Displacement is %d.", nYDsp); ExtTextOut(hdc, 5, rWndGBL.bottom - 20, 0, NULL, szOutString, nlength, NULL); SetBkMode(hdc, TRANSPARENT); // create and select the font hFont = CreateFontIndirect(&lfFontSel); hFontPrev = (HFONT)SelectObject(hdc, hFont); SetTextAlign(hdc, TA_BASELINE | TA_CENTER); // create shadow by drawing in black first, one over ExtTextOut(hdc, (rWndGBL.right >> 1) + 1, (rWndGBL.bottom >> 1) + 1, 0, NULL, "U S A", 5, NULL); SetTextColor(hdc, crrgbColors); // now in selected color ExtTextOut(hdc, rWndGBL.right >> 1, (rWndGBL.bottom >> 1), 0, NULL, "U S A", 5, NULL); if(hFontPrev) // delete the font { SelectObject(hdc, hFontPrev); DeleteObject(hFont); } if(bBmpIsSelected) // get fresh screen image BitBlt(hdcOrg, 0, 0, imageWidth, imageHeight, hdc, nbmpX, nbmpY, SRCCOPY); EndPaint( hwnd, &ps); bBypassTimer = 0; // reset bypass flag return(0L); } // end DoPaint #pragma argsused LONG PASCAL DoSize( STDWINARGS ) { // Handle Wm_SIZE message if( rWndGBL.right == LOWORD(lParam) && rWndGBL.bottom == HIWORD(lParam) ) return 0L; // if same size, exit // otherwise get new size rWndGBL.right = LOWORD(lParam); rWndGBL.bottom = HIWORD(lParam); // update invalid text rect rWndInv.top = rWndGBL.bottom - 60; rWndInv.bottom = rWndGBL.bottom; if(bBypassTimer == 255) { // if first pass, exit after setting size bBypassTimer = 0; return 0L; } if(wParam == SIZE_MAXIMIZED || wParam == SIZE_RESTORED) { // redraw on new size if(bBmpIsSelected != 2) InvalidateRect(hwnd, &rWndGBL, TRUE); else // restart if was icon and movement sel SelectBmp(hwnd, msg, wParam, lParam); return 0L; } if(wParam == SIZE_MINIMIZED && bBmpIsSelected == 1) { // If going to icon, stop movement, set sel to 2 for restore. UnSelectBmp(hwnd, msg, wParam, lParam); bBmpIsSelected = 2; // set Flag for size return } return (0L); } // end DoSize BOOL GetNewTimer(HWND hwnd) { // set up timer while( !SetTimer(hwnd, IMAGE_TIMER, nTimerValue, NULL) ) { if( IDCANCEL == MessageBox( hwnd, "Can't get timer for animation.", "Move Bitmap Information", MB_ICONEXCLAMATION | MB_RETRYCANCEL) ) return (FALSE); } return (TRUE); // got timer } // end GetNewTimer #pragma argsused LONG PASCAL // Handle IDM_SELECTBMP Menu message SelectBmp( STDWINARGS ) { HDC hdcClient; BITMAP bm; // Bitmap info HMENU hmenuMain = GetMenu(hwnd); if( !GetNewTimer(hwnd) ) return 0L; // return if can't obtain timer if(wParam == IDM_SELECTBMP) // chg menu select to stop ModifyMenu(hmenuMain, IDM_SELECTBMP, MF_BYCOMMAND, IDM_UNSELECTBMP, "Stop Movement"); // load bitmaps from resource file hbm[0] = LoadBitmap(hInstGBL, "image1"); // top hbm[1] = LoadBitmap(hInstGBL, "image2"); // right hbm[2] = LoadBitmap(hInstGBL, "image3"); // bottom hbm[3] = LoadBitmap(hInstGBL, "image4"); // left hbm[4] = LoadBitmap(hInstGBL, "imagebkg"); // mask GetObject(hbm[0], sizeof(bm), (LPSTR)&bm); imageWidth = bm.bmWidth; imageHeight = bm.bmHeight; btrblFlag = 1; // set top as first image hdcClient = GetDC( hwnd); hdcOrg = CreateCompatibleDC( hdcClient); hbmOrg = CreateCompatibleBitmap(hdcClient, imageWidth, imageHeight); hbmOldOrg = (HBITMAP)SelectObject(hdcOrg, hbmOrg); // save initial screen image BitBlt(hdcOrg, 0, 0, imageWidth, imageHeight, hdcClient, nbmpX, nbmpY, SRCCOPY); // clean up ReleaseDC(hwnd, hdcClient); bBmpIsSelected = 1; return (0L); } // end SelectBmp #pragma argsused LONG PASCAL // Handle IDM_SINGLEIMAGE command SingleImage( STDWINARGS ) { // Toggle Single Image/Multiple image switch int nChkType; HMENU hmenu = GetMenu(hwnd); bSingleImage = !bSingleImage; nChkType = (bSingleImage) ? MF_CHECKED : MF_UNCHECKED; CheckMenuItem(hmenu, IDM_SINGLEIMAGE, MF_BYCOMMAND | nChkType); return(0L); } // end SingleImage #pragma argsused LONG PASCAL // Handle IDM_UNSELECTBMP UnSelectBmp( STDWINARGS ) { HMENU hmenu = GetMenu(hwnd); HDC hdcClient; KillTimer(hwnd, IMAGE_TIMER); bBmpIsSelected = 0; // restore the screen hdcClient = GetDC( hwnd); BitBlt(hdcClient, nbmpX, nbmpY, imageWidth, imageHeight, hdcOrg, 0, 0, SRCCOPY); ReleaseDC( hwnd, hdcClient); if(wParam == IDM_UNSELECTBMP) { // if menu call, redraw: image may be under menu InvalidateRect(hwnd, &rWndGBL, TRUE); // chg menu select to start ModifyMenu(hmenu, IDM_UNSELECTBMP, MF_BYCOMMAND, IDM_SELECTBMP, "Start Movement"); } for(ndxGBL = 0; ndxGBL < 5; ndxGBL++) // delete the bitmaps DeleteObject( hbm[ndxGBL] ); SelectObject(hdcOrg, hbmOldOrg); // deselects hbmOrg DeleteDC(hdcOrg); DeleteObject(hbmOrg); return (0L); } // end UnSelectBmp
Copyright © 1994, Dr. Dobb's Journal