views:

22

answers:

1

I'm displaying a CPrintDialogEx dialog to choose a printer and modify the settings. I set the hDevNames member so that a default printer will be selected, but I leave hDevMode set to NULL. On successful return I pull some values such as paper size out of the returned DEVMODE structure from hDevMode.

I'm having a problem because hDevMode appears to be initialized with the values from the default printer that I passed in, not the printer that was finally selected. How do I get the parameters from the actual selected printer?


As requested here's the relevant part of the code. I've deleted some of it in the interest of space. TOwnedHandle is a smart pointer I wrote for holding a memory handle and locking it automatically.

CPrintDialogEx dlg(PD_ALLPAGES | PD_NOCURRENTPAGE | PD_NOPAGENUMS | PD_NOSELECTION, this);
ASSERT(dlg.m_pdex.hDevMode == NULL);
ASSERT(dlg.m_pdex.hDevNames == NULL);
dlg.m_pdex.hDevNames = GlobalAlloc(GHND, sizeof(DEVNAMES) + iSizeName);
DEVNAMES * pDevNames = (DEVNAMES *) GlobalLock(dlg.m_pdex.hDevNames);
// ...
GlobalUnlock(dlg.m_pdex.hDevNames);
if ((dlg.DoModal() == S_OK) && (dlg.m_pdex.dwResultAction == PD_RESULT_PRINT))
{
    TOwnedHandle<DEVMODE> pDevMode = dlg.m_pdex.hDevMode;
    TRACE("Printer config = %dx%d %d\n", (int)pDevMode->dmPaperWidth, (int)pDevMode->dmPaperLength, (int)pDevMode->dmOrientation);
    // ...
}


Edit: I've determined that I don't get the problem if I don't set the hDevNames parameter. I wonder if I've discovered a Windows bug? This is in XP, I don't have a more recent version of Windows handy to test with.

I've distilled the code into a test that doesn't use MFC, this is strictly a Windows API problem. This is the whole thing, nothing left out except the definition of pDefaultPrinter - but of course it doesn't do anything useful anymore.

    PRINTDLGEX ex = {sizeof(PRINTDLGEX)};
    ex.hwndOwner = m_hWnd;
    ex.Flags = PD_ALLPAGES | PD_NOCURRENTPAGE | PD_NOPAGENUMS | PD_NOSELECTION;
    ex.nStartPage = START_PAGE_GENERAL;
#if 1
    int iSizeName = (strlen(pDefaultPrinter) + 1) * sizeof(char);
    ex.hDevNames = GlobalAlloc(GHND, sizeof(DEVNAMES) + iSizeName);
    DEVNAMES * pDevNames = (DEVNAMES *) GlobalLock(ex.hDevNames);
    ASSERT(pDevNames != NULL);
    pDevNames->wDeviceOffset = sizeof(DEVNAMES);
    strcpy((char *)pDevNames + pDevNames->wDeviceOffset, pDefaultPrinter);
    GlobalUnlock(ex.hDevNames);
#endif
    HRESULT hr = PrintDlgEx(&ex);
    if ((hr == S_OK) && (ex.dwResultAction == PD_RESULT_PRINT))
    {
        DEVMODE * pdm = (DEVMODE *) GlobalLock(ex.hDevMode);
        ASSERT(pdm != NULL);
        TRACE("Printer config = %dx%d %d\n", (int)pdm->dmPaperWidth, (int)pdm->dmPaperLength, (int)pdm->dmOrientation);
        GlobalUnlock(ex.hDevMode);
        DEVNAMES * pdn = (DEVNAMES *) GlobalLock(ex.hDevNames);
        ASSERT(pdn != NULL);
        TRACE(_T("Printer device = %s\n"), (char *)pdn + pdn->wDeviceOffset);
        GlobalUnlock(ex.hDevNames);
    }

If I can't get a fix, I'd love to hear of a work-around.

A: 

After much head scratching I think I've figured it out.

When the dialog comes up initially, the hDevMode member gets filled with the defaults for the printer that is initially selected. If you select a different printer before closing the dialog, that DEVMODE structure is presented to the new printer driver; if the paper size doesn't make sense to the driver it may change it, and the drivers are not consistent.

The reason this tripped me up is that I was switching between three printers: two label printers with very different characteristics, and a laser printer with US Letter paper.

  • The laser printer always responds with the proper dimensions but may indicate a wrong paper size code.
  • The first label printer will override the size provided by the laser printer but not the other label printer.
  • The second label printer will accept the size provided by the first label printer, because it's capable of using that size even though it's not loaded and not configured. It modifies the size provided by the laser printer by returning the maximum width and the Letter size length of 11 inches.

I determined two ways to work around the problem. The first is to implement IPrintDialogCallback and respond to SelectionChange calls by reloading the default DEVMODE for the newly selected printer. EDIT: I tried this and it does not work. CPrintDialogEx already implements an IPrintDialogCallback interface, making this easy. It appears that PrintDlgEx has its own internal handle that it uses to track the current DEVMODE structure and only uses the one in the PRINTDLGEX structure for input/output. There's no way to affect the DEVMODE while the dialog is up, and by the time it returns it's too late.

The second solution is to ignore the returned results entirely and work from the default paper configuration for the printer. Any changes made from the printer defaults within the dialog are lost completely, but for my application this is acceptable.

bool MyDialog::GetPaperSize(const TCHAR * pPrinterName, double & dPaperWidth, double & dPaperLength)
{
    // you need to open the printer before you can get its properties
    HANDLE hPrinter;
    if (OpenPrinter((TCHAR *)pPrinterName, &hPrinter, NULL))
    {
        // determine how much space is needed for the DEVMODE structure by the printer driver
        int iDevModeSize = DocumentProperties(m_hWnd, hPrinter, (TCHAR *)pPrinterName, NULL, NULL, 0);
        ASSERT(iDevModeSize >= sizeof(DEVMODE);

        // allocate a DEVMODE structure and initialize it to a clean state
        std::vector<char> buffer(iDevModeSize, 0);
        DEVMODE * pdm = (DEVMODE *) &buffer[0];
        pdm->dmSpecVersion = DM_SPECVERSION;

        DocumentProperties(m_hWnd, hPrinter, (TCHAR *)pPrinterName, pdm, NULL, DM_OUT_BUFFER);
        ClosePrinter(hPrinter);

        // convert paper size from tenths of a mm to inches
        dPaperWidth = pdm->dmPaperWidth / 254.;
        dPaperLength = pdm->dmPaperLength / 254.;

        return true;
    }
    return false;
}
Mark Ransom