Delphi7 リモートデスクトップ環境でのデフォルトプリンターの取得に関する問題

今日はニッチな話。

過去記事の FreeRDP によるシンクライアントの運用を行う中で、以下の謎の現象に悩まされていた。

  • リモートデスクトップ環境の中で Delphi アプリケーション(Delphi7 でビルド)からデフォルトプリンターに印刷すると、デフォルトではないプリンターに印刷されてしまう。
  • この時、Excel 等のアプリケーションで印刷すると、問題なくデフォルトプリンターから印刷される
  • この現象は必ず起こるわけではないが、一度発生すると何度印刷しても同じ

色々調べたところ、以下のことがわかった。

  • Delphi で、デフォルトプリンターを得るコード(Printers.pas: SetToDefaultPrinter()の中にある)は以下の通り。

    • EnumPrinters() の第1引数に PRINTER_ENUM_DEFAULT を渡してデフォルトプリンターを得る
    • これに失敗した場合は、GetProfileString() でデフォルトプリンターを得る
  • どの Windows のバージョンからかはわからないが、少なくとも Windows 2012 Server では EnumPrinters() でデフォルトプリンターを得ることはできなくなっているようだ(EnumPrinters function (Windows) には PRINTER_ENUM_DEFAULT 引数について触れられていない)

  • そのため、GetProfileString() でデフォルトプリンターを得ることになるが、これは非常に古い API であるため、おそらくリモートデスクトップのようなマルチユーザー環境のことは考えられていないと思われる
  • つまり、GetProfileString() では、後からリモートデスクトップ環境にログインしたユーザーのデフォルトプリンター名が返されてしまう

解決方法

Windows 2000 以降では、GetDefaultPrinter() という API が用意されているのでこれを利用するように Printers.pas を改変する。

interface 部に以下を追加

function GetDefaultPrinterA(pszBuffer: PChar; var pcchBuffer: DWORD): BOOL; stdcall; external 'winspool.drv' name 'GetDefaultPrinterA';

SetToDefaultPrinter() に以下の変数を追加

LBuffer: String;
LSize: DWORD;

同じく

EnumPrinters(PRINTER_ENUM_DEFAULT, nil, 5, PrinterInfo, ByteCnt, ByteCnt, StructCnt);
if StructCnt > 0 then
  Device := PrinterInfo.pPrinterName
else begin
  GetProfileString('windows', 'device', '', DefaultPrinter, SizeOf(DefaultPrinter) - 1);
  Cur := DefaultPrinter;
  Device := FetchStr(Cur);
end;

コメントアウトし、代わりに以下を追加。

SetLength(LBuffer, 255);
LSize := Length(LBuffer);
GetDefaultPrinterA(PChar(LBuffer), LSize);
Device := PAnsiChar(LBuffer);

とりあえずこれで正しく動作するようになった。Windows95/98 をサポートするには少し変更する必要があるが、さすがにもう良いような気がする...。