Up to now, your drawing units have been display pixels, also known as device coordinates. The EX04A drawing units are pixels because the device context has the default mapping mode, MM_TEXT, assigned to it. The statement
pDC->Rectangle(CRect(0, 0, 200, 200));
draws a square of 200-by-200 pixels, with its top-left corner at the top left of the window's client area. (Positive y values increase as you move down the window.) This square would look smaller on a high-resolution display of 1024-by-768 pixels than it would look on a standard VGA display that is 640-by-480 pixels, and it would look tiny if printed on a laser printer with 600-dpi resolution. (Try EX04A's Print Preview feature to see for yourself.)
What if you want the square to be 4-by-4 centimeters (cm), regardless of the display device? Windows provides a number of other mapping modes, or coordinate systems, that can be associated with the device context. Coordinates in the current mapping mode are called logical coordinates. If you assign the MM_HIMETRIC mapping mode, for example, a logical unit is 1/100 millimeter (mm) instead of 1 pixel. In the MM_HIMETRIC mapping mode, the y axis runs in the opposite direction to that in the MM_TEXT mode: y values decrease as you move down. Thus, a 4-by-4-cm square is drawn in logical coordinates this way:
pDC->Rectangle(CRect(0, 0, 4000, -4000));
Looks easy, doesn't it? Well, it isn't, because you can't work only in
logical coordinates. Your program is always switching between device coordinates and logical coordinates, and you need to know when to convert between them. This section gives you a few rules that could make your programming life easier. First you need to know what mapping modes Windows gives you.
The MM_TEXT Mapping Mode
At first glance, MM_TEXT appears to be no mapping mode at all, but rather another name for device coordinates. Almost. In MM_TEXT, coordinates map to pixels, values of x increase as you move right, and values of y increase as you move down, but you're allowed to change the origin through calls to the CDC functions SetViewportOrg and SetWindowOrg. Here's some code that sets the window origin to (100, 100) in logical coordinate space and then draws a 200-by-200-pixel square offset by (100, 100). (An illustration of the output is shown in Figure 4-2.) The logical point (100, 100) maps to the device point (0, 0). A scrolling window uses this kind of transformation.
void CMyView::OnDraw(CDC* pDC) { pDC->SetMapMode(MM_TEXT); pDC->SetWindowOrg(CPoint(100, 100)); pDC->Rectangle(CRect(100, 100, 300, 300)); }
Figure 4-2. A square drawn after the origin has been moved to (100, 100).
The Fixed-Scale Mapping Modes
One important group of Windows mapping modes provides fixed scaling. You have already seen that, in the MM_HIMETRIC mapping mode, x values increase as you move right and y values decrease as you move down. All fixed mapping modes follow this convention, and you can't change it. The only difference among the fixed mapping modes is the actual scale factor, listed in the table shown here.
Mapping Mode | Logical Unit |
MM_LOENGLISH | 0.01 inch |
MM_HIENGLISH | 0.001 inch |
MM_LOMETRIC | 0.1 mm |
MM_HIMETRIC | 0.01 mm |
MM_TWIPS | 1/1440 inch |
The last mapping mode, MM_TWIPS, is most often used with printers. One twip unit is 1/20 point. (A point is a type measurement unit. In Windows it equals exactly 1/72 inch.) If the mapping mode is MM_TWIPS and you want, for example, 12-point type, set the character height to 12 × 20, or 240, twips.
The Variable-Scale Mapping Modes
Windows provides two mapping modes, MM_ISOTROPIC and MM_ANISOTROPIC, that allow you to change the scale factor as well as the origin. With these mapping modes, your drawing can change size as the user changes the size of the window. Also, if you invert the scale of one axis, you can "flip" an image about the other axis and you can define your own arbitrary fixed-scale factors.
With the MM_ISOTROPIC mode, a 1:1 aspect ratio is always preserved. In other words, a circle is always a circle as the scale factor changes. With the MM_ANISOTROPIC mode, the x and y scale factors can change independently. Circles can be squished into ellipses.
Here's an OnDraw function that draws an ellipse that fits exactly in its window:
void CMyView::OnDraw(CDC* pDC) { CRect rectClient; GetClientRect(rectClient); pDC->SetMapMode(MM_ANISOTROPIC); pDC->SetWindowExt(1000, 1000); pDC->SetViewportExt(rectClient.right, -rectClient.bottom); pDC->SetViewportOrg(rectClient.right / 2, rectClient.bottom / 2); pDC->Ellipse(CRect(-500, -500, 500, 500)); }
What's going on here? The functions SetWindowExt and SetViewportExt work together to set the scale, based on the window's current client rectangle returned by the GetClientRect function. The resulting window size is exactly 1000-by-1000 logical units. The SetViewportOrg function sets the origin to the center of the window. Thus, a centered ellipse with a radius of 500 logical units fills the window exactly, as illustrated in Figure 4-3.
Figure 4-3. A centered ellipse drawn in the MM_ANISOTROPIC mapping mode.
Here are the formulas for converting logical units to device units:
x scale factor = x viewport extent / x window extent
y scale factor = y viewport extent / y window extent
device x = logical x × x scale factor + x origin offset
device y = logical y × y scale factor + y origin offset
Suppose the window is 448 pixels wide (rectClient.right). The right edge of the ellipse's client rectangle is 500 logical units from the origin. The x scale factor is 448/1000, and the x origin offset is 448/2 device units. If you use the formulas shown on the previous page, the right edge of the ellipse's client rectangle comes out to 448 device units, the right edge of the window. The x scale factor is expressed as a ratio (viewport extent/window extent) because Windows device coordinates are integers, not floating-point values. The extent values are meaningless by themselves.
If you substitute MM_ISOTROPIC for MM_ANISOTROPIC in the preceding example, the "ellipse" is always a circle, as shown in Figure 4-4. It expands to fit the smallest dimension of the window rectangle.
Figure 4-4. A centered ellipse drawn in the MM_ISOTROPIC mapping mode.
Coordinate Conversion
Once you set the mapping mode (plus the origin) of a device context, you can use logical coordinate parameters for most CDC member functions. If you get the mouse cursor coordinates from a Windows mouse message (the point parameter in OnLButtonDown), for example, you're dealing with device coordinates. Many other MFC functions, particularly the member functions of class CRect, work correctly only with device coordinates.
The CRect arithmetic functions use the underlying Win32 RECT arithmetic functions, which assume that right is greater than left and bottom is greater than top. A rectangle (0, 0, 1000, -1000) in MM_HIMETRIC coordinates, for example, has bottom less than top and cannot be processed by functions such as CRect::PtInRect unless your program first calls CRect::NormalizeRect, which changes the rectangle's data members to (0, -1000, 1000, 0).
Furthermore, you're likely to need a third set of coordinates that we will call physical coordinates. Why do you need another set? Suppose you're using the MM_LOENGLISH mapping mode in which a logical unit is 0.01 inch, but an inch on the screen represents a foot (12 inches) in the real world. Now suppose the user works in inches and decimal fractions. A measurement of 26.75 inches translates to 223 logical units, which must be ultimately translated to device coordinates. You will want to store the physical coordinates as either floating-point numbers or scaled long integers to avoid rounding-off errors.
For the physical-to-logical translation you're on your own, but the Windows GDI takes care of the logical-to-device translation for you. The CDC functions LPtoDP and DPtoLP translate between the two systems, assuming the device context mapping mode and associated parameters have already been set. Your job is to decide when to use each system. Here are a few rules of thumb:
Suppose you need to know whether the mouse cursor is inside a rectangle when the user presses the left mouse button. The code is shown here.
// m_rect is CRect data member of the derived view class with MM_LOENGLISH // logical coordinates void CMyView::OnLButtonDown(UINT nFlags, CPoint point) { CRect rect = m_rect; // rect is a temporary copy of m_rect. CClientDC dc(this); // This is how we get a device context // for SetMapMode and LPtoDP // -- more in next chapter dc.SetMapMode(MM_LOENGLISH); dc.LPtoDP(rect); // rect is now in device coordinates if (rect.PtInRect(point)) { TRACE("Mouse cursor is inside the rectangle.\n"); } }
Notice the use of the TRACE macro (covered in Chapter 3).
As you'll soon see, it's better to set the mapping mode in the virtual CView function OnPrepareDC instead of in the OnDraw function.
The EX04B ExampleConverting to the MM_HIMETRIC Mapping Mode
EX04B is EX04A converted to MM_HIMETRIC coordinates. The EX04B project on the companion CD-ROM uses new class names and filenames, but the instructions here take you through modifying the EX04A code. Like EX04A, EX04B performs a hit-test so that the ellipse changes color only when you click inside the bounding rectangle.
void CEx04aView::OnPrepareDC(CDC* pDC, CPrintInfo* pInfo) { pDC->SetMapMode(MM_HIMETRIC); CView::OnPrepareDC(pDC, pInfo); }
The application framework calls the virtual OnPrepareDC function just before it calls OnDraw.
CEx04aView::CEx04aView() : m_rectEllipse(0, 0, 4000, -4000) { m_nColor = GRAY_BRUSH; }
void CEx04aView::OnLButtonDown(UINT nFlags, CPoint point) { CClientDC dc(this); OnPrepareDC(&dc); CRect rectDevice = m_rectEllipse; dc.LPtoDP(rectDevice); if (rectDevice.PtInRect(point)) { if (m_nColor == GRAY_BRUSH) { m_nColor = WHITE_BRUSH; } else { m_nColor = GRAY_BRUSH; } InvalidateRect(rectDevice); } }