When The Cancel Button is Pressed

「キャンセルボタンが押された時は?」

JpegファイルをTImageに読み込み、プリンターのプロパティを設定して印刷するとき、設定画面にある「OKボタン」ではなく、「キャンセルボタン」が「押された時」の対応方法がわからずハマった話。

1.つくった練習プログラム
2.キャンセルボタンが押されたら・・・?
3.TOpenDialogなら・・・
4.解決方法を知る
5.まとめ
6.お願いとお断り

1.つくった練習プログラム

Formに最低限必要なVCLコンポーネントを以下のように配置。
(右上にある「Printerを選択」のVCLはTLabel(のCaption)。これはなくてもOK。)

Formの幅は810、高さは480に設定

GUIを作成後、Mr.XRAYさんのWebサイトにあった情報を参照して、プリンタのプロパティのダイアログを表示して印刷設定する方法で、練習プログラムを書いてみた。Mr.XRAYさんのWebサイトにはいつも本当にお世話になっています。詳細かつ丁寧なご説明に心より感謝申し上げます。

013_プリンタのプロパティ設定

http://mrxray.on.coocan.jp/Delphi/plSamples/013_SetPrinterProperty.htm#04

グローバル変数の設定は、{ Private 宣言 } に以下の通り記述。

type
  TForm1 = class(TForm)
    Image1: TImage;
    ListBox1: TListBox;
    ComboBox1: TComboBox;
    Button1: TButton;
    Label1: TLabel;
    CheckBox1: TCheckBox;
    procedure FormCreate(Sender: TObject);
    procedure ComboBox1Select(Sender: TObject);
    procedure Button1Click(Sender: TObject);
  private
    { Private 宣言 }
    ADevice     : array[0..MAX_PATH - 1] of Char;
    ADriver     : array[0..MAX_PATH - 1] of Char;
    APort       : array[0..MAX_PATH - 1] of Char;
    ADeviceMode : THandle;
  public
    { Public 宣言 }
  end;

「プリンタのプロパティ」として表示したいダイアログは、TPrintDialog ダイアログ or TPrinterSetupDialog ダイアログを表示した際に、その画面にある「プロパティ」ボタンをクリックすると表示されるダイアログ。(自分的には、これが「かゆいところに手が届く」いちばんイイ!ダイアログじゃないかなー みたいな気がして・・・)

もっと正直な、ほんとうのところを言うと、コントロールパネル → ハードウェアとサウンドの「デバイスとプリンターの表示」で表示されるプリンタを右クリックして出てくるサブメニューにある「プリンターのプロパティ」をクリックして表示される「詳細設定」タブのある「(プリンター名)のプロパティ」の画面を出したかったんだけど、コレを出す方法がわからなかった。
(もちろん、今でもわかりません。わかる方、Web上のどこか、いつまでも、ずっと残る場所に情報を書いていただけるとすごくうれしいです!)

Printerの選択肢を表示する部分は・・・

procedure TForm1.FormCreate(Sender: TObject);
//プリンタのリストを取得してComboBox1にセット
begin
  ComboBox1.Items.Clear;
  ComboBox1.Items.Assign(Printer.Printers);
  ComboBox1.ItemIndex := Printer.PrinterIndex;
  //プリンタを初期化
  ComboBox1Select(nil);  //このprocedureはまだ作ってないから当然エラーになる!
end;

エラーは無視。そのままForm上のComboBox1をクリックして、表示されるオブジェクトインスペクタの OnSelect の右の空欄をダブルクリックして表示される新しいプロシージャに以下の内容を記述。

procedure TForm1.ComboBox1Select(Sender: TObject);
begin
  //選択したプリンタをアクティブなプリンタに設定
  Printer.PrinterIndex := ComboBox1.ItemIndex;
  //ADeviceModeには変更前のプリンタの情報が格納されている
  //その他の値は現在(変更後)のプリンタの情報
  Printer.GetPrinter(ADevice, ADriver, APort, ADeviceMode);
  //ADeviceMode初期化
  Printer.SetPrinter(ADevice, ADriver, APort, 0);
  //ADeviceModeが新しいプリンタドライバの値になる
  Printer.GetPrinter(ADevice, ADriver, APort, ADeviceMode);
