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

Vertical alignment of Grid control

「Gridコントロールの縦方向のアライメントを設定したい」

手書き答案を採点するプログラムで、「答案(の各解答欄)画像の高さ」と「得点を入力するStringGridのセルの高さ」が同じになるように設定したら、編集モード時に、データがセルの左上に表示されるのが何だか気になった(実用上は問題ない。あくまでも気分の問題)。編集してない時は、データはセルの中央に表示されてるので、編集中も垂直方向はセルの中央のまま、水平方向のみ左へ移動する形でデータを表示したい・・・と考えた。

セルの水平方向のアライメントはプロパティで設定できることは知ってたが、調べてみると、縦方向のアライメントは標準のStringGridでは設定できない(?)ようだ。なるべくなら、新しくコンポーネントをインストールしてこれを実現することは避けたい(PCを新しくした場合や、Delphi自体のバージョンアップ等の際に、再セットアップが必要なコンポーネントはなるべく少ない方がいい)ので、何とかならないかなーと思って調べてみた。そしたら、Web上に諸先輩が公開してくださっている数々のお知恵にすがりつきまくることで、案外、カンタンに、何とかなっちゃったというお話。

ここで利用させていただいた知恵のすべてを、自分で最初から作るとしたら、きっと途中で挫折するだろうし、もし、挫折しなくても、完成までには、「とほー」もない時間が必要なことだけは間違いありません。思い立ってわずか1時間で希望のプログラムができたのは、参照させていただいた資料を公開してくださっている皆様のおかげです。心から厚く御礼申し上げます。ほんとうにありがとうございました。

1.これをなんとかしたかった!
2.コンポーネントをインストールせずに使う方法
3.画像のスクロールとGridコントロールの連動
4.まとめ
5.お願いとお断り

1.これをなんとかしたかった!

作成した手書き答案採点プログラムの実行時の画像は、以下の通り。

答案画像から設問毎に解答画像をかきあつめて、受験者全員分をまとめて表示している

答案用紙画像から切り出した各解答欄の画像の高さと、StringGridのセルの高さを同じにした方が採点しやすいだろうと考えたので、次のコードでこれを設定。

  StringGrid1.RowHeights[0]:=24;
  for i := 1 to StringGrid1.RowCount-1 do
  begin
    //SrcRectは解答欄画像の矩形
    StringGrid1.RowHeights[i] := SrcRect.Height;;
  end;

で、なんとかしたい部分が、こちら。

編集中の採点欄のデータがセルの左上に表示されている。縦方向のアライメントも真ん中にしたい!

調べた限り・・・のことなので、もしかしたら間違ってるかもしれないが、標準のStringGridでは縦方向のアライメント設定はできないようだ・・・(もしかしたら、できるのかな?)。半分くらい、あきらめモードで(やっぱり、無理かなー? まぁいいかー)って思いつつもあきらめきれず、Web上の多くの資料に目を通していると、Mr.XRAYさんのWebサイトの「055_ドロップダウンリストを実装した TStringGrid コンポーネント」というページの中に、「06_インプレイスエディタの縦方向のアライメントと左右のインデント」という、まさに実現したいこと、そのものずばりの記事を発見。

055_ドロップダウンリストを実装した TStringGrid コンポーネント

http://mrxray.on.coocan.jp/Delphi/plSamples/055_TplDropStringGrid.htm

上記ページで、Mr.XRAYさんがドロップダウンリストの機能付きのTStringGrid コンポーネントとして公開してくださっているplDropStringGrid.pasには、インプレイスエディタ関係のプロパティとイベント類が追加されており、これをインストールすれば、StringGridで編集モード時に起動するインプレイスエディタの縦方向のアライメントが設定できるとのこと。

これで「夢見たことは実現可能であることがわかった」が、もし、できることなら、コンポーネントをインストールせずに使えないか? とさらに欲張りなことを考えてしまった・・・。理由はたった一つ。StringGridのセルの高さを変えるようなプログラムは、今後、たぶん書かないんじゃないかなーって、思ったから。

・・・ということで、今度は「コンポーネントをインストールせずに使う方法」を探してみた(探しつつ、前に見たことがあるような気がした)。

2.コンポーネントをインストールせずに使う方法

こちらも、そのものずばりの方法が次のWebサイトに公開されていました。作者の方に心から感謝申し上げます。

コンポーネントをインストールせずに使う方法

http://delfusa.main.jp/delfusafloor/technic/technic/024_ChangeComponent.html

上記Webサイトにあった情報をもとに、夢を実現。

まず、上記Mr.XRAYさんのWebサイトから「055_TplDropStringGrid.zip」をダウンロードして解凍。中に含まれている「plDropStringGrid.pas」をコピーして、Delphiのプロジェクトファイル(*.dproj)があるフォルダに貼り付け。

プログラムには、次のコードを加えた。

uses
  ・・・ 省略 ・・・
  plDropStringGrid, System.TypInfo;

  //plDropStringGrid, System.TypInfoは、実行時にコンポーネントを交換するために追加
  //-> StringGridの縦のアライメントを設定する目的

{$R *.dfm}

こちらの「コンポーネントを交換する関数」は、記事にあったものをそのまま、コピペ!

//コンポーネントを交換する関数
//usesにTypInfoの追加が必要
function ChangeComponent(Original: TComponent; NewClass: TComponentClass): TComponent;
var
  New: TComponent;
  Stream: TStream;
  Methods: array of TMethod;
  aPPropInfo: array of PPropInfo;
  MethodCount, i: Integer;
begin
  SetLength(aPPropInfo, 16379);
  MethodCount := GetPropList(Original.ClassInfo, [tkMethod], @aPPropInfo[0]);
  SetLength(Methods, MethodCount);
  for i := 0 to MethodCount - 1 do
    Methods[i] := GetMethodProp(Original, aPPropInfo[i]);

  Stream := TMemoryStream.Create;
  try
    Stream.WriteComponent(Original);
    New := NewClass.Create(Original.Owner);
    if New is TControl then
      TControl(New).Parent := TControl(Original).Parent;
    Original.Free;
    Stream.Position := 0;
    Stream.ReadComponent(New);
  finally
    Stream.free
  end;

  for i := 0 to MethodCount - 1 do
    SetMethodProp(New, aPPropInfo[i], Methods[i]);
  Result := New;
end;

この関数を、FormCreate時に呼び出して、実行。

procedure TFormCollaboration.FormCreate(Sender: TObject);
begin
  //コンポーネントを交換する関数を実行
  StringGrid1:= TStringGrid(ChangeComponent(StringGrid1, TplDropStringGrid));
end;

ここまでが準備で、縦のアライメントの設定は、次のたった1行(赤字)を追加するのみ!

procedure TFormCollaboration.StringGrid1GetEditText(Sender: TObject; ACol,
  ARow: Integer; var Value: string);
begin

  //縦のアライメントを設定
  TplDropStringGrid(StringGrid1).EditVertAlignment := vaCenter;

  //IMEの制御
  with TEdit(_TGrid(Sender).InplaceEditor) do
  begin
    //ImeMode := imClose;   //日本語入力OFF-> ×
    ImeMode := imDisable;   //日本語入力OFFは imDisable
  end;

  //現在Activeな行番号を取得
  intStringGrid1ActiveRow:=ARow;

end;

実行結果です。

インプレイスエディタ起動時、アライメント設定は「水平方向は左・垂直方向は中央」

旅行先で、ちょっと時間ができたので、前にユーザーと話しをする中で思い立った解答欄画像の高さと採点欄の高さを同じにするコードをちょこちょこっと書いて、動作を確認。そしたら今度は、編集モードでのセルの挙動が気になり、翌朝、早く目覚めたので、まさか旅先で書くとは思わなかったけど、PCは持参していたのでこれ幸いと、お日さまが昇るころまでにここまでの内容を記述(・・・というかほぼ全部コピペ)。

3.画像のスクロールとGridコントロールの連動

次に気になったのがTImageに表示した答案画像と、StringGridのスクロールの連動(同期)。

実はこれも前から気になっていたコトだったんだけれど、いろんな事情から、とりあえずプログラムを使える状態にすることが最優先だったので、ずっと後回しにしてきた課題。

今回、解答欄画像の高さと、採点欄の高さを揃えたら、以前にも増して同期の必要性を痛感。まだ、期待通りの動きになった・・・とは言い難い状態なんだけど、現在のコードは次の通り(こちらもずっと以前にMr.XRAYさんのWebサイトにあった記事を参考にさせていただいて書いたプログラムからコピペしたコードだったような記憶が・・・)。参考にさせていただいたのは、おそらく次のページ。

078_コントロールのマウスホイール操作によるスクロール

http://mrxray.on.coocan.jp/Delphi/plSamples/078_Control_MouseWheel.htm
調整値1-10を設定するComboBox

マウスに関する諸設定は、環境により異なるので、調整値は固定値にしないで、ユーザーが自由に設定できるようにした(つもり)。My PC環境で試したところ、次のコードでは、調整値「7~8」くらいが期待に近い動きをするようだ。

procedure TFormCollaboration.FormMouseWheel(Sender: TObject; Shift: TShiftState;
  WheelDelta: Integer; MousePos: TPoint; var Handled: Boolean);
var
  LDelta:Integer;
  LWinCtrl:TWinControl;
  LCurPos:TPoint;
  //スクロール量の調整(SA:Scroll Amount)
  intSA:integer;
begin

  //マウスカーソルが TScrollBox の領域内にある時だけスクロールを可能にする
  //(解答欄画像を表示しているTImageはTScrollBoxの上に配置)
  LCurPos := ScrollBox1.Parent.ScreenToClient(MousePos);
  if PtInRect(ScrollBox1.BoundsRect, LCurPos) then
  begin
    //スクロール量の調整
    if not TryStrToInt(調整値1-10を設定するComboBoxの値, intSA) then
    begin
      intSA:=1;
    end;
    //心配なので、念のために設定その1
    if 調整値1-10を設定するComboBoxの値 ='0' then
    begin
      intSA:=1;
    end;
    //心配なので、念のために設定その2
    if StrToInt(調整値1-10を設定するComboBoxの値) < 0 then
    begin
      intSA:=1;
    end;
    //大きい数値を選ぶとスクロール量も大きくなるように設定
    intSA:=11-intSA;
    LDelta := WheelDelta div intSA;
    if ssCtrl in Shift then
    begin
      ScrollBox1.HorzScrollBar.Position := 
        ScrollBox1.HorzScrollBar.Position - LDelta;
    end else begin
      ScrollBox1.VertScrollBar.Position := 
        ScrollBox1.VertScrollBar.Position - LDelta;
      //StringGridも連動してスクロールさせる
      if LDelta > 0 then
      begin
        StringGrid1.Perform(WM_VSCROLL, SB_LINEUP, 0);
      end else begin
        StringGrid1.Perform(WM_VSCROLL, SB_LINEDOWN, 0);
      end;
    end;
  end else begin
    //マウス直下のコントロールを取得
    LWinCtrl:=FindVCLWindow(MousePos);
    //TStringGridの場合
    if LWinCtrl is TStringGrid then
    begin
      if WheelDelta > 0 then
      begin
        LWinCtrl.Perform(WM_VSCROLL, SB_LINEUP, 0);
      end else begin
        LWinCtrl.Perform(WM_VSCROLL, SB_LINEDOWN, 0);
      end;
    end;
  end;
  //この1行を忘れないこと!
  Handled:=True;

end;

テストしたPCのマウス関連の諸設定は、以下の通り。

テストしたPCのマウス関連の諸設定①
テストしたPCのマウス関連の諸設定②
テストしたPCのマウス関連の諸設定③

4.まとめ

StringGridで編集モード時に、縦のアライメントを設定するには、標準のStringGridでは機能的に難しいので、それが可能な標準のStringGridを継承したコンポーネントを利用する。コンポーネントのインストールが難しい場合は、実行時に標準のStringGridと入れ替える形で、そのコンポーネントを動的に生成することで、目的を実現できる可能性がある(実行時の動的な生成で、目的を実現できるか・どうかは、十分なテストを行って確認する)。

5.お願いとお断り

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

I also want to enter the triangle mark!

「(採点ソフトで)〇と × だけじゃなく△も入力したい!」

手書き答案を採点するプログラムを書いた。当初の予定では、採点記号は正解(〇)か、不正解(×)のみとして、正解(〇)の場合は、その得点を採点記号の右に表示できるように設定したから、その点数の大小によってそれが完全正解なのか、△(部分点あり)なのかを判別できればイイ、「だから△なんてイラナイ」と、僕は考えていたんだけれど・・・。

今にして思えば、弱い自分への言い訳でした・・・ T_T

ほぼ完成に近づいたMy手書き答案採点プログラムのイメージ。
(この時点で、本人は「完成した」と思っていた・・・)

No,2とNo,6の -5x は完全正解の半分の2点しかあげないけど、でも × じゃないよー。みたいな・・・

でも、そんな時、偶然、Webのニュースで見ちゃったんだけど、東京都が公的に導入した業務改善用の採点ソフトでは・・・

あたりまえのコトですが、
採点記号に△も使ってるんですよ!

僕のプログラムでは、絶対に「表示できない」△マーク。
別に△マークがあったって、エラくなんか、ないもん・・・。うぐぐ。

でも、それって、
走召!ぐやじい!!!
じゃありませんか。

僕のプログラムの完成なんて、誰ひとり、待ってないケド・・・

一般庶民のフツーの感覚で言えば(僕の感覚と常識が正しいとして)、一昨年、
一般庶民には買えない価格のDelphiを、「個人で購入するという暴挙に出た」僕です

それはDelphiが、Object Pascalが好きだから。
出会った時から、ずっと 大好きな・・・Delphiの・・・
この文化が消えないように、この言語がいつまでも残るように、
Delphiと、Object Pascalが、ほんとうに大好きだからやったことなんだけど・・・。

あれだけの初期使用料と、高額な年度ごとのサブスクリプション代金を支払っても・・・。
僕はイイから。

(結婚した時、印鑑といっしょに、彼女に取り上げられた通帳の、今はまったく自由にならない預金口座からの引き落としだから、実は痛くもかゆくもないんだケド・・・ *(^_^)*♪ )


こんなに・・・。
どうしようもない、くらい、こんなに・・・。

こんなに Delphiが好きなのに、
たかが△マークすら表示できない・・・
なんて・・・


許せないよ・・・
絶対に許せない・・・


アマチュアとか、プロとか、関係なく、
△マークの表示が、東京都の御用達プログラムに出来て、
僕に出来ない理由なんて、
それをあきらめる場合以外には、探したくないし、
あきらめなければ、僕にもきっと出来るはずです。

アマチュアとか、プロであるとか、は関係ない。
△マークの有無が問題なのだ。

それが「ない」プログラムは、
決して、良い採点プログラムとは言えない!

なんでこんな大事なコトに今まで気づかなかったんだ・・・
(アンタにとって、それはいつものことでしょ)

よぉぉぉぉぉぉぉぉぉぉっし、
俺はやるぞ!!!

そう思ったら、思い出せました。

よくなりたい、自分を。

(長すぎる前置きですが、どうしても、話したかったことはここから・・・)

【もくじ】

1.△の使用をあきらめた理由
2.マイナスの点数は通常ありえないコトに気づく
3.採点アルゴリズムを改良
4.合計点の計算と印刷
5.まとめ
6.お願いとお断り

1.△の使用をあきらめた理由

手書き答案をスキャナーで読み取り、各設問毎に画像を切り出して合成、素早く・効率よく採点、で、採点記号&得点付き画像を元の答案画像へ書き戻し、合計点を付加(任意の位置に表示)して返却用答案を印刷するプログラムを作成した。

下の画像はその実行時のイメージ。Gridコントロールへ入力した数値に応じて答案画像の上に採点記号(〇 もしくは × )と、〇の場合は得点を表示している。× の場合に得点の表示がないのは、0と〇がよく似ていて、×0という表記は間違いなく混乱を招くと考えたため。いちおう、オプション設定で表示の有無を選択できるようにはしてあるが、デフォルト設定で「得点0は表示しない」のチェックはON。

×0は混乱のもと!(表示する選択肢は提供)
No,2とNo,6には 本当は△を表示 したい・・・

当初は次のような理由から採点記号△の使用を断念してしまった・・・。

ほんとうは 〇・× の他に採点記号として△も使いたかったのだ。が、採点の基本としたアルゴリズムではGridコントロールに「正の数が入力」された場合は正解で採点記号は「 〇 」、「0(ゼロ)が入力」されていたら不正解で採点記号は「 × 」、「空欄」の場合は何もしないと決めていたので、△の入り込む隙が見出せなかった・・・というのが一つ。

また、これは直接△とは関係ないけれど、人間である以上、採点ミスはつきもの!で、答案画像に採点記号と部分点を埋め込むのは最後の最後。返却用答案画像を作成する直前でなければならない。それまでは、Gridコントロールへの得点入力に応じて、採点記号を付加した答案画像をいつでも修正可能な状態にしておく必要がある。

もし、強引に「部分点あり」の採点記号を「△」にするなら、Gridコントロールへの入力値から、この「△」を見分ける手段を考えなければならない。この手段を思いつかなかったというのが一つ(当初から考えなかったわけではないが・・・、スマートな方法をどうしても見いだせなかった)。

さらに得点入力のしやすさを考えると、テンキーがあってもなくても、0(ゼロ)はキーボードの右側にあって、どうにも押しにくい(マウスを操作する右手は、マウスから離したくない & 左手で何回も0を押すのは、かなりめんどくさい)から、数値以外の入力はすべて0(ゼロ)と見なすプログラムを書けば、A・S・Dあたりのキーを押すことで理想的(?)に0(ゼロ)を入力できる。また、答案画像のクリック位置とGridコントロールのフォーカス位置が連動するようにプログラミングすれば、ぱっと見、全体的に出来の良い設問への得点入力は、プログラムから一括で行い、あとは間違いの解答だけ、その画像をクリックしてAキーあたりを押して0(ゼロ)を入力すれば、いちばん効率よく採点できる・・・はず。逆に、ぱっと見、全体的に出来が悪そうなら、一括して0(ゼロ)を入力し、正解の解答だけ選択して得点を入力すればいい。多くの場合、正解の得点は5点未満だろうから、これらのキーはキーボードの左にあり、左手で押しやすい。

百歩ゆずって、あるキー(例:「さんかく」だから「s」キーとか)を押した場合だけ、採点記号を△とするのは容易だが、後々やっかいな問題が生じる。

