Most applications need to display some type of data in their windows. One would 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 is exactly Windows' device independence that places a little extra burden on the programmer's shoulders. Because the programmer can never know in advance exactly what type of devices may be connected to a user's system, he or she can't make many assumptions about display capabilities. Functions that draw to the screen must do so indirectly through something called a display context (DC).
Visual C++'s MFC includes many classes that make dealing with DCs easier. These classes encapsulate such graphical objects as not only the DC itself, but also pens, brushes, fonts, and more.
As you know, every Windows application (in fact, every computer application) must manipulate data in some way. Most applications must also display data. Unfortunately, as we just said, because of Windows' device independence, this task is not as straightforward in Windows as it is in a nongraphical operating system like DOS.
However, 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 the user has 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 is 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 up to each specific artist.
For example, an artist using oil paints will choose canvas as his surface and paints in the colors suggested by the instructor, as the paint. On the other hand, an artist using watercolors will select watercolor paper on which to create her work, and she will, of course, use watercolors rather than oils for paint. Finally, the charcoal artist will select the appropriate drawing surface for charcoal and will use a single color.
The instructor in the preceding 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 is the device driver-the Windows artist-that 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. Monitors with high resolutions 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 is a data structure called 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 on the screen. 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.
This chapter's first sample program, which you can find in the CHAP11\PAINT1 folder of this book's CD-ROM, shows you how to use MFC to display many types of data in an application's window. When you run the program, the main window appears, showing text drawn in various-sized fonts (Figure 11.1). When you left-click in the application's window, it switches the type of information that it displays. For example, the first time you click, the window shows a series of blue lines that get thicker as they get closer to the window's bottom (Figure 11.2). This screen is produced by creating new pens and drawing the lines with those pens. A second click brings up a display comprised of rectangles, each of which contains a different pattern. The fill patterns in the rectangles are produced by the brushes created in the program (Figure 11.3). Finally, a third click brings you back to the font display.
Figure 11.1 : The font display shows how you can create different types of text output.
Figure 11.2 : Windows also enables you to draw with various styles of lines.
Figure 11.3 : By using different brushes, you can fill shapes with different patterns.
You can find the complete source-code files for the Paint1 application in the CHAP11 folder of this book's CD-ROM. In the following sections, you'll examine the techniques used in those files in order to create the program's displays. By the time you get to the end of this chapter, the words display context won't make you scratch your head in perplexity.
If you've taken the time to look over the source-code files that make up the Paint1 application, you're either now smugly confident that you know all there is to know about displaying data in a window, or you feel like you just tried to read the latest John Grisham novel in Latin. Whichever category you fall into, you'll almost certainly want to read on. You smug folks may get some surprises, while the rest of you will discover that the code isn't nearly as complex as it might appear at first glance.
In Chapter 5 "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. There are several events that cause Windows to send a WM_PAINT message. The first event occurs when the user simply runs the program. In a properly written Windows application, the application's window gets a WM_PAINT message almost immediately after being run in order to ensure that the appropriate data is displayed from the very start.
Another time a window might receive the WM_PAINT message is when the window has been resized or has recently been uncovered-either fully or partially-by another window. In either case, part of the window that wasn't visible before is now on the screen and must be updated.
Finally, a program can indirectly send itself a WM_PAINT message by invalidating its client area. Having this ability ensures that an application can change its window's contents almost any time it wishes. For example, a word processor might invalidate its window after the user pastes some text from the Clipboard.
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 doesn't sound familiar, you might want to review Chapter 5)
So, in order to paint your window's display, you need to add an ON_WM_PAINT() entry to your message map and then write an OnPaint() function. In the OnPaint() function, you write the code that will produce your window's display. Then, whenever Windows sends your application the WM_PAINT message, MFC automatically calls OnPaint(), which draws the window's display just how you want it.
If you look near the top of the Paint1 application's MAINFRM.CPP file, which is the frame-window class's implementation file, you'll see the application's main message map, as shown in Listing 11.1.
Listing 11.1 LST11_1.TXT-The Application's Message Map
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd) ON_WM_PAINT() ON_WM_LBUTTONDOWN() END_MESSAGE_MAP()
As you can tell by the message map's entries, the application can respond to WM_PAINT and WM_LBUTTONDOWN messages. The ON_WM_PAINT() entry maps to the OnPaint() message-map function. In the first line of that function, the program creates a DC for the client area of the frame window:
CPaintDC* paintDC = new CPaintDC(this);
CPaintDC is a special class for managing paint DCs, which are device contexts that are used only when responding to WM_PAINT messages. In fact, if you're going to use MFC to create your OnPaint() function's paint DC, you must use the CPaintDC class. This is because 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. Because the preceding code line uses the new operator to create the CPaintDC object dynamically on the heap, the program must call the delete operator on the returned CPaintDC pointer when the program is finished with the DC. Otherwise, you not only leave unused objects floating around the computer's memory, but you also fail to call the CPaintDC object's destructor, which in turn never gets a chance to call EndPaint().
After creating the paint DC, the OnPaint() function uses its m_display data member to determine what type of display to draw in the window. The m_display data member can be equal to Fonts, Pens, or Brushes, three values that are defined as an enumeration in the MAINFRM.H file, like this:
enum {Fonts, Pens, Brushes};
The function checks m_display in a switch statement, calling ShowFonts(), ShowPens(), or ShowBrushes() as appropriate. It is in these three functions that the Paint1 application actually creates its displays. You'll examine these functions of your CMainFrame class in the sections to come. Notice that the pointer to the paint DC is passed as a parameter to ShowFonts(), ShowPens(), and ShowBrushes(), which must use the DC in order to draw in the application's window.
The last line in OnPaint() deletes the CPaintDC object, paintDC, freeing it from memory, as well as ensuring that its destructor gets called properly. Note that, in most programs, you may find it more convenient to create your paint DC as a local variable on the stack, as is shown in Listing 11.2. When you do this, you no longer have to be concerned with deleting the object. It's automatically deleted when it goes out of scope. In the case of the Paint1 application, it's more efficient to pass a CPaintDC pointer as an argument to other functions than it is to pass the actual object.
Listing 11.2 LST11_2.TXT-Creating the Paint DC on the Stack
void CMainFrame::OnPaint() { CPaintDC paintDC(this); // Drawing code goes here. }
Fonts are one of the trickier GDI (Graphics Device Interface) objects to handle, so you might as well get them out of the way first. In order to select and use fonts, you must be familiar with the LOGFONT structure, which contains a wealth of information about a font, and you must know how to create new fonts when they're needed. Moreover, there are more typefaces and font types than galaxies in the universe (okay, maybe not quite that many), which means that you can never be sure exactly how your user's system is set up.
As we said, a Windows font is described in the LOGFONT structure, which is outlined in Table 11.1. The LOGFONT description in Table 11.1, however, gives only an overview of the structure. Before experimenting with custom fonts, you may want to look up this structure in your Visual C++ online help, where you'll find a more complete description of each of its fields, including the many constants that are already defined for use with the structure.
Field | Description |
lfHeight | Height of font in logical units. |
lfWidth | Width of font in logical units. |
lfEscapement | Angle at which to draw the text. |
LfOrientation | Character tilt in tenths of a degree. |
LfWeight | Used to select normal (400) or boldface (700) text. |
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. |
The LOGFONT structure holds a complete description of the font. This structure contains 14 fields, although many of the fields can be set to 0 or the default values, depending on the program's needs. In the ShowFonts() function, the Paint1 application creates its LOGFONT as shown in Listing 11.3.
Listing 11.3 LST11_3.TXT-Initializing a LOGFONT Structure
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_);
In the lines in Listing 11.3, the font is set to be eight pixels high, as determined by the value of the lfHeight field. Note that, in most cases, you should set the width to 0, as determined by lfWidth, which allows Windows to select a width that best matches the height. You can, however, create compressed or expanded fonts by experimenting with the lfWidth field.
The font's italic, underline, and strikeout attributes can be turned on by supplying a nonzero value for the lfItalic, lfUnderline, and lfStrikeOut fields. For example, the window shown in Figure 11.4 shows how the Paint1 application's font display would look if you set the lfItalic and lfUnderline members of the LOGFONT structure to 1.
Figure 11.4 : You can create all types of fonts by manipulating values in the LOGFONT structure.
In order to show the many fonts that are 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, like this:
logFont.lfHeight = 16 + (x * 8);
Because x starts at 0, the first font created in the loop will be 16 pixels in height. Each time through the loop, the new font will be eight pixels higher than the previous one.
After setting the font's height, the program creates a CFont object:
CFont* font = new CFont();
In case it's not obvious, CFont is MFC's font class. Using the CFont class, you can create and manipulate fonts using the class's member functions. One of the most important member functions is CreateFontIndirect(), which DisplayFonts() calls like this:
font->CreateFontIndirect(&logFont);
CreateFontIndirect() takes a single argument, which is the address of the LOGFONT structure that contains the font's attributes. When Windows receives the information stored in the LOGFONT structure, it will do its best to create the requested font. The font created isn't always exactly the font requested, so Windows fills in the LOGFONT structure with a description of the font that it managed to create.
After the program calls CreateFontIndirect(), the CFont object has been associated with a Windows font. At this point, you can select the font into the DC, like this:
CFont* oldFont = paintDC->SelectObject(font);
Remember that, in order to use a new graphical object with a DC, you must first select that object into the DC. The preceding call to the paint DC's SelectObject() member function replaces the current font in the DC with the new one. SelectObject()'s single parameter is the address of the new font. SelectObject() returns a pointer to the old font object that was deselected from the DC. You'll soon see why you must save this pointer.
After selecting the new font into the DC, you can use the font to draw text on the screen. In the ShowFonts() function, the first step in displaying text is to determine where in the window to draw the text:
position += logFont.lfHeight;
The local variable position holds the vertical position in the window at which the next line of text should be printed. This position depends upon 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 (which is most likely the height that you requested, but, then again, 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, using the DC object's TextOut() member function:
paintDC->TextOut(20, position, _A sample font._);
Here, TextOut()'s first two arguments are the X,Y coordinates in the window at which to print the text. The third argument is the text to print. TextOut() actually has a fourth argument, which is the number of characters to print. If you leave this last parameter off, MFC just assumes that you want to display the entire string given as the second argument.
Now you get to see why the program saved the pointer of the old font that was deselected from the DC when the program selected the new font. You must never delete a GDI object such as a font from a DC while it's still selected into the DC. That means you must first deselect the new font from the DC before deleting it in preparation for creating the next font. Unfortunately, if you search through your Windows programming manuals, you'll discover that there is no DeselectObject() function. This actually makes sense when you think about it. If you were allowed to deselect GDI objects without selecting new ones, you could leave the DC without a pen, brush, or other important object. So the only way to deselect an object is to select a new object into the DC. Therefore, to deselect the new font, the Paint1 application selects the old font back into the DC, like this:
paintDC->SelectObject(oldFont);
This time, the program doesn't bother to save the pointer returned from SelectObject() because that pointer is for the new font that the program created. The program already has that pointer stored in font. A quick call to delete gets rid of the font object, so the program can create the next font it needs for the display:
delete font;
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 only supply the pen's line style, thickness, and color. The Paint1 application's ShowPens() function displays in its window lines drawn using different pens created within a for loop. Within the loop, the program first creates a custom pen, like this:
CPen* pen = new CPen(PS_SOLID, x*2+1, RGB(0, 0, 255));
The first argument shown in the preceding line of code is the line's style, which can be one of the styles listed in Table 11.2. Note that only solid lines can be drawn with different thicknesses. Patterned lines always have a thickness of 1. The second argument in the preceding code is the line thickness, which, in the ShowPens() function, is calculated using the loop variable x as a multiplier.
Finally, 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 into 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. The previous code line creates a bright red 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.
Style | Description |
PS_DASH | Specifies a pen that draws dashed lines |
PS_DASHDOT | Specifies a pen that draws dash-dot patterned lines |
PS_DASHDOTDOT | Specifies a pen that draws dash-dot-dot patterned lines |
PS_DOT | Specifies a pen that draws dotted lines |
PS_INSIDEFRAME | Specifies a pen that's used with shapes, where the line's thickness must not extend outside of the shape's frame |
PS_NULL | Specifies a pen that draws invisible lines |
PS_SOLID | Specifies a pen that draws solid lines |
After creating the new pen, the program selects it into the DC, saving the pointer to the old pen, like this:
CPen* oldPen = paintDC->SelectObject(pen);
Once the pen is selected into the DC, the program can draw a line with the pen. To do this, the program first calculates a vertical position for the new line and then calls the paint DC's MoveTo() and LineTo() member functions, like this:
position += x * 2 + 10; paintDC->MoveTo(20, position); paintDC->LineTo(400, position);
The MoveTo() function positions the starting point of the line, whereas the LineTo() function draws a line-using the pen currently selected into the DC-from the point set with MoveTo() to the coordinates given as the function's two arguments.
Finally, the last step is to restore the DC by reselecting the old pen and deleting the new pen, which is no longer selected into the DC:
paintDC->SelectObject(oldPen); delete pen;
NOTE |
If you want to control the style of a line's end points or want to create your own custom patterns for pens, you can use the alternate CPen constructor, which requires a few more arguments than the CPen constructor described in this section. To learn how to use this alternate constructor, look up CPen in your Visual C++ on-line documentation. |
Creating and using brushes in an MFC program is not unlike using pens. In fact, just as with pens, you can create both solid and patterned brushes. You can even create brushes from bitmaps that contain your own custom fill patterns. As you've seen, the Paint1 application displays rectangles that have been filled by both patterned and solid rectangles. These rectangles are produced in the ShowBrushes() function, which, like the font and pen functions you've already examined, creates its graphical objects within a for loop. In the first line of the loop's body, the program defines a pointer to a CBrush object:
CBrush* brush;
Then, depending on the value of the loop variable x, the program creates either a solid or a patterned brush, as shown in Listing 11.4:
Listing 11.4 LST11_4.TXT-Creating Brush Objects
if (x == 6) brush = new CBrush(RGB(0,255,0)); else brush = new CBrush(x, RGB(0,160,0));
In the code lines in Listing 11.4, if x equals 6, the program calls the version of the CBrush constructor that creates a solid brush. The constructor's single argument is a COLORREF value, which is easily produced using the RGB macro that you were introduced to in the section on pens. For any other value of x, the program creates a patterned brush, using x as the pattern index. The second CBrush constructor takes the pattern index and the brush color as its two arguments. Although not used in the code segment in Listing 11.4, Windows defines several constants for the brush patterns (or hatch styles, as they're often called). Those constants are HS_BDIAGONAL, HS_CROSS, HS_DIAGCROSS, HS_FDIAGONAL, HS_HORIZONTAL, and HS_VERTICAL.
Once the program has created the new brush, a call to the paint DC's SelectObject() member function selects the brush into the DC and returns a pointer to the old brush:
CBrush* oldBrush = paintDC->SelectObject(brush);
Now the program calculates a new drawing position, and then it draws a rectangle with the new brush:
position += 50; paintDC->Rectangle(20, position, 400, position + 40);
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. When you run the Paint1 application and look at the brush window, you'll see that each rectangle is bordered by a thin black line. This line was drawn by the DC's default black pen. If you had selected a different pen into the DC, Windows would have used that pen to draw the rectangle's border. For example, in Figure 11.5, the Paint1 program shows rectangles drawn with a red, six-pixel-thick pen.
Figure 11.5 : Shape-drawing functions frequently draw borders with the currently selected pen.
After drawing a rectangle, the program deselects the new brush from the DC and deletes it:
paintDC->SelectObject(oldBrush); delete brush;
As you know, when you click in the Paint1 application's window, the window's display changes. This seemingly magical feat is actually easy to accomplish. The program routes WM_LBUTTONDOWN messages to the OnLButtonDown() message-response function, which sets the m_display flag as appropriate. At the program startup, the CMainFrame class's constructor initializes its data member m_display to Fonts, so that the window initially appears with the fonts displayed. When the user clicks in the window, the OnLButtonDown() function changes the value of m_display, as shown in Listing 11.5.
Listing 11.5 LST11_5.TXT-Changing the Value of m_display
if (m_display == Fonts) m_display = Pens; else if (m_display == Pens) m_display = Brushes; else m_display = Fonts;
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. Because OnPaint() determines which display to paint based on the value of m_display, all the program needs to do is to get OnPaint() to execute. This task is accomplished by calling the CMainFrame class's Invalidate() function:
Invalidate();
A 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. Thanks to MFC's message mapping, the WM_PAINT message gets routed to OnPaint(). Although it's not used in the example in Listing 11.5, Invalidate() actually has one argument, which MFC gives the default value of TRUE. This Boolean argument tells Windows whether to erase the window's background. If you use FALSE for this argument, Windows leaves the background alone. In Figure 11.6, you can see what happens to the Paint1 application if Invalidate() gets called with an argument of FALSE.
Although Windows is perfectly happy to choose a position and size for your application's window, often you may want to have control over these attributes. In an MFC program, you can size and position a window by overriding the window class's PreCreateWindow() function. This method of positioning a window is especially useful in an AppWizard-generated program, because you don't usually call Create() directly in such applications. Although the Paint1 application wasn't created by AppWizard, it does override the-window class's PreCreateWindow() function to position the window, as shown in Listing 11.6.
Listing 11.6 LST11_6.TXT-Overriding the PreCreateWindow() Member Function
BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs) { // Set size of the main window. cs.cx = 440; cs.cy = 460; // Call the base class_s version. BOOL returnCode = CFrameWnd::PreCreateWindow(cs); return returnCode; }
The PreCreateWindow() function, which MFC calls right before the window element that'll be associated with the class is created, receives one parameter, a reference to a CREATESTRUCT structure. The CREATESTRUCT structure contains essential information about the window that's about to be created and is declared by Windows, as shown in Listing 11.7.
Listing 11.7 LST11_7.TXT-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 like MFC, you'll recognize the information stored in the CREATESTRUCT structure. You 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 width and height, respectively, of the window. Similarly, modifying x and y changes the window's position. By overriding PreCreateWindow(), you get a chance to fiddle with the CREATESTRUCT structure before Windows uses it to create the window.
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, and so Windows never creates your window. When overriding class member functions, you often need to call the base class's version, either before or after your own code, depending on the function. The descriptions of member functions in your Visual C++ online documentation usually indicate whether or not the base class's version must be called.
Those famous screen rectangles called windows were developed for two reasons. The first reason is to partition screen space between various applications and documents. The second reason is to enable the user to view portions of a document when the document is too large to completely fit into the window. The Windows operating system and MFC pretty much take care of the partitioning of screen space. However, if you want to enable the user to view portions of a large document, you must create scrolling windows.
Adding scroll bars 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 get scrolling capabilities almost for free. I say "almost" because there are still a few details that you must handle. You'll learn those details in the following sections.
Figure 11.7 : You can create a scrolling window from within AppWizard.
NOTE |
If you create your application using 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 box, as shown in Figure 11.7. |
The sample program (called Scroll) for this section enables you to experiment with a scrolling window. You can find this program, along with its complete source code, in the CHAP11 folder of this book's CD-ROM. When you run Scroll, you see the window shown in Figure 11.8. The window displays five lines of text. Each time you click in the window with your left mouse button, the application adds five lines of text to the display. When you get more lines of text than fit in the window, a vertical scroll bar appears (Figure 11.9), enabling you to scroll to the parts of the documents that you can't see.
Figure 11.8 : The Scroll application starts off displaying five lines of text and no scroll bars.
Figure 11.9 : After displaying more lines than fit in the window, the vertical scroll bar appears.
The more lines you add to the window, the smaller the scroll thumb becomes. This is because the scroll bar represents the size of the document, and the scroll thumb represents the portion of the document that's visible. If you click in the scroll bar (not on the thumb or an arrow button), the window moves a full page forward or backward. In this case, a full page is the amount of the document that fits in the window. If you click the scroll arrows, the view moves a single line in the appropriate direction. Finally, if you right-click in the window, you can remove lines from the window, five at a time.
When you're using the document/view architecture, you'll usually want to initialize your scroll bars in the view window's OnInitialUpdate() member function. This is because OnInitialUpdate() gets called once early on in the window's construction. If you're using AppWizard to create your starting classes, AppWizard automatically overrides OnInitialUpdate() in your view class, and, providing that you've requested CScrollView as the base class, AppWizard also includes default scroll initialization code in the function. Listing 11.8 shows OnInitialUpdate() as it's created by AppWizard.
Listing 11.8 LST11'8.TXT-The OnInitialUpdate() Function
void CMyScrollView::OnInitialUpdate() { CScrollView::OnInitialUpdate(); CSize sizeTotal; // TODO: calculate the total size of this view sizeTotal.cx = sizeTotal.cy = 100; SetScrollSizes(MM_TEXT, sizeTotal); }
If you examine Listing 11.8, you will see that the program constructs a CSize object called sizeTotal to hold the default width and height of the view. This CSize object is used in the call to SetScrollSizes(), which is the member function that sets up the scroll bars to the correct sizes and states for the window. This function's first argument is the mapping mode, which is usually MM_TEXT. The second argument is a reference to the CSize object that holds the size of the view.
Chances are that the default view size will not suit your application, so you'll want to change the default source code in the OnInitialUpdate() function. Even if you choose to stick with the default size, however, you'll almost certainly need to change the scroll bars as the user changes the document contained in the view window. When using strict document/view design, you'll usually make these changes in the view window's OnUpdate() member function, which you must override in your view class. OnUpdate() gets called by the document class's UpdateAllViews() member function when the user changes the document. In OnUpdate(), you must calculate the document size, page size, and line size for the document and pass those values as arguments to the SetScrollSizes() function.
Because you're not working with a real document in the Scroll application, we thought we'd simplify things by handling everything in the view class. Further, because the OnDraw() function gets called whenever the user somehow changes the view window, the view class is a good place to demonstrate how this scroll bar stuff works. In the Scroll application, OnDraw() first initializes a LOGFONT structure, as shown in Listing 11.9.
Listing 11.9 LST11_9.TXT-Initializing the Scroll Application's LOGFONT Structure
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_);
Next, the program creates the font and selects it into the DC:
CFont* font = new CFont(); font->CreateFontIndirect(&logFont); CFont* oldFont = pDC->SelectObject(font);
Now that the DC has the font, the program can display the lines of text in the window, which it does as shown in Listing 11.10.
Listing 11.10 LST11_10.TXT-Displaying the Lines of Text
// Initialize the position of text in the window. UINT position = 0; // Create and display eight example fonts. 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; }
You should be familiar with all the code in Listing 11.10. Notice, however, that the loop that draws the text lines uses numLines as a control variable. Because numLines is the member variable that holds the number of lines to display, using it as the loop control variable ensures that the program draws the correct number of lines in the window. The numLines member variable gets initialized to 5 in the view class's constructor. From then on, it gets changed in the OnLButtonDown() and OnRButtonDown() functions, which respond to the user's mouse clicks. Calls to Invalidate() in these functions cause the window and scroll bars to be updated whenever the user changes the number of lines to display. Listing 11.11, for example, shows the code for the OnLButtonDown() function.
Listing 11.11 LST11_11.TXT-Changing the Line Count
void CMyScrollView::OnLButtonDown(UINT nFlags, CPoint point) { // TODO: Add your message handler code here and/or call default // Increase number of lines to display. numLines += 5; // Redraw the window. Invalidate(); CScrollView::OnLButtonDown(nFlags, point); }
After displaying the text lines, the program is ready to adjust the size of the scrollers. In order to do this, the program must know the document, page, and line sizes. The document size is the width and height of the screen area that could hold the entire document. The program calculates the document size by using 100 for the width and using the product of numLines times the font height for the height, like this:
CSize docSize(100, numLines*logFont.lfHeight);
CSize is an MFC class that was created especially for storing the widths and heights of objects. Here, the class's constructor takes the width and height as its two arguments.
The page size is the amount that the window should scroll up and down (or left and right, when you're working with a horizontal scroll bar) when the user clicks in the scroll bar on either side of the scroll thumb. This amount is usually the size of one window. In Scroll, the program calculates the page size, like this:
CRect rect; GetClientRect(&rect); CSize pageSize(rect.right, rect.bottom+2);
The GetClientRect() function fills in a CRect object with the coordinates of the client window. You can use the returned rectangle to determine the size of the window, as shown in the preceding code segment.
Finally, the line size is the amount that the window should scroll when the user clicks a scroll bar's arrow buttons. With a text display, this amount is usually a full text line vertically and a single character horizontally. Scroll calculates the line size by using 0 for the horizontal amount (thus disallowing horizontal scrolling) and using the font height for the vertical amount:
CSize lineSize(0, logFont.lfHeight);
Once the program has size objects created for the document, page, and line sizes, it can set the scroll bars by calling the view class's SetScrollSizes() member function:
SetScrollSizes(MM_TEXT, docSize, pageSize, lineSize);
This function takes as arguments the mapping mode, the document size, the page size, and the line size. The first argument can be any of the mapping-mode constants defined by Windows but will usually be MM_TEXT. The other three arguments are references to CSize objects. After the program calls SetScrollSizes(), MFC will have set the scroll bars properly for the currently viewed document. MFC automatically handles the user's interaction with the scroll bars.
You're really starting to master Visual C++ now. Take some time at this point to look over the CDC class, and the several classes derived from CDC, in your Visual C++ online documentation. You'll discover a wealth of member functions that you can use to create displays for your windows. Remember that CPaintDC is just one type of CDC-derived class, one that's used specifically in the OnPaint() message-response function. If you want to create a DC in order to paint your window in some other part of your program, you'll probably want to use the CClientDC class. To learn more about related topics, check out the following chapters: