BringToFront

最前面に表示する!

TImageに表示したある画像の上を、マウスでドラッグして、矩形選択するプログラムを書いた。

ちょっとだけ、同じセクションの仲間に「今度書いてるプログラムなんだけど。けっこうイイかんじで・・・」みたいな話をしたら、ICカードリーダーを使った出退勤管理のプログラムを作った時と同様に、「あいつがまた、ナニやらおかしなプログラムを書いたらしい」と、同僚から同僚へ、さらに別のセクションの人たちにも「風の便りに聞いたけど、これは内緒の話・・・」みたいな感じで次々に噂が広まり、果ては「あのイヤな仕事をかなり高速に処理できる魔法のプログラムが出来たらしい」と話しに「尾ひれ」が付いて評判となり、その「幻のプログラムの使い方」は職場内で「呪文の伝授」と呼ばれ、果ては公開前なのにすでに伝説化して、頼んでもいないのに積極的に人柱になりたい(=プログラムを試用したい)との申し出が若手の職員から殺到。

そんな時、プログラムの根幹をなす「画像の上をマウスでドラッグして、ラバーバンドを描きながら矩形選択」する部分で大問題が発生。僕自身が行ったテストでは何の問題もなかったのに、なんと、テストを頼んだ同僚が用意した画像上には「絶対にドラッグできない領域」が存在するとの報告が・・・。僕が用意した画像では何の問題もなくドラッグして、矩形選択できたのに・・・。

問題の解決までに2日半、エンエンと悩み続けることに。

以下、Delphiに触れて十数年も経って「初めて」知った「BringToFront」の物語。

1.プログラムの仕様
2.遭遇した摩訶不思議な現象
3.解決までの道のり
4.問題を再現
5.まとめ
6.お願いとお断り

1.プログラムの仕様

作成したプログラムはTImageを2個準備して、そこに表示した別々の画像を切り替えて仕事をするというもの。具体的には、画像Aの一部を範囲指定(矩形選択)してコピー、画像Bに貼り付け、次に、現在のそれとはまた別の画像をAに表示し、一部を矩形選択してコピー、画像Bに貼り付け、これを自動的に繰り返して仕事に必要な画像を一揃い画像Bに表示し、あとはユーザーが目視で内容を確認しながら、一斉に処理するというものだ。

だから、最初にコピーする範囲を指定する必要があり、画像Aで矩形選択ができなければ仕事にならない。よりによって、プログラムのいちばん根幹をなす部分で未知のトラブルに遭遇するとは・・・

2.遭遇した摩訶不思議な現象

僕自身が用意した画像でテストした時は、少なくとも矩形選択に関しては何も問題は起きなかった。他に細かなトラブルはたくさんあったけど、すべて原因がすぐわかり、これまでに培った知識で何とか解決できることばかりだった。テストのテスト的に、ごく近しい同僚に試用してもらった結果も良好だった。

そこで、テストのテストで見えてきた(実際に運用した際に発生すると予想される)エラーの処理をある程度書き加え、実用的なプログラムとして完成に近づいた段階で、職場内の別のセクションの仲間にテストを手伝って欲しいと依頼して、限られたセクションに属する人だけが入れるネットワークフォルダにプログラムを置き、テストを実施。

そして事件が発生。

支給されたノートPCを抱えてやってきた同僚が言うには、画像の一部に絶対に矩形選択できない場所があるというのだ。

そんなバカな! ・・・と思いながら、確かめたら本当だった。

信じたくない。信じたくない。信じたくない。夢なら覚めてくれ・・・。
そう祈りつつ、何遍クリックしても、同僚が用意した画像上の、「その領域」だけは絶対に選択できない。ラバーバンドの「ラ」の字も現れない。不思議なことに、同じ画像上の別の場所ならラバーバンドが普通に表示されるし、矩形選択もできるけど。その画像上の「ある領域」だけは、絶対に選択できない・・・。何度、何遍クリックしても・・・。

この現実は、絶対、信じたくないから、お互いに見つめあって、口をついて出た言葉は、お決まりの・・・

もしかして、マウスが壊れてるんじゃないか?

無線じゃなく、USBヒモの付いてる普段は絶対に使わないマウスに試しに交換。
祈りと願いを込めて画像上のある一点をクリックするも、PCは無反応。OnMouseDownイベントは発生の兆しすらなし。My TImageには、この期待も、祈りも、願いも、すべて届かず・・・

あぁ TImageよ、きみを泣く。
きみ、死にたもうこと なかれ。

なんとゆーことに。毎日午前2時から起きてプログラム書いたのに・・・
なんで夕陽が東から昇るんだって、疑問に感じながら毎朝ハンドル握ったのに・・・
今日の第2部の始まりだって、自分に言い聞かせて「昼間」ずっと働いたのにー