プログラムは最終的に、Gridコントロール上のデータから、合計点を計算して返却用答案画像のどこかに印刷する仕様。で、その際、データに余計な文字があれば除外して計算することも出来なくはないが、予期せぬ間違いの元になるような要素は、なるべくなら最初から排除しておきたい。かといって、△マークであることを示すなんらかのフラグをデータとして持っていなければ、データを再読み込みした際に、画像上に△記号を表示することはできない・・・。しかし、そのために、Gridコントロール上に「採点記号・部分点」を意味する「s1」みたいな表示をするのは、できるだけ避けたい。Gridコントロール上に数値以外の文字が「ない」のが、最初からの理想なのだ。

実は、見えないGridコントロールをもう一つ、別に準備してここに「〇・△・×」の情報を記録しようか・・・とも考え、実際にやってみたんだけど、これだとアルゴリズム他をかなり修正しないといけないことに気づく。なので、この案は却下。

で、八方塞がり状態に・・・

2.マイナスの点数は通常ありえないコトに気づく

(やっぱり、ダメかぁー)

そう思いながら、それでもあきらめきれずに、なんとなくStringGrid1DrawCell手続きのコードを眺めていて、次のコードを残したままだったことに気づく。

  //正負をチェック
  if StrToInt(StringGrid1.Cells[ACol,ARow])< 0 then
  begin
    StringGrid1.Canvas.Font.Color := clRed;
  end else begin
    StringGrid1.Canvas.Font.Color := clBlack;
  end;

これは、いつか他のプログラムで使用したコードを、このプログラムにコピーした際、そのままになっていたものだ。別に問題を起こすようにも見えなかったし、通常の採点でマイナス点の入力はアリエナイから、誤って? 負の数が入力されたら赤く表示した方が入力ミス?が防げてかえってイイか・・・くらいの気持ちで、消さずに残しておいたのだ。

何度も実行して検証したプログラムコードだけれど、このプログラムでは「負の数の入力」は最初から予定に「ない」ので、負の数は一度も入力したことがなかった・・・し、このコードを消さずに残しておいたこと自体を、その存在に気づくまで、僕は忘れていた。

(こんなコードも入ってたんだ・・・)

その瞬間、何かが、ひらめいた気がした・・・

(そうか! 負の数をフラグに使う手があった☆)

本質的に文字だと計算上、いろいろ問題が起きるけど、負の数なら絶対値をとってしまえば合計点の計算は何の問題もなくできるし、さらに良いことに、これまで何よりも問題だった△を意味するフラグとして、-の記号を利用できる!!

赤で表示する設定になってることも、ユーザーにとって親切だし・・・。僕的に言えば、「△なら部分点に-(マイナス)記号をつけて入力」なんだけど、これを一般的に言えば「部分点を与える場合は、負の数として入力してください」ってことで、これならユーザーに確実に伝わるし、かつ覚えやすい。

さらに採点アルゴリズムも全体の大幅な見直しは不要で、Gridコントロールのデータが負の数であった場合の処理だけを追加すればよさそうだ。

なんで、こんなイイことに最初から気づかなかったのか、それは僕が足りないせいだけど、そんなことはどーでもイイ。なんだがうれしくなってキタ。

やったぁ♪ これで「東京都御用達の採点プログラム」に負けないのが作れる☆☆☆
(ハナから相手にされてないのは十分わかってます・・・)

ただ、純粋によくなろうとした自分が久しぶりに愛しい。

3.採点アルゴリズムを改良

で、採点アルゴリズムを次のように改良。なお、採点マークと設問毎の得点の表示設定は、図の「表示」オプションから採点者が選択する仕様。なお、プログラムは、Gridコントロールが空欄である場合、すなわち、入力値がない場合は、答案画像に対する処理は何も行わない。

※ Captionが「種類」となっているRadioGroupがコード内のRadioGroup4。
※ →X,↓Yが表示位置調整用の各ComboBox。矢印は意味を視覚的に伝える工夫。
※ Sizeで採点記号及び得点のFontの大きさを指定。

表示のデフォルト設定は「採点記号も得点も両方表示する」
//Gridコントロールへの入力値がない場合は「何もしない」
procedure TFormCollaboration.StringGrid1DrawCell(Sender: TObject; ACol,
  ARow: Integer; Rect: TRect; State: TGridDrawState);
var
  ・・・ 必要な変数を宣言 ・・・
  //例
  intValue : integer;
begin

  // 以下、実際のプログラムコードから必要な部分のみ抜粋

  if StringGrid1.Cells[ACol,ARow]<>'' then
  begin
    // 誤入力'00'があれば'0'に変換
    if StringGrid1.Cells[ACol,ARow]='00' then
    begin
      StringGrid1.Cells[ACol,ARow]:='0';
    end;

    // 入力文字数が3文字以上なら'0'に変換
    if Length(WideString(StringGrid1.Cells[ACol,ARow])) > 2 then
    begin
      StringGrid1.Cells[ACol,ARow]:='0';
    end;

    // 入力値が「数値」に変換できなかった場合はすべて'0'に変換
    if not TryStrToInt(StringGrid1.Cells[ACol,ARow], intValue) then
    begin
      StringGrid1.Cells[ACol,ARow]:='0';
    end;

    //背景色を白に設定
    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);

  end;

  //Cellの値が0ではなかった場合の処理
  if not (StringGrid1.Cells[ACol,ARow]='0') then
  begin
    //Cellの値が正だった場合(完全正答〇の処理)
    if StrToInt(StringGrid1.Cells[ACol,ARow]) > 0 then
    begin

      //imgAnswerは答案画像を表示するTImage

      //Windows APIのSetBkMode関数でTRANSPARENTを指定
      SetBkMode(imgAnswer.Canvas.Handle, TRANSPARENT);
      imgAnswer.Canvas.Font.Color := clRed;
      imgAnswer.Canvas.Font.Size  := StrToInt(FontSize指定用ComboBox.Text);

      case RadioGroup4.ItemIndex of
        0:begin
          //cmbX, cmbYは表示位置調節用の値を入力するComboBox
          imgAnswer.Canvas.TextOut(DestRect.Left+StrToInt(cmbX.Text),
            DestRect.Top+StrToInt(cmbY.Text), '○');
        end;
        1:begin
          imgAnswer.Canvas.TextOut(DestRect.Left+StrToInt(cmbX.Text),
            DestRect.Top+StrToInt(cmbY.Text), StringGrid1.Cells[ACol,ARow]);
        end;
        2:begin
          imgAnswer.Canvas.TextOut(DestRect.Left+StrToInt(cmbX.Text),
            DestRect.Top+StrToInt(cmbY.Text), '○'+StringGrid1.Cells[ACol,ARow]);
        end;
      end;

    end else begin

      //Cellの値が負だった場合(△)-> この部分を新規に追加
      if StrToInt(StringGrid1.Cells[ACol,ARow]) < 0 then
      begin
        //Windows APIのSetBkMode関数でTRANSPARENTを指定
        SetBkMode(imgAnswer.Canvas.Handle, TRANSPARENT);        
        imgAnswer.Canvas.Font.Color := clRed;
        imgAnswer.Canvas.Font.Size  := StrToInt(FontSize指定用ComboBox.Text);

        case RadioGroup4.ItemIndex of
          0:begin
            imgAnswer.Canvas.TextOut(DestRect.Left+StrToInt(cmbX.Text),
              DestRect.Top+StrToInt(cmbY.Text), '△');
          end;
          1:begin
            imgAnswer.Canvas.TextOut(DestRect.Left+StrToInt(cmbX.Text),
              DestRect.Top+StrToInt(cmbY.Text),
              IntToStr(Abs(StrToInt(StringGrid1.Cells[ACol,ARow]))));
          end;
          2:begin
            imgAnswer.Canvas.TextOut(DestRect.Left+StrToInt(cmbX.Text),
              DestRect.Top+StrToInt(cmbY.Text), '△'+
              IntToStr(Abs(StrToInt(StringGrid1.Cells[ACol,ARow]))));
          end;
        end;
      end;
    end;

  end else begin

    //不正解の場合の処理(×)
    //Windows APIのSetBkMode関数でTRANSPARENTを指定
    SetBkMode(imgAnswer.Canvas.Handle, TRANSPARENT);    
    imgAnswer.Canvas.Font.Color := clRed;
    imgAnswer.Canvas.Font.Size  := StrToInt(FontSize指定用ComboBox.Text);

    case RadioGroup4.ItemIndex of
      0:begin
        imgAnswer.Canvas.TextOut(DestRect.Left+StrToInt(cmbX.Text),
          DestRect.Top+StrToInt(cmbY.Text), '×');
      end;
      1:begin
        imgAnswer.Canvas.TextOut(DestRect.Left+StrToInt(cmbX.Text),
          DestRect.Top+StrToInt(cmbY.Text), StringGrid1.Cells[ACol,ARow]);
      end;
      2:begin
        //chkZeroはCaption「得点0は表示しない」のCheckBox
        if not chkZero.Checked then
        begin
          imgAnswer.Canvas.TextOut(DestRect.Left+StrToInt(cmbX.Text),
            DestRect.Top+StrToInt(cmbY.Text), '×'+StringGrid1.Cells[ACol,ARow]);
        end else begin
          imgAnswer.Canvas.TextOut(DestRect.Left+StrToInt(cmbX.Text),
          DestRect.Top+StrToInt(cmbY.Text), '×');
        end;
      end;
    end;
  end;
