プリンタの使用可能な用紙名のリストを取得するには?

解決


かんとく  2013-07-16 07:54:13  No: 44846

お世話になっております。

DelphiXE2,Windows7です。

プリンタの使用可能な用紙名のリストを取得しようとしています。

MR.XRAYさんの以下のサイトを参考にさせてもらってます。

http://mrxray.on.coocan.jp/Delphi/plSamples/012_PrintResolution.htm#02
02_使用可能な用紙名のリスト

しかし、
プロジェクト→オプション→コンパイル→実行時エラーの範囲チェック
にチェックをつけると、
コードの
SGrid1.Cells[0,i+1] := String(pPNames^[i]);
SGrid1.Cells[2,i+1] := IntToStr(pPNumber^[i]);
の部分で、実行時エラー「範囲チェックエラー」が発生します。

実行結果の表は、1行目だけに値が入っています。

動作確認等  Windows XP(SP3) + Delphi 2009(UP3) Pro 
となっているので、DelphiXE2,Windows7ではエラーは仕方がないのでしょうか?
しかし、2009で動くから、XE2でもエラーが出ずに動くんじゃないかな?と思います。
実行時エラーが発生しないようにするには、どうしたらよいでしょうか?

よろしくお願いします。


Harry  2013-07-16 19:03:59  No: 44847

またMr.XRAYさんのサンプルの不具合かー      …というのはウソです。

まず、ここをよく見てください。元々、要素数が1個しかありません。
>  TPaperNames  = array [0..0] of TPaperName;
>  TPaperNumber = array [0..0] of WORD;

その一方、ビンの数はほぼ必ず2つ以上あるでしょうし、実際このユニット全体もビンの数が複数
あることを想定した作りになっています。
つまり、pPNames^[i] と pPNumber^[i] の i は宣言された要素の範囲 0..0 を超えるであろう、
ということが暗黙の前提となった上でコーディングされているのです。
その目的は、不定な(動的に可変する)アイテム数(ビンの数)への対応です。

この手法はかなり頻繁に使われています。C言語やClassicなDelphi(D3以前かな?)でも使える、
言わば 「ジェネリックな」テクニック …らしいです。
または、WindowsのAPIがキチ(ピ〜)なため、嫌でも使わざるを得ない局面もあると思います。

>実行時エラーが発生しないようにするには、どうしたらよいでしょうか?
もうお分かりだと思いますが、ここは「範囲チェック」をOFFにしておく、というのが正解です。

…などという知ったかぶりの説明では、かんとくさんが納得しないと思うので、回避策を挙げてみます。

1. [0..0] ではなく、[0..65535] とかにして、範囲チェックに引っかからないようにする。
(ただし、よりインチキ臭くなる。)※具体的なコードは割愛。

2. あらかじめ、必要以上と思われる大量の配列を実際に確保し、使用する。
(ただし、メモリの無駄遣い。)※具体的なコードは割愛。

3. 動的配列を使い、必要な分だけ配列を確保する。(今回の局面では楽に適用可能。)
※下記の変更で try〜finally〜end; も無意味になっているので、除去すべきです。

procedure TForm1.ComboBox1CloseUp(Sender: TObject);
type
  //用紙名リスト用.用紙名の文字数の最大は64
  TPaperName   = array [0..63] of Char;
//  TPaperNames  = array [0..0] of TPaperName;
//  TPaperNumber = array [0..0] of WORD;
//  pPaperNames  = ^TPaperNames;
//  pPaperNumber = ^TPaperNumber;
var
    i,j         : Integer;
    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;
    Count       : Integer;
//    pPNames     : pPaperNames;
//    pPNumber    : pPaperNumber;
    aPNames     : array of TPaperName;
    aPNumber    : array of WORD;
    AIndex      : Integer;
begin
    if ComboBox1.ItemIndex<0 then exit;

    //選択したプリンタを現在のプリンタとする
    Printer.PrinterIndex := ComboBox1.ItemIndex;
    //現在のプリンタに関する情報を取り出す
    Printer.GetPrinter(ADevice,ADriver,APort,ADeviceMode);

    //APortに接続しているプリンタADeviceのビンの数を取得
    Count := DeviceCapabilities(ADevice,APort,DC_PAPERNAMES,nil,nil);
    //その数分だけビン名称とビン番号の配列用メモリを確保
