The width of the TImage was not the width of the Graphic

「TImageの幅はグラフィックの幅じゃなかった」

1つのTImageに、様々に組み合わせた画像を、次々に表示して、その幅(と高さ)が頻繁に変わるようなプログラムを書いた。プログラムがほぼ完成?に近づいた段階で、画像の要素の組み合わせ方を変えるとTImageの幅が、画像全体の幅の変更に追随しなくなる現象に遭遇。原因はプログラムの記述ミス。これは、それを繰り返さないための備忘録。

1.最初に結論
2.練習プログラムで問題を再現
3.まとめ
4.お願いとお断り

1.最初に結論

TImage は、「グラフィックスを表示する」ためのコントロールであって、「描画そのものはグラフィッククラス側で行う必要」があり、TImage は「位置決めと描画タイミングの面倒を見ているだけ」とのこと。

つまり、TImageが内部に抱えているグラフィックスクラスに対して、適切な指示を与えてあげないとプログラムは予期した通りに動作しないよ!・・・ということになります。

そのことを教えてくれたのが、次の2つのWebサイトの情報です。心から感謝です。

TImageのリサイズ時に気をつけること

https://vogelbarsch.com/2016-01-13-212108/

TImage

http://tknakamuri.web.fc2.com/vcl2-12.htm

結論として、TImageに複数の画像を埋め込んで、切ったり、貼ったり、幅や高さを変更して使用する場合、TImage内部のグラフィックスクラスにTBitMapを使うのであれば(私の場合は全部そうです)「TImageが内部にBitMapを抱えていることを忘れてはいけない」ということです。

では、「TImageが内部にBitMapを抱えていることを忘れる」とどうなるのか? その具体例を次に示します。

作成しているプログラムは、100枚程度の答案画像をスキャナーで読み込み、ここから設問ごとに(1問ずつ)解答欄を切り出して1枚の画像に合成し、横に置いたGridコントロールに採点結果を入力。その後、採点結果はCSVファイル(必要であればExcelで作成した採点Sheetも利用可)に保存し、採点結果(○×)をつけた画像も元の答案画像に埋め込んで印刷して返却できるようにする・・・というもの(答案そのものは重要な証拠として保管し、適切な時期にシュレッダーにかけて処分するような運用を予定しています)。

スキャナーで読み込んだ答案画像(マークシートは別のプログラムで採点)

当初は、設問の解答欄のみを切り出して表示する機能のみ実装するつもりでしたが、「個人識別も可能にして欲しい(解答者の理解度や弱点を知るため、誰の解答か、すぐわかるようにして欲しい)」という要望があり、これに応えるため、解答欄の横に答案から切り出してきた組・番号・氏名等も表示できる機能を付け加えました。

解答者の氏名を表示しない(個人識別なし)で動作させた画面がこちらです。

解答欄の画像のみ切り出して合成
個人識別を行わない設定

この時のTImageの幅を確認すると、次の通り「628」でした。

次に、「個人識別」にチェックを入れ、解答欄の左に組・番号・氏名等を表示するモードに切り替えて動作させます。

数値15は垂直方向の表示位置です
解答欄が半分しか表示されていない・・・

なのに、TImageの幅は・・・

大きくなってるー!!
なんでー???

悪いのは確かに私なんですが、この時点で原因はまるでわかりません。
問題を作り出しているのはもちろん自分が書いたコードです。次に、個人識別なしの場合の誤りを含むコードを示します。ナニがいけないのか、お気づきになりますでしょうか?
(imgAnswerは、画像を表示しているTImageの名前です)

  //描画(修正前の誤りを含むコード)
  imgAnswer.Height := (解答欄1個の高さ) * ListBox1.Items.Count;
  imgAnswer.Width := 解答欄1個の幅;
  imgAnswer.Canvas.CopyRect(DestRect, Image1.canvas, SrcRect);

