In Lesson 4 of Chapter 3, you learned how a class derived from the MFC CView class is used to render an image of your application data. You learned how MFC provides a CDC class, which encapsulates a device context and a set of drawing functions that operate upon the device context, to render output on a device. You also learned how the framework passes a CDC-derived device context object, which corresponds to the current output device, to the CView::OnDraw() function. In this way, the OnDraw() function provides a centralized location for your application drawing code. The same code is used whether the output target is an application window, a print preview window, or a page on a printing device.
In this lesson, you will learn more about how to use classes derived from CView and CDC. You will also learn about MFC's drawing tool classes, which you use to send your application's data to the screen or to a printing device.
After this lesson, you will be able to:Estimated lesson time: 40 minutes
- Describe how to set up a logical coordinate system to make your application output appear correctly on any device that conforms to the Windows Graphical Device Interface (GDI).
- Describe the Windows coordinate mapping modes and explain how they are used.
- Describe how to implement scrolling for an MFC application.
- Describe how to use the MFC drawing tool classes to draw in a device context.
- Understand the printing and print preview processes, and how you can implement custom printing functions.
Before you proceed with this lesson, you will need to modify the application drawing function so that it displays the application data rather than "Hello World!"
void CMyAppView::OnDraw(CDC* pDC) { CMyAppDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); CFont aFont; aFont.CreateFont(16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, FF_ROMAN, 0); CFont * pOldFont = pDC->SelectObject(&aFont); CSize TextSize = pDC->GetTextExtent(pDoc->m_string); int nLinePos = 10; for(int i = 0; i < pDoc->m_nLines; i++) { pDC->TextOut(10, nLinePos, pDoc->m_string); nLinePos += TextSize.cy; } pDC->SelectObject(pOldFont); } |
Figure 5.5 MyApp application test output
Graphical devices render their output across a two-dimensional coordinate system. Screen output is mapped to a pixel grid; printers work with a two-dimensional array of dots on the printed page. The GDI drawing functions that are used by Windows applications specify their output in a similar manner. GDI drawing is measured and scaled in abstract units of measure known as logical units.
For example, the following line from our OnDraw() function initializes a font object for outputting text into the device context. The font will be created with a height of 16 logical units.
aFont.CreateFont(16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, FF_ROMAN, 0)); |
The following code moves to the point in the application drawing space with the coordinates (100,200), and draws a horizontal line 200 logical units long to the point (300,200).
pDC->MoveTo(100, 200); pDC->LineTo(300, 200); |
Logical units have no intrinsic value. It is up to you to specify the actual value, in terms of units of measure on the device, of a logical unit. To provide a standard interface to the many different types of screens, printers, and other output devices that an application might encounter, the GDI implements a coordinate mapping system. You can specify a mapping mode, which determines how the logical units of the application drawing space are to be mapped to the drawing space of the hardware device (specified in device units). The GDI ensures that the mapping will produce consistent output on whatever hardware device is attached.
The GDI defines a set of eight mapping modes that specify the ratio between logical coordinates and device coordinates. The default mapping mode is identified by the value MM_TEXT. This maps a logical unit to a single pixel on the device, regardless of the device resolution. This can cause problems with printed output, as illustrated by the following exercise.
Notice how the printer output appears extremely small. This is due to the difference in resolution between the screen and printer. The drawing functions are scaling their output in terms of the resolution of the target device. So a character that is 16 pixels high is about 0.21 inches tall on an 800 x 600 monitor. On a 600 x 600 dots per inch printer, it is only about 0.026 inches tall.
MM_TEXT is one of six fixed mapping modes, which establish predefined mappings between logical units and device units. Other fixed mapping modes map logical units to units of measure on the device. MM_LOENGLISH for example, specifies that a logical unit will correspond to 0.01 inches on the output device. The fixed mapping modes are shown in Table 5.2.
Table 5.2 Fixed Mapping Modes
Mapping mode | One unit maps to |
---|---|
MM_TEXT | 1 device pixel |
MM_LOENGLISH | 0.01 inch |
MM_HIENGLISH | 0.001 inch |
MM_LOMETRIC | 0.1 millimeter |
MM_HIMETRIC | 0.01 millimeter |
MM_TWIPS | 1/1440 inch |
NOTE
There is no true physical measurement for displays in Windows because a display driver has no knowledge of the actual physical size of the target monitor. Thus the inches and millimeters in the preceding table are really logical values based on the physical size of an idealized monitor. Printer measurements, on the other hand, are physically accurate.
In addition to the fixed mapping modes, you can use one of the unconstrained mapping modes MM_ISOTROPIC or MM_ANISOTROPIC. These modes do not set up a predefined mapping and so require you to specify the ratio between logical units and device units. When the MM_ISOTROPIC mode is set, the GDI will continually adjust the mapping to ensure that one logical unit maps to the same physical distance in both x and y directions. The MM_ANISOTROPIC mode allows the x and y coordinates to be adjusted independently.
To specify the ratio between logical units and device units, you must describe two rectangles that specify the relative dimensions of the logical space (the window) and the device space (the viewport). Window dimensions are specified in logical units using the CDC::SetWindowExt() function. Viewport extents are set using CDC::SetViewportExt() and are specified in device units. Information about the current device dimensions can be obtained by using the CDC::GetDeviceCaps() function.
The following code sets up a mapping mode similar to MM_LOENGLISH:
pDC->SetMapMode(MM_ANISOTROPIC); pDC->SetViewPortExt(pDC->GetDeviceCaps(LOGPIXELSX), pDC->GetDeviceCaps(LOGPIXELSY); pDC->SetWindowExt(100, -100); |
The LOGPIXELSX and LOGPIXELSXY parameters to the GetDeviceCaps() function return the number of pixels per logical inch along the width and height of the display. The code specifies that a 1 x 1-inch square on the device is to correspond to a square in the application drawing space that is 100 x 100 logical units. Or to put it another way, one logical unit will correspond to 0.01 inch on the output device.
Note that the y parameter of the SetWindowExt() function has a negative value. By default, device coordinates follow the Windows convention that the origin is located in the upper left corner of the window and y values increase downward. By specifying an inverse ratio between the window and the viewport y values, you can allow your drawing functions to describe output using the traditional mathematical coordinate system, with y values that increase upward. Be aware that because the origin of the onscreen display window (the position (0, 0)) is set by default to the top left corner of the window, your drawing will not be visible if you use y coordinates that increase upward. You will be drawing in the space above the top of the visible window. Either invert the values of your y coordinates or use the CDC::SetViewportOrg() function to offset the device window origin to a point that corresponds to the bottom left corner of your logical window.
The fixed mapping modes follow the mathematical convention that x coordinates increase in value toward the right and that y coordinates increase as they go up. The MM_TEXT mapping mode follows the Windows convention that y coordinate values increase as they go down.
After you have set up a suitable mapping mode for your view, you simply use the GDI drawing functions to render output using logical coordinates. The conversion between logical coordinates and physical (device) coordinates is handled for you. At times, you will need to convert coordinates yourself using the CDC methods LPtoDP() and DPtoLP() because certain types of information are supplied to you only in device coordinates. For example, the location of a mouse-click is supplied as a parameter to the CView::OnLButtonDown() handler function as a pair of physical coordinates. You would have to convert the coordinates if you needed to determine whether the location of the mouse click corresponded to a significant region of your logical drawing space.
When drawing your application output in logical space, you are limited only by the size of the coordinate range. Drawing coordinates in a Windows 95 or 98 application must lie within the range _32,768 through 32,767. Using the MM_LOENGLISH mapping mode, this range maps to nearly 3000 square feet of physical drawing space—considerably larger than any output device that you are likely to encounter.
Windows that are not able to display all of the application output in a single frame should implement scroll bars. Scroll bars should be implemented wherever the user can resize the window, and they should appear at the point where the window frame starts to clip the output in the client area.
Fortunately, MFC makes it easy to add scrolling to your document/view application. You will recall from Lesson 1 of Chapter 2 that the view class for the MyApp application was derived from the CScrollView class. You can implement scrolling yourself in any view class by using the CWnd scrolling functions and handling the WM_VSCROLL and WM_HSCROLL messages, but CScrollView makes it easy for you to implement scrolling by:
The following exercise introduces the scrolling capabilities offered by CScrollView.
Figure 5.6 Testing the MyApp scrolling capabilities
Notice that although scrolling is invoked automatically, it doesn't behave quite as it should. In Figure 5.6, you can see that the right side of the data lines is out of the view and that a horizontal scroll bar has not appeared. You would expect the scroll bars to appear as soon as the frame edges clip the data displayed in the window. Also notice that when you shrink the window further so that a horizontal scroll bar appears, you cannot scroll across to see an entire data line.
The reason for this behavior is that it is your responsibility to specify a logical size and a mapping mode for a scrolling view. When an AppWizard application is created with a view derived from CScrollView, the AppWizard adds the following code to the overloaded CView::OnInitialUpdate() method:
CSize sizeTotal; // TODO: calculate the total size of this view sizeTotal.cx = sizeTotal.cy = 100; SetScrollSizes(MM_TEXT, sizeTotal); |
The CScrollView::SetScrollSizes() function is used to specify the logical size and a mapping mode for the scrolling view. The default OnInitialUpdate() method will set up the view window with dimensions of 100 x 100 logical units. Under the MM_TEXT mapping mode, this corresponds to an onscreen viewport that is 100 x 100 pixels—about a square inch on an 800 x 600 monitor. The SetScrollSizes() function has additional parameters that specify the line size and the page size. The line size is the horizontal and vertical distance to scroll in each direction when the user clicks on a scroll arrow. The page size is the distance to scroll when the user clicks in the scroll bar, but not on a scroll arrow. These last two parameters have default values that are calculated to be in proportion to the total size of the window.
When the MyApp application window is large enough, you can see all the text. If the window is resized so that the view becomes smaller than the size set in the SetScrollSizes() function, the scroll bars are displayed. The scroll bar scale and positions are also calculated on the basis of the size of the scrolling view. This is why, when you were experimenting with the default scrolling behavior of the MyApp application, it was impossible to scroll to see data that lay outside the 100 x 100 logical window area.
To set up a simple scrolling view for a document/view application, you need to decide how large a logical drawing space is required to render your document data. Then determine a suitable mapping mode to scale your drawing to an output device, and set the values with a call to SetScrollSizes() in the view class's OnInitialUpdate() function. The following code snippet illustrates how you might set up a view of 8.5 x 11 inches:
sizeTotal.cx = 850; sizeTotal.cy = 1100; SetScrollSizes(MM_LOENGLISH, sizeTotal); |
This approach is appropriate if you can determine a fixed size for your document data. However, consider an application such as a word processing application, which does not place a constraint on the length of a document. As the user adds lines of text, you will be required to resize the logical window and to recalculate the scale of the scroll bars. In such a case, you will need to call the SetScrollSizes() function frequently as the size of the document data changes.
In the following exercise, you will use the SetScrollSizes() function to adjust the CMyAppView view so that the scroll bars will appear as the displayed data is obscured by the window frame. The SetScrollSizes() function will be called from within the drawing function so that the scroll sizes will be adjusted according to the size of the string that is displayed. The function will also set the mapping mode to MM_LOENGLISH so that the output will appear consistently on the screen and the printer.
CSize TextSize = pDC->GetTextExtent(pDoc->m_string); |
add these lines:
CSize scrollArea = CSize(TextSize.cx, TextSize.cy * pDoc->m_nLines); // Allow a margin scrollArea += CSize(20, 20); SetScrollSizes(MM_LOENGLISH, scrollArea); |
The scroll sizes are set relative to the size of the data that will be displayed and are specified by the length of the string and the number of lines that are to be displayed.
When the MM_TEXT mapping mode is used, the y coordinate values increase downward and the other fixed mapping modes follow the mathematical convention, with y values that increase upward. As you have changed the mapping mode from the default MM_TEXT to MM_LOENGLISH, you will need to invert the y coordinates in your display code so that their values decrease as you proceed down the page.
int nPos = 10; for(int i = 0; i < pDoc->m_nLines; i++) { pDC->TextOut(10, nPos, pDoc->m_string); nPos += TextSize.cy; } |
void CMyAppView::OnDraw(CDC* pDC) { CMyAppDoc* pDoc = GetDocument(); ASSERT_VALID(pDoc); CFont aFont; aFont.CreateFont(16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, FF_ROMAN, 0); CFont * pOldFont = pDC->SelectObject(&aFont); CSize TextSize = pDC->GetTextExtent(pDoc->m_string); CSize scrollArea = CSize(TextSize.cx, TextSize.cy * pDoc->m_nLines); // Allow a margin scrollArea += CSize(20, 20); SetScrollSizes(MM_LOENGLISH, scrollArea); int nPos = -10; for(int i = 0; i < pDoc->m_nLines; i++) { pDC->TextOut(10, nPos, pDoc->m_string); nPos -= TextSize.cy; } pDC->SelectObject(pOldFont); } |
The framework passes to your OnDraw() function a pointer to a device context object, which represents the client area of your application window. The CDC class, the base class for all MFC device context objects, provides a number of functions that enable you to draw lines, shapes, and fill areas; output text; and manipulate bitmap patterns. You have met a few of these functions, such as LineTo(), Rectangle(), and TextOut(), in the examples throughout this chapter.
The drawing functions work together with the MFC drawing tool classes, which include CPen (for drawing lines), CBrush (for filling areas), CFont, and Cbitmap, among others. A drawing tool of each type is selected into the device context for use with the drawing functions. Thus the CDC::Rectangle() function will draw a rectangle in the device context using the current CPen and fill it using the current CBrush.
The recommended procedure for using the drawing tool graphic objects is as follows:
The OnDraw() function that you implemented for the MyApp application provides a good example of the procedure. The following lines declare and create a new font and select it into the current device context:
CFont aFont; aFont.CreateFont(16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, FF_ROMAN, 0); CFont * pOldFont = pDC->SelectObject(&aFont); |
The aFont font is used in subsequent calls to the CDC::TextOut() function to display text in the device context. When the function has finished with the font, it discards the aFont object and uses the following line of code to restore the object stored in the pOldFont pointer:
pDC->SelectObject(pOldFont); |
TIP
Rather than saving and restoring each individual object in the device context, you can use the CDC methods SaveDC() and RestoreDC() to save and restore the entire device context.
In this courseware, we will only describe how to implement a passive display of application data. A fair amount of work needs to go into developing views that support features such as object selection, cut, copy, and paste, the manipulation of on-screen objects by the user, drawing with the mouse, and so forth. The DRAWCLI sample application that is provided with Visual C++ gives a good example of how to implement many of these features.
In Lesson 4 of Chapter 3, you learned that the process of rendering an image of your application data on a printed page is simple because the same OnDraw() function is used for both screen and printer output. The functions CView::OnPrint() and CWnd::OnPaint() both call the OnDraw() function to render output.
When printing, the framework will perform the following tasks:
Several of the functions, including those for implementing pagination for the printed document, allocating GDI resources for printing, and sending escape codes to change the printer mode before printing a given page, can be overridden. You can use these functions to customize the printing process. Table 5.3 lists the printing functions that can be overridden (all members of the CView class).
Table 5.3 Printing Functions That Can Be Overridden
Name | Function |
---|---|
OnPreparePrinting() | Allows you to modify a CPRINTIINFO structure to insert values into the Print dialog box—commonly used to set the length of the document. Passes the CPRINTIINFO structure to the CView::DoPreparePrinting() method, which displays the dialog box and creates the printer device context object. |
OnBeginPrinting() | Allows you to allocate fonts or other GDI resources used for printed output. |
OnPrepareDC() | Allows you to adjust attributes of the device context for a given page. If the length of the document hasn't been specified, a check for the end of the document is performed. |
OnPrint() | Allows you to print a given page. By default, this function simply calls OnDraw() to render the output, although you can override OnPrint() to provide printed output that is significantly different to the screen display. |
OnEndPrinting() | Allows you to disconnect GDI resources. |
Figure 5.7 illustrates the full printing cycle and shows the order in which these functions are called.
Figure 5.7 The MFC printing cycle
The Print dialog box in the MyApp application currently allows you to select a range of pages to print. This feature should be made unavailable for an application that allows you to print only one page of data. In the following exercise, you will add code to the CMyAppView::OnPreparePrinting() function to set the maximum number of document printer pages to 1. This has the effect of making the page range selection feature in the Print dialog box unavailable.
pInfo->SetMaxPage(1); |
When the user selects the Print Preview command from the File menu, the framework creates a CPreviewDC object. Whenever your application performs an operation that sets a characteristic of the printer device context, the framework also performs a similar operation on the preview device context. For example, if your application selects a font for printing, the framework selects a font for screen display that simulates the printer font. Whenever your application sends output to the printer, the framework sends the output to the screen.
Print Preview differs from printing in the order that the pages of a document are drawn. During printing, the framework loops until a certain range of pages has been rendered. During Print Preview, one or two pages are displayed and then the application waits. No further pages are displayed until the user clicks Next Page or Previous Page. During Print Preview, the application must also respond to WM_PAINT messages, just as it does during ordinary screen display.
The OnPreparePrinting() function is called when preview mode is invoked, just as it is when beginning a print job. The CPRINTINFO structure passed to the function contains several members whose values you can set to adjust certain characteristics of the print preview operation. For example, you can set the m_nNumPreviewPages member to specify whether you want to preview the document in one-page or two-page mode.
You can rather easily modify the behavior and appearance of print preview in a number of ways. Some of the modifications you can make include those listed on the next page.
The Windows GDI provides a layer of abstraction between applications and the many different types of output devices upon which an application can display output. The graphical output of applications is measured and scaled in logical units. The GDI implements a coordinate mapping system, which determines how the logical units of the application output are to be mapped to the drawing space of the hardware device. This ensures consistent application output on whatever hardware device is attached.
The GDI defines a set of eight mapping modes that specify the ratio between logical coordinates and device coordinates. You can choose one of the six fixed mapping modes that establish predefined mappings between logical units and units of measure on the device, or one of the two unconstrained mapping modes that allow you to specify the ratio between logical units and device units.
Scroll bars should be implemented for a window whenever the window is not able to display all of the application output in a single frame. MFC makes it easy to add scrolling to your document/view application by allowing you to derive your application view class from the MFC class CScrollView. It is your responsibility to specify a size and a mapping mode for a scrolling view so that the framework knows when and how to implement scrolling capabilities.
MFC provides a number of drawing tool classes, which are used in conjunction with the GDI drawing functions to render application output in a device context. Generally, you create and configure drawing tool objects to suit the needs of your application. You select these objects into the device context using the CDC::SelectObject() function, which returns a pointer to the previously selected object. You should always use this pointer to restore the object at the end of your drawing function to preserve the original state of the device context.
When printing a document, the framework calls a sequence of functions. All the functions are virtual member functions of the CView class. You can override these functions to customize the printing process. The most common task is to implement pagination for the printed document. Other tasks include the allocation of any GDI resources needed for printing, and sending escape codes to change the printer mode before printing a given page. You can also use some of these functions to perform customizations that are specific to the display of application data in a print preview window.