//    GetMem(pPNames,Count*SizeOf(TPaperName));
//    GetMem(pPNumber,Count*SizeOf(TPaperNumber));
    SetLength(aPNames, Count);
    SetLength(aPNumber, Count);

    //StringGridの行数
    SGrid1.RowCount := Count+1;
    //表示開始時は1行目を選択
    SGrid1.Row := 1;
    try
      //確保したメモリに用紙名と用紙番号がある
//      DeviceCapabilities(ADevice,APort,DC_PAPERNAMES,PChar(pPNames),nil);
//      DeviceCapabilities(ADevice,APort,DC_PAPERS,PChar(pPNumber),nil);
      DeviceCapabilities(ADevice,APort,DC_PAPERNAMES,PChar(aPNames)),nil);
      DeviceCapabilities(ADevice,APort,DC_PAPERS,PChar(aPNumber),nil);

      //StringGridに用紙名等を表示
      if Count<1 then begin
        SGrid1.RowCount := 2;
        SGrid1.Cells[0,1] := '-';
        SGrid1.Cells[1,1] := '-';
        SGrid1.Cells[2,1] := '';
      end else begin
        for i:=0 to Count-1 do  begin
//          SGrid1.Cells[0,i+1] := String(pPNames^[i]);
          SGrid1.Cells[0,i+1] := String(aPNames[i]);

          //用紙番号
//          AIndex := Integer(pPNumber^[i]);
          AIndex := Integer(aPNumber[i]);
          //用紙番号に相当する定数を検索
          for j:=0 to PaperList.Count-1 do begin
            if AIndex=Integer(PaperList.Objects[j]) then begin
              SGrid1.Cells[1,i+1] := PaperList[j];
              break;
            end;
          end;
//          SGrid1.Cells[2,i+1] := IntToStr(pPNumber^[i]);
          SGrid1.Cells[2,i+1] := IntToStr(aPNumber[i]);
        end;
      end;
    finally
//      FreeMem(pPNames);
//      FreeMem(pPNumber);
    end;
end;

こんな感じでよろしいですかね? 添削お願いします。>Mr.XRAYさん


かんとく  2013-07-17 09:01:10  No: 44848

ありがとうございます。

そういえば、以前、似たような質問をしていました。
前回は、StringGridで配列の範囲を宣言しているのに、範囲を超えた部分に値を出し入れしても範囲チェックエラーが出ずに、それが原因不明のAccess Violationを引き起こしているんじゃないかな?
というような内容でした。

https://www.petitmonte.com/bbs/answers?question_id=7028

(結局、Access Violationのはっきりした原因は、いまだにわかりません。)

そのようなことがあったので、範囲チェックエラーに厳しく対応しようと思い、常に
プロジェクト→オプション→コンパイル→実行時エラーの範囲チェック
にチェックをつけるように心がけています。

なので、今回も、「範囲チェック」をONにできる解決策を望んでいました。

Harryさんが送ってくれた方法は、配列を動的に確保してくれるので、エラーが出ないような気がします。

今すぐに試せないので、試してみて、また返信します。

Mr.XRAYさんのサンプルがなければ、プリンタの使用可能な用紙名のリストを取得するのに、もっと時間がかかっていたと思うので、とても感謝しています。

ありがとうございます。


Mr.XRAY  2013-07-17 09:46:01  No: 44849

一度試してみてください.
[別バージョンで作成されたプロジェクトの利用]
http://mrxray.on.coocan.jp/Delphi/Others/Delphi_Versionl.htm

サンプルプログラム集の [概要] のページからもリンクがあります.


DEKO  2013-07-17 11:11:25  No: 44850

> なので、今回も、「範囲チェック」をONにできる解決策を望んでいました。

Mr.XRAY さんのコードには特に問題はないので、

