Special Edition Using Visual C++ 6

Previous chapterNext chapterContents


- 5 -

Drawing on the Screen


Understanding Device Contexts

Most applications need to display some type of data in their windows. You'd think that, because Windows is a device-independent operating system, creating window displays would be easier than luring a kitten with a saucer of milk. However, it's exactly Windows' device independence that places a little extra burden on a programmer's shoulders. Because you can never know in advance exactly what type of devices may be connected to a user's system, you can't make many assumptions about display capabilities. Functions that draw to the screen must do so indirectly through something called a device context (DC).

Although device independence forces you, the programmer, to deal with data displays indirectly, it helps you by ensuring that your programs run on all popular devices. In most cases, Windows handles devices for you through the device drivers that users have installed on the system. These device drivers intercept the data that the application needs to display and then translates the data appropriately for the device on which it will appear, whether that's a screen, a printer, or some other output device.

To understand how all this device independence works, imagine an art teacher trying to design a course of study appropriate for all types of artists. The teacher creates a course outline that stipulates the subject of a project, the suggested colors to be used, the dimensions of the finished project, and so on. What the teacher doesn't stipulate is the surface on which the project will be painted or the materials needed to paint on that surface. In other words, the teacher stipulates only general characteristics. The details of how these characteristics are applied to the finished project are left to each specific artist.

For example, an artist using oil paints will choose canvas as his drawing surface and oil paints, in the colors suggested by the instructor, as the paint. On the other hand, an artist using watercolors will select watercolor paper and will, of course, use watercolors instead of oils for paint. Finally, the charcoal artist will select the appropriate drawing surface for charcoal and will use a single color.

The instructor in this scenario is much like a Windows programmer. The programmer has no idea who may eventually use the program and what kind of system that user may have. The programmer can recommend the colors in which data should be displayed and the coordinates at which the data should appear, for example, but it's the device driver--the Windows artist--who ultimately decides how the data appears.

A system with a VGA monitor may display data with fewer colors than a system with a Super VGA monitor. Likewise, a system with a monochrome monitor displays the data in only a single color. High-resolution monitors can display more data than lower-resolution monitors. The device drivers, much like the artists in the imaginary art school, must take the display requirements and fine-tune them to the device on which the data will actually appear. And it's a data structure known as a device context that links the application to the device's driver.

A device context (DC) is little more than a data structure that keeps track of the attributes of a window's drawing surface. These attributes include the currently selected pen, brush, and font that will be used to draw onscreen. Unlike an artist, who can have many brushes and pens with which to work, a DC can use only a single pen, brush, or font at a time. If you want to use a pen that draws wider lines, for example, you need to create the new pen and then replace the DC's old pen with the new one. Similarly, if you want to fill shapes with a red brush, you must create the brush and select it into the DC, which is how Windows programmers describe replacing a tool in a DC.

A window's client area is a versatile surface that can display anything a Windows program can draw. The client area can display any type of data because everything displayed in a window--whether it be text, spreadsheet data, a bitmap, or any other type of data--is displayed graphically. MFC helps you display data by encapsulating Windows' GDI functions and objects into its DC classes.

Introducing the Paint1 Application

In this chapter, you will build the Paint1 application, which demonstrates fonts, pens, and brushes. Paint1 will use the document/view paradigm discussed in Chapter 4, "Documents and Views," and the view will handle displaying the data. When run, the application will display text in several different fonts. When users click the application, it displays lines drawn with several different pens. After another click, it displays boxes filled with a variety of brushes.

The first step in creating Paint1 is to build an empty shell with AppWizard, as first discussed in Chapter 1, "Building Your First Windows Application." Choose File, New, and select the Projects tab. As shown in Figure 5.1, fill in the project name as Paint1 and fill in an appropriate directory for the project files. Make sure that MFC AppWizard (exe) is selected. Click OK.

FIG. 5.1 Start an AppWizard project workspace called Paint1.

Move through the AppWizard dialog boxes, change the settings to match those in the list that follows, and then click Next to move to the next step.

Step 1: Select Single Document.

Step 2: Use default settings.

Step 3: Use default settings.

Step 4: Deselect all check boxes.

Step 5: Use default settings.

Step 6: Use default settings.

After you click Finish on the last step, the New Project Information box should resemble Figure 5.2. Click OK to create the project.

FIG. 5.2 The starter application for Paint1 is very simple.

Now that you have a starter application, it's time to add code to make it demonstrate some ways an MFC program can display data onscreen. By the time you get to the end of this chapter, the words display context won't make you scratch your head in perplexity.


