月別アーカイブ: 2022年10月

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.お願いとお断り

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

I don’t want to press the enter key to confirm the input.

「入力確定のEnterキーは押したくない!」

TStringGridを使って何らかの入力作業を行う時、任意のあるキーを押したら直ちに、予め指定した内容をアクティブなセルに入力し(入力を確定)、次のセル(右 or 下)へフォーカスを移したいことがある。これは、そんな時のための備忘録。

1.Bキー押し下げでゼロを入力したい理由
2.StringGridを準備する
3.Bキー押し下げでゼロ入力を実装(その1)
4.任意の1文字+数字の入力を負の数に変換
5.Bキー押し下げでゼロ入力を実装(その2)
6.まとめ
7.お願いとお断り

1.Bキー押し下げでゼロを入力したい理由

手書き答案をスキャナーで読み込み、採点するプログラムを書いた。元の答案画像から設問ごとに解答欄をかき集めて一覧表示し、まとめて採点すれば効率よく採点できると思ったのだ(実際、試してみたら驚くほど速く採点できた!)。その手順を紹介。

ぱっと見て「よく出来てるなー」と思ったら、全員分採点記号と得点を一括入力
誤りの解答だけ採点記号を × にして、得点はゼロに変更

採点スタイルとして予定した(考えた)のは、「左手で入力作業、右手はマウス操作(解答欄のクリックと画像のスクロール)に専念する」というカタチ。

解答欄画像をクリックしたら、その座標から解答番号を計算し、採点欄のフォーカスが自動で移動するようプログラミング。(その方法は以下のリンク先を参照してください)

で、正の数値を入力したら、そのまま採点欄に、その数値が入力され、
Qとか、Sとか、何か文字を入力して確定したら、採点欄には0(ゼロ)が入り、さらに

オプション設定で「マイナス(ー)」記号に変換する文字を指定

上の設定であれば、aキーに続けて数字を入力して確定した場合は、採点欄に負の値が入力されるようにプログラミング。なぜaなのかというと、左手小指のホームポジションだからまず間違えずに(位置を確かめずに)押し下げ可能だと思ったから。

そもそも、なんで、こんな仕様(入力値が正負の数およびゼロ)にしたかというと、採点欄への数値入力と同時に、入力された数値に応じて、解答欄画像の方にも、採点記号と得点を(透過状態で)表示するプログラムにしたかったから。具体的な表示内容は次の通り。

(1)入力が正の数なら、解答欄画像の上に採点記号と得点を表示、
(2)入力が0(ゼロ)なら、解答欄画像の上に採点記号 × のみを表示
  (ゼロは〇:まるとまぎらわしいのでデフォルト設定では表示しない)、
(3)入力が負の数なら、解答欄画像の上に採点記号と部分点を表示。

当初、この採点補助プログラムでは、採点記号として〇と × しか利用できなかった(△とした場合に、それを見分ける良いフラグが用意できなかった)が、コピペしたプログラムコード中に残していた負の数は赤で表示するコードを見て、負の数を「部分点あり」のフラグとして利用できることに気づき、「部分点あり」の採点記号△も使えるように改良。

部分点を与える時は採点欄にマイナス記号に変換する文字と部分点になる数値を入力し、Enterキー押し下げ
負の数で「部分点あり」を表現(合計点は絶対値で計算すればイイ)

マウスは右手で操作する(左利きの方も?)ので、自ずと採点は左手で行うことに。

多くの場合、1問あたりの得点は5点未満だろうから、これらの数字キーはキーボードの左側にあって押しやすい。もし、数値でなく文字が入力された場合は、有無を言わさず0(ゼロ)に変換してしまえば、左手側にある1~5の数字キーの下には押しやすい文字キーがたくさんあるから、キーボード右側にあって、左手が届きにくい0(ゼロ)キーは押さなくてすむ。

あとは右手でマウスを操作し、解答欄画像を次々にクリックして、採点欄のフォーカスを切り替えて(=入力を確定して)行けば・・・

採点補助プログラムとして、十分使えるかなー?っと思ったんだけれど、

実際使ってみたら、入力後、次の解答欄画像をいちいちクリックして( or Enterキーを押し下げして)入力を確定 & 次の採点欄へフォーカスを移動させるのが、非常にめんどくさい。

せめて × の場合だけでも、採点欄に0(ゼロ)を入力した瞬間に、解答欄画像上に × を表示し、フォーカスが自動で次のセルへ移動するようにできないか?

そんな理由から、採点記号「 × 」は「ばってん」だから、BATTENで、Bキー押し下げ、即、0(ゼロ)を入力 & 確定、フォーカスは次のセルへ自動で移動するプログラムを書くことに決めました(Bキーも左手で押しやすい位置にあるのがうれしい!)。

2.StringGridを準備する

Bキー押し下げ、即、入力確定のプログラム自体は、前にStringGridで矢印キーの動作を制限したことがあったので、その時学んだテクニックを応用すれば、きっと書けると思ったので全然心配はなかったが、それを設定する対象のTStringGridは実に設定し甲斐のあるコントロールで、ある目的を実現(実装)しようとすると、そこに行きつくまでの工程が何段階も必要だったりする。

今回、この記事を書くのにあたり、いい機会だからStringGridの設定について(自分自身の勉強の復習の意味も込めて)まとめてみた。練習用に手間をかけずに作成したFormとコントロールは次の通り(Formに各VCLコントロールを置いただけ!)。

Form上に、StringGrid、Label、ComboBox、CheckBoxを各1個ずつ用意

で、FormCreate時の手続きは・・・

procedure TForm1.FormCreate(Sender: TObject);
begin

  //[Enter]でコントロールを移動させるために、Form上のコンポーネント
  //より先にキーボードイベントを取得する。
  KeyPreview := True;

  //描画処理は自前で行わずDelphiにおまかせ
  StringGrid1.DefaultDrawing := True;

  //Fixed(固定セル)のスタイル
  //現在のオペレーティングシステムのテーマを使用
  StringGrid1.DrawingStyle := gdsThemed;
  //標準のテーマの指定がないスタイル
  //StringGrid1.DrawingStyle := gdsClassic;
  //グラデーションのあるスタイル
  //StringGrid1.DrawingStyle := gdsGradient;

  //セルを強調表示
  StringGrid1.Options := StringGrid1.Options + [goDrawFocusSelected];

  //Clickでセル編集を可能にする-> [goEditing]をTrueに設定(方法は以下の通り)
  StringGrid1.Options := StringGrid1.Options + [goEditing];
  //常に編集可能に設定
  StringGrid1.Options := StringGrid1.Options + [goAlwaysShowEditor];

end;

KeyPreview := True の設定の他は、すべてStringGrid関係。僕がこのコントロールを使う時は、常に編集可能状態で起動するように設定することがほとんど。

続けてFormShow手続き。

procedure TForm1.FormShow(Sender: TObject);
var
  i : integer;
begin

  //行数と列数を適当に指定(Fixedセルを除いて10行10列あればテスト用途には十分)
  StringGrid1.RowCount := 11;
  StringGrid1.ColCount := 11;

  //FixedCols & FixedRows(固定列と固定行)を設定
  StringGrid1.FixedCols := 1;
  StringGrid1.FixedRows := 1;

  //フィールド名をセット(Rowに一括設定)
  //',Aの[,]に注意!-> セル[0,0]は空欄(フィールド名は入れない)
  //プログラムが長くなる時は['+]を使用してフィールド名を設定する
  StringGrid1.Rows[0].CommaText := ',A,B,C,D,E,F,'+
    'G,H,I,J';

  //FixedRows(固定行)に値をセット
  for i := 1 to 10 do
  begin
    StringGrid1.Rows[i].Append(IntToStr(i));
  end;

  //StringGrid1へフォーカスを移す。
  //下のようにまずフォーカスを移してからCol, Rowを指定。
  //でないとエラーになる。
  StringGrid1.SetFocus;
  StringGrid1.Col := 1;
  StringGrid1.Row := 1;
  StringGrid1.SetFocus;
  //カーソルが見えるようにする
  StringGrid1.EditorMode := True;

