Coordinate Mapping

Ron Gery
Microsoft Developer Network Technology Group

Created: March 20, 1992

Abstract

This article discusses mapping modes in the Microsoft® Windows™ graphical environment—what they are, how they work, and what they really mean. Basic use of the following mapping mode functions is included: SetMapMode, SetWindowExt, SetViewportExt, SetWindowOrg, SetViewportOrg, LPtoDP, and DPtoLP.

Introduction

By using the coordinate mapping scheme in the Microsoft® Windows™ graphical environment, an application can set up a logical coordinate system that the constraints of the physical world do not limit. In the default setting, one logical unit equals one pixel on the device, but an application can make that one logical unit mean something else—for example, 0.001 millimeter or one pixel on an imaginary 600 dpi printer. By using logical coordinates, an application can separate its drawing routines from the actual pixel resolution on the destination device.

The coordinate mapping mechanism involves defining two rectangles: the window, which defines a rectangle in the logical coordinate space, and the viewport, which defines a rectangle on the destination device. The two rectangles establish a ratio between logical units and device units—a window extent's worth of units fit in a viewport extent's worth of pixels and vice versa. Similarly, the point that the window origin defines maps directly to the point that the viewport origin defines. Figure 1 loosely illustrates this concept.

Figure 1.

Defining a negative extent can flip the direction of an axis. For example, a negative window y-extent and a positive viewport y-extent result in a logical coordinate system in which positive y goes up instead of following the Windows convention of positive y going down. An application that wants to operate in the traditional mathematical space can do so.

Coordinates are not restricted to the defining rectangles. The rectangles establish only a ratio, not a clipped space.

The graphics device interface (GDI) lets an application set up the window and viewport rectangles or choose from predefined settings. Eight mapping modes are defined in Windows; six have predefined settings, and two allow an application some flexibility. After a device context (DC) is set up with the desired coordinate mapping scenario, subsequent application code need concern itself only with the logical coordinate space that was established.

How Does It Work?

A GDI function that accepts logical coordinates as input converts the points to device units before using them for actual calculation or before passing them to the device. For DCs that belong to a specific window (retrieved from GetDC or using the CS_OWNDC style), an additional translation takes place in GDI because the DC's origin on the screen is not at the upper-left corner. This translation is invisible to the application. DCs that do not belong to a window (such as printer DCs and memory DCs) have device (0,0) at the upper-left corner. The result is that the device driver sees and understands only device units based on (0,0) being at the upper-left corner of the surface.

Conversely, the driver and GDI calculate values in device units and convert them to logical units before returning them to the application.

A pair of formulas describe the math involved in converting logical coordinates to device coordinates and vice versa:

Dx = ((Lx - WOx) * VEx / WEx) + VOx

Lx = ((Dx - VOx) * WEx / VEx) + WOx

The variables involved are:

Dx x value in device units
Lx x value in logical units
WOx window x origin
VOx viewport x origin
WEx window x-extent
VEx viewport x-extent

The same equations with y replacing x transform the y component of a point.

The equations are not complicated. First, the point is offset from its coordinate origin (logical from the window origin, device from the viewport origin). This value, no longer biased by the origin, is then scaled into the destination coordinate system by the ratio of the extents. Last, the scaled value is offset by the destination origin to its final mapping.

Windows version 3.1 and Windows version 3.0 handle computational overflow differently. In Windows version 3.0, as soon as an overflow occurs (in 16-bit numbers), the value is clamped to the largest possible number of the same sign (0x7FFF for positive, 0x8000 for negative), and the calculation is stopped. In Windows version 3.1, the calculation maintains 32-bit values throughout, essentially eliminating the chance for overflow until the very end, where the clamping is the same as in Windows version 3.0.

MM_TEXT

The default mapping mode is MM_TEXT. One logical unit equals one pixel. Positive x is to the right, and positive y is down. This mode maps directly to the device's coordinate system. The logical-to-physical mapping involves only an offset in x and y that is defined by the application-controlled window and viewport origins. The viewport and window extents are all set to 1, creating a one-to-one mapping.

MM_ANISOTROPIC

The MM_ANISOTROPIC mode allows the application full control of coordinate mapping. In this mode, the application sets the window and viewport extents to any desired values. Calling the SetMapMode function with MM_ANISOTROPIC does not change any currently existing mapping settings; mapping does not change until an extent changes.

MM_ISOTROPIC

An application using the MM_ISOTROPIC mapping mode can also change the extent values, the difference being that GDI maintains the mapping so that one logical unit maps to the same physical distance in both x and y. GDI adjusts the viewport extents to make the mapping isotropic. When either the viewport extents or the window extents are set, the viewport shrinks in one direction to match the aspect ratio of the window. Assuming that the viewport x-extent is the limiting factor, the relation between the aspect ratio of the screen and the window/viewport transform is as follows:

yVE = (yWE * ((xVE * xA) / yA)) / xWE