画像を表示し きみなれば
ほかに仕事はなかりしも、
せめてマウスのドラッグに
応える心を持ち給え、

祈りつつ、願いつつ、ドラッグしても、ポイントしても、TImageは期待を完全に無視。

ドラッグ無視してコケよとて
きみを完成間際まで育てしや。

愛してたのにー。 T_T
エラーすら出してくれないDelphiを逆恨み。可愛さあまって憎さ100000000倍。

あぁ 神さま
どうか 夢なら覚めてください・・・

正直、この時はプログラムの公開を目前に控えて「初めて遭遇した現象」に激しく狼狽。

(なんで、エラーすら出ないのかなー)

なんとかしなければならない。そう、なんとかしなければならないのだが、解決方法がまったくわからない。泣いても、喚いても、誰も助けてはくれない。

「プログラム使うの、楽しみにしてるね!」

そう言ってくれた同僚の笑顔が瞼に浮かぶ。

今更、「プログラム。コケました!」・・・なんて、絶対に言えない。

公開すると約束した日まで、あと3日。

仕事上の約束はこれまで必ず守ってきた。
今、この約束は破れない・・・。

3.解決までの道のり

問題が発生したのは金曜日の午後。幸いなことに土日は休日で予定は何も入っていないので、仕事に復帰する月曜日までの丸2日間、問題の解決に専念できる。それだけを唯一の救いにして、問題の原因と解決方法を考えよう・・・

(ほんとに、OnMouseDownイベントが発生してないのかな?)

初めにそれを確認することにして、ログを記録するように設定し、プログラムを走らせて矩形選択できない領域を何度もクリック&ドラッグ。プログラムを停めて、テキスト形式で記録されたログを確認。予想した通り、そのどこにもOnMouseDownイベント発生の記録はない。これでとにかく、「それが発生していない」ことだけは確実にわかった。

次に考えたのは、TImageに表示するJpeg画像の縮小率を変更して試してみること。・・・と言うのも、スキャナーで読み込んだ画像がPCの画面上で表示するのにデカすぎる場合は、任意の大きさに縮小できるよう予めプログラミングしてあり、その縮小率を変更したら、もしかしたら矩形選択できるようになるのではないか? と、思ったのだ。

問題が発生する前、A3サイズの用紙をスキャナーで読み込んでみたが、画像がかなり大きくて50~80%に縮小しないと、そのままでは作業に使えないことが判明。画像の状況により、ユーザーの判断で任意の縮小率を設定できるようにしてあった(プレビュー画面で確認しながら縮小率を設定できるようにプログラミングした)。

問題となった画像の縮小率は75%だったので、これを68%に変更。プログラムを動かして確認すると何の問題もなく動作する(実は、縮小率を変更したため、範囲指定する領域が変化して、たまたま矩形選択できない領域を外れてた・・・つまり、単にラッキーだった・・・だけなんだけど)。でも、これで一安心。原因はわからないが、もし問題が発生した場合は、縮小率を変更して試してもらえば、プログラムは正常に動作する可能性が「ある」ことが確認できた。

こうして「まったく動作しない」という最悪の事態だけは、なんとか、回避できる見込みがついた。しかし、問題の根本的な原因は一向に解明できないまま、土曜、日曜と時間だけが虚しく流れた。

日曜は、朝から机に向かって(あぁでもない・こうでもない)と思いつく限りの方法を試したが、「特定の縮小率を指定して作成したある画像に限って、絶対に矩形選択できない領域がある」という事実に変化はなく、前に困った時のことを思い出して、TImageのEnabledプロパティを間違いなくTrueに設定してもラバーバンドは出現せず、様々にキーワードを変えてGoogle先生に伺いを立てても、その解答に問題解決のそれらしきヒントとなるような内容は見当たらず、万策尽きて・・・。こんなことで時間を無駄にするよりは、まだ作りきってない部分や、手直しが必要な部分もいくつかあるから、そっちの仕事を先にやって・・・みたいな感じで、問題の原因調査とプログラム作成・手直しの間を1日中、行ったり、来たり。

(画像によっては、何の問題もなく処理できるし、問題の起きた画像でも縮小率変えれば使えたりするからー。ユーザーには、問題が起きることもあるけど・・・って、あらかじめ、そう説明して・・・使ってもらえばいいかぁ T_T)

日曜の夕方、折れかけた心を抱えたまま、僕はベッドに倒れ込んで・・・

目覚めたのは日曜の深夜・・・というより、月曜の早朝(?)午前0時すぎ。

(まだ、朝までには数時間ある・・・。選択肢はやるか・やらないか、のみ)

( やるしか、ない・・・ )

Google先生に再び訊ねた検索キーワードは「Delphi Image onMouseDown 起きない」
これはもう何度も使った検索キーワード。でも、もしかしたら、もしかしたら、どこかに見落とした情報があるんじゃないか・・・。やっぱりキーワードはこれしかないだろーって、そう思いながら、あるWebサイトの記事を見た瞬間。視野でない場所に、ナニかが見えた気がした・・・。その記事がこちら

Imageで描画したものが常にTop

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

気になった言葉は、ただ一言。「BringToFrontでは意味がなく…」

BringToFront ってナンだ? オレ、知らないぞ。
Delphiにそんなメソッド、あったっけ?

・・・ ってか、確か Formに配置した順に ・・・
・・・ コントロールは積み木を積むように ・・・
・・・ 上に、上に、重なって ・・・

そうだ。オレ、最初にFormを用意して、
ScrollBoxを置いて、
その上にImageを載せて・・・

そのあと、RadioGroupとか、ListBoxとか、Editとか、
アレも、コレも、たっぷりVCLコントロール積んだカラ。

Imageは 積み木のいちばん下になってる・・・
問題のほんとうの原因はきっと、コレだぁ!!!

あぁ 長かったぁ・・・

BringToFront が、なんのことか、まだわからんけど、
(おおよその挙動は文字から伝わるけど)
それを調べるより先にやることがある。それはナニかと言うと・・・

Imageを切り取って、ScrollBoxの上に貼りなおすコト!
これでImageが積み木のいちばん上にくるはず!!

プログラムを走らせる。
まったく言うことをきいてくれなかった画像を敢えて選ぶ。
絶対にクリック出来なかった領域を、クリック。

夢にまで 見た ラバーバンドが 現れた!!!

あきらめなくて・・・ よかったぁ

そして、さらに気がついたことは・・・
つい、さっきまでImageの上に表示されてた
( 今日:正確には昨日、作ったばかりの )
残り工程数を示すためのLabelが見えなくなってる・・・

調べなくても、これでわかった!
こんな時は、きっとLabelにBringToFrontなんだぁ

“BringToFront”

これまで使ったことないし、その存在さえすら知らなかったけど、
ここで早速、それが存在するもっともな理由を、自然な流れの中で確認&納得。

Imageには、いつも何か画像を表示するだけで、縮小・拡大・回転はしても、切ったり、貼ったりしたのはオレ、今回がたぶん、初めてだもんなー。

プログラムも、僕も、少しだけ
よく なれた かな?

4.問題を再現

簡単な検証プログラムで問題が再現できれば、今回のミスの原因は完全に解明できたことになる・・・。そう考えて作ってみたのが次の検証プログラム。科学する心。

Form上にScrollBox、その上にImageを二つ載せ、CheckBoxを二つ用意。

ここで重要なのは、VCLコントロールを設置する順番。

最初にScrollBox、
次がImage1、
最後にImage2 の順番でなければならない。

わかりやすくするため、TImageのAlignはalNoneのまま、大きさだけを上の図のように、Image1がImage2より大きくなるように設定。Image2は、Image1の上に乗ってる感じで設置。

で、Image1のVisibleプロパティはTrue、Image2のVisibleプロパティはFalseに設定。

それからTImageのEnabledプロパティはどちらもTrueにしておく。

(以前、ユーザーの誤ったクリックを防止するため、ある手続きの処理の最後に、これをFalseに設定し、Trueに戻すのを忘れたまま、次の手続きに進み、今回とは別の理由で矩形が描けなくて大騒ぎ。この場合もエラーメッセージ等は出ないので、原因がわかるまで相当長時間にわたり悩んだことがあった・・・)

ScrollBoxを置いたのは、問題を起こしたプログラムと同じ仕様にするためで、それ以外の特別な意味はない。

ラバーバンドの描画コードは、Mr.XRAYさんのWebサイトにあったコードをそのまま使わせていただく。

154_画像の矩形範囲選択とラバーバンド

http://mrxray.on.coocan.jp/Delphi/plSamples/154_Rubberband.htm#01

上のコードでは、ラバーバンドをFormに描画しているので、これをImageのCanvasに描画するように変更したコードが、以下。

まず、必要な変数の宣言と、Image1のOnMouseDownイベント。

  private
    { Private 宣言 }
    InitX : Integer;
    InitY : Integer;
    PrevX : Integer;
    PrevY : Integer;
    DragFlag : Boolean;