end;

プログラムを上書き保存(ComboBox1Select(nil);のエラーはここで解消される)。

で、最初に書いた(キャンセルボタンが押された場合の処理がない)印刷部分のプログラムは以下の通り(usesへの追加内容とButton1Click)。
EXEのある場所にDataフォルダを作成しておき、そこに印刷したいJpegファイルを入れておく。プログラムはこのJpegファイル1枚1枚へのPathをListBox1にセットして、セットされたItemの数だけ印刷Loopをまわすという内容。
なお、Jpegファイルの読み込みはGDI+を使用している。

implementation

uses
  Winapi.GDIPAPI, Winapi.GDIPOBJ, Winapi.GDIPUTIL,
  System.IOUtils, System.UITypes,
  Printers, Winapi.WinSpool,
  System.Types, Vcl.FileCtrl, System.StrUtils, System.Masks,
  Winapi.CommDlg;

  //Vcl.Imaging.jpeg, Winapi.MMSystem, Vcl.axCtrls,は削除した
  //System.UITypesはMessageDlgを使用するために追加
  //Printersは印刷を実行するために追加
  //Winapi.WinSpoolはPrinterPropertyを表示して印刷するために追加
  //System.IOUtils, System.Types, Vcl.FileCtrl, System.StrUtils, System.Masksは
  //ファイル名取得用の関数を使うために必要
  //Winapi.CommDlgはTPrintDlgの表示に必要

{$R *.dfm}
procedure TForm1.Button1Click(Sender: TObject);
var
  //PrinterプロパティのDialogを表示
  DevMode  : PDeviceMode;
  Mode     : Cardinal;
  hPrinter : THandle;
  //矩形
  rect:TRect;
  //印刷関連
  j:integer;
  //for フォルダの選択
  iStartFolder: string;
  iDirectories: TArray<string>;
  FileNames:TStringDynArray;  // -> ローカル変数でなければならない
  //TStringDynArrayの使用にはusesにTypesが必要
  strFileName, strSavePath:string;
  //for 画像読み込み
  graphics:TGPGraphics;
  bmp:TGPBitmap;

  //拡張子の異なるファイルをそれぞれ検索
  function MyGetFiles(const Path, Masks: string): TStringDynArray;
  var
    MaskArray: TStringDynArray;
    Predicate: TDirectory.TFilterPredicate;
  begin
    MaskArray := SplitString(Masks, ';');
    Predicate :=
      function(const Path: string; const SearchRec: TSearchRec): Boolean
      var
        Mask: string;
      begin
        for Mask in MaskArray do
          if MatchesMask(SearchRec.Name, Mask) then
            exit(True);
        exit(False);
      end;
    Result := TDirectory.GetFiles(Path, Predicate);
  end;