NOTE: Your starter application has menus, but you will ignore them completely. It would be quite a bit of work to remove them; just pretend they aren't there. n

Building the Paint1 Application

To build the Paint1 application, you first need to understand how painting and drawing work in an MFC program. Then you can set up the skeleton code to handle user clicks and the three different kinds of display. Finally, you'll fill in the code for each kind of display in turn.

Painting in an MFC Program

In Chapter 3, "Messages and Commands," you learned about message maps and how you can tell MFC which functions to call when it receives messages from Windows. One important message that every Windows program with a window must handle is WM_PAINT. Windows sends the WM_PAINT message to an application's window when the window needs to be redrawn. Several events cause Windows to send a WM_PAINT message:

When you studied message maps, you learned to convert a message name to a message-map macro and function name. You now know, for example, that the message-map macro for a WM_PAINT message is ON_WM_PAINT(). You also know that the matching message-map function should be called OnPaint(). This is another case where MFC has already done most of the work of matching a Windows message with its message-response function. (If all this message-map stuff sounds unfamiliar, you might want to review Chapter 3.)

You might guess that your next step is to catch the WM_PAINT message or to override the OnPaint() function that your view class inherited from CView, but you won't do that. Listing 5.1 shows the code for CView::OnPaint(). As you can see, WM_PAINT is already caught and handled for you.

Listing 5.1  CView::OnPaint()

void CView::OnPaint()
{
     // standard paint routine
     CPaintDC dc(this);
     OnPrepareDC(&dc);
     OnDraw(&dc);
}

CPaintDC is a special class for managing paint DCs--device contexts used only when responding to WM_PAINT messages. An object of the CPaintDC class does more than just create a DC; it also calls the BeginPaint() Windows API function in the class's constructor and calls EndPaint() in its destructor. When a program responds to WM_PAINT messages, calls to BeginPaint() and EndPaint() are required. The CPaintDC class handles this requirement without your having to get involved in all the messy details. As you can see, the CPaintDC constructor takes a single argument, which is a pointer to the window for which you're creating the DC. The this pointer points to the current view, so it's passed to the constructor to make a DC for the current view.

OnPrepareDC() is a CView function that prepares a DC for use. You'll learn more about it in Chapter 6, "Printing and Print Preview."

OnDraw() does the actual work of visually representing the document. In most cases you will write the OnDraw() code for your application and never touch OnPaint().

Switching the Display

The design for Paint1 states that when you click the application's window, the window's display changes. This seemingly magical feat is actually easy to accomplish. You add a member variable to the view to store what kind of display is being done and then change it when users click the window. In other words, the program routes WM_LBUTTONDOWN messages to the OnLButtonDown() message-response function, which sets the m_display flag as appropriate.

First, add the member variable. You must add it by hand rather than through the shortcut menu because the type includes an enum declaration. Open Paint1View.h from the FileView and add these lines after the //Attributes comment:

protected:
     enum {Fonts, Pens, Brushes} m_Display;


TIP: This is an anonymous or unnamed enum. You can learn more about enum types in Appendix A, " C++ Review and Object-Oriented Concepts."

Choose ClassView in the Project Workspace pane, expand the classes, expand CPaint1View, and then double-click the constructor CPaint1View(). Add this line of code in place of the TODO comment:

m_Display = Fonts;

This initializes the display selector to the font demonstration. You use the display selector in the OnDraw() function called by CView::OnPaint(). AppWizard has created CPaint1View::OnDraw(), but it doesn't do anything at the moment. Double-click the function name in ClassView and add the code in Listing 5.2 to the function, removing the TODO comment left by AppWizard.

Listing 5.2  CPaint1View::OnDraw()

void CPaint1View::OnDraw(CDC* pDC)
{
     CPaint1Doc* pDoc = GetDocument();
     ASSERT_VALID(pDoc);
     switch (m_Display)
     {
          case Fonts:
               ShowFonts(pDC);
               break;
          case Pens:
               ShowPens(pDC);
               break;
          case Brushes:
               ShowBrushes(pDC);
               break;
     }
}

You will write the three functions ShowFonts(), ShowPens(), and ShowBrushes() in upcoming sections of this chapter. Each function uses the same DC pointer that was passed to OnDraw() by OnPaint(). Add them to the class now by following these steps:

1. Right-click the CPaint1View class in ClassView and select Add Member Function.

2. Enter void for the Function Type.

3. Enter ShowFonts(CDC* pDC) for the Function Declaration.

4. Change the access to protected. Click OK.

5. Repeat steps 1 through 4 for ShowPens(CDC* pDC) and ShowBrushes(CDC* pDC).

The last step in arranging for the display to switch is to catch left mouse clicks and write code in the message handler to change m_display.

Right-click CPaint1View in the ClassView and select Add Windows Message Handler from the shortcut menu that appears. Double-click WM_LBUTTONDOWN in the New Windows Messages/Events list box. ClassWizard adds a function called OnLButtonDown() to the view and adds entries to the message map so that this function will be called whenever users click the left mouse button over this view.

Click Edit Existing to edit the OnLButtonDown() you just created, and add the code shown in Listing 5.3.

Listing 5.3  CPaint1View::OnLButtonDown()

void CPaint1View::OnLButtonDown(UINT nFlags, CPoint point)
{
     if (m_Display == Fonts)
          m_Display = Pens;
     else if (m_Display == Pens)
          m_Display = Brushes;
     else
          m_Display = Fonts
     Invalidate();
     CView::OnLButtonDown(nFlags, point);
}

As you can see, depending on its current value, m_display is set to the next display type in the series. Of course, just changing the value of m_display doesn't accomplish much; the program still needs to redraw the contents of its window. The call to Invalidate() tells Windows that all of the window needs to be repainted. This causes Windows to generate a WM_PAINT message for the window, which means that eventually OnDraw() will be called and the view will be redrawn as a font, pen, or brush demonstration.

Using Fonts

Changing the font used in a view is a technique you'll want to use in various situations. It's not as simple as you might think because you can never be sure that any given font is actually installed on the user's machine. You set up a structure that holds information about the font you want, attempt to create it, and then work with the font you actually have, which might not be the font you asked for.

A Windows font is described in the LOGFONT structure outlined in Table 5.1. The LOGFONT structure uses 14 fields to hold a complete description of the font. Many fields can be set to 0 or the default values, depending on the program's needs.

Table 5.1  LOGFONT Fields and Their Descriptions

Field Description
lfHeight Font height in logical units
lfWidth Font width in logical units
lfEscapement Angle at which to draw the text
lfOrientation Character tilt in tenths of a degree
lfWeight Font weight
lfItalic A nonzero value indicates italics
lfUnderline A nonzero value indicates an underlined font
lfStrikeOut A nonzero value indicates a strikethrough font
lfCharSet Font character set
lfOutPrecision How to match requested font to actual font
lfClipPrecision How to clip characters that run over clip area
lfQuality Print quality of the font
lfPitchAndFamily Pitch and font family
lfFaceName Typeface name

Some terms in Table 5.1 need a little explanation. The first is logical units. How high is a font with a height of 8 logical units, for example? The meaning of a logical unit depends on the mapping mode you're using, as shown in Table 5.2. The default mapping mode is MM_TEXT, which means that one logical unit is equal to 1 pixel. Mapping modes are discussed in more detail in Chapter 6.

Table 5.2  Mapping Modes

Mode Unit
MM_HIENGLISH 0.001 inch
MM_HIMETRIC 0.01 millimeter
MM_ISOTROPIC Arbitrary
MM_LOENGLISH 0.01 inch
MM_LOMETRIC 0.1 millimeter
MM_TEXT Device pixel
MM_TWIPS 1/1440 inch

Escapement refers to writing text along an angled line. Orientation refers to writing angled text along a flat line. The font weight refers to the thickness of the letters. A number of constants have been defined for use in this field: FW_DONTCARE, FW_THIN, FW_EXTRALIGHT, FW_ULTRALIGHT, FW_LIGHT, FW_NORMAL, FW_REGULAR, FW_MEDIUM, FW_SEMIBOLD, FW_DEMIBOLD, FW_BOLD, FW_EXTRABOLD, FW_ULTRABOLD, FW_BLACK, and FW_HEAVY. Not all fonts are available in all weights. Four character sets are available (ANSI_CHARSET, OEM_CHARSET, SYMBOL_CHARSET, and UNICODE_CHARSET), but for writing English text you'll almost always use ANSI_CHARSET. (Unicode is discussed in Chapter 28, "Future Explorations.") The last field in the LOGFONT structure is the face name, such as Courier or Helvetica.

Listing 5.4 shows the code you need to add to the empty ShowFonts() function you created earlier.

Listing 5.4  CPaint1View::ShowFonts()

