Search code examples
winapiprintinggdi

How to determine horizontal and vertical extents in device units with SetViewportExtEx() and printer?


I am experimenting with using the Windows GDI API for printing and have been doing a few experiments to attempt to understand the translation and how window and viewport extents work.

Examples I have found are using GetDeviceCaps() to get the HORZRES and VERTRES dimensions (despite the known fact they can be unreliable and inaccurate) and then using these values with SetViewportExtEx() however they divide the values returned by GetDeviceCaps() by two.

Why is the cxpage and the cypage values halved and how can I predict the values to use and the effect on the printed output? Is this due to using MM_ISOTROPIC as the mapping mode?

Examples use something like the following code:

int cxpage = GetDeviceCaps (hDC, HORZRES);
int cypage = GetDeviceCaps (hDC, VERTRES);
SetMapMode (hDC, MM_ISOTROPIC);
SetWindowExtEx(hDC, 1500, 1500, NULL);
SetViewportExtEx(hDC, cxpage/2, cypage/2, NULL);
SetViewportOrgEx(hDC, 0, 0, NULL);

In my actual test program I have the following function to print a page when my main Windows message handler sees the message IDM_PRINT generated when the user selects Print from the File menu of the test application. The handler uses PrintDlg() to get a handle to a Device Context (hDC) then calls this function to exercise the printing.

int PrintMyPages (HDC hDC)
{
    int cxpage = GetDeviceCaps (hDC, HORZRES);
    int cypage = GetDeviceCaps (hDC, VERTRES);

    // When MM_ISOTROPIC mode is set, an application must call the
    // SetWindowExtEx function before it calls SetViewportExtEx. Note that
    // for the MM_ISOTROPIC mode certain portions of a nonsquare screen may
    // not be available for display because the logical units on both axes
    // represent equal physical distances.
    SetMapMode (hDC, MM_ISOTROPIC);

    // Since mapping mode is MM_ISOTROPIC we need to specify the extents of the
    // window and the viewport we are using to see the window in order to establish
    // the proper translation between window and viewport coordinates.
    SetWindowExtEx(hDC, 1500, 1500, NULL);
    SetViewportExtEx(hDC, cxpage/2, cypage/2, NULL);
    SetViewportOrgEx(hDC, 0, 0, NULL);

    // figure out the page size in logical units for the loop that is printing
    // out the pages of output. we must do this after setting up our window and
    // viewport extents so Windows will calculate the DPtoLP() for the specified
    // translation correctly.
    RECT pageRect = {0};
    pageRect.right = GetDeviceCaps (hDC, HORZRES);
    pageRect.bottom = GetDeviceCaps (hDC, VERTRES);
    DPtoLP(hDC, (LPPOINT)&pageRect, 2);

    // create my font for drawing the text to be printed and select it into the DC for printing.
    HFONT DisplayFont = CreateFont (166, 0, 0, 0, FW_DONTCARE, false, false, false, DEFAULT_CHARSET,
                                      OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY,
                                      DEFAULT_PITCH | FF_DONTCARE, _T("Arial Rounded MT Bold"));
    HGDIOBJ hSave = SelectObject (hDC, DisplayFont);

    POINT ptLine = {300, 200};  // our printer line cursor for where printing should start.

    static DOCINFO di = { sizeof (DOCINFO), TEXT ("INVOICE TABLE : Printing...")};
    StartDoc (hDC, &di);
    StartPage (hDC);

    for (int i = 1; i < 30; i++) {
        TCHAR xBuff[256] = {0};
        swprintf (xBuff, 255, _T("This is line %d of my text."), i);
        TextOut (hDC, ptLine.x, ptLine.y, xBuff, _tcslen(xBuff));
        // get the dimensions of the text string in logical units so we can bump cursor to next line.
        SIZE  lineSize = {0};
        GetTextExtentPoint32(hDC, xBuff, _tcslen(xBuff), &lineSize);
        ptLine.y += lineSize.cy;    // bump the cursor down to the next line of the printer. X coordinate stays the same.
        if (ptLine.y + lineSize.cy > pageRect.bottom) {
            // reached the end of this page so lets start another.
            EndPage (hDC);
            StartPage (hDC);
            ptLine.y = 200;
        }
    }

    // end the final page and then end the document so that physical printing will start.
    EndPage (hDC);
    EndDoc (hDC);

    // Release the font object that we no longer need.
    SelectObject (hDC, hSave);
    DeleteObject (DisplayFont);

    return 1;
}