end;

さらにStringGrid1DrawCell手続きで、Fixed(固定)セルの表示方法と、入力された数値の右寄せ表示を指定。

implementation

uses
  Vcl.GraphUtil;

  //GraphUtilはFixedセルのセンタリング用に追加

procedure TForm1.StringGrid1DrawCell(Sender: TObject; ACol, ARow: Integer;
  Rect: TRect; State: TGridDrawState);
var
  i : integer;
begin
  //Fixedセルをセンタリング
  with StringGrid1 do
  begin
    if (gdFixed in State) then
    begin
      //usesにGraphUtilを追加(Vcl.GraphUtilではないことに注意!)
      //->Vcl.GraphUtilとすると「未定義の識別子エラー」になる!
      //GraphUtil.GradientFillCanvas(Canvas, GradientStartColor,
      //  GradientEndColor, Rect,gdVertical);
      //Vcl.GraphUtilとusesした場合
      //これは未定義の識別子エラーにならない
      Vcl.GraphUtil.GradientFillCanvas(Canvas, GradientStartColor,
        GradientEndColor, Rect,gdVertical);
      //センタリング
      DrawText(Canvas.Handle, PChar(Cells[ACol, ARow]),
        -1, Rect, DT_CENTER OR DT_VCENTER OR DT_SINGLELINE);
    end;
  end;

  //セルの表示を制御
  if not (gdFixed in state) then
  begin
    if StringGrid1.Cells[ACol,ARow] <> '' then
    begin
      //数値であるかどうかをCheck
      if not TryStrToInt(StringGrid1.Cells[ACol,ARow],i) then Exit;
      {数値である場合}
      //背景色を白に設定
      StringGrid1.Canvas.Brush.Color := clWhite;
      //正負をチェック
      if StrToInt(StringGrid1.Cells[ACol,ARow]) < 0 then
      begin
        StringGrid1.Canvas.Font.Color := clRed;
      end else begin
        StringGrid1.Canvas.Font.Color := clBlack;
      end;
      //セルを塗りつぶす
      StringGrid1.Canvas.FillRect(Rect);
      //数値は中央寄せで表示
      {DrawText(StringGrid1.Canvas.Handle,
              PChar(StringGrid1.Cells[ACol,ARow]),
              //[+1]は数値描画位置の調整のため
              Length(StringGrid1.Cells[ACol,ARow])+1,Rect,
              DT_CENTER or DT_VCENTER or DT_SINGLELINE);}
      //数値は右寄せで表示
      DrawText(StringGrid1.Canvas.Handle,
              PChar(StringGrid1.Cells[ACol,ARow]),
              //[+1]は数値描画位置の調整のため
              Length(StringGrid1.Cells[ACol,ARow])+1,Rect,
              DT_RIGHT or DT_VCENTER or DT_SINGLELINE);
    end;
  end;
end;

ついでにIMEも設定(IME ONの列は任意指定)。まず、次のように宣言しておいて・・・

//Col毎のIMEの制御(制御内容はStringGrid1GetEditTextを参照)
type
  _TGrid = class(TCustomGrid);

var
  Form1: TForm1;

implementation

StringGrid1GetEditText手続きで、次のように設定。

procedure TForm1.StringGrid1GetEditText(Sender: TObject; ACol, ARow: Integer;
  var Value: string);
begin
  //IMEの制御
  with TEdit(_TGrid(Sender).InplaceEditor) do
  begin
    case ACol of  //最初のAColは「 0 」
      2: ImeMode := imHira; //日本語入力ON
    else
      //ImeMode := imClose;   //日本語入力OFF-> ×
      ImeMode := imDisable;   //日本語入力OFFは imDisable
    end;
  end;
end;

ここまでの設定で、実行時の画面は、こんな感じ。

某有名表計算ソフト風の画面が出現

Enterキーでフォーカスを移動するために、FormKeyPress手続きで、次のように設定。

procedure TForm1.FormKeyPress(Sender: TObject; var Key: Char);
begin
  //[Enter]キーでコントロールを移動
  //StringGridは編集可能にFormCreateで設定しておく
  //->忘れるとセルの移動にEnter×2回必要!
  //この方法を使う時はKeyPreview:=True;をFormCreateで指定。
  if Ord(Key) = VK_RETURN then
  begin
    if ActiveControl is TStringGrid then
    begin
      if TStringGrid(ActiveControl).EditorMode then
      begin
        //VK_TABではカーソルがレコードの項目を右へ移動。
        //ActiveControl.Perform(WM_KEYDOWN,VK_TAB,0);
        //VK_DOWNにすると同じ項目の次のレコードへ移動。
        //if intStringGrid1ActiveRow < StringGrid1.RowCount-1 then
        if TargetRow < StringGrid1.RowCount-1 then
        begin
          ActiveControl.Perform(WM_KEYDOWN,VK_DOWN,0);
        end else begin
          ActiveControl.Perform(WM_KEYDOWN,VK_UP,0);
        end;
        Key := #0;
      end;
    end else begin
      SelectNext(ActiveControl,True,True);
      Key := #0;
    end;
  end;
end;

さらに、列幅を自動調整したい場合は・・・

procedure TForm1.CheckBox1Click(Sender: TObject);
var
  iCOL: Integer;
  iROW: Integer;
  MaxColWidth: Integer;
  TmpColWidth: Integer;
begin
  //DefaultColWidthを設定(これでCheck OFF時に元に戻る!)
  StringGrid1.DefaultColWidth:=64;
  //AutoAllColFit(全列幅の自動調整)
  if CheckBox1.Checked then
  begin
    for iCOL := 0 to StringGrid1.ColCount-1 do begin
      MaxColWidth := 0;
      for iROW := 0 to StringGrid1.RowCount-1 do
      begin
        //数字は列幅の調整用
        TmpColWidth := Canvas.TextWidth(StringGrid1.Cells[iCOL,iROW]) + 40;
        if MaxColWidth < TmpColWidth then
          MaxColWidth := TmpColWidth;
      end;
      StringGrid1.ColWidths[iCOL] := MaxColWidth;
    end;
  end;
end;

列幅自動調整実行時の画面は・・・(チェックOFFで、列幅は元に戻る)

列幅調整用の数値を調整して好みの幅に設定(上の画像では40を使用)

これでStringGridの準備が完了!

3.Bキー押し下げでゼロ入力を実装(その1)

※ 各セルに対して10以上の値の入力がないことが前提です!

この機能の実装にあたり、次のWebサイトにあった情報を参考にさせていただきました。質問者様と解答者様のご両名に対して、心から厚く御礼申し上げます。

@NIFTY FDELPHI Delphi Users’ Forum15番会議室「FAQ編纂委員会」に寄せられた「よくある質問の答え」

http://delfusa.main.jp/delfusafloor/archive/www.nifty.ne.jp_forum_fdelphi/faq/00075.htm

StringGrid 行移動の把握

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