end;

で、実行結果は・・・(解答はテキトーなので、それ自体に意味はありません。ここでは+5xを正解で得点4点とし、-5xを△で部分点2点としている)。

採点欄への入力が正の数なら〇、ゼロなら×、負の数なら採点記号は△を表示

今、こうしてコードを眺めて見れば、別に変わったコトなんてなぁーんにもしてない、ほんとに単純なif文のネストにすぎないんだ・・・けど。

ここに、たどり着くまでは、ほんとうに長かったなぁ

あらためて(あたりまえのことですが)、処理の基礎となる考え方・・・アルゴリズムの重要性がわかった気がしました。

こんな、なんでもないような工夫で、自分の中ではかなり大きかった(△マークが使えない)という問題を解決できるんだ。ただ、そこにたどり着くためには、残念ながら、僕にはすごく「時間」がかかるんだ。でも、あきらめずに(時間はかかるけど)出来るまで頑張れば、プロが書いたプログラムと同じことが、僕にもできるんだ・・・って。

多くの人にとって、おそらく、まったく参考にならない、こんなことを、お金までかけて公開するのは、つまり、もしかしたら、どこかにいる、かもしれない、僕と同じような気持ちでいる誰かに、(あきらめないで)って伝えたかったから・・・かも、しれない。

あなたの夢を、あきらめないで・・・って。

4.合計点の計算と印刷

続いて、合計点の計算・その印刷位置の指定から返却用答案画像の印刷へと繋げる部分。まず、絶対値に換算して合計を計算するようにコードを修正。ただ、Abs( )を追加しただけで、あんなに悩んだ△マークの処理が実現できるなんて、なんだか、夢のよう。

var
  i,j,k : integer;
begin
  //合計点を入れる変数kを初期化
  k := 0;
  //合計点を計算
  for i := 1 to StringGrid1.RowCount-1 do
  begin
    for j := 1 to StrToInt(解答欄数.Text) do
    begin
      if StringGrid1.Cells[j,i] <> '' then
      begin
        //△に非対応
        //k := K + StrToInt(StringGrid1.Cells[j,i]);
        //△は負の数で入力しているから絶対値で計算
        k := K + Abs(StrToInt(StringGrid1.Cells[j,i]));
      end;
    end;
    //合計点を保存(StringGrid.Cells[列, 行])
    StringGrid1.Cells[StrToInt(解答欄数.Text)+1, i] := IntToStr(k);
    //合計点を初期化
    k := 0;
  end;
end;

返却用答案画像の印刷にあたっては、合計点表示の有無を選択しないと印刷ボタンをクリックできない仕様として・・・。ユーザーが合計点「有り」を選択した場合は、合計点を上記コードで計算後、返却用答案画像をTImageに表示し、このTImageへのMouseDownイベントを利用して、ユーザーに合計点印刷位置を指示してもらい、合計点入りのサンプル画像を提示(合計点の印刷位置の修正は、ユーザーが納得できるまで何回でも可能)。最終的に位置が決まったら印刷ボタンへフォーカスを移して、クリックで印刷という流れ。

合計点の印刷の有無を指定し、「有り」の場合は必要な処理を行わないと、印刷ボタンはクリックできない。
ユーザーのクリックした位置を左上座標(0,0)として合計点を挿入&返却用答案のサンプルを提示。
(サンプル画像にある矩形は、実際には印刷されない)
「いいえ」をクリックすれば何回でも位置指定のやり直しが可能。

以下のコードが合計点の印刷位置決め部分。「いいえ」を選択した場合は、合計点サンプルを表示したのと同じ場所に、同じ内容を「赤」ではなく、「白」で再描画して消去したように見せかけている(よく見ると若干、先に赤で表示した合計点の輪郭が残っているのがわかる。その原因は不明。今後、原因を調べたい)。