そうです。TImageの高さと幅をいじっています。冒頭で述べた通り、TImageは表示する画像の「位置決めと描画タイミングの面倒を見ているだけ」なのです。重要なのは、そのTImageが内部に抱えているTBitMapです。こちらの高さと幅を設定し忘れています。おそらく画像の切り出しに注意が集中しすぎ、(TImageの幅と高さを設定すればOK!)と勘違いして、TBitMapのことまでアタマがまわらなかったのだと思います。

こんな恥ずかしい間違いを私以外にする人がいるとは思えませんが・・・。もしかしたら、どこかにおひと方くらい、同じ問題で悩む方がいるかもしれません。その方の一助にでもなれば幸いと、あえて誤りを公開する次第です。

  //描画(修正後)
  imgAnswer.Picture.Bitmap.Height := (解答欄1個の高さ) * ListBox1.Items.Count;
  imgAnswer.Picture.Bitmap.Width := 解答欄1個の幅;
  imgAnswer.Canvas.CopyRect(DestRect, Image1.canvas, SrcRect);

で、描画手続きの最後に以下のコードでTImageの高さと幅を設定。Visible も True に。

  //表示
  imgAnswer.Height:=imgAnswer.Picture.Bitmap.Height;
  imgAnswer.Width:=imgAnswer.Picture.Bitmap.Width;
  imgAnswer.Visible:=True;
正しく表示されるようになりました☆

現象を確認してから、問題の本質に気付き、プログラムを修正するまで2時間少々かかりました。修正しながら、この問題そのものに「既視感」を感じたことも事実であり、もしかしたら(はっきり覚えていないだけで)、私は以前にも今回と同じ問題で悩んだことがあるのかもしれません。いや、きっとあります。問題解決の重要なヒントとさせていただいた上記Webサイト様の情報にも確かな既視感がありましたから。

なお、本題に関係ない部分ですが、採点のマーキング位置と個人識別情報の表示位置は細かな指定ができる設計にしてあります。上記画像も「ちゃんと」すれば、次の画像のようにもう少し見やすく変更できます。これが自動で出来ればスゴイのですが・・・

2.練習プログラムで問題を再現

FormにTImage1つと、Buttonを2つおいて、ボタンをクリックすると表示される画像を切り替える練習プログラムで問題を再現してみます。

点線で囲まれた部分がTImage(幅と高さは右下のハンドルを適当にドラッグしたままの状態で設定)

で、TImageに表示する画像は次の2枚。

Picture01.jpg
Picture02.jpg(幅を1/2にした画像)

Button1をクリックした時の手続き

procedure TForm1.Button1Click(Sender: TObject);
var
  pFileName:string;
  //GDI+を利用する
  Graphics:TGPGraphics;
  GPbmp:TGPBitmap;
begin

  //Image(メモリ)を初期化
  Image1.Picture.Assign(nil);  //縦横比が変化しないと、再描画されないことがあった!

  //表示する画像のファイル名を取得
  pFileName:=ExtractFilePath(Application.ExeName)+'Data\Picture01.jpg';

  //オブジェクトを生成
  GPbmp:=TGPBitmap.Create(pFileName);
  //回転・反転ともになし
  GPbmp.RotateFlip(RotateNoneFlipNone);

  //描画
  Image1.Picture.Bitmap.Width:=GPbmp.GetWidth;
  Image1.Picture.Bitmap.Height:=GPbmp.GetHeight;
  Image1.Picture.Bitmap.PixelFormat:=pf24bit;
  Graphics:=TGPGraphics.Create(Image1.Picture.Bitmap.Canvas.Handle);
  try
    //イメージを表示
    Graphics.DrawImage(GPbmp, 0, 0, GPbmp.GetWidth, GPbmp.GetHeight);
    Image1.AutoSize:=True;
  finally
    GPbmp.Free;
    graphics.Free;
  end;

  //画像を表示
  Image1.Visible:=True;

end;

button2をクリックした時の手続きも同様に記述して(画像の名前を「Picture02.jpg」に変更するだけです)、動作を確認。まず、button1をクリック。

画像が表示される

続けて、button2をクリック。

こちらも問題なし(予期した通りに動作する)