{$RANGECHECKS OFF}
...
{$RANGECHECKS ON}

のように、範囲チェックエラーの出る箇所 (for ループ全体) を
$RANGECHECKS コンパイラ指令で括るというのも一つのテだと思います。


かんとく  2013-07-18 17:02:55  No: 44851

ありがとうございます。

Mr.XRAYさんの
[別バージョンで作成されたプロジェクトの利用]を見て、
.dpr .dfm .pas以外のファイルを削除する
uses 部に、ユニットスコープをつける(Winapi.Windows, Winapi.Messages  など)
を試させてもらいましたが、範囲チェックエラーが発生しました。

Harryさんの
    SetLength(aPNames, Count);
    SetLength(aPNumber, Count);
を使ったコードでは、範囲チェックエラーは出ませんでした。

DEKOさんの
  {$RANGECHECKS OFF}
   ...
  {$RANGECHECKS ON}
を使うと、範囲チェックエラーは出なくなりました。
でも、教えてもらってこんなことを言うのは失礼かもしれないですが、
array[0..0]なのに、2個以上の要素を代入して、どうして問題がないのかが、わかりません。
宣言した範囲以外の部分に値を書き込むことで、不具合を起こして悩んだ経験があるので、どうして問題がないか教えていただけると、ありがたいです。

不具合が起きたときしかここに書き込みをしてませんが、今まで何度もMr.XRAYさんのサンプルを参考にさせていただいて、とても助かってます。
うまくいったサンプルのときは書き込みをしていませんが、Mr.XRAYさんにとても感謝しています。
いつもありがとうございます。


Mr.XRAY  2013-07-18 20:26:05  No: 44852

>uses 部に、ユニットスコープをつける(Winapi.Windows, Winapi.Messages  など)

ユニットスコープ名は関係ないと思います.
一応,以下の先頭に書いてはいますが...

[前のバージョンのプロジェクトを XE2 で使用する場合]
http://mrxray.on.coocan.jp/Delphi/Others/DelphiXE2_UnitScope.htm#02


DEKO  2013-07-18 21:46:10  No: 44853

> array[0..0]なのに、2個以上の要素を代入して、どうして問題がないのかが、わかりません。

概要は Harryさんが書かれていたと思いますが、ちょっとだけ詳しく。

TPaperNames: array of [0..0] TPaperName;
pPaperNames: TPaperNames へのポインタ
pPNames: pPaperNames の変数

pPNames のアドレスが A で、
その指している先 (TPaperNames) の初期アドレスが B だとします。

GetMem() すると指定したサイズのメモリが確保され、そのアドレスは C となります。
この時 pPNames の指している (A に格納されている)アドレスも C になります。

B ではなく C をアクセスしているのですから、範囲を超えて読み書きできるという訳です。
ご指摘の通り範囲チェックには引っかかりますけどね。

# 動的配列のないバージョンの Delphi でも使えるテクニックです。

意図して配列の範囲外 (確保したメモリは範囲内) をアクセスするコードなのですから、
「ここは問題ない」と判断して $RANGECHECKS を局所的にオフにするのもアリだと思うのです。


Harry  2013-07-19 01:28:08  No: 44854

ではまだ指摘されてない部分を言ってみます。

>array[0..0]なのに、2個以上の要素を代入して、どうして問題がないのかが、わかりません。
いやいや、実行時にここできちんとメモリの確保が行われているから大丈夫なんです。
    GetMem(pPNames,Count*SizeOf(TPaperName));
    GetMem(pPNumber,Count*SizeOf(TPaperNumber));
これが実行されることにより、実質的に array[0..Count-1] となりますので。
Countがいくつになるかは実行時にしか分からないので、それに対応するためですね。

かんとくさんに納得してもらえるよう、説得(脅迫?)を試みます。(以下、私なりの解釈です。)

・ Delphiは腐ってもネイティブコンパイラです。それは、”性能第一”を目指すものだと思うのです。
・ 何が言いたいのかというと、「メモリの確保や配列の添字を範囲内に収めることは、プログラマが責任を持って
やってください」というのが大前提なんじゃないかと。