「いいえ」を選択した場合の画像(白で上書きした合計点の輪郭が残ってしまう)
//合計点VCLはLabel
//フォントサイズ指定VCL、解答欄数VCLはComboBox
//変数bSumは、合計点印刷の有無を確認するBoolean型変数
procedure TFormCollaboration.Image1MouseDown(Sender: TObject;
  Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
var
  MyPath : string;
  TempBmp : TBitmap;
  //MessageDlgの押されたボタンを知る
  Ans : Word;
  //BalloonHintの表示
  LTitle : string;
  LText  : string;
  LhIcon : HICON;
  LPos   : TPoint;
  LArrow : TBalloonArrow;

  //普通の四捨五入を行う関数を設定
  function Roundoff(X: Extended): Longint;
  begin
    if x >= 0 then Result := Trunc(x + 0.5)
              else Result := Trunc(x - 0.5);
  end;

  procedure GetXY(iX,iY:Integer);
  begin
    //合計点印刷位置の座標を取得
    iX:=Roundoff(iX/(TrackBar1.Position/100));
    iY:=Roundoff(iY/(TrackBar1.Position/100));

    //Imageに画像をセットする際、自動でサイズ調整を行っている
    合計点の位置X:=iX;
    合計点の位置Y:=iY;

    //矩形を描画
    with Image1 do
    begin
      //Canvas.Brush.Style:=bsClear;  //Pythonを使っていない時はこれでOK!
      //Pythonを使っている時は明示的に書く必要がある
      //(Python.pasにもbsClearが定義されている)
      Canvas.Brush.Style:=Vcl.Graphics.bsClear;
      Canvas.Pen.Color:=clRed;
      Canvas.Pen.Width:=3;
      //矩形を描画
      合計点VCL.Font.Size:=StrToInt(フォントサイズ指定VCL.Text);
      Canvas.Rectangle(合計点の位置X, 合計点の位置Y, 
        合計点の位置X+合計点VCL.Width, 合計点の位置Y+合計点VCL.Height);
      Canvas.Font.Color:=clRed;
      Canvas.Font.Size:=StrToInt(フォントサイズ指定VCL.Text);
      //LabelにStringGridから合計点を取得しておく
      合計点VCL.Caption:=StringGrid1.Cells[StrToInt(解答欄数VCL.Text)+1, 1];
      Canvas.TextOut(合計点の位置X, 合計点の位置Y, 合計点VCL.Caption);
    end;
  end;

begin

  if bSum then
  begin

    //合計点の印刷位置の座標を指定&取得
    //Imageをクリックするたびに、GetXY(X,Y)が呼び出される(実行される)
    GetXY(X,Y);

    Ans:= MessageDlg('印刷位置は、この位置でよろしいですか?'+#13#10+#13#10+
      '(左寄せで印刷。矩形は印刷されません。)',
      mtInformation, [mbYes, mbNo, mbCancel], 0);

    if Ans = mrYes then
    begin

      //[はい]が選ばれた時

      //案内
      MessageDlg('印刷ボタンをクリックしてください。', mtInformation,[mbOK],0);

      //バルーンヒントのタイトルとヒントの内容
      LTitle := '印刷ボタン';
      LText  := 'ココです!' + sLineBreak + 'クリックしてください';

      //バルーンヒントの表示のとスタイル
      LArrow:= baTopLeft;       //VCLの上・左へ向けて表示
      //LArrow:= baTopCenter;     //VCLの上・中央
      //LArrow:= baTopRight;        //VCLの上・右へ向けて表示
      //LArrow := baBottomRight;  //VCLの下・右へ向けて表示
      //LArrow := baBottomCenter; //VCLの下・中央
      //LArrow := baBottomLeft;   //VCLの下・左へ向けて表示

      //バルーンヒントの吹き出しの始点
      LPos:=印刷ボタン.ClientToScreen(Point(Trunc(印刷ボタン.Width div 2), 0));

      //システムのInfoアイコンを使用
      LhIcon := LoadIcon(0, IDI_INFORMATION);

      try
        //引数はタイトル、ヒント、アイコン、表示位置、吹き出しの始点、時間はミリ秒
        BalloonHint(LTitle, LText, LhIcon, LArrow, LPos, 5000);
      finally
        DestroyIcon(LhIcon);
      end;

      //カーソルを元に戻す
      Screen.Cursor:=crDefault;
      Image1.Visible:=False;
      Image1.Picture.Assign(nil);
      //SetFocus
      印刷ボタン.Enabled:=True;
      印刷ボタン.SetFocus;
    end;

    if Ans = mrNo then
    begin
      //[いいえ]が選ばれた時
      with Image1 do
      begin
        //Canvas.Brush.Style:=bsClear;  //Pythonを使っていない時はこれでOK!
        //Pythonを使っている時は明示的に書く必要がある
        //(Python.pasにもbsClearが定義されている)
        //クリック位置に同じ内容を「白で上書き」してサンプルを消去
        Canvas.Brush.Style:=Vcl.Graphics.bsClear;  
        Canvas.Pen.Color:=clWhite;
        Canvas.Pen.Width:=3;
        合計点VCL.Font.Size:=StrToInt(フォントサイズ指定VCL.Text);
        Canvas.Rectangle(合計点の位置X, 合計点の位置Y, 
          合計点の位置X+合計点VCL.Width, 合計点の位置Y+合計点VCL.Height);
        Canvas.Font.Color:=clWhite;
        Canvas.Font.Size:=StrToInt(フォントサイズ指定VCL.Text);
        Canvas.TextOut(合計点の位置X, 合計点の位置Y, 合計点VCL.Caption);
      end;
    end;

    if Ans = mrCancel then
    begin
      //キャンセルが選ばれた時
      //カーソルを元に戻す
      Screen.Cursor:=crDefault;

      //その他の処理

    end;
  end;
end;

5.まとめ

数値のみを用いて、〇・△・× を表現する。解決までに2ヶ月近くを要した課題だった。最終的には、「Gridコントロールへの入力が、正の数なら〇、負の数なら△(マイナス記号はフラグとして利用)、0(ゼロ)なら × 、空欄なら何もしない。」として解決。

この単純なアルゴリズムにたどり着くまで、僕はあきらめかけたり、再びチャレンジしたり、様々に思い悩んだ。夢見た通り、プログラムはよくなったが、果たして僕自身は成長したのだろうか・・・

僕は天才でも、なんでもない。
特別なことなんて何一つできない。
他の誰かより優れたモノなんて
何ひとつ、持たない・・・。

何をやらせてもトロいし、
物事の理解にかける時間は、ヒトの何倍も必要だけど、
でも、時間さえかければ、
僕にも、かたちにできるものは、ある・・・

いつか、TVで見たんだ。

若き日の山中 伸弥先生が、利根川 進先生に質問してた。
「日本では研究の継続性が大切だと言われますが、先生はどうお考えですか?」 と。

利根川先生は即答してた。
「重要で、面白い研究であれば何でもいいじゃないか」と。

人々に、社会に、貢献「したか・しなかったか」が、すべてなんだと。

RFKも、同じ言葉を残してる。

The purpose of life is to contribute in some way to making things better.
「人生の目的は、ものごとを良くすることに対してなんらかの貢献をすることだ。」

さらに・・・

You’re happiest while you’re making the greatest contribution.
「最高の貢献を成そうとする時、あなたは最高の幸福を知る。」

とも(命の使い方を、彼自身の人生が代弁している気がするけど・・・)。

ようやくカタチにできた、僕の夢を、
職場のみんなに自由に使ってもらえるプログラムとして公開する。
たったひとり、でもいい。
このプログラムでしあわせを手にする人が、どうか、いてほしい。

それをもし、貢献と呼んでもらえるなら、
どんなにか、うれしいだろう・・・

そして、僕がこの世界から消えた後まで、
これまでにかたちにしたいくつかの夢を・・・もし、残せたら
どんなにか、しあわせだろう・・・

Delphiといっしょに、
Object Pascalで組んだ、
夢のかたち。

そう、夢のかたち・・・。

この胸にずっと、思い描いてきた
僕の夢のかたちを。

6.お願いとお断り

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

Organize items displayed in recently opened files

「最近開いたファイルに表示される項目を整理する」

不要になったプロジェクトをバックアップ後、フォルダごと削除したり、プロジェクトファイルを入れたフォルダの名前そのものを変更してしまったりすると、Delphi起動直後に表示される「ウェルカムページ」の「最近開いたファイル」の項目も整理したくなる。その方法を調べた。これはその覚え書き。

1.「最近開いたファイル」の項目の整理方法
2.「ウェルカムページ」そのものを表示しない
3.まとめ
4.お願いとお断り

1.「最近開いたファイル」の項目の整理方法

最新のバージョン11.2の場合、次のように操作する。IDEの[ファイル]->[最近開いたファイル]->[プロパティ](旧バージョンの場合は、[ファイル] -> [開き直す] -> [プロパティ]の順のようだ)。

[ファイル] -> [最近開いたファイル] -> [プロパティ]の順にクリック

[開き直す]メニューのプロパティが表示される。

「存在しないファイルの削除」をクリックすれば、(Pathの有無を確認しているのでしょう)全自動で項目を整理してくれる。これはすごい便利!!

任意の項目を選んで「削除」したり、「クリア」ボタンで履歴を全部消すこともできる。ちなみに「クリア」ボタンをクリックした場合は・・・

確認メッセージが表示される

「はい」をクリックすると・・・

全部消えた!

2.「ウェルカムページ」そのものを表示しない

ウェルカムページの必要性を感じない場合は、IDE起動時に「表示しない」ように設定することもできるようだ。

Delphiへのショートカットを右クリックしてプロパティを表示し、「ショートカット」タブのリンク先(T):「”C:\Program Files (x86)\Embarcadero\Studio\22.0\bin\bds.exe” -pDelphi」の「-pDelphi」の後ろに「(半角スペース)-np」を追加して、「”C:\Program Files (x86)\Embarcadero\Studio\22.0\bin\bds.exe” -pDelphi -np」にする。

Delphi 11.2 Alexandriaの場合

「OK」もしくは「適用」をクリックすると、確認のメッセージが表示される。

あなたの責任だよ!ってコト?

「続行」をクリック。で、次回の起動時からは・・・

すっきりー!

ウェルカムページを表示する設定に戻すには、Delphiへのショートカットを右クリックしてプロパティを表示し、先ほど追加した「(半角スペース)-np」を削除して「OK」をクリック。表示されるメッセージの「続行」をクリックすれば、次回のIDE起動時からウェルカムページが再び表示されるようになる。

3.まとめ

(1)ウェルカムページに表示される項目の整理方法は、次の通り。

 ・[ファイル] -> [開き直す] -> [プロパティ]から項目の整理ができる。

(2)ウェルカムページそのものを表示しない設定も可能。

 ① Delphiへのショートカットを右クリックしてプロパティを表示。
 ② ショートカットタブのリンク先(T)末尾に「 -np」を追加。

4.お願いとお断り

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

Delphi 11.2 Alexandria has arrived!

「アレキサンドリアがやってきた!」

2022年9月17日(土)早朝、てか、普通のヒト的には深夜、MyPCにDelphi 11.2 Alexandriaがやってきた。わぁーい*(^_^)*♪ インストールに時間がかかりそうだから、土曜日を待ってたんだ。きゃっほー♪ isoファイルをDLして、マウントして、インストーラを起動。しばらく待って無事インストール完了。それから、My Secret Weapon、大好きなPython4Delphiも入れて、今、作ってるプログラムを読み込んで実行したら・・・。

あれー? バルーンヒントが指定したVCLじゃなくて、マウスのポインタ位置に表示されるんだけど・・・。ふーん、今度からそうなったんだ。Delphi すごーい。でも、なんでー? みたいな・・・ T_T

1.11.2でバルーンヒントが大変なコトに
2.VCLの位置をTPointでGet!
3.まとめ
4.お願いとお断り

1.11.2でバルーンヒントが大変なコトに

MyPCだけで起きていることカモしれないけど、Delphi 11.2 Alexandriaをインストールして、以前のバージョンで作ったプログラムを読み込んで実行したら、バルーンヒントの表示される位置が・・・、んー。設定と・・・かなり「違う」。みたいな・・・

早速、検証用プログラムを作って、動作確認。

Button3をクリックしたら・・・ の手続きの中で、
(※注意:バルーンヒントにアイコンを表示する方法は、この下で解説)

procedure TForm1.Button3Click(Sender: TObject);
begin
  //バルーンヒントを表示
  BalloonHint1.Title := 'ヒント';
  BalloonHint1.Description := 'ここをクリックしてください';
  BalloonHint1.HideAfter := 12000; //表示時間(単位:ms)
  BalloonHint1.ShowHint(button2.ClientToScreen(CenterPoint(button2.ClientRect)));
  //案内アイコンも追加
  BalloonHint1.ImageIndex := 0;
end;

バルーンヒントを表示するのは、「button2」の真ん中だよって、ちゃんと指定してるのに・・・

なぜか Button2ではなく、マウスポインタ位置にバルーンヒントが表示・・・される

これでは役に立たないけれど、案内アイコンを付けてバルーンヒントを表示する方法をいちおうメモ(11.2より前のバージョンのDelphiなら、期待通りに動くはず)。

(1)FormにImageList1を置いて、HeightプロパティとWidthプロパティ両方に「32」を設定。

ImageList1のHeightプロパティとWidthプロパティ両方に「32」を設定。

(2)BalloonHint1のImagesプロパティにImageList1を指定。

BalloonHint1のImagesプロパティにImageList1を指定。

(3)IconExplorerをDLして、インストール。

Icon Explorer

https://www.mitec.cz/iconex.html

(4)IconExplorerを起動し、c:\Windows\System32\Shell32.dllをクリックするとアイコン一覧が表示されるので、その中から目的のIconを探して、以下のように操作。

c:\Windows\のSystem32フォルダをクリック
Shell32.dllをクリック
目的のアイコンをさがしてクリック
32×32を右クリック

で、表示されるサブメニューから、「Save to Bitmap」を選択し、任意のフォルダに保存する(PNGだと背景が透明になる・・・。Jpegは試していない)。

(5)TImageListをダブルクリックして表示されるWindowの「追加」をクリックして、上で任意のフォルダに保存したInfoアイコンを選択して「OK」をクリックする。

「追加」をクリックして、上で任意のフォルダに保存したInfoアイコンを選択してOKをクリック

(6)上で紹介したコードを記述して実行すれば、11.2より前のバージョンのDelphiなら期待した通りに動作するはず。バルーンヒントが表示される位置が、目的のVCLコントロールの上だったり、下だったり、その表示位置を自由に制御できないのがもどかしかったり、ヒントの色が背景と同じで、実際に使ってみると思ったほどヒントが目立たなかったり・・・ みたいな不満は、正直ずっとあったけど。少なくても「そこに出せ!」とコードで指示したVCLを無視するようなことだけはなかった・・・。11.2より前のバージョンのDelphiなら・・・

でも、もう前のバージョンには戻せない。

何回コンパイルしても、頑なまでに、指示を無視する11.2。
生まれたてなのに、イイ根性してます・・・。

でもね。

Delphiを心から信じ、愛している人間は、きっとこう思うはずなんですよ。

これは11.2で「バルーンヒントの表示位置は、マウスポインタがアクティブな場合、プログラム内容よりポインタの現在位置を優先する」仕様へとDelphiが進化したため・・・。

一瞬、そう思いたくもなったのですが。次の瞬間、

こんなプログラム。フツーのヒトは、
壊れてるとしか思わねーだろ!

・・・という声が聞こえ(た気がする)、僕は自分を取り戻した次第です。

そう言えば、ある冬の寒い朝、これと似た出来事がありました。

ハナが冷たくて目が覚めた僕は、
となりでまどろんでる彼女に、小さな声でききました。

『ねぇ 今日もさむいー?』

想像を絶する大音量で、返事が。

冬だから寒いに決まってんだろ!

おまけに、

冬をなめとんのか? オマエは

はい。すみません。

ですが、そこまで言わなくても・・・。
クー。クー。眠ってたはずなのに。もしかして、寝言?

こんな、違うだろ・・・みたいな出来事は、たくさんあって、僕は彼女が大好き。

パスタが大好きな僕ですが、ある晩、無茶苦茶美味しいパスタを彼女が作ってくれて・・・。ほんとに美味しかったから、翌朝、夢で味わったようなパスタを思い出して

『ねぇ まだおかわり、あるー?』って、やっぱり夢の中にいる彼女にきいたら、

ヨーシ、髪の毛で増量!

この人と結婚してよかったぁ☆

彼女とのことは、これでよくても、プログラムは、良くないです。
もし、本当に仕様変更であったにしても、この設定は受け入れられません。

で、Google先生に、どうしたらイイかを、いっぱい訊ねて得た僕なりの結論は・・・

現段階で、どうしてもバルーンヒントを表示したい。・・・なら
自前で作ったバルーンヒントを表示するしかない(したい)。

VCLコントロールのHintプロパティに「言い訳」的に何かを入力して、ShowHintプロパティをTrueに設定。で、実行時、マウスポインタがそのVCLコントロールをポイントしたら、操作方法のヒントを表示するみたいな「控えめ」なユーザーへの案内でなく、何かVCLをクリックしたら、プログラムを初めて使うユーザーにも「こっちだよー!」と手招きするような案内を、僕は表示したくて・・・。

普通のヒントでなく、バルーンヒントを表示させたいだけなら、こちらのWebサイトで紹介されていた方法もあるけど。

Delphi2010 バルーンヒント(BalloonHint)

http://afsoft.jp/program/del2010/p11_047.html

Mr.XRAYさんのWebサイトに完璧な答えが掲載されていました。
以下、その記事を引用して書いたプログラムです。

06_バルーンヒントウィンドウを自作

http://mrxray.on.coocan.jp/Delphi/Others/BalloonHintWindow.htm

上記サイトからDLできるplBalloonHint.pasをdprojファイルがあるのと同じフォルダに入れて、usesに次のように記述。

implementation

uses
  plBalloonHint;

{$R *.dfm}

Button1Click手続きに、以下のコードを記述。

procedure TForm1.Button1Click(Sender: TObject);
var
  LTitle : string;
  LText  : string;
  LhIcon : HICON;
  LPos   : TPoint;
  LArrow : TBalloonArrow;
begin

  //バルーンヒントを表示

  //タイトルとヒントの内容
  LTitle := 'ヒント';
  LText  := 'バルーンヒントを表示' + sLineBreak + '2行目'+ sLineBreak + '3行目';

  //表示のスタイル
  //LArrow:= baTopLeft;       //VCLの上・左へ向けて表示
  //LArrow:= baTopCenter;     //VCLの上・中央
  LArrow:= baTopRight;        //VCLの上・右へ向けて表示
  //LArrow := baBottomRight;  //VCLの下・右へ向けて表示
  //LArrow := baBottomCenter; //VCLの下・中央
  //LArrow := baBottomLeft;   //VCLの下・左へ向けて表示

  //吹き出しの始点
  GetCursorPos(LPos);   //マウスでクリックした位置に表示

  //システムのInfoアイコンを使用
  LhIcon := LoadIcon(0, IDI_INFORMATION);

  try
    //引数はタイトル、ヒント、アイコン、表示位置、吹き出しの始点、時間はミリ秒
    BalloonHint(LTitle, LText, LhIcon, LArrow, LPos, 12000);
  finally
    DestroyIcon(LhIcon);
  end;

end;

で、実行すると・・・

これくらい目立って欲しかった! Mr.XRAYさん、ほんとうにありがとうございます。

2.VCLの位置をTPointでGet!

んじゃ、Button1をクリックしたら、Button2の上に「こっちだよー」みたいにバルーンヒントを表示できたらいいなーっと思って、コードを書こうとしたら、なんと! その書き方を知らないことに気がつきました。

とりあえず、Button2の位置が取得できればいいわけですから、イロイロ調べた結果、次のstack overflow の記事を発見。

How can I get the X,Y position of a TWinControl (relative to the screen)

https://stackoverflow.com/questions/290000/how-can-i-get-the-x-y-position-of-a-twincontrol-relative-to-the-screen

で、以下のコードで、Button2の位置をLabel1に表示できることを確認。
(Pointを使うためにusesにSystem.Typesを追加)

implementation

uses
  plBalloonHint,
  System.Types;

  //System.TypesはButtonの位置を取得するPointを使用するために追加

{$R *.dfm}

procedure TForm1.Button3Click(Sender: TObject);
var
  LPos: TPoint;
begin
  //Button2の左上座標を取得して表示
  LPos := Button2.ClientToScreen(Point(0,0));
  Label1.Caption := Format('Screen: %d, %d', [LPos.X, LPos.Y]);
end;

で、Button1Click手続きのコードを次のように変更。

procedure TForm1.Button1Click(Sender: TObject);
var
  LTitle : string;
  LText  : string;
  LhIcon : HICON;
  LPos   : TPoint;
  LArrow : TBalloonArrow;
begin

  //バルーンヒントを表示

  //タイトルとヒントの内容
  LTitle := 'ヒント';
  LText  := 'バルーンヒントを表示' + sLineBreak + '2行目'+ sLineBreak + '3行目';

  //表示のスタイル
  //LArrow:= baTopLeft;       //VCLの上・左へ向けて表示
  //LArrow:= baTopCenter;     //VCLの上・中央
  LArrow:= baTopRight;        //VCLの上・右へ向けて表示
  //LArrow := baBottomRight;  //VCLの下・右へ向けて表示
  //LArrow := baBottomCenter; //VCLの下・中央
  //LArrow := baBottomLeft;   //VCLの下・左へ向けて表示

  //吹き出しの始点
  //GetCursorPos(LPos);   //マウスでクリックした位置に表示
  //Button2の上・幅の1/2の位置に吹き出しの始点がくるように表示
  LPos := Button2.ClientToScreen(Point(Trunc(Button2.Width div 2), 0));

  //システムのInfoアイコンを使用
  LhIcon := LoadIcon(0, IDI_INFORMATION);

  try
    //引数はタイトル、ヒント、アイコン、表示位置、吹き出しの始点、時間はミリ秒
    BalloonHint(LTitle, LText, LhIcon, LArrow, LPos, 12000);
  finally
    DestroyIcon(LhIcon);
  end;

end;
実現したかったのは、まさにコレ!

バルーンヒントを表示する位置によっては、ヒントが画面からはみ出して見えなくなってしまうことがあるので、表示位置の上下・表示する向きは実際の場面に合わせて調整する必要があるけれど、表示位置はDelphiまかせで制御できない(・・・と思ってるのは私だけ?)TBalloonHintより、見た目もくっきり・はっきりしていて目立つし、plBalloonHint.pasを公開してくださったMr.XRAYさんに心から感謝です。

うまく動かなかったTBalloonHintのコードの一部を使って、次のコードにすれば、

  //LPos := Button2.ClientToScreen(Point(Trunc(Button2.Width div 2), 0));
  LPos := Button2.ClientToScreen(CenterPoint(button2.ClientRect));

ボタンの中心に吹き出しの始点を持ってくることもできます。

ほんとに微妙な違いですが・・・僕はButtonのCaptionが全部見える方が好きです。

バルーンヒント表示対象のVCLコントロールの大きさや位置によって、VCLの周囲に表示するか、内部に表示するか、その判断が異なってくると思うので、ClientRectで座標を取得する方法も覚えておいた方が賢明かと。

3.まとめ

MyPCだけで発生する現象なのかもしれないが、Delphi 11.2 をインストールしたらバルーンヒントの表示位置がオカしくなった。

Mr.XRAYさんが配布してくださっている「自作のバルーンヒント」が表示可能なplBalloonHint.pasを使用すれば、この問題は解決でき、さらに「より良く目立つ」バルーンを表示できる。

バルーンヒントを表示するターゲット(VCL)の左上座標は、

  LPos := ターゲットとするVCLの名称.ClientToScreen(Point(0,0));

上のコードで取得できるので、結果をTPoint型の変数に代入して、バルーンヒントの引数に指定(必要に応じてX、Y座標の値が増加するような式を付加する)。また、VCLコントロールの大きさによっては、ClientRectで座標を取得した方がよい場合もありそう。

LPos := ターゲットとするVCLの名称.ClientToScreen(Point(VCLの名称.ClientRect));

で、表示するコードは、

  //引数はタイトル、ヒント、アイコン、表示位置、吹き出しの始点、時間はミリ秒
  BalloonHint(LTitle, LText, LhIcon, LArrow, LPos, 12000);

4.お願いとお断り

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

Management of Printing Equipment

「プリンタの管理で悩む」

1.Windows10のプリンタ管理方法の変更で困ったこと
2.AD環境下で管理者として実行するとネットワークプリンタが見えない!
3.プログラムから「デバイスとプリンター」設定画面を呼び出し
4.まとめ
5.お願いとお断り

1.Windows10のプリンタ管理方法の変更で困ったこと

Windows10になって、いちばん困ったのはプリンタの管理方法の変化だった。デフォルト設定で、最後に使ったプリンタが通常使うプリンタと見なされるようになってから、職場のあちこちで「印刷ができない!」という声が上がることが多くなった。駆け付けてみると、出力先プリンタはいつも「Microsoft Print to PDF」みたいな・・・。

そのたびに「Windowsで通常使うプリンターを管理する」のチェックをOFFにして、AD環境下に置かれた最も近いネットワークプリンタを「通常使うプリンタに設定」する作業を繰り返してきた。

プリンタ設定の方法を文書にして配布しても、どこかへなくしてしまったり、設定方法を忘れた頃にWindows Updateがあってプリンタの設定が勝手に(?)変更されたり・・・、

一般的ユーザーにとっては、「設定やコントロールパネルを開いて操作する」というのは、やはりどこか嫌な感じがする作業のようで、AD環境下でのプリンタ設定は、もうずっと前から思い出すと悩ましい、あまり考えたくないことのひとつだった。

2.AD環境下で管理者として実行するとネットワークプリンタが見えない!

そのように状況が変化する中で、僕は上司から要請されて、出張・休暇関係の申請文書を処理するシステムを組んだ。職員がPCで申請手続きを行うと、申請内容がそのままデータベース化され、管理職が電子決済を行い、出張・休暇を承認する。で、日報や週報のカタチで出張・休暇者の一覧が帳票形式で出力できる、そんなシステムだ。手続きの全部を電子データで行えば「紙」は必要ないと思うのだが、僕が所属する業界では(最終的には本社へ)事務方から「紙」のカタチで様々な報告がなされるようで、どうしても「印刷」作業が必要とのこと(ほぼ同時期に、某公的機関が全県一斉に出退勤時刻の記録方式を改めたことに追随するよう、これまた上司から要請され、新規にICカードとICカードリーダーを用いた勤務記録の管理システムも組んだが、こちらは本社への報告を含め、全て電子データでの処理となっている)。

OSをめぐる状況の変化から、当然、「Windowsで通常使うプリンターを管理する」のチェックがONで、通常使うプリンタが明示的に設定されておらず、出力先プリンタが「Microsoft Print to PDF」になっていて、印刷が「できない」PCが出現することは予測できた。

AD環境下なので、PCごとにグループポリシーでプリンタの割り当ては行ってあるのだが、そのプリンタはADにログオンした時、ネットワークプリンタとして「見える」だけで、通常使うプリンタに明示的に設定されているわけではない。

「通常使うプリンタに設定」するには、どうしても「誰か」が手動でこれを設定しなければならない。しかし、現在動かしている〇〇プログラムとは別に、「設定」もしくは「コントロールパネル」を開いてプリンタの設定を変更する方法が「組織全体の記憶」としてなかなか定着しないのだ・・・。

困った僕は次の方法で、この問題を解決しようとした。それは・・・

プリンターの選択ダイアログを表示して、設定を変更!

印刷の際にプリンターの選択ダイアログを「必ず」表示し、もし「通常使うプリンタに設定」されているプリンタがなかった場合は、出力先プリンタを右クリックして表示されるサブメニューから「通常使うプリンタに設定」を選んでクリックしてもらい、そのプリンタへ出力してもらうというもの(クリックして単に選択しただけでは出力されない)。

この方法をとれば、設定やコントロールパネルをいちいち呼び出す必要がないし、プリンタ名を右クリックすれば簡単に「通常使うプリンタに設定」できるから、PCの操作に自信のないユーザーにも敷居が低いのではないか? と考えたのだ。

こうして、職場にある多くのノートPCで、通常使うプリンタの指定がなされていない場合(=Windowsに管理を任せている場合)に、印刷データが「Microsoft Print to PDF」に出力され、紙に印刷できなくなってしまう問題をなんとか回避することができた。

ちなみに、この方法は次のWebサイトで紹介されていた情報から考案。
Mr.XRAYさんに心より感謝申し上げます。

015_プリンタ設定関係ダイアログ API の使用方法

http://mrxray.on.coocan.jp/Delphi/plSamples/015_PrintDlgAPI.htm#01

Mr.XRAYさんのサイトの情報に援けられて、なんとかその場はしのいだけれど、僕自身の中ではずっと「しこり」のようなものが残って・・・。

たまたま印刷ダイアログに表示されたプリンタ名を右クリックしたら通常使うプリンタに設定できた!」のではなくて、「① ComboBoxの選択肢から通常使うプリンタに設定したいプリンタ名を選び、ボタンクリックで設定」もしくは「② 最初から通常使うプリンタに設定することを目的にワンクリックでコントロールパネルの『デバイスとプリンター』を開きたい」みたいな想いが・・・。

あれからずっと・・・、僕の中に。

そこで今回、自分自身の勉強も兼ねて、さらにいろいろ調べて最初に①の方法が実現できないか、試してみた。コードは次の通り。

type
  TForm1 = class(TForm)
    ComboBox1: TComboBox;
    Button1: TButton;
    procedure FormCreate(Sender: TObject);
    procedure ComboBox1Select(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure FormShow(Sender: TObject);
  private
    { Private 宣言 }
    FDevice : array[0..MAX_PATH - 1] of Char;
    FDriver : array[0..MAX_PATH - 1] of Char;
    FPort : array[0..MAX_PATH - 1] of Char;
    FDeviceMode : THandle;
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

uses
  Vcl.Printers,
  Winapi.WinSpool,
  System.Win.ComObj,
  Vcl.ComCtrls;

{$R *.dfm}
procedure TForm1.Button1Click(Sender: TObject);
var
  APP : Variant;
  str : String;
begin
  //ネットワークプリンタに接続
  str := ComboBox1.Text;
  APP := CreateOleObject('WScript.Network');
  try
    APP.SetDefaultPrinter(str);
    ShowMessage(str + 'を既定のプリンタに設定しました');
  except
    ShowMessage('既定のプリンタへの設定に失敗しました');
  end;
end;

procedure TForm1.ComboBox1Select(Sender: TObject);
begin
  //選択したプリンタを現在のプリンタとする
  Printer.PrinterIndex := ComboBox1.ItemIndex;
  //ここで取得するFDeviceMode1には,変更前のプリンタの情報が格納されている
  //その他の値は現在(変更後)のプリンタの情報となっている
  Printer.GetPrinter(FDevice, FDriver, FPort, FDeviceMode);
  //FDeviceMode初期化
  Printer.SetPrinter(FDevice, FDriver, FPort, 0);
  //FDeviceModeが新しいプリンタドライバの値となる
  Printer.GetPrinter(FDevice, FDriver, FPort, FDeviceMode);
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  ComboBox1.Items.Clear;
  ComboBox1.Items.Assign(Printer.Printers);
  ComboBox1.ItemIndex := Printer.PrinterIndex;
  //選択したプリンタを初期化
  //ここでは通常使うプリンタとなっている
  ComboBox1Select(nil);
end;

procedure TForm1.FormShow(Sender: TObject);
begin
  //Formを画面の中央に表示
  Left:=(Screen.Width-Width) div 2;
  Top:=(Screen.Height-Height) div 2;
end;

実行すると・・・

ネットワークプリンタを選んで、設定ボタンをクリックする
MyPCでは、問題なく設定できた!

管理者権限でログオンしているMyPCでは上の例のように「何の問題もなく」動作する。が、AD環境下ではどうだろうか? 通常、ADにログオンする場合は、何でもできるネットワーク管理者権限ではなく、誰もが一般制限ユーザーとしてログオンする。この管理者でないユーザーが果たしてプリンタの設定を、このプログラムで変更可能か・どうか、試してみた。

結論から先に。
動作したり・しなかったりで、挙動が不安定だった。なぜ、Aパソコンでは動作するのに、Bパソコンでは動作しないのか。明示的に通常使うプリンタを設定したAでは、「設定しました」というメッセージが出て、コントロールパネルのデバイスとプリンターの画面にも反映される。が、明示的に通常使うプリンタを設定していないBでは「設定しました」というメッセージは出ても、コントロールパネルのデバイスとプリンターの画面には反映されない。「Windowsで通常使うプリンターを管理する」のチェック状態でこの違いは生まれるのか? (ちなみにAもBもT社製のまったく同じ時期に導入したリース機材)。

このBパソコンではさらに不思議なことが発生。僕の書いたDelphiのプログラムのプリンタ選択画面では「ユーザーが通常使うプリンタに明示的に指定したプリンタが緑のチェックマーク付きで表示されている」のに、コントロールパネルの「デバイスとプリンター」を開くと、そこでは「通常使うプリンタの設定がない」状態で表示され、さらに変更を加えようとすると「このプリンターを通常使うプリンターに設定すると、Windowsは通常使うプリンターの管理を停止します。」の注意メッセージが表示されてしまった・・・。これに関しては、もう、わけがわかりません・・・。が、結論として、①案は、今回はちょっとダメかなーみたいな・・・。

では、これを管理者権限で実行したらどうなるのか?

管理者権限でログオンしているPCであれば、このプリンタ設定プログラムは何の問題もなく動く。それならばということで、ネットワークプリンタがデバイスとプリンターに表示されているAD環境下で、プログラムのアイコンを右クリックして表示される「管理者として実行」を試してみた。すると・・・

ComboBoxの選択肢からは、ネットワークプリンタが全部きれいに・・・

消えたー!!

いとをかし。
ローカルPCにログオンするカタチになるからなのでしょうか・・・?

そんなこんなで、①的アプローチは「今回は」あきらめることに決定。
でもまだ心は折れてないので、②「ワンクリックでデバイスとプリンターを表示する」にチャレンジ!

3.プログラムから「デバイスとプリンター」設定画面を呼び出し

Mr.XRAYさんのWebサイトに、そのものズバリの答えがありました!!

468_各種システム設定ダイアログ表示

http://mrxray.on.coocan.jp/Delphi/plSamples/468_ShowDialog_System.htm

さまざまなシステム設定ダイアログの表示方法の詳細を学ぶことができました。
またまたお世話になり、本当にありがとうございました!!

で、紹介されていたコードをワンクリック用に書き換えたものがこちら。

unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
  System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs,
  Vcl.StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;  //基本的に必要なVCLはこれだけ
    EditPath: TEdit;  //確認用に置いてあるだけで絶対に必要なわけではない
    procedure Button1Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

uses
  Winapi.ShellAPI, System.StrUtils;

{$R *.dfm}

//ワンクリックでデバイスとプリンターを表示する
procedure TForm1.Button1Click(Sender: TObject);
var
  RetCode : Integer;
  strList : TStringList;
  OrgCmd : string;
  EnvPath : string;
  strPath : string;
  CmdPath : string;
  CmdParam : string;

  //環境変数を含む(%等の文字を含む)を実際のパス名に変換
  function ExpandEnvironmentString(S: String): String;
  var
    LDstChar:array [0..MAX_PATH - 1] of Char;
  begin
    ExpandEnvironmentStrings(PChar(S), LDstChar, MAX_PATH);
    Result := LDstChar;
  end;

begin

  //コントロールパネルの「デバイスとプリンター」を表示
  strPath := '%SystemRoot%\System32\control.exe /name Microsoft.DevicesAndPrinters';

  //選択中のItems文字列を取得してコマンド文字列を作成
  OrgCmd := Trim(strPath);
  EnvPath := ExpandEnvironmentString(OrgCmd);

  //実行ファイル名とパラメータに分解
  strList := TStringList.Create;
  try
    strList.Delimiter := ' ';
    strList.StrictDelimiter := True;
    strList.DelimitedText := EnvPath;

    if strList.Count = 1 then
    begin
      CmdPath := Trim(EnvPath);
      CmdParam := '';
    end else begin
      CmdPath := Trim(strList[0]);
      CmdParam := Trim(StringReplace(EnvPath, CmdPath, '', [rfIgnoreCase]));
    end;
  finally
    FreeAndNil(strList);
  end;

  //パス名の空白までをパスと認識してしまうのでダブルクォーテーションで囲む
  //パラメータはそのままとする
  if Pos(' ', CmdPath) > 1 then begin
    if LeftStr(CmdPath, 1) <> '"' then begin
      CmdPath := AnsiQuotedStr(CmdPath, '"');
    end;
  end;

  //Pathを確認用に表示
  EditPath.Text := CmdPath;

  //ShellExecute
  RetCode := ShellExecute(Handle, '', PChar(CmdPath), PChar(CmdParam), nil, SW_SHOW);

  //エラー対策
  if RetCode <= 32 then begin
    MessageBox(Handle, PChar(SysErrorMessage(RetCode)), '情報', MB_ICONINFORMATION);
  end;

end;

end.
設計時の画面
実行時の画面(ボタンをクリックした直後の状態)
Button1クリックで、デバイスとプリンターの画面が表示された

Mr.XRAYさんのおかげで無事目的を達成することができた!
(もちろん、このプログラムが、AD環境下、一般制限ユーザーとしてログオンしている状態でも完全に動作することを確認)

・・・ということで、このButton1をクリックした時の手続きを、業務に使用するプログラムへコピーしてコンパイル、職場のネットワーク上に「新しい更新プログラムとして公開」すれば、クライアントPCのプログラムは自動更新されるように組んであるから、いちばん最初に夢見たカタチで、カンタン・明示的なプリンタ設定の変更が実現できる・・・。

4.まとめ

AD環境下で、自主開発した業務用ソフトウェアを操作する「PC操作にあまり詳しくない」ユーザーに「通常使うプリンタに設定」等の作業をお願いしなければならない時は、コントロールパネルの「デバイスとプリンター」の画面をワンクリックで表示できるプログラムを、その業務用ソフトウェア内に埋め込んで提供するのがいちばんイイ(・・・と今回の経験から僕は思った。あくまでも個人的感想です)

5.お願いとお断り

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

Global And Local Variables

「グローバル変数とローカル変数」

ずっとDelphiを使ってきて、今回初めて「アレっ?」と思ったことがあり、そんなことも知らなかったの? って、バカな自分にあらためて驚愕したという、大変恥ずかしいお話。

1.代入済みの文字列型グローバル変数がなぜか空欄に
2.原因はすぐに判明
3.まとめ
4.お願いとお断り

1.代入済みの文字列型グローバル変数がなぜか空欄に

あるプログラムの中で、あるファイルまでのフルパスを入れておくグローバル変数を宣言した。

  private
    { Private 宣言 }
    strFilePath : string;

Button1をクリックしたら、あるファイルまでのフルパスを取得し、Button2をクリックしたら、そのプロシージャの中で取得済みのパスを使用するつもりだった。

実は、Button2側のプロシージャの中にも、Button1クリックで行ったのと同じ、あるファイルまでのフルパスの取得作業があり、既にButton1クリックで取得済みであれば、Button1をクリックした後、必ずButton2をクリックする設計なので、既に取得済みのパスがある場合は、そのまま使うコードでプログラミングした・・・はずだった。

コードを書いて、実行してみる。
順調に動き始めたように見える。

Button1をクリック。エラーなし。

Button1Clickでファイルまでのパスは取得済みだから、
Button2Clickではファイル選択のダイアログは出ないはず・・・

procedure TForm1.Button2Click(Sender: TObject);
begin
  ・・・略・・・
  if strFilePath='' then
  begin
    ・・・ファイル選択のダイアログを表示・・・
  end;
end;

Button2をクリック。
ファイル選択のダイアログが・・・表示・・・

される。

なんでー!?

取得済みのパスはどこへ消えた?

確認すると、Button2クリックの段階で、取得したはずのパスは、なぜか空欄に。

2.原因はすぐに判明

Button1Clickのプロシージャの先頭にある変数の初期化コード strFilePath:= ” を選択して、Ctrl+Fで検索を実行。で、ここにしか strFilePath:= ” が「ない」ことを確認。

続いて strFilePath だけを選択して再び Ctrl+F

全プログラムコード中にある strFilePath を1つずつ確認して行く・・・。

最優先されるのは、ローカル変数。で、Button1クリックの処理が実行されて、その処理が終わった時点で、Var宣言されたButton1Clickプロシージャ内でのみ有効なローカル変数は破棄される・・・。Google先生から教えてもらった「新」知識を胸に刻みつつ、検索を繰り返すこと、数回・・・

procedure TForm1.Button1Click(Sender: TObject);
var
  ・・・省略・・・
  strFilePath:string;
  ・・・省略・・・
begin

あ、れ?
ナンでこんなところにキミが!?

グローバル変数に、ローカル変数と同じ名前の変数があっても、ローカル変数から自動で代入なんかされない!!・・・ってコトを、今回初めて知りました。たぶん、変数をローカルに宣言した時は、何にも考えていなかったか、ローカル変数に入れた値がそのまま自動でグローバル変数に代入されるって思って(信じて)いたのでしょう。

つまり、グローバル変数の strFilePath は最初からずっと、空欄のまま・・・。

Var宣言の strFilePath : string。書いたのは、誰? はぁーい。ボクです。T_T

3.まとめ

似たような変数名を使いたい時は、例えばグローバル変数ならG_strFilePathのように最初に G_ を付け、ローカル変数なら最初は L_ から始めるとか、そういうほんのちょっとした自分自身との約束があれば起こるはずのないミスでした。今度からは、こんなことが起きないように気を付けたいと思います!

4.お願いとお断り

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