begin

  //プロパティの設定Dialogの表示がチェックされていたら実行
  //これを設定しない場合は、デフォルトの設定で印刷される(A4・縦とか)
  if CheckBox1.Checked then
  begin

    //FDeviceModeのメモリをロックしDEVMODE構造体のポインタを取得
    DevMode:=GlobalLock(ADeviceMode);

    try

      //対象のプリンタのハンドルを取得
      if Winapi.WinSpool.OpenPrinter(ADevice, hPrinter, nil) then begin
        try
          //このLModeの値によって,ダイアログ表示かプロパティの設定かが決まる
          Mode:=DM_IN_PROMPT or DM_OUT_BUFFER or DM_IN_BUFFER;
          //必要な場合は、ここで DevModeを修正する
          //例:A4・横の設定にしてからダイアログを表示する
          with DevMode^ do begin
            dmPaperSize:=DMPAPER_A4;  //A4
            dmOrientation:=DMORIENT_LANDSCAPE;  //横
          end;
          //プリンターのプロパティ設定Dialogを表示
          //キャンセルボタンが押された場合の処理なし(これを何とかしたい
          DocumentProperties(Handle, hPrinter, ADevice, DevMode^, DevMode^, Mode);
        finally
          //閉じる
          ClosePrinter(hPrinter);
        end;
      end;
    finally
      //ロック解除
      GlobalUnlock(ADeviceMode);
    end;

  end;

  //設定を反映させる
  Printer.SetPrinter(ADevice, ADriver, APort, ADeviceMode);

  //印刷実行
  //表示に設定
  Image1.Visible:=True;
  //ListBoxも初期化
  ListBox1.Items.Clear;
  //表示位置を設定
  Image1.Left:=10;
  Image1.Top:=10;

  //フォルダの選択
  MessageDlg('OKをクリックするとフォルダ選択ダイアログが表示されます。'+#13#10+#13#10+
      '採点したいデータのあるフォルダを選択してください。', mtInformation, [mbOk] , 0);

  iStartFolder:=ExtractFilePath(Application.ExeName);
  if SelectDirectory(iStartFolder, iDirectories,
    [sdHidePinnedPlaces, sdNoDereferenceLinks, sdForceShowHidden,
    sdAllowMultiselect], 'フォルダを選択してください', 'Folder', 'Ok') then
  begin

    //開いた時のPathを採点結果の保存先Pathとして取得しておく
    strSavePath:=iDirectories[0];

    //拡張子が'jpg'のファイルの名前を取得してListBoxへ格納
    FileNames:=MyGetFiles(iDirectories[0], '*.jpg');
    for strFileName in FileNames do
    begin
      ListBox1.Items.Add(strFileName);
    end;

    //答案の枚数分Loopする
    for j := 0 to ListBox1.Items.Count-1 do
    begin

      //画像の初期化
      Image1.Picture:=nil;

      //GDI+で読む
      bmp:=TGPBitmap.Create(ListBox1.Items[j]);
      Image1.Picture.Bitmap.Width:=bmp.GetWidth;
      Image1.Picture.Bitmap.Height:=bmp.GetHeight;
      Image1.Picture.Bitmap.PixelFormat:=pf24bit;
      graphics:=TGPGraphics.Create(Image1.Picture.Bitmap.Canvas.Handle);

      try
        Graphics.DrawImage(bmp, 0, 0, bmp.GetWidth, bmp.GetHeight);
        Image1.Align:=alNone;
        Image1.Center:=True;  //Imageの中央に表示
        Image1.AutoSize:=False; //サイズは固定しない
        //StretchとProportionalをセットで指定して、伸縮と縦横比維持を両立
        Image1.Stretch:=False; //Image枠内収まるよう画像の大きさを変更しない
        Image1.Proportional:=True;  //画像の縦横比を維持
      finally
        bmp.Free;
        graphics.Free;
      end;

      //処理の表示を止めないおまじない
      Application.ProcessMessages;

      //印刷実行
      //if MessageDlg('印刷しますか?',mtConfirmation,[mbYes,mbNo],0) = mrYes then
      //begin
        //ページを印刷する
        with Printer do
        begin
          {
          if j=0 then
          begin
            BeginDoc;
          end else begin
            NewPage;
          end;
          }
          BeginDoc;
          //大きさを指定
          rect.Top    := 0;
          rect.Left   := 0;
          rect.Bottom := Trunc(( PageWidth / Image1.Picture.Width) * Image1.Picture.Height);
          rect.Right  := PageWidth;
          //ファイルを描画
          Printer.Canvas.StretchDraw(rect, Image1.Picture.Graphic);
          {
          if j=ListBox1.Items.Count-1 then
          begin
            EndDoc;
          end;
          }
          EndDoc;
        end;
      //end;
    end;
  end;

end;