TImageの幅の変更は適切に処理され、期待した通りに動作することが確認できました。
次に、button1 クリック時のコードを誤りのあるものに変更。
ついでにGDI+のビットマップの幅と、Image1の幅も表示して比較できるように設定。

  //Image1.Picture.Bitmap.Width:=GPbmp.GetWidth;
  //Image1.Picture.Bitmap.Height:=GPbmp.GetHeight;

  //誤ったコード
  Image1.Width:=GPbmp.GetWidth;
  Image1.Height:=GPbmp.GetHeight;
  //Freeする前にGPbmpの幅を見ておく
  ShowMessage(GPbmp.GetWidth.ToString);

  Image1.Picture.Bitmap.PixelFormat:=pf24bit;
  Graphics:=TGPGraphics.Create(Image1.Picture.Bitmap.Canvas.Handle);
  try
    ・・・(略)・・・
  finally
    GPbmp.Free;
    graphics.Free;
  end;

  //画像を表示
  Image1.Visible:=True;
  //TImageの幅を確認
  ShowMessage(IntToStr(Image1.Width));

上記設定で動かしてみると、ShowMessage(GPbmp.GetWidth.ToString) の結果は・・・

GDI+のBitMapに画像が読み込まれている証拠

続けて、Image1の幅はどうなっているかというと・・・

予期した通りゼロ

Image1のPicture.Graphicに代入されるTBitmapがカラだから、結果としてImage1の幅が変更されない? もちろん、画像も表示されません。

誤りのあるコードで、button1をクリックした結果

ShowMessageではなく、OutPutDebugString関数を使えば、次のようにメッセージを表示せずに確認することもできます。

  //ShowMessage(GPbmp.GetWidth.ToString);
  OutPutDebugString(PChar(GPbmp.GetWidth.ToString));

  //ShowMessage(IntToStr(Image1.Width));
  OutPutDebugString(PChar(IntToStr(Image1.Width)));

実行(F9)でイベントログにOutPutDebugString関数の結果が出力される。

GPbmp.GetWidthが「468」、Image1.Widthが「0」であることがわかる

私の場合、いつもはShowMessage関数で確認して、消去するか、そのままコメントアウトしてしまうことが多いのですが・・・。

なぜ、この例を追加したかというと、問題の原因がどこにあるのかわからない時は、「基本に返る」ことの大切さを、この間違い探しの作業を通じてしみじみ実感したからです。

実は自分の書いたコードのどこに誤りがあるのか、当初はまったくわからず(見当すらつかず)、少しずつコードを追いかけてようやくImageの幅がおかしなコトになっているという点に気が付きました。

で、幅の設定を手掛かりにして、GDI+で画像を読み込む手続きの部分を見直した時、

  Image1.Picture.Bitmap.Width:=GPbmp.GetWidth;
  Image1.Picture.Bitmap.Height:=GPbmp.GetHeight;

(あれっ なんかついて る・・・)という違和感を感じたのです。

画像の読み込みでは、Picture.Bitmap があるのに、
画像の合成部分には、確か、それがなかった・・・

やっと、わかったー(解決)

基本に戻るということの大切さを、しみじみ実感しました。
で、あらためてTImageの幅について勉強し直した結果、上記Webサイト様の情報に行きつき、問題の本質が明らかになった次第です。

しかし、またココで新たな謎が・・・

ShowMessage(GPbmp.GetWidth.ToString);

この1行、実行にすごく時間がかかる気が・・・
なんでかなー???

3.まとめ

TImageは、そのPicture.Graphicに読み込んだ画像を表示するVisual Component Library (VCL)。TImage は「位置決めと描画タイミングの面倒を見ているだけ」で、「描画そのものはグラフィッククラス側で行う必要」がある。だから高さや幅の設定も・・・

  Image1.Picture.Bitmap.Width:=GPbmp.GetWidth;
  Image1.Picture.Bitmap.Height:=GPbmp.GetHeight;

TImageが内部に抱えているBitmapに対して行う必要がある。

4.お願いとお断り

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