where:

yVE — viewport y-extent
yWE — window y-extent
xVE — viewport x-extent
xWE — window x-extent
xA — device x-aspect (XASPECT index to GetDeviceCaps)
yA — device y-aspect (YASPECT index to GetDeviceCaps)

This mode exists for applications that want to view the world as having square pixels.

Strangely enough, when an application calls the SetMapMode function with MM_ISOTROPIC as the mode, the window and viewport extents change to resemble an MM_LOMETRIC mapping mode.

Other Modes

In other available mapping modes—MM_TWIPS, MM_HIENGLISH, MM_LOENGLISH, MM_HIMETRIC, and MM_LOMETRIC—the viewport and window extents reflect a mapping of one logical unit to a measurable physical distance on the target output device. All of these modes share the behavior that positive y goes up; this is contrary to the Windows convention but in line with the mathematical view of the world.

Mapping mode One unit maps to
MM_TWIPS 1/1440 inch
MM_HIENGLISH 0.001 inch
MM_LOENGLISH 0.01 inch
MM_HIMETRIC 0.01 millimeter
MM_LOMETRIC 0.1 millimeter

Unfortunately, there is no such thing as a true physical measurement for displays in Windows because a display driver has no knowledge of the actual physical size of the target monitor. (Printer measurements, on the other hand, are physically accurate.) As a result, the inches and millimeters in the preceding table are really logical values based on the physical size of an idealized monitor. This should not prevent an application from using these mapping modes; it is only intended as a disclaimer.

When an application calls the SetMapMode function with one of these modes, GDI sets the window and viewport extents based on values retrieved from the driver. The driver is responsible for specifying the correct data that validates these mapping modes. All these modes are inherently isotropic: A logical unit in either x or y maps to the same physical unit.

Setting the Extents

An application can directly alter window and viewport extents only if the DC's mapping mode is set to MM_ANISOTROPIC or MM_ISOTROPIC. To set the extents, an application calls the SetWindowExt and SetViewportExt functions. An application can also alter these values by calling the ScaleWindowExt and ScaleViewportExt functions. Values are always in absolute units (not logical units) and are not affected by the current mapping mode. Setting an extent to 0 is not allowed. Because the pair of extents basically set a ratio to be used for conversion, the magnitude of the extents should be as small as possible to simplify calculations—using extents of 300 and 200 has the same effect as using extents of 3 and 2.

To "flip" an axis from the Windows standard of positive x going right and positive y going down, the ratio of the corresponding extents must be negative. If both the window and the viewport extent components are negative, the effect is the same as if both were positive. To establish an MM_TEXT-like mapping but with positive y going up instead of down, use the following code:

SetMapMode(hDC, MM_ANISOTROPIC);
SetViewportExt(hDC, 1, -1);
SetWindowExt(hDC, 1, 1);

The extents that these calls define do not restrict the coordinates that can be used. The ratio made by these extents converts logical coordinates to device units. The device units are clipped based on the DC's clipping scenario; the mapping setup does not affect clipping.

Using SetMapMode to change the DC's mapping mode to any mode other than MM_ANISOTROPIC or MM_ISOTROPIC causes GDI to change the extents to reflect the new mapping. If an application attempts to change the extents while in one of these modes, nothing happens.

Setting the Origins

You set the window and viewport origins with the SetWindowOrg and SetViewportOrg functions. The origins are independent of the extents, and an application can set them regardless of the current mapping mode. Changing a mapping mode does not affect the currently set origins (although it can affect the extents). Origins are specified in absolute units that the current mapping mode does not affect. You can also alter the origins with the OffsetWindowOrg and OffsetViewportOrg functions.

Examples

To set up a mapping in which one logical unit maps to three device units, an application uses the following code:

SetMapMode(hDC, MM_ANISOTROPIC);
SetWindowOrg(hDC, 0, 0);
SetWindowExt(hDC, 1, 1);
SetViewportOrg(hDC, 0, 0);
SetViewportExt(hDC, 3, 3);

If you insert this code before code that assumes a one-to-one mapping, the resulting output on the destination device is three times as big. If an application also wants the output to move 50 pixels down and to the right, it moves the viewport origin:

SetViewportOrg(hDC, 50, 50);

An application that wants to work in a fixed physical space such as millimeters can use one of the predefined modes. These modes set up a direct mapping to a physical distance, so not much math is involved on the application side. The following code draws a 1-by-2-mm rectangle:

SetMapMode(hDC, MM_HIMETRIC);
SetViewportOrg(hDC, 0, 100);    // place the viewport for visibility
SetWindowOrg(hDC, 0, 0);
Rectangle(hDC, 0, 0, 100, 200);

Because positive y goes up in this mode, the code sets the viewport origin away from (0,0) to let the rectangle be seen. The rectangle starts at (0,100) on the destination device and extends 2 mm up and 1 mm to the right. If 2 mm up is more than 100 pixels on the device, the top of the rectangle is clipped by the top of the output surface.