Private宣言に、次のローカル変数とAppMessage手続きを追加。

  private
    { Private 宣言 }
    //入力=確定&フォーカスの移動用に追加
    //行・列位置を記憶する変数
    TargetRow:integer;
    TargetCol:integer;
    //ある(矢印他)キーが押されたことを知る
    procedure AppMessage(var Msg: TMsg; var Handled: Boolean);

Shift+Ctrl+C で AppMessage手続きを作成して、次の内容を設定。

※ usesに System.UITypes を追加するのを忘れないこと!(忘れるとBキーを意味するVKBが「未定義の識別子エラー」になる。

重要 次のコードでは、各セルに対して10以上の数値の入力は「ない」ものとしている。

implementation

uses
  Vcl.GraphUtil,
  System.UITypes;

  //GraphUtilはFixedセルのセンタリング用に追加
  //System.UITypesはキーコードでBキー(=VKB)を指定するために追加

{$R *.dfm}

procedure TForm1.AppMessage(var Msg: TMsg; var Handled: Boolean);
begin
  //任意のキーの押し下げをキャッチ
  if Msg.message = WM_KEYDOWN then
  begin
    //StringGridがアクティブだったら
    if ActiveControl is TStringGrid then
    begin
      //StringGridが編集可能だったら
      if TStringGrid(ActiveControl).EditorMode then
      begin
        //Bキー or 0キー押し下げでゼロを入力(入力値は10未満であることが前提)
        if (Msg.wParam=VKB) or (Msg.wParam=VK0) then
        begin
          //keybd_event(VK_TAB,0,0,0);
          //VK_TABではカーソルがレコードの項目を右へ移動。
          //ActiveControl.Perform(WM_KEYDOWN,VK_TAB,0);
          //VK_DOWNにすると同じ項目の次のレコードへ移動。
          if TargetRow < StringGrid1.RowCount-1 then
          begin
            //アクティブなセルが最終行でない場合はフォーカスは下へ移動
            StringGrid1.Cells[TargetCol, TargetRow]:='0';
            ActiveControl.Perform(WM_KEYDOWN,VK_DOWN,0);
          end else begin
            //最終行ならフォーカスは上へ移動
            StringGrid1.Cells[TargetCol, TargetRow]:='0';
            ActiveControl.Perform(WM_KEYDOWN,VK_UP,0);
          end;
          //Msg.wParam:=#0; //エラーになる
          Msg.wParam:=0;
        end;
      end;
    end;
  end;
end;

FormCreate時に、AppMessageを有効にする。これを忘れると動かない!

procedure TForm1.FormCreate(Sender: TObject);
begin

  ・・・ 省略(StringGridその他の初期設定) ・・・

  //入力=確定&フォーカスの移動用に追加
  //StringGridの初期位置の設定
  TargetRow := 1;
  TargetCol := 1;
  //AppMessageを有効にする
  Application.OnMessage := AppMessage;

end;

AppMessage手続きの引数にはACol, ARowがないから、その代わりにStringGrid1SelectCell手続きの最後で、行列位置を変数に取得。

procedure TForm1.StringGrid1SelectCell(Sender: TObject; ACol, ARow: Integer;
  var CanSelect: Boolean);
begin
  //入力=確定&フォーカスの移動用に追加
  //セルを選んだときに行位置を記憶
  TargetRow := ARow;
  //セルを選んだときに列位置を記憶
  TargetCol := ACol;
end;

実行時の様子は・・・

Bキーもしくは0キーの入力と同時にフォーカスは下のセルに移動する

4.任意の1文字+数字の入力を負の数に変換

Formに用意したLabel1のCaptionプロパティには「マイナス記号に置換する文字:」を設定し、ComboBox1のTextプロパティに「a」を設定。

FormCreate手続きの最後で、マイナス記号に置換する文字の選択肢を準備。

procedure TForm1.FormCreate(Sender: TObject);
begin

  ・・・ 省略 ・・・

  //入力=確定&フォーカスの移動用に追加
  //StringGridの初期位置の設定
  TargetRow := 1;
  TargetCol := 1;
  //AppMessageを有効にする
  Application.OnMessage := AppMessage;

  //マイナス記号に変換する文字の選択肢
  ComboBox1.Items.Add('q');
  ComboBox1.Items.Add('a');
  ComboBox1.Items.Add('z');

end;

StringGrid1DrawCell手続きに、次の赤字の部分を追加。
(1列目であったら、文字の入力はすべてゼロに変換する処理も追加している)

procedure TForm1.StringGrid1DrawCell(Sender: TObject; ACol, ARow: Integer;
  Rect: TRect; State: TGridDrawState);
var
  i: integer;
  str1, str2: string;
begin

  ・・・ 省略 ・・・

  //セルの表示を制御(中央寄せ・負の数は赤で表示)
  if not (gdFixed in state) then
  begin
    if StringGrid1.Cells[ACol,ARow] <> '' then
    begin

      //文字数が2文字なら実行
      if Length(WideString(StringGrid1.Cells[ACol,ARow])) = 2 then
      begin
        //指定文字が入力されたら'-'に変換
        str1 := LowerCase(Copy(StringGrid1.Cells[ACol,ARow],1,1));
        str2 := Copy(StringGrid1.Cells[ACol,ARow],2,1);
        if str1 = LowerCase(ComboBox1.Text) then
        begin
          StringGrid1.Cells[ACol,ARow] := '-'+str2;
        end;
      end;

      if ACol = 1 then
      begin
        //「文字」はすべて'0'に変換
        if not TryStrToInt(StringGrid1.Cells[ACol,ARow], i) then
        begin
          StringGrid1.Cells[ACol,ARow] := '0';
        end;
      end;

  ・・・ 省略 ・・・

実行時の様子は・・・

a2と入力してEnterキー押し下げで確定
無事、目的を達成

左手だけで、視線をキーボードに落とすことなく、負の数も簡単に入力できるようになった☆

ただし、上の手続きでは、StringGridのセルへの入力が3桁であった場合に対応できない。こと採点に関しては、部分点が2桁の数値になることは、多分アリエナイから、採点補助プログラム用のアルゴリズムとしての利用に限れば、上の手続きでも、まず問題は起きないと思うが・・・もし、どうしても3桁以上の入力値に対応させたいなら、コードを次のように変更すればOK!

//StringReplaceuses関数を使用するので uses節に System.SysUtils を追加
uses
  System.SysUtils
procedure TForm1.StringGrid1DrawCell(Sender: TObject; ACol, ARow: Integer;
  Rect: TRect; State: TGridDrawState);
var
  i: integer;
  str1, str2: string;
begin

  ・・・ 省略 ・・・

  //セルの表示を制御(中央寄せ・負の数は赤で表示)
  if not (gdFixed in state) then
  begin
    if StringGrid1.Cells[ACol,ARow] <> '' then
    begin

      //文字数が2文字なら実行 -> コメント化
      {if Length(WideString(StringGrid1.Cells[ACol,ARow])) = 2 then
      begin
        //指定文字が入力されたら'-'に変換
        str1 := LowerCase(Copy(StringGrid1.Cells[ACol,ARow],1,1));
        str2 := Copy(StringGrid1.Cells[ACol,ARow],2,1);
        if str1 = LowerCase(ComboBox1.Text) then
        begin
          StringGrid1.Cells[ACol,ARow] := '-' + str2;
        end;
      end;}

      //文字数が2文字以上なら実行
      if Length(WideString(StringGrid1.Cells[ACol,ARow])) >= 2 then
      begin
        //指定文字が入力されたら'-'に変換
        str1 := LowerCase(Copy(StringGrid1.Cells[ACol,ARow],1,1));
        //2桁以上の入力値に対応
        str2 := StringReplace(
          LowerCase(StringGrid1.Cells[ACol,ARow]), 
          str1, '', [rfReplaceAll, rfIgnoreCase]);
        if str1=LowerCase(ComboBox1.Text) then
        begin
          StringGrid1.Cells[ACol,ARow] := '-'+str2;
        end;
      end;

      if ACol = 1 then
      begin
        //「文字」はすべて'0'に変換
        if not TryStrToInt(StringGrid1.Cells[ACol,ARow], i) then
        begin
          StringGrid1.Cells[ACol,ARow] := '0';
        end;
      end;

  ・・・ 省略 ・・・
適当な値を入力してEnterキーを押し下げて確定
採点プログラムとしての実用性は感じられないが・・・プログラム的には目的を達成

5.Bキー押し下げでゼロ入力を実装(その2)

※ 各セルに対して10以上の値の入力がある場合

各セルに対して10以上の値の入力がある場合は、入力された0(ゼロ)が不正解の0(ゼロ)なのか、10の2桁目の0(ゼロ)なのか、判定する工夫が必要になるが、良い判定方法が思いつかなかった。

そこで思い切って問題を単純化し、「高速入力モード」を作成して、それが ON の場合は入力値を0-9に限定し、ユーザーがそのことを理解した上で操作できるように工夫してみた。もし、各セルに対して10以上の値の入力がある場合は、「高速入力モード」は OFF で使用して貰い、Bキーが押された場合のみ、0(ゼロ)に変換して入力確定 ⇨ フォーカスを移動することにして、数字キーの0(ゼロ)の入力に対しては、直ちに入力の確定としないことにした。

あと、ついでだから、「高速入力モード」の名に恥じないよう、それが ON の場合は、0-9の数字キー押し下げで、直ちに入力確定、次のセルへフォーカスが移動する処理も追加してみた。以下、その実装。

CheckBox2を追加し、Captionを設定
procedure TForm1.AppMessage(var Msg: TMsg; var Handled: Boolean);
var
  str1:string;
begin
  //任意のキーの押し下げをキャッチ
  if Msg.message = WM_KEYDOWN then
  begin
    //StringGridがアクティブだったら
    if ActiveControl is TStringGrid then
    begin
      //StringGridが編集可能だったら
      if TStringGrid(ActiveControl).EditorMode then
      begin

        //高速入力使用の有無で処理を切り替え
        if not CheckBox2.Checked then
        begin

          //高速入力を使用しない場合の処理
          //Bキー押し下げでゼロを入力
          //0キー押し下げは無視
          if (Msg.wParam=VKB) then
          begin
            //keybd_event(VK_TAB,0,0,0);
            //VK_TABではカーソルがレコードの項目を右へ移動。
            //ActiveControl.Perform(WM_KEYDOWN,VK_TAB,0);
            //VK_DOWNにすると同じ項目の次のレコードへ移動。
            if TargetRow < StringGrid1.RowCount-1 then
            begin
              //下のセルへ移動
              StringGrid1.Cells[TargetCol, TargetRow]:='0';
              ActiveControl.Perform(WM_KEYDOWN,VK_DOWN,0);
            end else begin
              //上のセルへ移動
              StringGrid1.Cells[TargetCol, TargetRow]:='0';
              ActiveControl.Perform(WM_KEYDOWN,VK_UP,0);
            end;
            //Msg.wParam:=#0; //エラーになる
            Msg.wParam:=0;
          end;

        end else begin

          //高速入力を使用する場合の処理
          //Bキー押し下げでゼロを入力
          //0キー押し下げにも対応
          if (Msg.wParam=VKB) or (Msg.wParam=VK0) then
          begin
            //keybd_event(VK_TAB,0,0,0);
            //VK_TABではカーソルがレコードの項目を右へ移動。
            //ActiveControl.Perform(WM_KEYDOWN,VK_TAB,0);
            //VK_DOWNにすると同じ項目の次のレコードへ移動。
            if TargetRow < StringGrid1.RowCount-1 then
            begin
              //下のセルへ移動
              StringGrid1.Cells[TargetCol, TargetRow]:='0';
              ActiveControl.Perform(WM_KEYDOWN,VK_DOWN,0);
            end else begin
              //上のセルへ移動
              StringGrid1.Cells[TargetCol, TargetRow]:='0';
              ActiveControl.Perform(WM_KEYDOWN,VK_UP,0);
            end;
            //Msg.wParam := #0; //エラーになる
            Msg.wParam := 0;
          end;

          //1-9の入力があった場合
          if StringGrid1.Cells[TargetCol, TargetRow] <> '' then
          begin
            str1:=Copy(StringGrid1.Cells[TargetCol, TargetRow],1,1);
          end else begin
            str1 := '';
          end;

          //任意の1文字+数字の入力を負の数に変換する処理用に追加
          if (str1 <> '-') and (str1 <> ComboBox1.Text) then
          begin
            if (Msg.wParam = VK1) then
            begin
              //keybd_event(VK_TAB,0,0,0);
              //VK_TABではカーソルがレコードの項目を右へ移動。
              //ActiveControl.Perform(WM_KEYDOWN,VK_TAB,0);
              //VK_DOWNにすると同じ項目の次のレコードへ移動。
              //if intStringGrid1ActiveRow < StringGrid1.RowCount-1 then
              if TargetRow < StringGrid1.RowCount-1 then
              begin
                //下のセルに移動
                StringGrid1.Cells[TargetCol, TargetRow] := '1';
                ActiveControl.Perform(WM_KEYDOWN,VK_DOWN,0);
              end else begin
                //上のセルに移動
                StringGrid1.Cells[TargetCol, TargetRow] := '1';
                ActiveControl.Perform(WM_KEYDOWN,VK_UP,0);
              end;
              //Msg.wParam := #0; //エラーになる
              Msg.wParam := 0;
            end;

            if (Msg.wParam = VK2) then
            begin
              if TargetRow < StringGrid1.RowCount-1 then
              begin
                //下のセルに移動
                StringGrid1.Cells[TargetCol, TargetRow] := '2';
                ActiveControl.Perform(WM_KEYDOWN,VK_DOWN,0);
              end else begin
                //上のセルに移動
                StringGrid1.Cells[TargetCol, TargetRow] := '2';
                ActiveControl.Perform(WM_KEYDOWN,VK_UP,0);
              end;
              Msg.wParam := 0;
            end;

            if (Msg.wParam = VK3) then
            begin
              if TargetRow < StringGrid1.RowCount-1 then
              begin
                //下のセルに移動
                StringGrid1.Cells[TargetCol, TargetRow] := '3';
                ActiveControl.Perform(WM_KEYDOWN,VK_DOWN,0);
              end else begin
                //上のセルに移動
                StringGrid1.Cells[TargetCol, TargetRow] := '3';
                ActiveControl.Perform(WM_KEYDOWN,VK_UP,0);
              end;
              Msg.wParam := 0;
            end;

            if (Msg.wParam = VK4) then
            begin
              if TargetRow < StringGrid1.RowCount-1 then
              begin
                //下のセルに移動
                StringGrid1.Cells[TargetCol, TargetRow] := '4';
                ActiveControl.Perform(WM_KEYDOWN,VK_DOWN,0);
              end else begin
                //上のセルに移動
                StringGrid1.Cells[TargetCol, TargetRow] := '4';
                ActiveControl.Perform(WM_KEYDOWN,VK_UP,0);
              end;
              Msg.wParam := 0;
            end;

            if (Msg.wParam = VK5) then
            begin
              if TargetRow < StringGrid1.RowCount-1 then
              begin
                //下のセルに移動
                StringGrid1.Cells[TargetCol, TargetRow] := '5';
                ActiveControl.Perform(WM_KEYDOWN,VK_DOWN,0);
              end else begin
                //上のセルに移動
                StringGrid1.Cells[TargetCol, TargetRow] := '5';
                ActiveControl.Perform(WM_KEYDOWN,VK_UP,0);
              end;
              Msg.wParam := 0;
            end;

            if (Msg.wParam = VK6) then
            begin
              if TargetRow < StringGrid1.RowCount-1 then
              begin
                //下のセルに移動
                StringGrid1.Cells[TargetCol, TargetRow] := '6';
                ActiveControl.Perform(WM_KEYDOWN,VK_DOWN,0);
              end else begin
                //上のセルに移動
                StringGrid1.Cells[TargetCol, TargetRow] := '6';
                ActiveControl.Perform(WM_KEYDOWN,VK_UP,0);
              end;
              Msg.wParam := 0;
            end;

            if (Msg.wParam = VK7) then
            begin
              if TargetRow < StringGrid1.RowCount-1 then
              begin
                //下のセルに移動
                StringGrid1.Cells[TargetCol, TargetRow] := '7';
                ActiveControl.Perform(WM_KEYDOWN,VK_DOWN,0);
              end else begin
                //上のセルに移動
                StringGrid1.Cells[TargetCol, TargetRow] := '7';
                ActiveControl.Perform(WM_KEYDOWN,VK_UP,0);
              end;
              Msg.wParam := 0;
            end;

            if (Msg.wParam = VK8) then
            begin
              if TargetRow < StringGrid1.RowCount-1 then
              begin
                //下のセルに移動
                StringGrid1.Cells[TargetCol, TargetRow] := '8';
                ActiveControl.Perform(WM_KEYDOWN,VK_DOWN,0);
              end else begin
                //上のセルに移動
                StringGrid1.Cells[TargetCol, TargetRow] := '8';
                ActiveControl.Perform(WM_KEYDOWN,VK_UP,0);
              end;
              Msg.wParam := 0;
            end;

            if (Msg.wParam = VK9) then
            begin
              if TargetRow < StringGrid1.RowCount-1 then
              begin
                //下のセルに移動
                StringGrid1.Cells[TargetCol, TargetRow] := '9';
                ActiveControl.Perform(WM_KEYDOWN,VK_DOWN,0);
              end else begin
                //上のセルに移動
                StringGrid1.Cells[TargetCol, TargetRow] := '9';
                ActiveControl.Perform(WM_KEYDOWN,VK_UP,0);
              end;
              Msg.wParam := 0;
            end;

          end;
        end;
      end;
    end;
  end;
end;

6.まとめ

重要 各セルへの入力値が10未満であることが前提のコードです!

Bキーを押すだけでStringGridのアクティブなセルにゼロを入力し、フォーカスを次のセルへ移動するプログラムで、必要な変数と手続きは次の通り。

各セルへの入力値が10以上の場合、「まとめ」のコードは期待通りに動作しません。
10以上の入力値にも対応させたい場合は、「5.各セルに対して10以上の値の入力がある場合」が参考になるかもしれません。

  private
    { Private 宣言 }
    //行・列位置を記憶する変数
    TargetRow:integer;
    TargetCol:integer;

    //ある(矢印他)キーが押されたことを知る
    procedure AppMessage(var Msg: TMsg; var Handled: Boolean);

//Col毎のIMEの制御(制御内容はStringGrid1GetEditTextを参照)
type
  _TGrid = class(TCustomGrid);

var
  Form1: TForm1;

implementation

uses
  Vcl.GraphUtil,
  System.UITypes;

  //GraphUtilはFixedセルのセンタリング用に追加
  //System.UITypesはキーコードでBキー(=VKB)を指定するために追加

{$R *.dfm}

procedure TFormCollaboration.FormCreate(Sender: TObject);
begin
  //StringGridの初期位置の設定
  TargetRow := 1;
  TargetCol := 1;
    //AppMessageを有効にする <- 忘れないこと!
  Application.OnMessage := AppMessage;
  //[Enter]でコントロールを移動させるために、Form上のコンポーネント
  //より先にFormがキーボードイベントを取得する。
  KeyPreview := True;
end;

procedure TForm1.StringGrid1SelectCell(Sender: TObject; ACol, ARow: Integer;
  var CanSelect: Boolean);
begin
  //入力=確定&フォーカスの移動用に追加
  //セルを選んだときに行位置を記憶
  TargetRow := ARow;
  //セルを選んだときに列位置を記憶
  TargetCol := ACol;
end;

procedure TForm1.AppMessage(var Msg: TMsg; var Handled: Boolean);
begin
  //任意のキーの押し下げをキャッチ
  if Msg.message = WM_KEYDOWN then
  begin
    //StringGridがアクティブだったら
    if ActiveControl is TStringGrid then
    begin
      //StringGridが編集可能だったら
      if TStringGrid(ActiveControl).EditorMode then
      begin
        //Bキー or 0キー押し下げでゼロを入力(入力値は10未満であることが前提)
        if (Msg.wParam=VKB) or (Msg.wParam=VK0) then
        begin
          //keybd_event(VK_TAB,0,0,0);
          //VK_TABではカーソルがレコードの項目を右へ移動。
          //ActiveControl.Perform(WM_KEYDOWN,VK_TAB,0);
          //VK_DOWNにすると同じ項目の次のレコードへ移動。
          if TargetRow < StringGrid1.RowCount-1 then
          begin
            //アクティブなセルが最終行でない場合はフォーカスは下へ移動
            StringGrid1.Cells[TargetCol, TargetRow] := '0';
            ActiveControl.Perform(WM_KEYDOWN,VK_DOWN,0);
          end else begin
            //最終行ならフォーカスは上へ移動
            StringGrid1.Cells[TargetCol, TargetRow] := '0';
            ActiveControl.Perform(WM_KEYDOWN,VK_UP,0);
          end;
          //Msg.wParam := #0; //エラーになる
          Msg.wParam := 0;
        end;
      end;
    end;
  end;
end;

7.お願いとお断り

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

Causes of hard-to-find bugs

「また、やっちゃった。発見困難なバグの原因(は自分)」

数十枚の画像を次々に切り貼りして保存するプログラムを書いた際に、少しでも高速に処理するため、VCLのBmpをGDI+のBmpに変換して保存する方法を採用した。その際、var宣言に画像処理用の変数を、付け足し、付け足し・・・してプログラムを書いたら、自ら発見困難なバグを作り出してしまった・・・というお話。

1.不思議な現象が発生(バグその1)
2.原因を解明(したはずだった)
3.さらに不思議な現象が発生(バグその2)
4.バグ作成の元になった状況を再現
5.Createしないで使った場合は・・・
6.まとめ
7.お願いとお断り

1.不思議な現象が発生(バグその1)

TImageに数十枚の画像を切り貼りする処理は、それなりに時間がかかる。なので、処理が完了するまでは、ButtonのEnabledプロパティをFalseに設定して、気の短いユーザーに何度もボタンをクリックされるのを防止する。

このような場合、try ~ finally ~ end; を使って・・・

begin
  Button1.Enabled := False;
  try
    //処理
  finally
    Button1.Enabled := True;
  end;
end;

処理の途中で、なにかエラーがあっても、最終的にはTButtonのEnabledプロパティがTrueになるように組むことが基本だと学んだ。

同様に、TImageをマウスでクリックした際のイベントを拾う処理でも、間違ったクリックを拾うのを防止するため、ユーザーのクリックを拾った場合はTImageのEnabledプロパティを、一時的にFalseに設定して、メッセージを表示し、ユーザーの受け答えに応じてEnabledプロパティをTrue/Falseのいずれかに設定するようにしたのだが、この処理のどこかでTImageのEnabledプロパティがFalseのままになってしまって、いて・・・。

そのため「ある特定の画像処理手続き(その1)」を実行した後では、TImageのEnabledプロパティがFalseになっているから、TImageの画像をクリックすると走る「ある特定の画像処理手続き(その2)」が絶対に実行できない。プログラムを再起動して、手続き(その1)を実行せずに、手続き(その2)を実行した場合は、何でもなかったかのように問題なく手続き(その2)が実行できる。・・・という、理由がわかってみれば当たり前なんだけど、原因がわかるまでは何とも摩訶不思議な現象が発生(これがSetFocusならエラーが発生するから、話はまた別なんだけど・・・)。

2.原因を解明(したはずだった)

プログラムが完成に近づいたところで、(なんでかなー?)って、真剣に考えてようやくTImageのEnabledプロパティ設定の切り替え忘れだと気づき、あわててTImageに対する処理の直前に Image1.Enabled := True; を入れてプログラムを修正。

こんなことにならないよう、画像処理(その2)の手続きの最初に、先に述べたように、Image1.Enabled := True; と記述して強制的にエラー防止策をとるか、「TImageのEnabledプロパティがFalseで変更できません!」みたいなエラーメッセージが表示されるよう、if not Image1.Enabled then のようなエラー回避の処理を入れておくべきだったのだ。そうすれば、もっと早く間違いを発見できたと思うのだが、実際には、EnabledプロパティがFalse状態のTImageをクリックしても「何も起こらない」(もちろんエラーも起きない)ので、Enabledプロパティの設定が原因だと気づくまでに(なんでかなー?)っと、考えに考え、それなりに時間がかかってしまったのだ。

これで原因は解明され、バグは消えた(・・・と僕は思っていた)。

3.さらに不思議な現象が発生(バグその2)

TImageのEnabledプロパティ設定を修正したプログラムを実行してみると、今度は画像処理(その1)を行ったあと、連続して画像処理(その2)を確実に実行できるようになった。

MyPCで最初にテストした時は、画像処理(その1)に続けて、画像処理(その3)も確かにエラーなく実行できた。何回か、その後もMyPCでテストを繰り返し、僕は問題が完全に解決できたと信じ、MyPCではない、このプログラムを実際に実行(運用)する予定の業務用ノートPCで試しにプログラムを動かしてみた。すると・・・

MyPCではエラーを起こしたことは1回もなかったのに、業務用のノートPCでは画像処理(その3)で時々エラーが発生する。しかも、それが毎回必ず発生するわけではなく、起きる時と、起きない時があり、どちらかと言えば、起きるほうが少ない。画像処理(その3)は数十枚の画像に変更を加えて、さらにそれを1枚ずつ保存する時間のかかる重たい処理なので、途中で何らかの障害が発生してエラーになるのかと思ったが、エラーが起きなくても(エラーメッセージが明示的に表示されなくても)、画像に対して行った変更が「まったく保存されていない場合がある」ことにも気づく。同じループの中で処理した画像なのに、変更が保存される場合と、されない場合の2通りがあるなんて! しかも、ランダムに。これはもう、完全に想定外。・・・てか、Delphi環境下、Object Pascalで書いたプログラムで、まさか、こんなことが起きるなんて・・・信じられない。Delphiとの思い出を過去20年遡って、こんなエラーを、僕は、これまでに経験したことが「ない」。

混乱の中で思いついたことは、GDI+を使った保存処理の記述のどこかに問題があるのは間違いないから、いったん、GDI+で処理していた部分をコメント化して、旧来のオーソドックスなJpeg画像の保存処理に変更してみることだった。これでエラーが起こらずに、変更を加えた画像データがきちんと保存できれば、最後の一手だけは確保できる。

procedure TForm1.ButtonXClick(Sender: TObject);
var
  jpg: TJPEGImage;
  s, strText: string;
begin
  //エラーが発生しても処理を止めない
  try
    for i := 1 to StringGrid1.RowCount-1 do
    begin
      S := ChangeFileExt(ListBox2.Items[i-1], '.jpg');
      Jpeg := TJPEGImage.Create;
      try
        //Jpeg.Assign(Image1.Picture.Bitmap);
        Jpeg.Assign(Image1.Picture.Graphic);
        Jpeg.Compress;
        Jpeg.SaveToFile(S);
      finally
        Screen.Cursor := crDefault;
        Jpeg.Free;
      end;
    end;
  except
    //エラー発生時の処理
    on E: Exception do begin
      strText := E.ClassName + sLineBreak + E.Message;
      Application.MessageBox(PChar(strText), '情報', MB_ICONINFORMATION);
    end;
  end;
end;

期待した通り、これなら、まったくエラーは起きない。ただし、GDI+を使った画像の高速な保存処理に慣れてしまった自分には、耐え難いほど処理速度が遅い・・・

すごく悩む。
時々、エラーは起こすけど、とりあえず動くし、何より速いGDI+のままで行くか、
それとも、遅いけど、確実に動作する旧来のJpeg画像の保存方法に変えるか、
それとも、いっそのこと、ユーザーが画像の保存処理方法を選択できるようにするか、
それとも、エラーが起きた時だけ、旧来の保存方法に戻そうか、
でも、明示的なエラーが起こらずに、変更した画像が保存されてない場合もあるし・・・

どうしよう・・・

困ったことにGDI+を使った方法では、もし明示的なエラーが発生しても、続けてもう一度保存処理を実行すれば「何事もなかったか」のようにプログラムは走り、多くの場合、「何事もなかったか」のように画像が保存されるのだ。ただ、ループ処理10回に1回くらいの割合で、エラーが出ないにもかかわらず、しかも同一ループの中で保存処理する画像全部ではなく、そのうちの数枚だけ、加えた変更が「なぜか、反映されない」不思議な現象がランダムに起こってしまう。

いくらGDI+で保存する処理のプログラムを眺めても、原因が見出せない。
(この時はVCLからGDI+へのビットマップの変換部分に原因があると思っていた)

GDI+の保存処理のどこに原因があるのか、それがどうしてもわからなくて困った僕は、要するにJpegで保存するから、圧縮に時間がかかって遅いんだと考え、試しにビットマップ画像で保存する処理も試してみることにした。

  S:=ChangeFileExt(ListBox2.Items[i-1], '.bmp');
  tmpBmp:=TBitmap.Create;
  try
    tmpBmp.Assign(Image1.Picture.graphic);
    tmpBmp.SaveToFile(S);
  finally
    tmpBmp.Free;
  end;

エラーも出ず、全ての画像が確実に保存され、かつ、処理速度も速い。
ただ、Jpegで保存すれば、1枚あたり数百KBしかなかった画像が、わずか1枚で10MBを超える容量を食ってしまう・・・。ここさえ目をつぶればBMPも「あり」なんだけど。

だが、作業する度にとんでもない容量を食いつぶす画像データが生成されることを考えると、いくらGBオーダーのSSDを積んでるって言っても、やっぱりBMPでの保存は無理だ。

ここにきて、ようやく僕はTImageのEnabledプロパティの設定以外の、より重大で、深刻なバグが自分の書いたプログラムのどこかに潜んでいることに気づく。

溺れる者は何とかで(せめて、毎回、確実にエラーが起きてくれれば・・・)と、とんでもないことまで考えてしまう(これまでいろんなプログラムを作ったけれど、エラーが発生して欲しいと、心から願ったことは多分なかった気がする・・・)。

何度、プログラムの怪しいと思われる(ビットマップへの変換処理)部分を見返しても原因がわからない。援けてくれる人は誰もいない。泣いても、喚いても、自分で何とかするしかない。今までにも数限りなく、これを繰り返してきたんだけれど、ここでまた・・・

選択肢は次のいずれか。
GDI+をあきらめる、か、あきらめないか だ。
自分で決めるしかない。どちらをとる? 決心するための、自問自答を繰り返す。

( あきらめたら、僕は、もう、よくなれない )

答えは一つしかない。それは最初からわかっている。大量の画像を保存するから、処理速度が速いことが、絶対条件だ。ならば、今、自分が知っている最良・最速の処理方法であるGDI+を使うしかない。GDI+のプログラム自体に間違いがあるとは思えないから、データの保存処理の記述を見直して、誤りを発見・修正し、その保存の確実性を100%にすればいいだけだ。少なくとも、今、画像データの保存処理のどこかに重大な問題が隠れていることだけは、わかった。

( きっと、もう少しだ )

このプログラムより先にGDI+を使って書いた1枚の画像を保存する処理は、確実に成功して、エラーが発生したことは1度もない。だから、GDI+のプログラム自体には絶対に間違いはない。自分の書き方のどこかに問題がある。もし、GDI+の使用をあきらめないなら、時々エラーになるその原因を探し、修正すること以外に、その解決方法はない。

( いま、僕に、できることは・・・? )
( 原因がわかるまで、最初から1行ずつ、プログラムを見直すんだ・・・。 )

そう決心した僕は、試しに書いたTJPEGImageを使う保存手続きをコメント化して、もう一度、GDI+を使った保存手続きを最初から1行ずつ、読んでみることに決めた。

4.バグ作成の元になった状況を再現

次のプログラムが「バグ作成!の元になった状況を再現」したもの。どこに重大な問題があるのか、すぐに気がつかないのは僕だけかもしれないが・・・。
(実際には、この他にもその他の変数の宣言、数々のエラー処理や、ループの中で複数の画像を加工する処理が書かれている。また、ファイルの保存パスはプログラム内で明示的に指定し、SaveDialogは使用していない)

重要 コピペ厳禁!!(このプログラムには重大な誤りがあります)

implementation

uses
  System.IOUtils,
  Winapi.GDIPAPI, Winapi.GDIPOBJ, Winapi.GDIPUTIL;

  //System.IOUtilsはPathから拡張子を取得するTPath.GetExtensionを使うために追加
  //GDIPAPI, GDIPOBJ はGDI+を利用した描画に資料するために必要
  //GDIPUTILを宣言すればGetEncoderClsid関数を利用してGUIDを取得できる

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
var
  //TImageへの画像読み込み用に使用
  bmp:TGPBitmap;
  //VCL TBitmapからGDI+ Bitmapへの変換に使用
  Graphics:TGPGraphics;
  srcBMP:TBitmap;
  dstBMP:TGPBitmap;
  stream:TMemoryStream;
  //拡張子を取得するために使用
  dotExt, strExt:string;
  //GetEncoderClsid関数の利用とTGUIDを使用するには、usesにWinapi.GDIPUTILが必要
  ImgGUID:TGUID;
begin

  //Create
  bmp:=TGPBitmap.Create;
  try
    //TImageへ画像を読み込む処理
  finally
    //確実に解放
    bmp.Free;
  end;

  {TImageに読み込んだ画像に数々の変更を加える処理}

  //SaveDialogのプロパティはExecuteする前に設定しておくこと
  With SaveDialog1 do begin
    //デフォルトのファイル名を設定
    FileName:='Test';
    //表示するファイルの種類をcsvに設定
    //Filter:='コンマ区切りテキストファイル(*.csv)|*.csv';
    //表示するファイルの種類を設定
    //Filter:='JPEG Files (*.jpg, *.jpeg)|*.jpg;*.jpeg';
    Filter:='画像ファイル|*.png;*.jpg;*.gif;*.bmp;*.tif;*.emf;*.wmf;*.ico' +
    '|*.png|*.png' +
    '|*.jpg|*.jpg' +
    '|*.gif|*.gif' +
    '|*.bmp|*.bmp' +
    '|*.tif|*.tif';
    //データの読込先フォルダを指定
    InitialDir:=ExtractFilePath(Application.ExeName)+'Data';
    //拡張子の指定がなかった場合に付加される拡張子を指定
    DefaultExt:='jpg';
    //上書き保存の確認の設定
    Options:=[ofOverWritePrompt];
  end;

  if not SaveDialog1.Execute then Exit; //キャンセルに対応

  //保存(VCL TBitmap -> GDI+ Bitmap)
  srcBMP:=TBitmap.Create;
  srcBMP.Width:=Image1.Width;
  srcBMP.Height:=Image1.Height;
  srcBMP.Assign(Image1.Picture.graphic);
  //データ受け渡し用のストリームを生成して保存
  stream:=TMemoryStream.Create;
  srcbmp.SaveToStream(stream);
  //保存GDI+のBMPを生成
  dstbmp:=TGPBitmap.Create(TStreamAdapter.Create(stream));
  ////変更できるのはBitmapを含む画像のみですのエラーが発生
  //Graphics:=TGPGraphics.Create(Image1.Canvas.Handle);
  //これならエラーは発生しない
  Graphics:=TGPGraphics.Create(Image1.Picture.Bitmap.Canvas.Handle);
  try
    Graphics.DrawImage(dstbmp,0,0);
    //拡張子を小文字に変換して取得(.XXX形式:Dotが付いている)
    dotExt:=LowerCase(TPath.GetExtension(SaveDialog1.FileName));
    //JPEGに対応する
    if dotExt='.jpg' then begin
      strExt:='jpeg';
    end;
    //指定された拡張子を付けて保存
    if GetEncoderClsid('image/'+strExt, ImgGUID) >= 0 then
    begin
      bmp.Save(ChangeFileExt(SaveDialog1.FileName, dotExt), ImgGUID);
    end;
  finally
    Graphics.Free;
    srcbmp.Free;
    dstBMP.Free;
    stream.Free;
  end;
end;

上のコードのもとのプログラムは、SaveDialogでファイル名を含めた保存パスを取得してGDI+で保存処理するものだった。だから、ビットマップ変換用の変数は必要なく、一つだけ、ビットマップデータを入れるTGPBitmap型の変数bmpを用意すれば事足りた。

GDI+を使った画像の保存処理を実現するために、どうしても必要だったのが「VCLのビットマップ」を「GDI+のビットマップ」に変換する作業で、これが出来なかった僕はさんざん悩みながら、Web上の情報に援けてもらって、この変換処理を行う方法を学んだ(その詳細は、次のリンク先を参照)。

で、僕はビットマップ変換処理用に、srcBMP:TBitmap; dstBMP:TGPBitmap; stream:TMemoryStream; 等の変数を今回のプログラムに追加した。

GDI+で書いた元々のプログラムは、ファイルとして存在する画像データをOpenDialogを使ってGDI+ビットマップに読み込み、SaveDialogでファイル名を含めて保存パスを指定して処理するものだった。だから、ビットマップ変換用の変数は必要なく、bmp:TGPBitmap; として、ビットマップデータを入れる変数を1つだけ var 宣言して、もちろん、読み込み時にも、書き込み時にも、それぞれの手続きで同じように、これをローカル変数として使用した。

読み込み、書き込みの手続きはそれぞれ独立していたから try 文のfinallyブロックで、bmp.Free として最後に確実に解放すれば、何も問題は起きなかった。

しかし、このプログラムの保存手続きでは、GDI+を利用して、高速にTImageへ画像を読み込み、その画像に変更を加え、TImageのVCLのビットマップからGDI+のビットマップに変換して、保存処理を行っている。

1行ずつプログラムを確認して行く。そして、ついにバグの原因に気づく。

var
  //TImageへの画像読み込み用に使用
  bmp:TGPBitmap;
  //VCL TBitmapからGDI+ Bitmapへの変換に使用
  dstBMP:TGPBitmap;
  ・・・ 省略 ・・・
begin
  //Create
  bmp:=TGPBitmap.Create;
  try
    //TImageへ画像を読み込む処理
  finally
    //確実に解放
    bmp.Free;
  end;

画像の読み込み完了時に、変数bmpは解放済みだから、この変数は再度Createしない限り、もう使えない。しかし、この手続き内で有効な変数としてvar宣言してあるので、Freeした後の保存手続き内でうっかり(Createの有無にかかわらず)記述してしまっても、「未定義の識別子エラー」にはならない。もちろん、コンパイルも警告なしで通る・・・。

【誤りのあるコード】

    //指定された拡張子を付けて保存
    if GetEncoderClsid('image/'+strExt, ImgGUID) >= 0 then
    begin
      bmp.Save(ChangeFileExt(SaveDialog1.FileName, dotExt), ImgGUID);
    end;

【正しいコード】

    //指定された拡張子を付けて保存
    if GetEncoderClsid('image/'+strExt, ImgGUID) >= 0 then
    begin
      //bmp.Save(ChangeFileExt(SaveDialog1.FileName, dotExt), ImgGUID);
      dstbmp.Save(ChangeFileExt(SaveDialog1.FileName, dotExt), ImgGUID);
    end;

同じ手続き内の最後の部分で、僕は間違えて(というか、おそらくビットマップ変換処理を追加した際に書き換えるのを忘れて)本来、ここでは使えないはずの変数bmpを指定したまま、そのSaveメソッドを使った画像の保存処理を書いてしまっている(正しくは、ビットマップ変換用に用意した変数dstbmpを指定しなければならない)。

論理的に明らかな誤りを含んだこのプログラムは、しかし、記述時に「未定義の識別子エラー」は出ず、実行時のコンパイルも問題なく通る。

(その理由は僕にはわからないが)
さらに恐ろしいことに、かなりの確率でデータの保存にも成功!してしまう。
明示的なエラーが発生するのは、ループを数百回まわして1回程度。

元にした保存処理のコードが確実に動作することは確認済みだから、(間違ってない)ビットマップ変換のどこかに誤りがあることを疑いはしても、まさかSaveメソッドの変数名が「誤り」で、バグの原因になっているとは(そこは絶対!大丈夫)と思い込んでいるから、疑ってもみない・・・。

絶対、大丈夫。
そう思っていた部分に誤りがあったことは、これまでにも無数にあったのに。

5.Createしないで使った場合は・・・

手続き最初の画像読み込み部分をコメント化して実行した場合は、Createしていないからインスタンスのない変数bmpのSaveメソッドを使うことになるので、次のような警告が表示され、さらに、プログラムで保存の手続きを実行した場合は、ほとんどの場合(明示的なエラーは発生せずに)ファイルの保存に成功するが、プログラム終了後に、しばらくしてからエラーメッセージが表示される。

  //Create
  {
  bmp:=TGPBitmap.Create;
  try
    //何らかの処理

  finally
    //解放してしまう
    bmp.Free;
  end;
  }
コンパイルは通っても警告がきちんと表示される。
プログラムを終了してしばらくすると、エラーメッセージが表示される。

6.まとめ

Createして使用後、Freeした変数を再宣言しないで使うと使えてしまうことがあることをこの例から初めて学んだ。この場合は、Delphiのデフォルト設定のままでは、警告も、エラーメッセージも出ない。さらに、その理由はわからないが、多くの場合Saveメソッドは成功し、データは実際に保存される(時々、保存されないこともある)。明らかに誤りのあるロジックを構築したのは僕だから、Delphiのコンパイラにはまったく責任はない。コーディングの決まりを無視して、想定外のコードを書いたのは僕自身だ。今回の問題解決の経験から、あらためて、Delphiのコンパイラが発見困難なバグの真の原因は自分が作り出していることを学んだ。

たまたま、偶然、変数の修正を忘れたままになっていることに気がつけたからよかったが・・・(不幸中の幸いとは、まさにこのこと)。万一、真の原因が解明できないまま、バグの原因究明をあきらめて、誤りを含んだまま、このプログラムをユーザーに配布していたらと思うと・・・。

あらためて、思った・・・。
少しでも不具合がある場合は、徹底的に原因を解明して100%確実に動作する状態であることを確認しなきゃいけないって。僕は、自分自身と大切な約束を交わした。それは・・・

バグ探しのポイントは、(絶対、大丈夫)と思い込んでる場所を重点的に確認すること。

なぜ、MyPCではまったくエラーが起きなかったのに、業務用のPCではそれなりにエラーが起きたのか、それは今でも謎のままなんだけれど・・・(その後、MyPCでもテスト中に明示的なエラーが発生することを1回確認)。

MyPCで動作確認して問題なかったプログラムが、業務用PCで走らせるとエラーを起こす不思議は、これまでにも何回もあった。職場の業務用PCに、明日、(ありがとう)って感謝の気持ちを伝えよう。

7.お願いとお断り

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

When the date and time display is hidden

「日付と時刻の表示が隠れた時は・・・」

Windows11を使うようになってから、画面右下の日付と時刻の表示が半分隠れて見えなくなってしまう現象を、しばしば目にするようになった。(10の時はなかったけどなー)・・・と思いながら、再起動してみたり、直し方を調べてみたりしたけど・・・。

偶然、発見したいちばんカンタンな、その直し方とは?(※ 僕にとって、です)

1.時々発生する困った現象
2.偶然発見したいちばんカンタンな直し方
3.まとめ
4.お願いとお断り

1.時々発生する困った現象

Windows11になってから、タスクバーの設定の自由度が失われてしまったことを最初の頃はとても残念に感じていたんだけれど、いつの間にか「その仕様」に慣れて、タスクバーの設定はほとんどいじらずに、今はデフォルト状態のまま。

で、時々、発生するのが、次の現象。
気がつくと、いつの間にか、日付と時刻の表示の右側が切れちゃってる・・・。

いつの間にか、日付と時刻の表示が半分になっちゃった・・・
ほんの少しだけ、切れちゃってることも。

2.偶然発見したいちばんカンタンな直し方

Google先生に直し方を尋ねても、「これだ!」みたいな直し方はヒットせず。実害というほどの実害もないし、いつの間にか、直っていたりするから、気にしないでいたんだけど、偶然、走召!カンタンな直し方を発見。

「隠れているインジケーターを表示します」をクリック!

上の図に示したように、画面右下の「隠れているインジケーターを表示します」の「∧」マークをクリックすると、クリックした瞬間に正しい表示に戻ります(※ MyPCでは)。

3.まとめ

Windows11で、画面右下の「日付と時刻の表示」がオカしくなった時は、「隠れているインジケーターを表示します」の「∧」マークをクリックすると、クリックした瞬間に正しい表示に戻る(※ MyPCでは)。

拝啓 Microsoft OS開発ご担当者 様
バグなら、早くなおして欲しい・・・です。

4.お願いとお断り

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