・ じゃーなぜ「範囲チェック」があるの? → いえ、デフォルトではOFFだと思いますし、Borlandもこう言ってました。
>範囲チェックを有効にすると,プログラムの速度が低下しサイズも大きくなります。
>したがって,{$R+} はデバッグ用にのみ使用してください。
※↑Delphi6 Personal ヘルプの、「RANGECHECKS (範囲チェック)」の項目より抜粋

・それでも [0..0] は納得できません! → 困りましたねー。実はVCLのソースの中には、山盛りたくさんの [0..0] が
含まれているのですが…。それでも嫌と言い続けますか?
一度試しに、Delphiのインストールディレクトリの中を [0..0] でファイル検索してみてください。

#演出の都合上、勝手にかんとくさんのキャラ付けをさせていただきました。失礼しました。


Mr.XRAY  2013-07-19 09:05:32  No: 44855

こんにちは.
また,いろいろ書くと,物議の元となる懸念もありますが,話のついでに.
まず,

[別バージョンで作成されたプロジェクトの利用]
http://mrxray.on.coocan.jp/Delphi/Others/Delphi_Versionl.htm

ですが,私の環境では,Delphi XE2 では「範囲外」のエラーは発生しなくなりました.
これは,詳しいことは忘れてしまいましたが,Delphi XE2 あたりで,動的配列の範囲チェックの
何かが変更になったようです.どこかのコミュニティで話題になった記憶があります.
(ユニットのスコープ名のことではありません)

DEKO さんが書いた

># 動的配列のないバージョンの Delphi でも使えるテクニックです。

ですが,動的配列は Delphi 4 からです.
私のサイトでは,Delphi 3 当時からコンポーネントを配布しています.
そのため,利用者の便宜を考えて,なるべく広い範囲のバージョンで動作するように
していたため,そのままになっています.

サンプルプログラムも例外ではありません.
したがって,古いコードをそのまま使っているものが多くあります.
動作確認環境を書いていますが,実際には他のバージョンでも動作するものが多くあります.

ただし,最近の更新では,だんだんと,古いバージョンでは動作しないコードが
増えてきています.
このあたりの判断は難しいところです.
なるべく多くの方にも利用してもらいたいとも思いますし.新しいバージョンの Delphi の
機能を使えば便利だし.

Harry さんが既にレスしているコードの方がスッキリしているでしょう.
しかし,一方で,DEKO さんのレスも正解だと思います.
(何か,昔の名前の方が呼びやすいなぁー,でもこちらのハンドル名も格好いいし)
また,配列ではなく,最初から String 型や PChar 型で取得するコードにもできると思います.
ケースバイケースでしょうね.好みもあるかも知れません.

とにかく,アプリケーションは「勝てば官軍,動けば正義」です.


かんとく  2013-07-23 05:24:48  No: 44856

ありがとうございます。

DEKOさん
実は、
pPaperNames  = ^TPaperNames;
という変数の宣言のところの、
^
の意味がよくわかっていません。
なので、意味が分からずに、サンプルのコードをそのまま使わせてもらっていました。
^の意味が何なのかは、ここで今質問することではないと思うので、勉強しときます。

Harryさん
実は、
変数宣言部分の、
type  と  var
の違いがよくわかっていません。
なので、これも、教えていただいたとおりにコードを使わせてもらいました。
(これも、ここで今質問することではないと思うので、勉強しときます。)

わたしのキャラは、とても素直なので、教えていただいた通りにコードを書きます。
(でも、理解できなくてもそのまま使わせてもらうので、それはそれでエラーの原因になるかも…)

Mr.XRAYさん
Mr.XRAYさんは、「勝てば官軍,動けば正義」の言葉が好きなんですね。。
わたしが好きな(嫌いな)言葉は、「プログラムは、思った通りに動くのではなく、書いた通りに動く」です。

ありがとうございました。


※返信する前に利用規約をご確認ください。

※Google reCAPTCHA認証からCloudflare Turnstile認証へ変更しました。






  このエントリーをはてなブックマークに追加