When I modify the call of SetViewportExtEx() from SetViewportExtEx(hDC, cxpage/2, cypage/2, NULL); (output on the right in the image below) to SetViewportExtEx(hDC, cxpage, cypage, NULL); (output on the left in the image below) the printed text seems almost double in height and width.

enter image description here

Additional Notes on Extents and Mapping Modes

Charles Petzold Programming Windows 5th Edition (Chapter 5 - Basic Drawing, page 180) writes:

The formulas also include two points that specify "extents": the point (xWinExt, yWinExt) is the window extent in logical coordinates; (xViewExt, yViewExt) is the viewpoort extent in device coordinates. In most mapping modes, the extents are implied by the mapping mode and cannot be changed. Each extent means nothing by itself, but the ratio of the viewport extent to the window extent is a scaling factor for converting logical units to device units.

For example, when you set the MM_LOENGLISH mapping mode, Windows sets xViewExt to be a certain number of pixels and xWinExt to be the length in hundredths of an inch occupied by xViewExt pixels. The ratio gives you pixels per hundredths of an inch. The scaling factors are expressed as ratios of integers rather than floating point values for performance reasons.

Petzold then goes on to discuss MM_ISOTROPIC and MM_ANISOTROPIC on page 187.

The two remaining mapping modes are named MM_ISOTROPIC and MM_ANISOTROPIC. These are the only two mapping modes for which Windows lets you change the viewport and window extents, which means that you can change the scaling factor that Windows uses to translate logical and device coordinates. The word isotropic means "equal in all directions"; anisotropic is the opposite - "not equal." Like the metric mapping modes shown earlier, MM_ISOTROPIC uses equally scaled axes. Logical units on the x-axis have the same physical dimensions as logical units on the y-axis. This helps when you need to create images that retain the correct aspect ratio regardless of the aspect ratio of the display device.

The difference between MM_ISOTROPIC and the metric mapping modes is that with MM_ISOTROPIC you can control the physical size of the logical unit. If you want, you can adjust the size of the logical unit based on the client area. This lets you draw images that are always contained within the client area, shrinking and expanding appropriately. The two clock programs in Chapter 8 have isotropic images. As you size the window, the clocks are resized appropriately.

A Windows program can handle the resizing of an image entirely through adjusting the window and viewport extents. The program can then use the same logical units in the drawing functions regardless of the size of the window.


Solution

  • ... why do so many examples use SetViewportExtEx(hDC, cxpage/2, cypage/2, NULL); where cxpage and cypage are GetDeviceCaps(hDC, HORZRES) and GetDeviceCaps(hDC, VERTRES) respectively[?]

    I suspect MM_ISOTROPIC is often used for plotting graphs where the origin would be in the center of the page rather than the corner. If we took your code and tweaked it to move the origin to the center of the printable region, like this:

    SetWindowExtEx(hDC, 1500, 1500, NULL);
    SetViewportExtEx(hDC, cxpage/2, cypage/2, NULL);
    SetViewportOrgEx(hDC, cxpage/2, cypage/2, NULL);
    

    Then you could plot using logical coordinates that range from -1500 to +1500. (You might also want to flip the sign of one of the y-extents to get positive "up".)

    For textual output, I don't see any advantage to halving the viewport extents and I would keep the origin in the upper left.