void CPaint1View::ShowFonts(CDC * pDC)
{
    // Initialize a LOGFONT structure for the fonts.
    LOGFONT logFont;
    logFont.lfHeight = 8;
    logFont.lfWidth = 0;
    logFont.lfEscapement = 0;
    logFont.lfOrientation = 0;
    logFont.lfWeight = FW_NORMAL;
    logFont.lfItalic = 0;
    logFont.lfUnderline = 0;
    logFont.lfStrikeOut = 0;
    logFont.lfCharSet = ANSI_CHARSET;
    logFont.lfOutPrecision = OUT_DEFAULT_PRECIS;
    logFont.lfClipPrecision = CLIP_DEFAULT_PRECIS;
    logFont.lfQuality = PROOF_QUALITY;
    logFont.lfPitchAndFamily = VARIABLE_PITCH | FF_ROMAN;
    strcpy(logFont.lfFaceName, "Times New Roman");
    // Initialize the position of text in the window.
    UINT position = 0;
    // Create and display eight example fonts.
    for (UINT x=0; x<8; ++x)
    {
         // Set the new font's height.
         logFont.lfHeight = 16 + (x * 8);
         // Create a new font and select it into the DC.
         CFont font;
         font.CreateFontIndirect(&logFont);
         CFont* oldFont = pDC->SelectObject(&font);
         // Print text with the new font.
         position += logFont.lfHeight;
         pDC->TextOut(20, position, "A sample font.");
         // Restore the old font to the DC.
         pDC->SelectObject(oldFont);
    }
}

ShowFonts()starts by setting up a Times Roman font 8 pixels high, with a width that best matches the height and all other attributes set to normal defaults.

To show the many fonts displayed in its window, the Paint1 application creates its fonts in a for loop, modifying the value of the LOGFONT structure's lfHeight member each time through the loop, using the loop variable x to calculate the new font height:

logFont.lfHeight = 16 + (x * 8);

Because x starts at 0, the first font created in the loop will be 16 pixels high. Each time through the loop, the new font will be 8 pixels higher than the previous one.

After setting the font's height, the program creates a CFont object and calls its CreateFontIndirect() function, which attempts to create a CFont object corresponding to the LOGFONT you created. It will change the LOGFONT to describe the CFont that was actually created, given the fonts installed on the user's machine.

After ShowFonts() calls CreateFontIndirect(), the CFont object is associated with a Windows font. Now you can select it into the DC. Selecting objects into device contexts is a crucial concept in Windows output programming. You can't use any graphical object, such as a font, directly; instead, you select it into the DC and then use the DC. You always save a pointer to the old object that was in the DC (the pointer is returned from the SelectObject() call) and use it to restore the device context by selecting the old object again when you're finished. The same function, SelectObject(), is used to select various objects into a device context: the font you're using in this section, a pen, a brush, or a number of other drawing objects.

After selecting the new font into the DC, you can use the font to draw text onscreen. The local variable position holds the vertical position in the window at which the next line of text should be printed. This position depends on the height of the current font. After all, if there's not enough space between the lines, the larger fonts will overlap the smaller ones. When Windows created the new font, it stored the font's height (most likely the height that you requested, but maybe not) in the LOGFONT structure's lfHeight member. By adding the value stored in lfHeight, the program can determine the next position at which to display the line of text. To make the text appear onscreen, ShowFonts() calls TextOut().

TextOut()'s first two arguments are the X and Y coordinates at which to print the text. The third argument is the text to print. Having printed the text, you restore the old font to the DC in case this is the last time through the loop.

Build the application and run it. It should resemble Figure 5.3. If you click the window, it will go blank because the ShowPens() routine doesn't draw anything. Click again and it's still blank, this time because the ShowBrushes() routine doesn't draw anything. Click a third time and you are back to the fonts screen.

FIG. 5.3 The font display shows different types of text output.

Sizing and Positioning the Window

As you can see in Figure 5.3, Paint1 doesn't display eight different fonts at 800*600 screen settings--only seven can fit in the window. To correct this, you need to set the size of the window a little larger than the Windows default. In an MFC program, you do this in the mainframe class PreCreateWindow() function. This is called for you just before the mainframe window is created. The mainframe window surrounds the entire application and governs the size of the view.

PreCreateWindow() takes one parameter, a reference to a CREATESTRUCT structure. The CREATESTRUCT structure contains essential information about the window that's about to be created, as shown in Listing 5.5.

Listing 5.5  The CREATESTRUCT Structure

