The term bitmap is frequently misunderstood by new Windows programmers. For most people, a bitmap is a picture that can be displayed on the screen. While this definition is generally true, there are actually two main types of bitmaps used with Windows applications. The first, called device-independent bitmaps (DIBs), are found in picture files with a .BMP file extension and are the type of bitmap with which most people are familiar. You'll learn about DIBs in the next chapter.
The second type of bitmap, called a device-dependent bitmap (DDB), resides only in the computer's memory and is usually not a picture per se but rather some sort of image that a Windows application needs to create its display. In this chapter, you learn about DDBs and how they're used to build an application's display.
As I just said, DIBs are the picture files that most people think of as bitmaps. DIBs are device independent because their file includes the color information needed to reproduce the picture on other devices. DDBs, on the other hand, don't include color tables, so you'll never (well, maybe I shouldn't say never) find DDBs saved to files. Instead, these types of images usually are created directly in the computer's memory and disappear when the application that created them terminates.
Because of their nature, DDBs are more utilitarian than DIBs. That is, the Windows programmer uses DDBs as tools for creating an application's display rather than displaying DDBs simply as pictures. Think of a Windows paint application. Often, a paint application enables the user to copy and paste a portion of the display. The user might, for example, copy the image of a tree and paste it down in various places on the display to create a forest. The tree image is a bitmap in the computer's memory. It's unlikely that the tree image will ever be saved to disk. On the other hand, the entire forest picture probably will be saved to disk as a DIB.
Another popular way to use DDBs (hereafter called bitmaps) is to use them to represent an application's entire display area in memory. The application makes changes to its display by drawing on the bitmap in memory and then copying the bitmap to the application's window. Using a bitmap in this way, an application can quickly update its display whenever it needs to without having to redraw the data from scratch. In the following section, you'll create a Windows application that handles its display in exactly this way.
As mentioned, one of the most common uses for bitmaps is to store an application's display. You'll now create the Bitmap application, which uses a bitmap to store its window's contents. Whenever the Window needs a full repainting, such as when being uncovered from beneath another window, the application copies the bitmap from memory to the application's window. To create the Bitmap application, perform the following steps:
The complete source code and executable file for the Bitmap application can be found in the CHAP16\Bitmap directory of this book's CD-ROM.
1. Start a new AppWizard project workspace called Bitmap, as shown in Figure 16.1.
Start an AppWizard project workspace called Bitmap.
2. Give the new project the following settings in the AppWizard dialog boxes. When you're finished, the New Project Information dialog box should look like Figure 16.2.
Step 1: Single document
Step 2: Default settings
Step 3: Default settings
Step 4: Turn off all options except 3D Controls
Step 5: Default settings
Step 6: Default settings
These are the AppWizard settings for the Bitmap project.
3. Using the resource editor, remove the application's Edit menu. Also remove all items from the File menu except Exit, as shown in Figure 16.3.
Use the resource editor to edit the application's menus.
4. Delete all accelerators from the application's resources.
5. Use ClassWizard to associate the WM_CREATE Windows message with the OnCreate() message response function in the view class, as shown in Figure 16.4.
Add the OnCreate() function to the view class.
6. Click the
Edit Code button and then add the lines shown in Listing 16.1 to the new OnCreate() function, right after the TODO: Add your specialized creation code here comment.Listing 16.1 LST16_01.CPP Code for the OnCreate() Function
// Create a bitmap compatible with the window's DC. CClientDC clientDC(this); m_pBitmap = new CBitmap; m_pBitmap->CreateCompatibleBitmap(&clientDC, 800, 600); // Create a memory DC in which to store the bitmap. CDC memDC; memDC.CreateCompatibleDC(&clientDC); memDC.SelectObject(m_pBitmap); // Fill the bitmap with white. CBrush brush(RGB(255,255,255)); memDC.FillRect(CRect(0,0,799,599), &brush); // Draw the grid on the screen and bitmap. DrawGrid(&memDC);7. Use ClassWizard to associate the WM_LBUTTONDOWN Windows message with the OnLButtonDown() message response function in the view class, as shown in Figure 16.5.
Add the OnLButtonDown() function to the view class.
8. Click the
Edit Code button and then add the lines shown in Listing 16.2 to the new OnLButtonDown() function, right after the TODO: Add your message handler code here and/or call default comment.Listing 16.2 LST16_02.CPP Code for the OnLButtonDown() Function
// If the user clicked in the grid... if (ClickInsideGrid(point)) { // Calculate the square's column and row. UINT col = ((point.x - OFFSET) / SQUARESIZE) * SQUARESIZE + OFFSET; UINT row = ((point.y - OFFSET) / SQUARESIZE) * SQUARESIZE + OFFSET; // Draw the new wall square on the screen. DrawSquare(col, row); }9. Add the functions shown in Listing 16.3 to the end of the BitmapView.cpp file.
Listing 16.3 LST16_03.CPP New Functions for the CBitmapView Class
void CBitmapView::DrawGrid(CDC *pDC) { // Draw the grid's vertical lines. for (int x=0; x<=COLSIZE; ++x) { pDC->MoveTo(SQUARESIZE*x+OFFSET, OFFSET); pDC->LineTo(SQUARESIZE*x+OFFSET, SQUARESIZE*COLSIZE+OFFSET); } // Draw the grid's horizontal lines. for (int y=0; y<=ROWSIZE; ++y) { pDC->MoveTo(OFFSET, SQUARESIZE*y+OFFSET); pDC->LineTo(SQUARESIZE*ROWSIZE+OFFSET, SQUARESIZE*y+OFFSET); } } BOOL CBitmapView::ClickInsideGrid(CPoint point) { // Calculate width and height of grid in pixels. int gridSize = SQUARESIZE * ROWSIZE; // If the user clicked within the grid, return TRUE. if ((point.x < gridSize + OFFSET) && (point.x > OFFSET) && (point.y < gridSize + OFFSET) && (point.y > OFFSET)) return TRUE; // Otherwise, return FALSE. return FALSE; } void CBitmapView::DrawSquare(UINT x, UINT y) { // Create a DC for the window and a memory // DC compatible with the window DC. CClientDC clientDC(this); CDC memDC; memDC.CreateCompatibleDC(&clientDC); // Select the grid bitmap into the memory DC. memDC.SelectObject(m_pBitmap); // Create a brush for the wall from the color array. CBrush* pBrush = new CBrush(RGB(255,0,0)); // Select the brush into both DCs. CBrush* oldBrush1 = memDC.SelectObject(pBrush); CBrush* oldBrush2 = clientDC.SelectObject(pBrush); // Draw the square in both DCs. memDC.Rectangle(x, y, x + SQUARESIZE + 1, y + SQUARESIZE + 1); clientDC.Rectangle(x, y, x + SQUARESIZE + 1, y + SQUARESIZE + 1); // Restore DCs and delete the brush. memDC.SelectObject(oldBrush1); clientDC.SelectObject(oldBrush2); delete pBrush; }10. Add the following line to the CBitmapView class's destructor:
delete m_pBitmap;
This line deletes the bitmap that is created in the OnCreate() function.
11. Add the lines shown in Listing 16.4 to the OnDraw() function, right after the TODO: Add draw code for native data here comment.
Listing 16.4 LST16_04.CPP Code for the OnDraw() Function
// Create a memory DC that's compatible // with the window's DC. CDC memDC; memDC.CreateCompatibleDC(pDC); // Display the bitmap in the window. memDC.SelectObject(m_pBitmap); pDC->BitBlt(0, 0, 800, 600, &memDC, 0, 0, SRCCOPY);12. Load the BitmapView.h file and then add the following lines to the class's Attributes section, right after the line CBitmapDoc* GetDocument().
protected: CBitmap* m_pBitmap;13. Add the following lines to the class's Implementation section, right after the protected keyword.
void DrawGrid(CDC* pDC); BOOL ClickInsideGrid(CPoint point); void DrawSquare(UINT x, UINT y);14. Add the lines shown in Listing 16.5 to the top of the BitmapView.h file, right before the class's declaration.
Listing 16.5 LST16_05.CPP Constant Declarations for the CBitmapView Class
const SQUARESIZE = 12; const ROWSIZE = 32; const COLSIZE = 32; const OFFSET = 2;These lines define constants for some frequently used values in the program. SQUARESIZE is the size in pixels of each square in the grid; ROWSIZE is the number of rows in the grid; COLSIZE is the number of columns in the grid; and OFFSET is the number of pixels from the top and left that the grid is positioned in the main window.
15. Load the MainFrm.cpp file and then add the following lines to the PreCreateWindow() function, right before the function's return line:
// Set the window's size. cs.cx = 402; cs.cy = 440;These lines set the width and height of the application's main window.
You've now completed the Bitmap application. To compile the application, select Developer Studio's Build, Build command. You can then run the
program by selecting the Build, Execute command. When you do, the application's main window appears (Figure 16.6).
The Bitmap application sports a grid of squares in its display.
Click anywhere in the onscreen grid, and the selected square turns red. Go ahead and paint some red squares (Figure 16.7), and then minimize and restore the window. When the window restores, it instantly redraws its display. Thanks to the bitmap, the program doesn't need to redraw each individual square or even keep track of where those squares are located. (Of course, if you want to save your work to disk, you have to keep track of which squares are filled in.)
By clicking with the mouse, you can change the grid squares from white to red.
Now that you've had a chance to experiment with the Bitmap application, you probably want to take a look under the hood to see how this bitmap stuff really works. In the following sections, you'll examine each of the key functions in the Bitmap application. When you're through, you should have a solid understanding of how to use bitmaps in your own applications.
Whenever a new window is created, Windows sends the application a WM_CREATE message. Because the window has a valid handle when WM_CREATE is sent, the function OnCreate() (which responds to the WM_CREATE message in an MFC program) is a good place to do initialization that requires a valid window. In the Bitmap application, the program uses OnCreate() to set up the bitmap that holds the window's display.
To update Bitmap's window as quickly as possible when it needs to be redrawn, the program stores a copy of the window's image in a bitmap in memory. For the program to display this bitmap in the window's client area, the bitmap must be selected into a device context that's compatible with the window's device context. So the first task in OnCreate() is to create a CClientDC object for the window:
CClientDC clientDC(this);
The CClientDC constructor requires a pointer to the window for which the DC is being created. In this case, you just use the this pointer. Then the program constructs a CBitmap object and uses it to create a bitmap that's compatible with the window's DC:
m_pBitmap = new CBitmap; m_pBitmap->CreateCompatibleBitmap(&clientDC, 800, 600);
The CBitmap class's member function, CreateCompatibleBitmap(), takes as arguments the address of the device context with which the bitmap must be compatible and the width and height of the bitmap. CreateCompatibleBitmap() is an MFC version of a Windows API function of the same name.
Now, because this bitmap must be associated with a DC before you can draw on it, the program creates a memory DC in which to hold the bitmap:
CDC memDC; memDC.CreateCompatibleDC(&clientDC);
The memory DC can be an object of the CDC class. Calling the class's CreateCompatibleDC() function (which is an MFC version of a Windows API function of the same name) ensures that the memory DC is compatible with the client window's DC. The function's single argument is the address of the DC with which the new DC should be compatible.
Phrases like "create a compatible DC" sound very technical and can be confusing which is unfortunate, because creating a compatible DC is really a simple process. As you know, a device context is simply a structure that describes the attributes for a device. These attributes include things such as pen color, a drawing surface of a specific size, a default font, and so on. Creating a compatible DC simply means creating a DC that has the same attributes as another DC.
Imagine that you're sitting at a table with a sheet of 81/2-[ts]-11-inch typing paper, a red pencil, and a stencil for drawing Old English lettering. You can think of these drawing implements as a device context. Now suppose that a friend shows up who wants to help you with your project, so you give her a set of supplies just like yours. Giving your friend a second set of identical drawing supplies is similar to what happens when you create a compatible DC.
Next, because you want to draw on the bitmap that you created, you must associate the bitmap with the DC (called "selecting the bitmap into the DC"):
memDC.SelectObject(m_pBitmap);
The SelectObject() function, the CDC class's version of a Windows API function of the same name, takes care of this task. Its single argument is a pointer to the bitmap.
After selecting the bitmap into the DC, the program has an in-memory surface on which it can draw the image that will be transferred to the window's display. The first thing that the program must do to prepare this surface is to paint it entirely white:
CBrush brush(RGB(255,255,255)); memDC.FillRect(CRect(0,0,799,599), &brush);
Here, the program creates a white brush and uses the brush in a call to the FillRect() function. To construct a CBrush object, you need only supply the RGB values for the brush's colors. These values are the intensity of the red, green, and blue color components, respectively, which can be any value from 0 to 255. The higher the color value, the brighter the color component.
The FillRect() function, which is the CDC class's version of a Windows API function, takes two arguments: a CRect object containing the position and size of the rectangle to fill and the address of the brush with which to fill the rectangle.
At this point, the bitmap is a white rectangle. The next thing to do is to draw the lines that make up the grid. This line drawing is performed by a call to the CBitmapView class's DrawGrid() function:
DrawGrid(&memDC);
The DrawGrid() function contains two for loops. The first loop draws the vertical lines, as shown in Listing 16.6.
Listing 16.6 LST16_06.CPP Drawing the Grid's Vertical Lines
for (int x=0; x<=COLSIZE; ++x) { pDC->MoveTo(SQUARESIZE*x+OFFSET, OFFSET); pDC->LineTo(SQUARESIZE*x+OFFSET, SQUARESIZE*COLSIZE+OFFSET); }
As you can see, the line drawing is accomplished by calling the MoveTo() and LineTo() functions through the device-context pointer that was passed as one of DrawGrid()'s parameters. The MoveTo() function positions the drawing cursor at the X,Y coordinates given as the function's arguments. The LineTo() function then draws a line from the cursor position to the X,Y coordinates given as the LineTo() function's arguments.
As Listing 16.7 shows, the program draws the grid's horizontal lines in much the same way as it draws the vertical lines.
Listing 16.7 LST16_07.CPP Drawing the Grid's Horizontal Lines
for (int y=0; y<=ROWSIZE; ++y) { pDC->MoveTo(OFFSET, SQUARESIZE*y+OFFSET); pDC->LineTo(SQUARESIZE*ROWSIZE+OFFSET, SQUARESIZE*y+OFFSET); }
All of the bitmap-finagling that you've done in OnCreate() only creates an image in memory. That image still doesn't appear in the Bitmap application's window. As you know, in an AppWizard-generated application, it's the OnDraw() function that is charged with actually updating a window's display.
In the OnDraw() function, the program first creates a memory DC that's compatible with the DC passed to the function:
CDC memDC; memDC.CreateCompatibleDC(pDC);
The program then selects the bitmap (which contains the image to be drawn to the window) into the memory DC:
memDC.SelectObject(m_pBitmap);
Finally, a quick call to the window DC's BitBlt() function transfers the bitmap's image from the memory DC to the window's client area:
pDC->BitBlt(0, 0, 800, 600, &memDC, 0, 0, SRCCOPY);
BitBlt(), which is the CDC class's version of a Windows API function, takes eight arguments: the X,Y coordinates of the bitmap's destination rectangle; the width and height of the destination rectangle; a pointer to the source DC (the one containing the bitmap); the X,Y coordinates of the rectangle in the bitmap to copy; and a flag indicating how the source rectangle should be combined with the destination rectangle. The flag SRCCOPY means that the bitmap will completely overwrite any other image in the window.
As you just learned, the OnDraw() function takes care of repainting the application's display when the application receives a WM_PAINT message. However, what about when the user clicks a square? When this happens, the application must change the square's color from white to red. One way to accomplish this task is to draw the red square on the computer's in-memory bitmap and then transfer the bitmap to the display.
Another way is to draw the square directly on the screen, without bothering with OnDraw(), and also to update the bitmap in memory. In fact, every time you draw a red square in the Bitmap application's grid, that square gets drawn twice once directly on the screen and once on the bitmap in memory. Whenever you want to place a new red square in the grid, you left-click the square. This causes the program's OnLButtonDown() function to be called.
OnLButtonDown() first checks whether the user's mouse click was inside the grid area:
if (ClickInsideGrid(point))
The ClickInsideGrid() function returns TRUE if the mouse pointer was over the grid and returns FALSE if the mouse pointer was over some other part of the window. If the user's mouse click wasn't over the grid, there's nothing to do, because you're not allowed to place a red square anywhere except within the boundaries of the grid.
If the mouse click is inside the grid, OnLButtonDown() calculates the square's coordinates, as shown in Listing 16.8.
Listing 16.8 LST16_08.CPP Calculating the Square's Location in the Grid
UINT col = ((point.x - OFFSET) / SQUARESIZE) * SQUARESIZE + OFFSET; UINT row = ((point.y - OFFSET) / SQUARESIZE) * SQUARESIZE + OFFSET;
The point data object (an instance of the CPoint class) is passed as a parameter to OnLButtonDown(). point holds the coordinates of the user's mouse click.
Finally, in OnLButtonDown(), a call to the DrawSquare() function draws a rectangle in the grid square located at col,row:
DrawSquare(col, row);
In the Bitmap application, it's the DrawSquare() function that actually places a red square on the grid. To avoid a sloppy screen update, however, DrawSquare() draws the square directly on the screen, as well as on the in-memory bitmap, giving the user an instant response to his or her mouse click.
First, DrawSquare() gets a device context for the view window, and then it creates a memory DC that's compatible with the window's DC:
CClientDC clientDC(this); CDC memDC; memDC.CreateCompatibleDC(&clientDC);
DrawSquare() then selects the bitmap that is holding the screen's image into the memory DC, where the program can draw on it:
memDC.SelectObject(m_pBitmap);
Next, the function needs a red brush with which to draw the square:
CBrush* pBrush = new CBrush(RGB(255,0,0));
A call to both of the DC's SelectObject() member functions associates the new brush with both DCs:
CBrush* oldBrush1 = memDC.SelectObject(pBrush); CBrush* oldBrush2 = clientDC.SelectObject(pBrush);
DrawSquare() can then call both DC objects' Rectangle() member functions to draw the rectangle on both the screen and on the in-memory bitmap, as shown in Listing 16.9.
Listing 16.9 LST16_09.CPP Drawing the Red Square on the Screen and on the Bitmap
memDC.Rectangle(x, y, x + SQUARESIZE + 1, y + SQUARESIZE + 1); clientDC.Rectangle(x, y, x + SQUARESIZE + 1, y + SQUARESIZE + 1);
Keeping the bitmap updated this way ensures that when OnDraw() needs to repaint the window's client area, the bitmap image will contain the correct image.
Finally, the function selects the old brushes back into the window and memory DCs, freeing the brush used to draw on the bitmap. This brush is then deleted:
memDC.SelectObject(oldBrush1); clientDC.SelectObject(oldBrush2); delete pBrush;
Now that you know how to handle DDBs, you're ready to move forward into the much more complicated world of DIBs, which you do in the next chapter.