procedure TForm1.Image1MouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin

  if Button = mbLeft then
  begin
    //TImageのCanvasの描画をクリア
    Image1.Canvas.Brush.Style := bsSolid;
    Image1.Canvas.Brush.Color := Self.Color;
    Image1.Canvas.FillRect(ClientRect);
    //クリック位置の座標を取得
    InitX := X;
    InitY := Y;
    PrevX := X;
    PrevY := Y;
    DragFlag := True;
    //描画設定
    Image1.Canvas.Pen.Mode := pmNotXor;
    Image1.Canvas.Pen.Color := clBlue;
    Image1.Canvas.Pen.Width := 1;
    Image1.Canvas.Pen.Style := psDot;
    Image1.Canvas.Brush.Style := bsClear;
    //矩形を描画
    Image1.Canvas.Rectangle(InitX, InitY, PrevX, PrevY);
  end;

end;

同じくImage1のOnMouseMoveイベント。

procedure TForm1.Image1MouseMove(Sender: TObject; Shift: TShiftState; X,
  Y: Integer);
begin

  if not DragFlag then exit;

  //不要な部分を消去
  Image1.Canvas.Rectangle(InitX, InitY, PrevX, PrevY);
  //新しい矩形を描画
  Image1.Canvas.Rectangle(InitX, InitY, X, Y);
  //現在値を取得
  PrevX := X;
  PrevY := Y;

end;

最後にImage1のOnMouseUpイベント。

procedure TForm1.Image1MouseUp(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin

  if not DragFlag then exit;

  //ラバーバンドの描画終了
  DragFlag := false;

  //不要な部分を消去
  Image1.Canvas.Rectangle(InitX, InitY, PrevX, PrevY);

  //ラバーバンドの最終サイズで矩形を描画
  Image1.Canvas.Pen.Color := clBlue;
  Image1.Canvas.Pen.Width := 3;
  Image1.Canvas.Rectangle(InitX, InitY, PrevX, PrevY);

end;

この状態で保存して実行すると・・・

ドラッグ中は、クリック位置(矩形の左上)を起点に、点線で矩形が描画される
マウスの左ボタンを離すと、選択範囲に太い実線で矩形が描画される

で、問題の再現。

Form右上に置いたCheckBox1のCaptionを「Image2を表示」にして、そのOnClickイベントに次のコードを書く。

procedure TForm1.CheckBox1Click(Sender: TObject);
begin
  if CheckBox1.Checked then
  begin
    Image2.Visible := True;
  end else begin
    Image2.Visible := False;
  end;
end;

プログラムを保存して、実行。CheckBox1にチェックを入れて、(見えないけど)Image2を表示。この状態でさっきと同じ場所をドラッグして、矩形選択しようとしても・・・

あたりまえですが、エラーも起きません!

ここで、いよいよ “BringToFront” の出番。

CheckBox2のCaptionを「Image1を最前面に表示」として、OnClickイベントには次のコードを記述。

procedure TForm1.CheckBox2Click(Sender: TObject);
begin
  if CheckBox2.Checked then
  begin
    Image1.BringToFront;
  end else begin
    Image1.SendToBack;
  end;
end;

保存して実行。

最初に、ChekBox1をチェック -> Image2が表示される(見えないけど)。

いちおうImage2があると思しきあたりをドラッグして矩形選択できないことを確認。

次に、CheckBox2をチェック -> Image1が最前面に表示される(はず)。

で、Image2があると思しきあたりをドラッグすると、今度は矩形が描かれる。

Image2のVisibleプロパティがTrueでも、Image1をBringToFrontすれば矩形が描画される

わかってしまえば、ほんとに、なんでもないこと・・・なんだけれど。
それが「わかる」までは(こんなことで悩むのは僕だけかもしれませんが)、
ほんとうに、苦しかった。

僕が調べた範囲では、Web上に「こんなことがあったよ!」っていう情報はなかったので、ここに “BringToFrontの物語” として記録しておけば、100万人にひとりくらい、もしかしたらいるかもしれない、同じ問題で困っている人へのヒントになるかもしれない・・・。

そう思ったのです。

5.まとめ

画面に描画処理を行う必要があるTImageを含む、複数のTImageを切り替えて使うプログラムを作るときは、描画処理対象のTImageを呼び出す(VisibleプロパティをTrueにする)際に必ず、次のようにして、これを最前面に表示しておく。

  //最前面に表示(持ってくる)
  Image1.BringToFront;

この設定を忘れると、他のVCLコントロールの設置状況によるが、矩形が描画できない領域が生じることがある。

この “BringToFront” と対をなすメソッドは “SendToBack” 。
こちらの使い方は、例えばCheckBoxのチェックの有無に対応させて、

procedure TForm1.CheckBox2Click(Sender: TObject);
begin
  if CheckBox2.Checked then
  begin
    Image1.BringToFront;
  end else begin
    Image1.SendToBack;
  end;
end;

Checkedの時は “BringToFront” でいいけど、not Checkedの時はどうしたらイイ?
・・・と思って調べて、”SendToBack” メソッドの存在も知りました!

6.お願いとお断り

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