typedef struct tagCREATESTRUCT {
   LPVOID    lpCreateParams;
   HANDLE    hInstance;
   HMENU     hMenu;
   HWND      hwndParent;
   int       cy;
   int       cx;
   int       y;
   int       x;
   LONG      style;
   LPCSTR    lpszName;
   LPCSTR    lpszClass;
   DWORD     dwExStyle;
} CREATESTRUCT;

If you've programmed Windows without application frameworks such as MFC, you'll recognize the information stored in the CREATESTRUCT structure. You used to supply much of this information when calling the Windows API function CreateWindow() to create your application's window. Of special interest to MFC programmers are the cx, cy, x, and y members of this structure. By changing cx and cy, you can set the window width and height, respectively. Similarly, modifying x and y changes the window's position. By overriding PreCreateWindow(), you have a chance to fiddle with the CREATESTRUCT structure before Windows uses it to create the window.

AppWizard created a CMainFrame::PreCreateWindow() function. Expand CMainFrame in ClassView, double-click PreCreateWindow() to edit it, and add lines to obtain the code shown in Listing 5.6. This sets the application's height and width. It also prevents users from resizing the application by using the bitwise and operator (&) to turn off the WS_SIZEBOX style bit.

Listing 5.6  CMainFrame::PreCreateWindow()

BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
     cs.cx = 440;
     cs.cy = 480;
     cs.style &= ~WS_SIZEBOX;
      if( !CFrameWnd::PreCreateWindow(cs) )
            return FALSE;
      return TRUE;
}

It's important that after your own code in PreCreateWindow(), you call the base class's PreCreateWindow(). Failure to do this will leave you without a valid window because MFC never gets a chance to pass the CREATESTRUCT structure on to Windows, so Windows never creates your window. When overriding class member functions, you usually need to call the base class's version.

Build and run Paint1 to confirm that all eight fonts fit in the application's window. Now you're ready to demonstrate pens.

Using Pens

You'll be pleased to know that pens are much easier to deal with than fonts, mostly because you don't have to fool around with complicated data structures like LOGFONT. In fact, to create a pen, you need to supply only the pen's line style, thickness, and color. The Paint1 application's ShowPens() function displays in its window the lines drawn by using different pens created within a for loop. Listing 5.7 shows the code.

Listing 5.7  CPaint1View::ShowPens()

void CPaint1View::ShowPens(CDC * pDC)
{
     // Initialize the line position.
     UINT position = 10;
     // Draw sixteen lines in the window.
     for (UINT x=0; x<16; ++x)
     {
          // Create a new pen and select it into the DC.
CPen pen(PS_SOLID, x*2+1, RGB(0, 0, 255));
          CPen* oldPen = pDC->SelectObject(&pen);
          // Draw a line with the new pen.
          position +=  x * 2 + 10;
          pDC->MoveTo(20, position);
          pDC->LineTo(400, position);
          // Restore the old pen to the DC.
          pDC->SelectObject(oldPen);
     }
}

Within the loop, ShowPens() first creates a custom pen. The constructor takes three parameters. The first is the line's style, one of the styles listed in Table 5.3. (You can draw only solid lines with different thicknesses. If you specify a pattern and a thickness greater than 1 pixel, the pattern is ignored and a solid line is drawn.) The second argument is the line thickness, which increases each time through the loop. The third argument is the line's color. The RGB macro takes three values for the red, green, and blue color components and converts them to a valid Windows color reference. The values for the red, green, and blue color components can be anything from 0 to 255--the higher the value, the brighter that color component. This code creates a bright blue pen. If all the color values were 0, the pen would be black; if the color values were all 255, the pen would be white.

Table 5.3  Pen Styles

Style Description
PS_DASH A pen that draws dashed lines
PS_DASHDOT A pen that draws dash-dot patterned lines
PS_DASHDOTDOT A pen that draws dash-dot-dot patterned lines
PS_DOT A pen that draws dotted lines
PS_INSIDEFRAME A pen that's used with shapes, in which the line's thickness must not extend outside the shape's frame
PS_NULL A pen that draws invisible lines
PS_SOLID A pen that draws solid lines


NOTE: If you want to control the style of a line's end points or create your own custom patterns for pens, you can use the alternative CPen constructor, which requires a few more arguments than the CPen constructor described in this section. To learn how to use this alternative constructor, look up CPen in your Visual C++ online documentation.

After creating the new pen, ShowPens() selects it into the DC, saving the pointer to the old pen. The MoveTo() function moves the pen to an X,Y coordinate without drawing as it moves; the LineTo() function moves the pen while drawing. The style, thickness, and color of the pen are used. Finally, you select the old pen into the DC.


