delphiwinapidelphi-xe6

DocumentProperties fails in XE6; works in Delphi 7


There's another bug tucked away in Delphi XE6 (probably added around the time Unicode support was added).

You can originally expose it by trying to call:

procedure TForm1.Button1Click(Sender: TObject);
begin
   Printer.Orientation := poLandscape; //use Vcl.Printers
end;

This fails with the cryptic error

Operation not supported on the selected printer

Debug their code

When you trace into the VCL, the problem comes down to the fact that the global TPrinter was unable to get a DEVMODE structure for the printer. This fails when it tries to call the Windows DocumentProperties function from Vcl.Printers:

if DeviceMode = 0 then  // alloc new device mode block if one was not passed in
begin
    DeviceMode := GlobalAlloc(GHND,
          DocumentProperties(0, FPrinterHandle, ADevice, nil, nil, 0));
    //...snip...
end;

bufferSize := DocumentProperties(0, FPrinterHandle, ADevice, PDeviceMode(@dummyDevMode), PDeviceMode(@dummyDevMode), 0); //20160522 Borland forgot to check the result

What is strange is that DocumentProperties is failing: it is returning -1. This is strange because there's nothing particularly conceptually wrong with the parameters.

DocumentProperties isn't documented to SetLastError on failure, but GetLastError consistently returns:

50 - The request is not supported

Code Review

There is some pretty bad code going on here:

But it only fails in XE6

What is strange is that the same code works in Delphi 7. It shouldn't be failing in Unicode-enabled XE6. Looking at the header translation of DocumentProperties from Winapi.WinSpool in XE6:

function DocumentProperties( hWnd: HWND; hPrinter: THandle; pDeviceName: LPWSTR; const pDevModeOutput: TDeviceMode;  var pDevModeInput: TDeviceMode;  fMode: DWORD): Longint; stdcall; overload;
function DocumentProperties( hWnd: HWND; hPrinter: THandle; pDeviceName: LPWSTR;       pDevModeOutput: PDeviceMode;      pDevModeInput: PDeviceMode;  fMode: DWORD): Longint; stdcall; overload;
function DocumentPropertiesA(hWnd: HWND; hPrinter: THandle; pDeviceName: LPSTR;  const pDevModeOutput: TDeviceModeA; var pDevModeInput: TDeviceModeA; fMode: DWORD): Longint; stdcall; overload;
function DocumentPropertiesA(hWnd: HWND; hPrinter: THandle; pDeviceName: LPSTR;        pDevModeOutput: PDeviceModeA;     pDevModeInput: PDeviceModeA; fMode: DWORD): Longint; stdcall; overload;
function DocumentPropertiesW(hWnd: HWND; hPrinter: THandle; pDeviceName: LPWSTR; const pDevModeOutput: TDeviceModeW; var pDevModeInput: TDeviceModeW; fMode: DWORD): Longint; stdcall; overload;
function DocumentPropertiesW(hWnd: HWND; hPrinter: THandle; pDeviceName: LPWSTR;       pDevModeOutput: PDeviceModeW;     pDevModeInput: PDeviceModeW; fMode: DWORD): Longint; stdcall; overload;

They did some pretty fancy const-var / typed-untyped overloading footwork there.

Where Delphi 7 has the simpler:

function DocumentProperties( hWnd: HWND; hPrinter: THandle; pDeviceName: PChar;     const pDevModeOutput: TDeviceMode;  var pDevModeInput: TDeviceMode;  fMode: DWORD): Longint; stdcall;
function DocumentPropertiesA(hWnd: HWND; hPrinter: THandle; pDeviceName: PAnsiChar; const pDevModeOutput: TDeviceModeA; var pDevModeInput: TDeviceModeA; fMode: DWORD): Longint; stdcall;
function DocumentPropertiesW(hWnd: HWND; hPrinter: THandle; pDeviceName: PWideChar; const pDevModeOutput: TDeviceModeW; var pDevModeInput: TDeviceModeW; fMode: DWORD): Longint; stdcall;

Complete Minimal Test Program

It's after midnight here. Some of you are just waking up. And i'm past my bedtime, with a lot of cursing and swearing:

program Project1;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils, Windows, WinSpool;

var
    dwBufferLen: DWORD;
    defaultPrinter: string;
    ADevice: PChar; //Pointer to printer name
    printerHandle: THandle;
    devModeSize: Integer;
    deviceMode: THandle;
begin
    dwBufferLen := 1024;
    SetLength(defaultPrinter, dwBufferLen);
    GetDefaultPrinter(PChar(defaultPrinter), @dwBufferLen);
    SetLength(defaultPrinter, dwBufferLen);
    ADevice := PChar(defaultPrinter);

    if not OpenPrinter(ADevice, {var}printerHandle, nil) then
        raise Exception.Create('Error checking removed for expository purposes');

    devModeSize := DocumentProperties(0, printerHandle, ADevice, nil, nil, 0);
    if devModeSize < 0 then
    begin
        //DocumentProperties is documented to have failed if it returns a value less than zero.
        //It's not documented to have also SetLastError; but we'll raise it anyway (Error code 50 - The request is not supported)
        RaiseLastOSError;
        Exit;
        //It's a good thing we fail. Because the return value -1 is coerced into an unsigned $FFFFFFFF.
        //Delphi then asks GlobalAlloc to try to allocate 4 GB of memory. *le sigh*
    end;

    deviceMode := GlobalAlloc(GHND, NativeUInt(devModeSize));
    if deviceMode = 0 then
        raise Exception.Create('It''s DocumentProperties above that fails. GlobalAlloc is just the victim of being asked to allocate 4GB of memory.');
end.

How to make go?

Bonus Chatter


Solution

  • I have had this bug too ... It always returns -1 but only if I debug in IDE. The bug just appeared out of the blue. I think it was a Windows Update or an automated driver update. I didn't change anything specific on my Workstation setup. Well after hours of testing and debugging, I noticed a trick which solved the problem:

    Querying for "GetDriverInfos" seems to issue some kind of reset and the PrinterSystem starts working.

    DevSize := DocumentPropertiesA(0,FDriverHandle,FDeviceName,nil, nil,0);
    if DevSize = -1 then
    begin
      log('Failed to communicate with printer driver! Trying to ByPass Bug ');
      GetDriverInfos(FDriverHandle);
      DevSize := DocumentPropertiesA(0,FDriverHandle,FDeviceName,nil, nil,0);
      if DevSize <> -1 then
         log('Bug bypassed.');
    end;
    

    I know that's strange, it worked for me (Using Berlin 10.1). We had this bug in all Delphi versions before, appearing randomly.