前の章で、ScanLineの戻り値がPointer型でデータの参照はP[x]の様な配列要素で行うと説明しましたが、「あれっ?」と思いませんでしたか?
DelphiのObjectPascalでは、配列は静的配列として定義されなければならず、Basicのような要素数を変更できる動的配列は使用できないのです(但し、Delphi4では新機能として動的配列が使えるようになりました)。
Basicに慣れている私にとって、この制約はとても窮屈に感じたものでした。
Delphiの参考書によっては「想定できる要素数の最大を定義すればよい」というようなことが書いてあります。
例えば、生徒100人分のテストの点数を納める配列として、
var tensu: array [1 . 200] of integer;.
とでもしておけば十分というわけです。
しかし、このプログラムは200人以下の田舎の学校で使えても、大都会の学校では使えないことになりますね。
他にはVarinat型を使うという方法もありますが、スピードが犠牲になってしまいます。
で、話を戻しますが、前章のプログラムはScanLine関数ヘルプのプログラムサンプルを元に作ったのですが、PByteArrayを最初に見た時に「あれ?」と思いました。
ビットマップの大きさは大小さまざまで、VCL設計時に特定できるものではありません。「さては途方もない大きな配列を定義して、メモリを無駄にしてるのでは・・・?」と疑ってVCLのソースを覗いてみました。
PByteArrayという単語を検索してみると、標準VCLのSysUtils.pasにその定義がありました。DelphiのProバージョン以上をお持ちの人は是非このファイルを見て下さい。
PByteArray = ^TByteArray;
TByteArray = array[0..32767] of Byte;
と定義されています。
「はは〜ん、やっぱり32000バイトも無駄なメモリを使っているな」と、最初思ったのですが、それは早合点でした。ファイルの上の方を見ていき、この宣言がType宣言の中にあることに気づきました。
つまり、Type宣言されている段階ではメモリ上にこの32000バイトの領域が確保されているわけではないのです。例えば、
var tensu: TByteArray;.
とした時に初めて配列用のメモリが確保されるのです。
さらに、PByteArrayはPointer型なので、
var tensu: PByteArray;.
としても、変数用には4バイトしか確保されません。それでいて、tensu[0]からtensu[32767]まで配列要素としてアクセスできるのです。但し、実際にはtensu[]用のデータが入るメモリ領域は何らかの方法で確保されていなければなりません。
今回のテーマで扱っているデータはビットマップデータで、メモリ領域はすでに確保されているので、画像の幅を越えない限り配列要素としてアクセスできるわけです。
私はこのことに気が付いて、改めてDelphiの奥の深さのようなものを感じました。
さて、前置きが長くなりすぎました。本題に戻ります。
前回のテーマでは、ScanLine関数で高速化されたもののプログラムがスマートではないという問題が残りました。
そこで、今回のテーマでは上で述べたPByteArrayのテクニックを応用して、もう少しスマートなプログラミングをしてみましょう。
Unit1.pasのForm宣言の前に以下のTypeを宣言します。
//*********** RGB配列 ********** RgbStr = record B, G, R: Byte; //***** バイトの並びは B->G->R end; TRgbArray = array [0..65535] of RgbStr; PRgbArray = ^TRgbArray; |
まずは、
RgbStr = record
B, G, R: Byte;
end;
として、RGB3バイト分のレコード型を作り、
TRgbArray = array [0..65535] of RgbStr;
で、配列化しています。
PRgbArray = ^TRgbArray;
は、配列へのポインタ変数です。で、実際のプログラミングは、
//*********** レコード型配列ルーチン *********** procedure TForm1.btnScanlClick(Sender: TObject); var x, y: Integer; // R, G, B: Byte; Pnt: PRgbArray; //***** RGB配列に変更 begin Image1.Picture.Bitmap.PixelFormat := pf24bit; Tmstart; for y := 0 to Image1.Height -1 do begin Pnt := Image1.Picture.Bitmap.ScanLine[y]; for x := 0 to Image1.Width -1 do begin //***** RGB配列を使うことで分解は不要 if not chkR.Checked then Pnt[x].R := 0; if not chkG.Checked then Pnt[x].G := 0; if not chkB.Checked then Pnt[x].B := 0; end; end; Image1.Refresh; Tmend; end; |
です。
Pnt: PRgbArray;
で、先ほど作った配列変数を使い、
Pnt := Image1.Picture.Bitmap.ScanLine[y];
で、アドレスを受け取ります。Pntはポインタ変数ですが、同時に配列へのポインタでもあるので、Pnt[x]として要素にアクセスできます。
if not chkR.Checked then Pnt[x].R := 0;
if not chkG.Checked then Pnt[x].G := 0;
if not chkB.Checked then Pnt[x].B := 0;
配列の各要素はレコード型にそのまま入るので、今度はRGBに分解する必要もなく、データの出し入れも必要ありません。直接操作できます。これを前ページののメモリマップと合わせて見ると、
メモリ上のビットマップデータ | ||||||
0ピクセル目 | 1ピクセル目 | ..... | ||||
+0 | +1 | +2 | +0 | +1 | +2 | |
青 | 緑 | 赤 | 青 | 緑 | 赤 |
↓ ↓ ↓
TRgbArray型のPnt変数 | ||||||
Pnt[0] | Pnt[1] | Pnt[n] | ||||
.B | .G | .R | .B | .G | .R | |
青 | 緑 | 赤 | 青 | 緑 | 赤 |
と、ピタリ一致します。プログラムの方もすっきりしましたね。
このように新しいTypeを作ってプログラミングすることで、プログラムが簡潔になるばかりでなく、拡張性も増します。例えば、このプログラムで32ビット画像を扱うようにするには、
RgbStr = record
B, G, R, P: Byte;
end;
と、レコード内に1バイト分追加し、
Image1.Picture.Bitmap.PixelFormat := pf24bit;
と変更すればOKです。
実際、実用的なプログラムを作っていて、色々と機能を付けていくうちに、巨大で複雑になりすぎて、後になって手が付けられない...というのはよく経験することです。
このようにスマートなプログラミングを心がけていけば、単に見やすいだけでなく、後々のデバッグや改造にも助かるものですね。
さて、ここまでくれば十分かと思うのですが、私個人的にはScanLineで得られるデータが1次元配列であるということが、どうしてもしっくり来ません。
というのは、ビットマップは2次元座標 (x,y) で表されているので、データを操作するのも2次元配列
[x,y] でやりたい、というわがままが押さえられないのです。(^^;)
例題のように画像全体を操作する場合は別として、フォトショップのフィルターの様に、ある部分だけを操作したいという場合は2次元配列の方が便利そうです。
そこで、次のテーマではDelphi4で新しく取り入れられた多次元動的配列を使って、このわがままを解消します。