TIP:[ There are a number of line drawing functions other than LineTo(), including Arc(), ArcTo(), AngleArc(), and PolyDraw().

Build and run Paint1 again. When the font display appears, click the window. You will see a pen display similar to the one in Figure 5.4.

Using Brushes

A pen draws a line of a specified thickness onscreen. A brush fills a shape onscreen. You can create solid and patterned brushes and even brushes from bitmaps that contain your own custom fill patterns. Paint1 will display both patterned and solid rectangles in the ShowBrushes() function, shown in Listing 5.8.

FIG. 5.4 The pen display shows the effect of setting line thickness.

Listing 5.8  CPaint1View::ShowBrushes()

void CPaint1View::ShowBrushes(CDC * pDC)
     // Initialize the rectangle position.
     UINT position = 0;
     // Select pen to use for rectangle borders
     CPen pen(PS_SOLID, 5, RGB(255, 0, 0));
     CPen* oldPen = pDC->SelectObject(&pen);
     // Draw seven rectangles.
     for (UINT x=0; x<7; ++x)
     {
          CBrush* brush;
          // Create a solid or hatched brush.
          if (x == 6)
               brush = new CBrush(RGB(0,255,0));
          else
              brush = new CBrush(x, RGB(0,160,0));
          // Select the new brush into the DC.
          CBrush* oldBrush = pDC->SelectObject(brush);
          // Draw the rectangle.
          position += 50;
          pDC->Rectangle(20, position, 400, position + 40);
          // Restore the DC and delete the brush.
          pDC->SelectObject(oldBrush);
          delete brush;
     }
     // Restore the old pen to the DC.
     pDC->SelectObject(oldPen);
}

The rectangles painted with the various brushes in this routine will all be drawn with a border. To arrange this, create a pen (this one is solid, 5 pixels thick, and bright red) and select it into the DC. It will be used to border the rectangles without any further work on your part. Like ShowFonts() and ShowPens(), this routine creates its graphical objects within a for loop. Unlike those two functions, ShowBrushes() creates a graphical object (in this routine, a brush) with a call to new. This enables you to call the one-argument constructor, which creates a solid brush, or the two-argument constructor, which creates a hatched brush.

In Listing 5.8, the first argument to the two-argument constructor is just the loop variable, x. Usually, you don't want to show all the hatch patterns but want to select a specific one. Use one of these constants for the hatch style:

In a pattern that should be familiar by now, ShowBrushes() selects the brush into the DC, determines the position at which to work, uses the brush by calling Rectangle(), and then restores the old brush. When the loop is complete, the old pen is restored as well.

Rectangle()is just one of the shape-drawing functions that you can call. Rectangle() takes as arguments the coordinates of the rectangle's upper-left and lower-right corners. Some others of interest are Chord(), DrawFocusRect(), Ellipse(), Pie(), Polygon(), PolyPolygon(), Polyline(), and RoundRect(), which draws a rectangle with rounded corners.

Again, build and run Paint1. Click twice, and you will see the demonstration of brushes, as shown in Figure 5.5.


NOTE: Remember the call to Invalidate() in CPaint1View::OnLButtonDown()? Invalidate() actually takes a Boolean argument with a default value of TRUE. This argument tells Windows whether to erase the window's background. If you use FALSE for this argument, the background isn't erased. In Figure 5.6, you can see what happens to the Paint1 application if Invalidate() is called with an argument of FALSE. 

FIG. 5.5 The brushes display shows several patterns inside thick-bordered rectangles.

FIG. 5.6 Without erasing the background, the Paint1 application's windows appear messy.

Scrolling Windows

Those famous screen rectangles known as windows enable you to partition screen space between various applications and documents. Also, if a document is too large to completely fit within a window, you can view portions of it and scroll through it a bit at a time. The Windows operating system and MFC pretty much take care of the partitioning of screen space. However, if you want to enable users to view portions of a large document, you must create scrolling windows.

Adding scrollbars to an application from scratch is a complicated task. Luckily for Visual C++ programmers, MFC handles many of the details involved in scrolling windows over documents. If you use the document/view architecture and derive your view window from MFC's CScrollView class, you have scrolling capabilities almost for free. I say "almost" because you still must handle a few details, which you learn about in the following sections.


NOTE: If you create your application with AppWizard, you can specify that you want to use CScrollView as the base class for your view class. To do this, in the Step 6 of 6 dialog box displayed by AppWizard, select your view window in the class list and then select CScrollView in the Base Class dialog box, as shown in Figure 5.7. 