【20220830 追記】
印刷部分の動作検証は、普段愛用している印刷ユーティリティソフトのFinePrintで行いました(FinePrintは、株式会社NSDさんが販売している多機能プリンタドライバで、ページを縮小してのまとめ印刷等が簡単に行える非常に使いやすいソフトです)。FinePrintに出力している分については、上記コードで全ページがきちんと出力されるのですが、実際に業務で使用しているプリンタを指定して出力すると、ページが「抜ける」大変困った現象が発生することを確認しました。そこで、まとめて印刷せず、1枚毎にプリンタへ出力するようコードを修正してあります。

【20220901追記】
次のWebサイトに紹介されている方法を、上記印刷プログラムに対して試したところ、印刷の「抜け」が発生しなくなりました。貴重な情報をご提供くださいました中村 様、本当にありがとうございました。

ビットマップがプリンタに印刷できない

http://tknakamuri.web.fc2.com/tips004.htm

最後の印刷部分は、メタファイルに流し込む等は行わず、TImageのグラフィックをそのままプリンターのCanvasに大きさを合わせて描画している。

この方法は、次のWebサイトで紹介されていたものを、複数枚印刷用にコードを変更して利用させていただきました。ありがとうございました!

Delphi/400 Tips プログラムからの印刷 の 画像の印刷

https://www.migaro.co.jp/contents/products/delphi400/tips/introduction/4_19/01/02.html

プリンターのプロパティ設定を行わずに印刷すると(当たり前だが)、プリンタのデフォルト設定がそのまま適用される。

プリンターのプロパティ設定を行って印刷すると(当たり前だが)、変更した設定が適用されて印刷が実行される。

何の問題もない。これで出来上がり。OK!・・・でも、よかったんだけど、

2.キャンセルボタンが押されたら・・・?

ふと気になって、プリンターのプロパティ設定画面にある「キャンセルボタン」をクリックしてみた。

設定Dialog画面は確かにキャンセルされるんだけど・・・
プログラムがそのまま先へ進んでしまう・・・。
(気持ち的にはキャンセルした時点でExitして欲しいところだ)

んじゃ、キャンセルボタンが押されたらExitすればイイ。
で、その処理はどう書くの?

えぇっ ボク、知らないよー (T_T)
ここで、またハマりました!!

3.TOpenDialogなら・・・

よく使うTOpenDialogなら、こうするのが定番(・・・だと思う)

begin
  if OpenDialog1.Execute then
  begin
    ShowMessage(OpenDialog1.FileName);
  end else begin
    ShowMessage('キャンセル');
  end;
end;

でも、DocumentProperties( )には .Execute はありませんでした。これで、さらに(T_T)

×:DocumentProperties(Handle, hPrinter, ADevice, DevMode^, DevMode^, Mode).Execute

なんだか、面白いコトになってキタ!

4.解決方法を知る

Google先生しか頼りになるヒトはいませんから、さっそくカタカタきいてみます。
(実際には、最終的な解決に至るまでには、かなりの時間を費やしたのですが・・・)

Delphi DocumentProperties関数 ・・・というキーワードでお伺いをたてると、上から3番目に次の情報がHit!

Googleの検索結果から引用

この解説の下の方に、以下の記事がありました☆

関数がプロパティ シートを表示する場合、戻り値は、ユーザーが選択するボタンに応じて IDOK または IDCANCEL になります。

https://docs.microsoft.com/ja-jp/windows/win32/printdocs/documentproperties

あったー!!

さっそくこの情報を元にプログラムを書き換えます。

//DocumentProperties(Handle, hPrinter, ADevice, DevMode^, DevMode^, Mode);

//キャンセルボタンに対応
if DocumentProperties(
  Handle, hPrinter, ADevice, DevMode^, DevMode^, Mode) = IDCANCEL then Exit;

あっけないくらい、カンタンに解決☆
(やったー! コレでまたBlog書けるー!! ・・・みたいな気も)

5.まとめ

DocumentProperties関数は、キャンセルボタンが押されるとIDCANCELが戻り値として返る。これを利用してExitするには・・・

//キャンセルボタンに対応
if DocumentProperties( 略 ) = IDCANCEL then Exit;

6.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。記載した内容を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。