A more interesting example is an application that wants to lay out its output on an imaginary 600-dpi device. By maintaining image information at this resolution, the application does not lose information until coordinates are converted to a lesser resolution device for output. In this scenario, most of the application's routines can operate in logical 600-dpi space, and only minor setup is required to switch the mapping for a new device. Given a DC all output routines will use, the following code sets up the DC so that the logical units are in 600-dpi space and the device units map to the physical device's resolution:

SetMapMode(hDC, MM_ANISOTROPIC);
SetWindowOrg(hDC, 0, 0);
SetWindowExt(hDC, 600, 600);        // logical window is 600 dpi
SetViewportOrg(hDC, 0, 0);
// Device viewport is dpi of actual output device.
SetViewportExt(hDC, GetDeviceCaps(hDC, LOGPIXELSX), 
                    GetDeviceCaps(hDC, LOGPIXELSY));

The above code maintains logical (0,0) at the upper-left corner, with positive x going right and positive y going down.

To emulate the behavior of the Clipboard viewer application in which the image displayed is sized to the dimensions of the application's window, the mapping mode is set up as follows:

SetMapMode(hDC, MM_ANISOTROPIC);
SetViewportOrg(hDC, 0, 0);
SetWindowOrg(hDC, 0, 0);
// The window is the logical size of the image.
SetWindowExt(hDC, ImageWidth, ImageHeight);
GetClientRect(hWnd, &Rect);
// The viewport is the size of the client window.
SetViewportExt(hDC, Rect.right - Rect.left, Rect.bottom - Rect.top);

This setup assumes that the logical size of the image is known so that the window extents can be set properly.

LPTODP and DPTOLP

An application can map points from logical units to device units with the LPtoDP function and can map points from device units to logical units with the DPtoLP function. These functions calculate the conversions for the application. Internally, GDI uses them to do the computations. Device units are calculated relative to the upper-left corner of the DC.

Scalar Values

Scalar values, that is, values that define a distance and not a location (for example, the extent of a text string), must be converted from logical units to device units and vice versa without being affected by the origins of either the window or the viewport. By treating scalar values as distances from (0,0), the mapping conversion functions can be used for scalar conversions. The values are treated as point values and converted using the appropriate routine; the same routine also converts the point (0,0). To arrive at scalar values, the two converted points are then subtracted, maintaining the distance information. The following code sample converts two scalar values, LogX and LogY, from logical units to device units, DevX and DevY:

pt[0].x = 0;
pt[0].y = 0;                          // logical (0,0)
pt[1].x = LogX;
pt[1].y = LogY;                       // scalar values in question
LPtoDP(hDC, (LPPOINT)pt, 2);
DevX = pt[1].x - pt[0].x;             // scalar x in device units
DevY = pt[1].y - pt[1].y;             // scalar y in device units

An alternative way to calculate this is to use the extents ratio out of the mapping equation because only that actually affects the scalar values. This involves knowing the values of the extents being used (available using the GetWindowExt and GetViewportExt functions). The following code demonstrates this approach:

DevX = MulDiv(LogX, ViewportExtX, WindowExtX);
DevY = MulDiv(LogY, ViewportExtY, WindowExtY);

This code also solves a problem with the first method. Because rounding is necessary, converted values could change depending on the origins being used. By taking the origins out of the picture, pure scalars are converted. In Windows version 3.0, GDI converts all scalar values with the first method; in Windows version 3.1, some conversions are rewritten to use the second method.

When using the MM_TEXT mapping mode, scalar values are the same in both logical units and device units and need not be converted. The mapping between the two modes involves only a translation, which does not affect scalar values.

Interesting Behavior Effects

Whenever the mapping mode or any extent changes, the pen and font that are currently selected into the DC are marked as "dirty" because the meaning of some of their logical components changes. The next time one of these objects is needed for output, the system re-realizes it. The net effect is that of reselecting the object into the DC, which can alter the physical object used. The re-realization does not take place if only the window or the viewport origins change because scalar values (pen width and font height and width) are not affected.

When BitBlt is used with two DCs that have different mapping modes, the result could be a stretched blt instead. The origins and extents of the blt are converted from logical units to device units for both DCs separately, using that DC's mapping mode. If the extents are no longer the same after the conversion, the call becomes, in effect, a call to StretchBlt, and GDI calls the stretching code to perform the operation.

An application that wants to scale an image defined by one of the physical-size modes can do so nicely. When the normal mapping is set for the image, calling the SetMapMode function with MM_ANISOTROPIC changes the mapping mode but also maintains the existing extent information. The image still draws the same but can now be scaled easily by scaling the extents with the ScaleWindowExt and SetViewportExt functions. Changing the mapping mode to a mode other than MM_ANISOTROPIC does not maintain the previous extent information.