FIG. 5.7 You can create a scrolling window from within AppWizard.

Building the Scroll Application

In this section, you'll build a sample program called Scroll to experiment with a scrolling window. When Scroll first runs, it displays five lines of text. Each time you click the window, five lines of text are added to the display. When you have more lines of text than fit in the window, a vertical scrollbar appears, enabling you to scroll to the parts of the documents that you can't see.

As usual, building the application starts with AppWizard. Choose File, New, and select the Projects tab. Fill in the project name as Scroll and fill in an appropriate directory for the project files. Make sure that MFC AppWizard (exe) is selected. Click OK.

Complete the AppWizard steps, selecting the following options:

Step 1: Select Single Document.

Step 2: Use default settings

Step 3: Use default settings.

Step 4: Deselect all check boxes.

Step 5: Use default settings.

Step 6: Select CScrollView from the Base Class drop-down box, as in Figure 5.7.

The New Project Information dialog box should resemble Figure 5.8. Click OK to create the project.

FIG. 5.8 Create a scroll application with AppWizard.

This application generates very simple lines of text. You need to keep track only of the number of lines in the scrolling view at the moment. To do this, add a variable to the document class by following these steps:

1. In ClassView, expand the classes and right-click CScrollDoc.

2. Choose Add Member Variable from the shortcut menu.

3. Fill in int as the variable type.

4. Fill in m_NumLines as the variable declaration.

5. Select Public for the Access.

Variables associated with a document are initialized in OnNewDocument(), as discussed in Chapter 4. In ClassView, expand CScrollDoc and double-click OnNewDocument() to expand it. Replace the TODO comments with this line of code:

m_NumLines = 5;

To arrange for this variable to be saved with the document and restored when the document is loaded, you must serialize it as discussed in Chapter 7, "Persistence and File I/O." Edit CScrollDoc::Serialize() as shown in Listing 5.9.

Listing 5.9  CScrollDoc::Serialize()

void CScrollDoc::Serialize(CArchive& ar)
{
     if (ar.IsStoring())
     {
          ar << m_NumLines;
     }
     else
     {
          ar >> m_NumLines;
     }
}

Now all you need to do is use m_NumLines to draw the appropriate number of lines. Expand the view class, CMyScrollView, in ClassView and double-click OnDraw(). Edit it until it's the same as Listing 5.10. This is very similar to the ShowFonts() code from the Paint1 application earlier in this chapter.

Listing 5.10  CMyScrollView::OnDraw()

void CMyScrollView::OnDraw(CDC* pDC)
{
     CScrollDoc* pDoc = GetDocument();
     ASSERT_VALID(pDoc);
     // get the number of lines from the document
     int numLines = pDoc->m_NumLines;
     // Initialize a LOGFONT structure for the fonts.
     LOGFONT logFont;
    logFont.lfHeight = 24;
    logFont.lfWidth = 0;
    logFont.lfEscapement = 0;
    logFont.lfOrientation = 0;
    logFont.lfWeight = FW_NORMAL;
    logFont.lfItalic = 0;
    logFont.lfUnderline = 0;
    logFont.lfStrikeOut = 0;
    logFont.lfCharSet = ANSI_CHARSET;
    logFont.lfOutPrecision = OUT_DEFAULT_PRECIS;
    logFont.lfClipPrecision = CLIP_DEFAULT_PRECIS;
    logFont.lfQuality = PROOF_QUALITY;
    logFont.lfPitchAndFamily = VARIABLE_PITCH | FF_ROMAN;
     strcpy(logFont.lfFaceName, "Times New Roman");
     // Create a new font and select it into the DC.
    CFont* font = new CFont();
     font->CreateFontIndirect(&logFont);
     CFont* oldFont = pDC->SelectObject(font);
     // Initialize the position of text in the window.
     UINT position = 0;
     // Create and display eight example lines.
     for (int x=0; x<numLines; ++x)
     {
          // Create the string to display.
          char s[25];
          wsprintf(s, "This is line #%d", x+1);
          // Print text with the new font.
          pDC->TextOut(20, position, s);
          position += logFont.lfHeight;
     }
     // Restore the old font to the DC, and
     // delete the font the program created.
     pDC->SelectObject(oldFont);
     delete font;
}

Build and run the Scroll application. You will see a display similar to that in Figure 5.9. No scrollbars appear because all the lines fit in the window.

FIG. 5.9 At first, the scroll application displays five lines of text and no scrollbars.

Adding Code to Increase Lines

To increase the number of lines whenever users click the window, you need to add a message handler to handle left mouse clicks and then write the code for the handler. Right-click CMyScrollView in ClassView and choose Add Windows Message Handler. Double-click WM_LBUTTONDOWN to add a handler and click the Edit Existing button to change the code. Listing 5.11 shows the completed handler. It simply increases the number of lines and calls Invalidate() to force a redraw. Like so many message handlers, it finishes by passing the work on to the base class version of this function.

Listing 5.11  CMyScrollView::OnLButtonDown()

void CMyScrollView::OnLButtonDown(UINT nFlags, CPoint point)
{
     CScrollDoc* pDoc = GetDocument();
     ASSERT_VALID(pDoc);
     // Increase number of lines to display.
     pDoc->m_NumLines += 5;
     // Redraw the window.
     Invalidate();
     CScrollView::OnLButtonDown(nFlags, point);
}

Adding Code to Decrease Lines

So that you can watch scrollbars disappear as well as appear, why not implement a way for users to decrease the number of lines in the window? If left-clicking increases the number of lines, it makes sense that right-clicking would decrease it. Add a handler for WM_RBUTTONDOWN just as you did for WM_LBUTTONDOWN, and edit it until it's just like Listing 5.12. This function is a little more complicated because it ensures that the number of lines is never negative.

Listing 5.12  CMyScrollView::OnRButtonDown()

void CMyScrollView::OnRButtonDown(UINT nFlags, CPoint point)
{
     CScrollDoc* pDoc = GetDocument();
     ASSERT_VALID(pDoc);
     // Decrease number of lines to display.
     pDoc->m_NumLines -= 5;
     if (pDoc->m_NumLines < 0)
     {
          pDoc->m_NumLines = 0;
     }
     // Redraw the window.
     Invalidate();
     CScrollView::OnRButtonDown(nFlags, point);
}

If you build and run Scroll now and click the window, you can increase the number of lines, but scrollbars don't appear. You need to add some lines to OnDraw() to make that happen. Before you do, review the way that scrollbars work. You can click three places on a vertical scrollbar: the thumb (some people call it the elevator), above the thumb, or below it. Clicking the thumb does nothing, but you can click and hold to drag it up or down. Clicking above it moves you one page (screenful) up within the data. Clicking below it moves you one page down. What's more, the size of the thumb is a visual representation of the size of a page in proportion to the entire document. Clicking the up arrow at the top of the scrollbar moves you up one line in the document; clicking the down arrow at the bottom moves you down one line.

What all this means is that the code that draws the scrollbar and handles the clicks needs to know the size of the entire document, the page size, and the line size. You don't have to write code to draw scrollbars or to handle clicks on the scrollbar, but you do have to pass along some information about the size of the document and the current view. The lines of code you need to add to OnDraw() are in Listing 5.13; add them after the for loop and before the old font is selected back into the DC.

Listing 5.13  Lines to Add to OnDraw()

// Calculate the document size.
CSize docSize(100, numLines*logFont.lfHeight);
// Calculate the page size.
   CRect rect;
GetClientRect(&rect);
CSize pageSize(rect.right, rect.bottom);
// Calculate the line size.
CSize lineSize(0, logFont.lfHeight);
// Adjust the scrollers.
SetScrollSizes(MM_TEXT, docSize, pageSize, lineSize);

This new code must determine the document, page, and line sizes. The document size is the width and height of the screen area that could hold the entire document. This is calculated by using the number of lines in the entire document and the height of a line. (CSize is an MFC class created especially for storing the widths and heights of objects.) The page size is simply the size of the client rectangle of this view, and the line size is the height of the font. By setting the horizontal component of the line size to 0, you prevent horizontal scrolling.

These three sizes must be passed along to implement scrolling. Simply call SetScrollSizes(), which takes the mapping mode, document size, page size, and line size. MFC will set the scrollbars properly for any document and handle user interaction with the scrollbars.

Build and run Scroll again and generate some more lines. You should see a scrollbar like the one in Figure 5.10. Add even more lines and you will see the thumb shrink as the document size grows. Finally, resize the application horizontally so that the text won't all fit. Notice how no horizontal scrollbars appear, because you set the horizontal line size to 0.

FIG. 5.10 After displaying more lines than fit in the window, the vertical scrollbar appears.


Previous chapterNext chapterContents

© Copyright, Macmillan Computer Publishing. All rights reserved.