投稿者「サイト管理者」のアーカイブ

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

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

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

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

TDirectory.GetFiles Function

「特定ファイルの名称をフルパス付きで取得したい!」

例えば、特定のフォルダ内にある拡張子が「jpg」のファイルの名称と、そのファイルまでのフルパスをイッキに全部!取得したい・・・なんて場合の覚書。

1.特定のフォルダ内にあるJpeg画像名を全部取得
2.サブフォルダがあったらその中も探す
3.ListBoxに高速で項目を追加する方法
4.まとめ
5.お願いとお断り

1.特定のフォルダ内にあるJpeg画像名を全部取得

ある特定のフォルダにある拡張子が「jpg」のファイル全てについて、そこまでのフルパスを取得してListBoxに表示する方法は次の通り。

FormにButtonとListBoxを一つずつ追加して、以下を記述。

implementation

uses
  System.Types,
  System.IOUtils,
  System.StrUtils,
  System.Masks,
  Vcl.FileCtrl,
  System.UITypes;

  //System.TypesはTStringDynArrayを使うために追加
  //System.IOUtilsはTDirectory.TFilterPredicateを使うために追加
  //System.StrUtilsはSplitStringを使うために追加
  //System.MasksはMatchesMaskを使うために追加
  //Vcl.FileCtrlはSelectDirectoryを使うために追加
  //System.UITypesはMessageDlgを使うために追加

{$R *.dfm}

procedure TForm1.ButtonXClick(Sender: TObject);
var
  //フォルダの選択
  iStartFolder: string;
  iDirectories: TArray<string>;
  //ファイルリストの取得
  FileNames: TStringDynArray;
  strFileName: String;

  //ファイルを検索
  function MyGetFiles(const Path, Masks: string): TStringDynArray;
  var
    MaskArray: TStringDynArray;
    Predicate: TDirectory.TFilterPredicate;
  begin
    MaskArray := SplitString(Masks, ';');
    Predicate :=
      function(const Path: string; const SearchRec: TSearchRec): Boolean
      var
        Mask: string;
      begin
        for Mask in MaskArray do
          if MatchesMask(SearchRec.Name, Mask) then
            exit(True);
        exit(False);
      end;
    Result := TDirectory.GetFiles(Path, Predicate);
  end;
begin
  //フォルダを選択
  iStartFolder:=ExtractFilePath(Application.ExeName);
  if SelectDirectory(iStartFolder, iDirectories,
    [sdHidePinnedPlaces, sdNoDereferenceLinks, sdForceShowHidden,
    sdAllowMultiselect], 'フォルダを選択してください', 'Folder', 'Ok') then
  begin
    FileNames:=MyGetFiles(iDirectories[0], '*.jpg');
    for strFileName in FileNames do
    begin
      ListBox1.Items.Add(strFileName);
    end;
  end else begin
    //確認(キャンセルされた時に何かしたい場合)
    MessageDlg('キャンセルされました', mtInformation, [mbOk] , 0);
  end;
end;

たくさんのファイルを扱う場合には、ファイルへのPathとそれぞれに異なるファイル名の処理がまず問題になるが、上記の方法を用いれば、用意したListBoxに指定した拡張子のファイルのみ、フルパス付きでリストが作成される。TListBoxを表示する必要がなければVisibleプロパティをFalseにしておけば、その存在はまったく気にならないし、あとは、使いたい時にItemIndexやItems.Countを参照するだけでOKだから、Pathとファイル名の取得についてはもうなぁーんにも気にすることがなくなり、非常に便利!

例えば、以下の通り。

  //ファイルの数だけLoopする
  for i := 0 to ListBox1.Items.Count-1 do
  begin
    ShowMessage(ListBox1.Items[i]);
  end;

2.サブフォルダがあったらその中も探す

上の例のTDirectory.GetFiles関数では検索する際、サブフォルダを無視しているが、引数の指定を次のように変更すれば、サブフォルダ内も検索できる。

procedure TForm1.ButtonXClick(Sender: TObject);
var
  //フォルダの選択
  iStartFolder: string;
  iDirectories: TArray<string>;
  //ファイルリストの取得
  FileNames: TStringDynArray;
  strFileName: String;
  //検索するファイルの拡張子を指定
  SearchPattern: string;
  //サブフォルダも検索
  Option: TSearchOption;
begin
  //初期化
  ListBox1.Items.Clear;
  //フォルダを選択
  iStartFolder:=ExtractFilePath(Application.ExeName);
  if SelectDirectory(iStartFolder, iDirectories,
    [sdHidePinnedPlaces, sdNoDereferenceLinks, sdForceShowHidden,
    sdAllowMultiselect], 'フォルダを選択してください', 'Folder', 'Ok') then
  begin
    SearchPattern:= '*.jpg';
    //検索モード
    //Option:= TSearchOption.soTopDirectoryOnly; //指定フォルダ直下のみ
    Option:= TSearchOption.soAllDirectories; //サブフォルダ内も検索
    //指定拡張子のファイル名をフルパス付きで取得
    FileNames:= TDirectory.GetFiles(iDirectories[0], SearchPattern, Option);
    for strFileName in FileNames do
    begin
      ListBox1.Items.Add(strFileName);
    end;
  end else begin
    //確認(キャンセルされた時に何かしたい場合)
    MessageDlg('キャンセルされました', mtInformation, [mbOk] , 0);
  end;
end;

どうやらTDirectory.GetFiles関数はいろんな引数を指定できるらしい。
せっかくだから調べてみた。

System.IOUtils.TDirectory.GetFiles

https://docwiki.embarcadero.com/Libraries/Sydney/ja/System.IOUtils.TDirectory.GetFiles
以下、上記Webサイトより引用

class function GetFiles(const Path: string): TStringDynArray;
class function GetFiles(const Path: string;  const Predicate: TFilterPredicate): TStringDynArray;
class function GetFiles(const Path, SearchPattern: string): TStringDynArray;
class function GetFiles(const Path, SearchPattern: string;  const Predicate: TFilterPredicate): TStringDynArray;
class function GetFiles(const Path, SearchPattern: string;  const SearchOption: TSearchOption): TStringDynArray; overload; static;
class function GetFiles(const Path, SearchPattern: string;  const SearchOption: TSearchOption; const Predicate: TFilterPredicate): TStringDynArray; overload; static;
class function GetFiles(const Path: string;  const SearchOption: TSearchOption; const Predicate: TFilterPredicate): TStringDynArray; overload; static;

こんなにあったんだ。びっくり☆

もし、検索対象フォルダが決まっているのであれば、さらに簡単に・・・
(サブフォルダまで検索するか・どうかはOptionを切り替えて指定)

procedure TForm1.ButtonXClick(Sender: TObject);
var
  Path:string;
  SearchPattern:string;
  Option:TSearchOption;
  FileNames:TStringDynArray;
  FileName:String;
begin
  //初期化
  ListBox1.Items.Clear;
  //検索先のPathは指定
  Path:=ExtractFilePath(Application.ExeName) + 'Data';
  //ファイル名に一致する検索パターン
  SearchPattern:='*.jpg';
  //検索モード
  //Option:=TSearchOption.soTopDirectoryOnly; //指定フォルダ直下のみ
  Option:=TSearchOption.soAllDirectories; //サブフォルダ内も検索
  //ファイルのリストを作成
  FileNames:=TDirectory.GetFiles(Path, SearchPattern, Option);
  for FileName in FileNames do
  begin
    ListBox1.Items.Add(FileName);
  end;
end;

3.ListBoxに高速で項目を追加する方法

ファイルのリスト作成をより一層高速化するには、以下のようにリストをAddするfor文の前後にListBox1.Items.BeginUpdate / EndUpdateを入れると良いそうで・・・
(TListBoxのStyleプロパティがlbStandardの場合)

  ListBox1.Items.BeginUpdate;
  for FileName in FileNames do
  begin
    ListBox1.Items.Add(FileName);
  end;
  ListBox1.Items.EndUpdate;

あくまでもMyPC環境での値だが、1000枚のJpeg画像の名称を取得するのに、UpDate命令なしの場合が5回平均で120ms、UpDate命令ありの場合が同じく5回平均で17ms。

たった1000枚でこれだけの差。ファイルの数が増えれば増えるほど、その差が大きくなるのは自明の理。

これはTListBoxだけでなく、TMemo等のDelphiのコンポーネントに多数の項目を追加または変更する際に共通して起きる現象で、発生理由は、変更のたびに画面が再描画されるためとのこと。例えば、今回のようにListBoxに多数の項目を追加すると追加項目数分の再描画が発生し、項目数が多いほど処理時間が必要に。

そこでアイテムに変更を加える前に、 BeginUpdateを呼び出し、すべての変更が完了したら、EndUpdateを呼び出して変更を画面に表示するように指定すると画面の再描画が抑止され、処理時間は大幅に短縮される・・・ということで、大量の項目の追加・変更を行うときは忘れずにUpDate命令を使ったほうがよさそう。

ちなみにTMemoの場合であれば・・・

  Memo1.Lines.BeginUpdate;
  for i := 1 to 5000 do
  begin
    Memo1.Lines.Add('あいうえお');
  end;
  Memo1.Lines.EndUpdate;

つまり、BeginUpdateが画面更新の停止で、EndUpdateが再開ってことでいいのかな?
私はそんなふうに理解しました☆

4.まとめ

ある特定のフォルダ内にある、特定の拡張子を持つファイルのリストを作成したい場合は、TDirectory.GetFiles関数が便利。
取得したフルパス付きのファイル名はListBoxに入れておけば、ItemIndexやItems.Countを参照するだけで具体的な名前やPathを気にせずに使えてこれも便利。
ListBoxに大量に項目を追加する時は、BeginUpDateとEndUpDateを忘れずに使おう! というお話でした。

5.お願いとお断り

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

InputQuery Where Only Numeric Values Can Be Entered

「数値のみ入力可能なInputQuery」

ユーザーからの数値入力を受け取って動作するプログラムを作成した。このような場合には、以前からInputQueryを使用してきた(自前のDialogを作成したこともあった)が、今回、「数値だけ入力可能」なInputQueryを作成してみた。これは、その覚書。

1.入力をチェックして数値のみの入力を実現
2.MyInputQueryを作る
3.まとめ
4.お願いとお断り

1.入力をチェックして数値のみの入力を実現

Delphiでユーザーからの入力を受け取るプログラムを作る時、戻り値がString型のInputBox関数や、Boolean型のInputQuery関数を使う。僕はこれまでユーザーが「どのボタンを押したのか?」がはっきりわかるInputQuery関数を多用してきた(対し、InputBox関数ではデフォルトで設定しておいた文字列が返る)。

procedure TForm1.Button1Click(Sender: TObject);
var
  Ret:string;
begin
  if InputQuery('InputQuery', '値を入力:', Ret) then
  begin
    //OKボタンがクリックされた時

  end else begin
    //キャンセルボタンがクリックされた時(ESCキーで閉じた場合もFalseになる)

  end;
end;
InputQuery実行時の画面

今回、多くの画像の印刷を実行するプログラムの中で、何ページめの画像を印刷するのか、ユーザーに指定してもらう必要があり、そこで、やはりInputQuery関数を利用した。

以前から、このようなシーンで「数値のみ入力可能」なInputQuery関数が欲しいなー、とずっと思ってきたんだけど、とりあえず、プログラムの完成を急ぎたくて、そのたびに、この問題は先送りにされてきた。 僕の中で、もう長いこと ずっと・・・。

Excel VBAで帳票印刷を行う時は、「ちゃっちゃ」っと次のようなFormを作成して

コード書いてないのにEnterキーでFocusまで移動する・・・

「ちゃっちゃ」っと次のコードを書いて・・・

Private Sub CommandButton1_Click()

    Dim PrintNo1 As Integer
    Dim PrintNo2 As Integer
    Dim i As Integer
    
    If UserForm1.TextBox1.Text = "" Then
        MsgBox ("開始番号を半角数字で入力してください。")
        TextBox1.SetFocus
        Exit Sub
    End If
    
    If UserForm1.TextBox2.Text = "" Then
        MsgBox ("終了番号を半角数字で入力してください。")
        TextBox2.SetFocus
        Exit Sub
    End If
    
    PrintNo1 = UserForm1.TextBox1.Text
    PrintNo2 = UserForm1.TextBox2.Text
    i = PrintNo1

    For i = PrintNo1 To PrintNo2
        Range("A2").Select
        ActiveCell.FormulaR1C1 = i
        Range("B6:AB38").Select
    
        ActiveSheet.PageSetup.PrintArea = "$B$6:$AB$38"
        ActiveWindow.SelectedSheets.PrintOut Copies:=1, Collate:=True
    Next i
    
    Range("A2").Select

End Sub

Private Sub CommandButton2_Click()
    'キャンセルボタンがクリックされた場合
    Unload UserForm1
    Exit Sub
End Sub

で、FormのTextBoxをクリックして・・・

TextBox1を選択

IME ModeプロパティをDisableに設定。

IMEは絶対利用できないようにDisableを指定

あとは実行!
サク・・・っと印刷して終わり。みたいな感じで、VBAならカンタンなんだけど、これをDelphiでやるとなると、全然できるんだけど、でも、ちょっと・・・めんどくさい。

今回も、「数値のみ入力可能なInputQuery」は(現時点で)実現できないから、とりあえず、ユーザーが入力した値をチェック(数値であるか・どうか)して対応してしまった・・・。

それが次のコード。

implementation

uses
  System.UITypes;

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
var
  Ret: string;
  intNum: integer;
  //全角 -> 半角に変換
  Chr: array [0..255] of char;
begin
  if InputQuery('印刷', 'ページを指定', Ret) then
  begin
    //OKボタンがクリックされた時
    //全角->半角変換
    //全角だった場合は半角数字に変換、すでに半角のものは半角のまま
    //半角にできない文字、たとえばひらがな等は変換されない
    Winapi.Windows.LCMapString(
      GetUserDefaultLCID(),
      LCMAP_HALFWIDTH,
      PChar(Ret),  //変換する文字列
      Length(Ret)+1,  //サイズ
      chr,  //変換結果
      Sizeof(chr)  //サイズ
      );
      Ret := Chr;
    //数値であるかチェック
    if TryStrToInt(Ret, intNum) then
    begin
      //数値である
      j := StrToInt(Ret);
      //本当に使える数値か、さらにチェック
      if (j = 0) or (j > 印刷ページの上限値) then
      begin
        MessageDlg('入力された値は印刷できない番号です。'+#13#10+
        '処理を中止します。', mtInformation, [mbOk] , 0);
        Exit;
      end else begin
        // j の値を使って印刷実行
        ・・・ 省略 ・・・
      end;
    end else begin
      MessageDlg('入力された値は数値ではありません!'+#13#10+
        '処理を中止します。', mtInformation, [mbOk] , 0);
      Exit;
    end;
  end;
end;

まぁ、確かにこれで目的は実現できてるから、イイっちゃイイんだけど・・・。
なんか、スマートじゃない・・・気がして。
あと出しジャンケンみたいで・・・

そこで、今回だけは逃げずに自分と戦うことにしました☆

2.MyInputQueryを作る

とりあえずの目標はVBAでやったように、InputQueryのテキストボックスのIMEモードをDisableに設定すること。

Google先生に訊いたら、次の情報を教えてくれた。

Vcl.StdCtrls.TCustomEdit.NumbersOnly

https://docwiki.embarcadero.com/Libraries/Sydney/ja/Vcl.StdCtrls.TCustomEdit.NumbersOnly

Delphi2009からNumbersOnlyプロパティがTEditに実装されたとのこと。で、さらに、そのTEditを内部に抱えているInputQueryの正体については、次のサイト他で情報をGet!

InputQueryについて

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

どうやらいちばんの問題解決方法は、InputQueryのソースコードをそのままコピペして(挙動不審にならないように)名前を変更し、それぞれの目的が実現できるようにコードを修正することのようだ。

上記Webサイト他に掲載されていたInputQueryのコードを読むと、自分でもなんとかなりそうだったので、さっそくやってみることにした。

で、書いたのが次のコード(ほとんどコピペですが・・・)。

//Formのメンバーにはしていません。
//名前は MyInputQuery に変更
function MyInputQuery(const ACaption, APrompt: string;
  var Value: string): Boolean;
var
  Form: TForm;
  Prompt: TLabel;
  Edit: TEdit;
  DialogUnits: TPoint;
  ButtonTop, ButtonWidth, ButtonHeight: Integer;

  function GetAveCharSize(Canvas: TCanvas): TPoint;
  var
    I: Integer;
    Buffer: array[0..51] of Char;
  begin
    for I := 0 to 25 do Buffer[I] := Chr(I + Ord('A'));
    for I := 0 to 25 do Buffer[I + 26] := Chr(I + Ord('a'));
    GetTextExtentPoint(Canvas.Handle, Buffer, 52, TSize(Result));
    Result.X := Result.X div 52;
  end;

begin
  Result := False;
  Form := TForm.Create(Application);
  with Form do begin
    try
      Canvas.Font := Font;
      DialogUnits := GetAveCharSize(Canvas);
      BorderStyle := bsDialog;
      Caption := ACaption;
      ClientWidth := MulDiv(180, DialogUnits.X, 4);
      Position := poScreenCenter;

      Prompt := TLabel.Create(Form);
      with Prompt do
      begin
        Parent := Form;
        Caption := APrompt;
        Left := MulDiv(8, DialogUnits.X, 4);
        Top := MulDiv(8, DialogUnits.Y, 8);
        Constraints.MaxWidth := MulDiv(164, DialogUnits.X, 4);
        WordWrap := True;
      end;
      Edit := TEdit.Create(Form);
      with Edit do
      begin
        Parent := Form;
        Left := Prompt.Left;
        Top := Prompt.Top + Prompt.Height + 5;
        Width := MulDiv(164, DialogUnits.X, 4);
        MaxLength := 255;
        Text := Value;
        SelectAll;

        //Password入力用にInputQueryを使用するための設定(Password Mask)
        //EditコントロールではPasswordCharに設定した文字が
        //入力した文字の代わりに表示される(デフォルトは'#0')
        //パスワードマスクするなら
        //PasswordChar:= '*';
        //これでマスクしなくなる('#0'として文字列化しないこと)
        PasswordChar := #0;

        //Delphi2009からTEditにNumbersOnlyプロパティ(数字だけを入力可能にする)が
        //実装されているそうなので、せっかくだからTrueにしてみた!
        //全角文字の「123」も「数値である」と判断してくれます・・・
        NumbersOnly := True;

        //IMEは使用不可(この1行がどうしても書きたかった!)
        ImeMode := imDisable;

        //文字位置
        //Alignment := taCenter;
        Alignment := taLeftJustify;
        //Alignment := taRightJustify;

      end;
      ButtonTop := Edit.Top + Edit.Height + 15;
      ButtonWidth := MulDiv(50, DialogUnits.X, 4);
      ButtonHeight := MulDiv(14, DialogUnits.Y, 8);
      with TButton.Create(Form) do
      begin
        Parent := Form;
        Caption := 'OK';
        ModalResult := mrOk;
        Default := True;
        SetBounds(MulDiv(38, DialogUnits.X, 4), ButtonTop, ButtonWidth,
          ButtonHeight);
      end;
      with TButton.Create(Form) do
      begin
        Parent := Form;
        Caption := 'キャンセル';
        ModalResult := mrCancel;
        Cancel := True;
        SetBounds(MulDiv(92, DialogUnits.X, 4), Edit.Top + Edit.Height + 15,
          ButtonWidth, ButtonHeight);
        Form.ClientHeight := Top + Height + 13;
      end;
      if ShowModal = mrOk then
      begin
        Value := Edit.Text;
        Result := True;
      end;
    finally
      Form.Free;
    end;
  end;
end;

このInputQueryをボタンクリックで呼び出します。

procedure TForm1.Button1Click(Sender: TObject);
var
  Ret:string;
begin
  if MyInputQuery('Dialog Caption', 'Please Enter the number:', Ret) then
  begin
    ShowMessage('Entered: '+ Ret);
  end else begin
    ShowMessage('False!');
  end;
end;

上のコードを実行すると・・・

InputQueryのTextBoxではIMEモードは変更できない
Form上のEditコントロールではIMEモードの切り替えが可能

これで、ずっと夢だった「数値のみ入力可能な」InputQueryが完成しました☆
さらに工夫すれば、入力できる数値の範囲を制限したりすることもできると思います。

また、次のWebサイトではマウスポインタの近くにInputQueryを表示させるという技も紹介されていました。

.InputQueryのポップアップ位置

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

3.まとめ

数値のみ入力可能なInputQueryを実現するには、InputQueryのソースコードをコピペして、名前を変更したInputQuery関数(例:MyInputQuery)を作成し、その中でやりたいことを書いていけばイイということ。

ここでは、「IMEModeの設定」を主に、それに加えて「パスワードマスク、NumbersOnlyプロパティ、文字位置(Alignment)」等の設定を行ってみた。

4.お願いとお断り

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

Mouse Down Event Usage Example

「MouseDownイベントの活用例」

手書き答案をスキャナーで読み込んで採点するプログラムを書いた。Gridコントロールへの入力に対し、各々の解答欄の画像に ○ や × や得点を表示できるようにしたら、合計点がなければ採点済み答案とは言えないコトに気付いた。でも、それを「どこ」に記入するかは答案ごとに違うし、採点ミスがあった場合は個別の修正に対応する必要もある。そこでMouseDownイベントが役に立ったというお話。

1.合計点はいつ・どこに書く?
2.画像に ○ や × それから得点も表示
3.「やっぱりココ!」に対応
4.まとめ
5.お願いとお断り

1.合計点はいつ・どこに書く?

多くの場合、それは答案の右上か、右下に書かれている。左上や左下、まして「ど真ん中」ってのはまず見たコトない・・・ケド。とりあえず、合計点を書く場所は、まったく採点者の自由で、法的に「ココじゃなきゃダメ」って、決められているなんて話は聞かない。だから、合計点を書く(プログラム的には「置く」の方がしっくりするが)場所は、採点者が「ココ!」ってクリックした位置にすることにした。100%自由ってステキ。

それから採点者も人間である以上、当然のように間違える。採点ミスがあれば、もちろん合計点も変わる。・・・ってコトは、PCと協働作業する以上、合計点の計算はPCに任せるからイイとして、それを答案画像に「二度と修正できない」カタチで「埋め込んで」しまうわけにはいかない。合計点は、返却用の答案画像を印刷するときに、「どっかから持ってきて」、答案画像の上に一時的に「置く」くらいがちょうどイイ。

・・・ということで、基本方針だけを決め、新しいチャレンジがはじまった!

2.画像に や × それから得点も表示

最初は正誤の表示(○と×)だけだったけれど、得点も表示することにした

上のような画面に、スキャンした答案画像から設問ごとにかき集めた解答欄を表示してイッキに採点する。上のように全員が同じ解答なら得点の一括入力も可能だ。採点が済んだら、読み込み元の答案画像とは別に用意した返却用の答案画像に、集めてきた時とは逆のアルゴリズムで書き戻す。

答案に書き戻してみたところ(もっとズレるかと思ったが案外ずれてない)

で、決定的に足りないモノがあることに気づく・・・。ここまでやって「合計点がない」というのは、仏作って魂入れず & 画竜点睛を欠く & ツメが甘い & 九仞の功を一簣に虧く 以外の何者でもない。日本語の豊かさに感動しつつ、もっと簡単な言い方をすれば、プログラムは、どう考えても不完全。元よりこれを売るつもりはまったくナイけど(買うヒトがいるとも思えない)、合計点が「出ない」採点プログラムなんて詐欺だ。

なにより、この答案は、なんだかさみしい・・・

恐るべし。合計点の存在感。

・・・ということで、合計点も入れることに。

3.「やっぱりココ!」に対応

StringGridに見えない列を1つ追加して、ここに計算した合計点を書き込んでおけばいつでも印刷に使える。合計点の計算そのものはカンタン。何も問題はない。

var
  i, j, k:integer;
begin
  //初期化
  k:=0;
  //合計点を計算
  for i := 1 to StringGrid1.RowCount-1 do
  begin
    for j := 1 to ( 解答欄の数 ) do
    begin
      if StringGrid1.Cells[j,i] <> '' then
      begin
        k := K + StrToInt(StringGrid1.Cells[j, i]);
      end;
    end;
    //StringGrid.Cells[, ]
    StringGrid1.Cells[( 解答欄の数 ) + 1, i]:= IntToStr(k);
    //初期化
    k:=0;
  end;

これで合計点の準備はOK!
あとは「いつ」印刷するか・・・ってか、採点ミスがあった時、カンタンに合計点も修正できるようにしなければならない。これは結構、難しい。合計点を答案画像に画像データとして埋め込んでしまうと、まず修正はできない。どぉーするか・・・

よく考えたら(よく考えなくても)、この解答用紙には合計点を記入する場所すらない。まぁもともとがマークシートで、そこに無理やり手書き用の解答欄を付け加えたのがほんとうだから、ないのが当然なんだが。

こうなったらもう、面倒なことは全部やめて、答案画像を印刷する直前に、採点者が適当に「ココ!」ってクリックした場所に合計点を置いて、印刷はするけど、その後、画像は保存しない仕様で行こう。

印刷日が異なると、ビミョー(ヒトによっては大きく)に合計点印刷位置がズレる・・・という問題?は、「気にしない」ことにしよう。法的には何の問題もない。返却された答案に合計点が「ある」ことが大切なのだ。

フォントの大きさは、得点入力のところで使った指定をそのまま使えばイイ。

FontのSizeは50

これでアルゴリズムは決まり。あとは採点者に「ココ!」って指定してもらうプログラムをDelphiで書くだけ。でも、その「ココ!」はきっと、やっぱりもぉちょっと上とか、左とか、位置指定をやり直したい場合が絶対あるよなー。どぉするか・・・

TImageの上でマウスのボタンを押すたびにMouseDownイベントが起こるから、コレをうまく活用すればイイ。きっとそれだなー。で、書いたのがコレです。

合計点を印刷したい場所をクリックするとサンプル99を表示
procedure TFormCollaboration.Image1MouseDown(Sender: TObject;
  Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
var
  //表示倍率
  rate:Double;
  ・・・ 省略 ・・・

  //普通の四捨五入を行う関数を設定
  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));
    //表示倍率を計算(答案画像は縮小してWidth1000にセットしている)
    rate:= 1000/Image1.Picture.Bitmap.Width;
    合計点のX座標:= Trunc(iX/rate);
    合計点のY座標:= Trunc(iY/rate);
    //矩形を描画
    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;
      //矩形を描画
      (サンプル99はLabelのCaption).Font.Size:=StrToInt(ComboBox.Text);
      Canvas.Rectangle(合計点のX座標, 
                       合計点のY座標, 
                       合計点のX座標 + Label.Width, 
                       合計点のY座標 + Label.Height);
      //サンプル合計点を描画
      Canvas.Font.Color:=clRed;
      Canvas.Font.Size:=StrToInt(ComboBox.Text);
      Canvas.TextOut(合計点のX座標, 合計点のY座標, Label.Caption);
    end;
  end;

begin
  //座標を指定する手続きを呼び出し
  GetXY(X,Y);
  //Information
  if MessageDlg('印刷位置は、この位置でよろしいですか?' + #13#10 + #13#10 +
    '(左寄せ・数字はサンプル。矩形は印刷されません。)', 
    mtInformation, [mbYes, mbNo], 0) = mrYes then
  begin
    //[はい]が選ばれた時
    //案内
    MessageDlg('印刷ボタンをクリックしてください。', mtInformation,[mbOK],0);
    //バルーンヒントを表示
    BalloonHint1.Title := '印刷ボタン';
    BalloonHint1.Description := 'ココです!';
    BalloonHint1.HideAfter := 3000; //表示時間(単位:ms)
    BalloonHint1.ShowHint(button.ClientToScreen(CenterPoint(button.ClientRect)));
    //案内アイコンも追加
    BalloonHint1.ImageIndex := 0;
    //カーソルを元に戻す
    Screen.Cursor:=crDefault;
    Image1.Visible:=False;
    Image1.Picture.Assign(nil);
    //SetFocus
    button.Enabled:=True;
    button.SetFocus;
  end else 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;
      //矩形を描画
      (サンプル99はLabelのCaption).Font.Size:=StrToInt(ComboBox.Text);
      Canvas.Rectangle(合計点のX座標, 
                       合計点のY座標, 
                       合計点のX座標 + Label.Width, 
                       合計点のY座標 + Label.Height);
      //サンプル合計点を描画
      Canvas.Font.Color:=clWhite;
      Canvas.Font.Size:=StrToInt(ComboBox.Text);
      Canvas.TextOut(合計点のX座標, 合計点のY座標, Label.Caption);
    end;
  end;
end;

ユーザーが「ココ!じゃない」=「いいえ」を選択した場合は、サンプルとして表示した合計点を消去しなければならない。Undoの実装方法をいろいろ調べてみたのだが、よくわからない。で、思いついたのが上の方法。「いいえ」が選択された場合は、サンプルを「赤」じゃなくて「白」で書いちゃう。正直、完全に消えるわけじゃなくて、なぜか、よく見るとうっすらと赤が残っているけれど、気にしない。これで全然イケます。

合計点も印刷できるようになりました!

【追記 20221003】

合計点はサンプル「99」ではなく、個々の合計点を取得して表示できるよう、プログラムを修正しました。下のリンク先をご参照ください。

4.まとめ

そこにTImageがあれば、彼はいつでも OnMouseDown を待ち続けているから、このイベントをうまく利用すれば、再帰的な処理(?)が実現できてしまう。画像として保存したくは「ない」んだけれど、印刷時には「ちょっとイジりたい」時にはこんな方法もあります・・・というお話でした。

5.お願いとお断り

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

To Be Broken

その一瞬で、君に夢中になったんだ。
今でも、忘れない・・・。
初めて君を見た、あの日。

あれから、もう20年が過ぎた。

カタログを手にしただけでうれしかった。
毎晩、何度も、眺めた。

思い出のカタログは、今も、手元にある。

開発責任者を務めた湯川伸次郎さんが、『2002年、「奇跡の名車」フェアレディZはこうして復活した』(講談社+α新書)を出版してくださった。

こんなすごい、ハードカバーのカタログが、タダで、何冊ももらえた理由を
ずっと知りたかった僕は、湯川さんの本を、即日、入手して読んだ。

命を削るような苦悩の中で、湯川さんがご自身と戦ったことを初めて知った。
僕の中で、謎だったことは、全部、「感謝」に変わった。

Yearモデルを出すってことすら、とてつもなく大変なことだったんだ・・・。
僕はずっと、ドキドキしていただけ・・・だった・・・のに。

振り返れば、僕のクルマ人生は NISSAN とともにあった。
初めて乗ったクルマは Skyline Japan(譲ったトモダチが今も乗ってる)。
その後の430セドリックも、思い出は深いけれど・・・、

人生の1/3をともに過ごした z33 は、「最高のパートナー」。
彼女は酔って吐いたりしてたけど、それは僕の運転のせい。

君のカタチは「スポーツカー」の Rule そのもの。でも、その輝きは・・・ 違ったんだ。

Rule is existing.
それに頼りそうになる時もあるけど・・・

君が教えてくれた輝きは・・・
To Be Broken.

君こそが・・・
Best of The Best.

z33
いつも、君と。

今も・・・
そして、 いつまでも。

風の中 君を探して

きれいな空
夏を忘れたみたいに
空気が乾いてる・・・

今日は風になりたいって、思った。
16の頃から、ずっと
憧れていた 風に。

だから握った Key は、z33 じゃなくて
古いバイクの Key.

若かった あの日。
KAWASAKI が創ってくれた ・・・
夢をカタチにしたようなオートバイ。

30年が、一瞬のように過ぎてしまって
今があるけれど・・・
夢のカタチは、何にも変わらない。

もう、ラインアップされることのない
空冷4気筒DOHCエンジンの咆哮が
僕は、たまらなく好き・・・。

きらめく 風の中
君を 探して ・・・

How to use the StatusBar

「StatusBarの使い方」

これまでStatusBarを使う時は、FormCreate 時にその SimplePanel プロパティを True に設定して、あとは必要なところで SimpleText に表示したい文字列を指定したり、ProgressBar の Parent を StatusBar にして、時間のかかる処理の進捗状況を示したりしてきた。が、今回、いろいろあって TStatusPanels を使って、StatusBar に表示する文字の色を変えたり、背景色を変更する方法を学んだ。これは、その覚書。

1.いちばんカンタンな使い方(SimplePanel:=True;)
2.TStatusPanelsを使う(SimplePanel:=False;)
3.こんなこともできた! (その1) (その2)
4.まとめ
5.お願いとお断り

1.いちばんカンタンな使い方

StatusBar のいちばんカンタンな使い方は、SimplePanel プロパティを True (デフォルトは False )にして、StatusBar の SimpleText プロパティに表示したい文字列を指定する方法だ。

以下、いちばんカンタンな使い方の例。

新しいVCLアプリケーションを作成し、適当に名前を付けてプロジェクトを保存して、パレットにStatusと入力、表示されたTStatusBarをダブルクリックする。

FormにTStatusBarを追加したところ

Formのいちばん下に(自動的に位置は指定される)StatusBar1が追加される。

次に、Form をクリックして選択し、オブジェクト インスペクタのイベントタブをクリックして、OnCreate イベントの右の空白部分をダブルクリック。FormCreate 手続きを作成する。で、忘れないうちに次の1行を入力する。

procedure TForm1.FormCreate(Sender: TObject);
begin
  //設定
  StatusBar1.SimplePanel:= True;
end;

StatusBar1.SimplePanel:= True; とすることで、StatusBar1.SimpleText に指定した文字列がそのまま自動的にStatusBarに表示されるようになる(SimplePanel のデフォルト設定は False なので、これを True に変更しておかないと Statusbar への描画は StatusBar1DrawPanel イベントに全て自前で記述しなければ「行われない!」)。

あとは使いたいシーンで、SimpleTextに代入した文字列を表示するようコードを書けばOK! 例えば、「進捗状況:」と表示するなら、FormにButtonを1個追加して、Button1Click手続きを作成し、以下のように記述。

procedure TForm1.Button1Click(Sender: TObject);
begin
  //表示
  StatusBar1.SimpleText:= '進捗状況:';
end;

実行(F9)すれば ・・・

StatusBarに文字列を表示

進捗状況を表示するコントロールと言えば ProgressBar。これをFormに追加して、FormCreate 及び Button1Click の各手続きに以下のコードを記述(赤文字が追加するコード)。

procedure TForm1.FormCreate(Sender: TObject);
var
  w: integer;
begin

  //設定
  StatusBar1.SimplePanel:=True;

  //文字列の長さを取得
  w:=StatusBar1.Canvas.TextWidth('進捗状況:');

  //ProgressBarの設定
  with ProgressBar1 do begin
    Parent  := StatusBar1;
    Top     := 2;  //表示位置の調整
    Left    := w;
    Height  := StatusBar1.Height-2;
    Width := StatusBar1.Width-20;
    Visible :=False;
  end;

end;
procedure TForm1.Button1Click(Sender: TObject);
begin
  //表示
  StatusBar1.SimpleText:= '進捗状況:';
  ProgressBar1.Visible:=True;

end;

保存(Ctrl+S)して、実行(F9)

ボタンをクリックすると、StatusBarに文字列とProgressBarが表示される

せっかくここまで書いたのですから、ProgressBarに進捗状況を表示しましょう。以下のコードをButton1Click手続きに追加します(赤文字が追加するコード)。

procedure TForm1.Button1Click(Sender: TObject);
var
  i: integer;
begin
  //表示
  StatusBar1.SimpleText:= '進捗状況:';

  //ProgressBarの設定
  ProgressBar1.Visible:=True;
  ProgressBar1.Position:=0;
  ProgressBar1.Max:=100;

  for i:= 0 to 100 do
  begin
    ProgressBar1.Position:= i; // -> MAXまで表示されない
    Sleep(25);
    Application.ProcessMessages;
  end;
  ProgressBar1.Visible:= False;
  ShowMessage('Done!');

end;

保存(Ctrl+S)して、実行(F9)してみるとわかるのですが、このコードには問題があります。上にすでにコメントとして書いてある通り、ProgressBarのPositionが100まで表示されないのです。機能的には、「処理が進んでいることがユーザーに伝わればいい」ので、それほど大きな問題ではありませんが、表示できるものならやはり100まで表示できたほうが気持ちイイ。

実は少し工夫すれば100まできちんと表示できます。以前、どこかWeb上の情報で知ったのですが、Loopの中で増加する i の値をそのままProgressBarのPositionに代入せず、現在の i 値より「1大きな値をまず代入」し、その後、「1小さい値をセットしなおす」という技です。どなたが考えた技か知りませんが、これで100まできちんと表示できます。以下に、そのコードを示します(赤文字が追加するコード)。

procedure TForm1.Button1Click(Sender: TObject);
var
  i, j:integer;
begin
  //表示
  StatusBar1.SimpleText:= '進捗状況:';

  //ProgressBarの設定
  ProgressBar1.Visible:=True;
  ProgressBar1.Position:=0;
  ProgressBar1.Max:=100;
  //初期化
  j:= 0;
  for i:= 0 to 100 do
  begin
    //ProgressBar1.Position:= i; // -> MAXまで表示されない

    // 100まで表示するコード
    inc(j);
    If ProgressBar1.Position < ProgressBar1.Max Then
    begin
      //目的の値より一つ大きくしてから、目的の値にする
      ProgressBar1.Position:= j + 1;
      ProgressBar1.Position:= j;
    end else begin
      //最大値にする時
      //最大値を1つ増やしてから、元に戻す
      ProgressBar1.Max:= 100 + 1;
      ProgressBar1.Position:= j + 1;
      ProgressBar1.Max:= 100;
      ProgressBar1.Position:= j;
    end;
    Sleep(25);
    Application.ProcessMessages;
  end;
  ProgressBar1.Visible:= False;
  ShowMessage('Done!');

end;

さらにLabelを使って、次のような表示の追加も可能です。FormにLabelを1つ追加して、次のコードを FormCreate と Button1Click の各手続きに追加します(赤文字が追加するコード)。

Labelを1つFormに追加
procedure TForm1.FormCreate(Sender: TObject);
var
  w: integer;
begin

  //設定
  StatusBar1.SimplePanel:= True;

  //文字列の長さを取得
  w:=StatusBar1.Canvas.TextWidth('進捗状況:');

  //ProgressBarの設定
  with ProgressBar1 do begin
    Parent  := StatusBar1;
    Top     := 2;  //表示位置の調整
    Left    := w;
    Height  := StatusBar1.Height - 2;
    Width := StatusBar1.Width - 20;
    Visible := False;
  end;

  //LabelをProgressBarの中心に表示
  Label1.Parent:= ProgressBar1;  // ParentがStatusBarでないことに注意する
  Label1.AutoSize:= False;
  Label1.Transparent:= True;
  Label1.Caption:= '';
  Label1.Visible:= False;

end;
procedure TForm1.Button1Click(Sender: TObject);
var
  i, j:integer;
begin

  //進捗状況を表示するLabelの設定
  Label1.Visible:=True;
  Label1.Top:= 0;
  Label1.Left:= 0;
  Label1.Width:= ProgressBar1.ClientWidth;
  Label1.Height:= ProgressBar1.ClientHeight;
  Label1.Alignment:= taCenter;
  Label1.Layout:= tlCenter;

  //表示
  StatusBar1.SimpleText:= '進捗状況:';

  //ProgressBarの設定
  ProgressBar1.Visible:= True;
  ProgressBar1.Position:= 0;
  ProgressBar1.Max:= 100;
  //初期化
  j:= 0;
  for i:= 0 to 100 do
  begin
    //ProgressBar1.Position:= i; // -> MAXまで表示されない

    //値を増やす
    inc(j);
    If ProgressBar1.Position < ProgressBar1.Max Then
    begin
      //目的の値より一つ大きくしてから、目的の値にする
      ProgressBar1.Position:= j + 1;
      ProgressBar1.Position:= j;
    end else begin
      //最大値にする時
      //最大値を1つ増やしてから、元に戻す
      ProgressBar1.Max:= 100 + 1;
      ProgressBar1.Position:= j + 1;
      ProgressBar1.Max:= 100;
      ProgressBar1.Position:= j;
    end;

    //数値でも進捗状況を表示
    Label1.Caption:= '( '+IntToStr(j-1)+' %)';

    Sleep(25);
    Application.ProcessMessages;
  end;
  ProgressBar1.Visible:= False;
  ShowMessage('Done!');

end;

保存(Ctrl+S)して、実行(F9)。

数値でも進捗状況を表示

Button1Click手続きの最後にある「ProgressBar1.Visible:= False;」をコメント化して表示の状況を確認することもできます。「Label1.Visible:=False;」としなくてもLabelが非表示になるのは、Label1のParentを「Label1.Parent:=ProgressBar1;」としているためだと思われます。

【ヒントをStatusBarに表示する】

引用先には、設定したヒントをことごとくStatusBarに表示するという技が紹介されていました。StatusBar1.SimplePanel プロパティを True に設定して使うのであれば、内容もシンプルで、実際に使えるテクニックだと思います。

ヒントをステータスバーに表示するようなアプリケーションのためのコード

http://hp.vector.co.jp/authors/VA015850/delphi/fragments/AppHint.html

以上、いちばんカンタンなStatusBarの使い方でした!

2.TStatusPanelsを使う方法

2つめが、StatusBarがもともと持っているPanelを1つ、または複数個設置して、各々のPanelに対して描画の指示を出すという方法です。

新しいプロジェクトを作成し、FormにStatusBarを1つ、追加します。で、これを選択したまま、オブジェクト インスペクタのPanelsプロパティの(TStatusPanels)の右の…をクリックすると、次のような別Windowが表示されます。

(TstatusPanels)の右にある…をクリック

左上の「新規追加」アイコンを2回クリックして、StatusPanelを2つ追加します。

StatusPanelを2つ追加したところ

ここではStatusBarに表示する文字の文字色を変えたり、背景色を変更してみたいと思います。「0-TStatusPanel」をクリックして選択し、オブジェクト インスペクタのStyleプロパティをデフォルトの「psText」から「psOwnerDraw」へ変更します。

0-TStatusPanelを選択
StyleプロパティをpsOwnerDrawに変更

同じ操作を「1-TStatusPanel」にも行います。
これでStatusBarへの描画は「すべて自力で行う」ことになります。
設定したら右上の閉じるボタンをクリックして、設定画面を閉じます。

TStatusPanelをコードで設定する場合は、FormCreate手続きに以下の内容を入力します(赤文字が追加するコード)。

procedure TForm1.FormCreate(Sender: TObject);
var
  w:integer;
begin

  //コードで設定
  //StyleをpsOwnerDrawに変更
  StatusBar1.Panels[0].Style:=psOwnerDraw;
  StatusBar1.Panels[0].Text:='Information:';

  //Panel[0]の幅の調整(表示する予定の文字列で初期化)
  w:= StatusBar1.Canvas.TextWidth('Information:');
  StatusBar1.Panels[0].Width:= w + 15;
  StatusBar1.Refresh;

  //StyleをpsOwnerDrawに変更
  StatusBar1.Panels[1].Style:= psOwnerDraw;
  StatusBar1.Panels[1].Text:= '最初の表示';

end;

保存(Ctrl+S)して、実行(F9)。

よく見ると、StatusBarに StatusBar1.Panels[0] と StatusBar1.Panels[1] があることがわかります。コードで設定したはずの文字列は、Styleプロパティを psOwnerDraw へ変更したので当然表示されていません。

描画コードを設定して、文字列が表示されるようにします。
StatusBarをクリックして選択し、オブジェクト インスペクタのOnDrawPanelプロパティ右側の空欄をダブルクリックして StatusBar1DrawPanel 手続きを作成し、次のコードを入力します。

StatusBar1DrawPanel 手続きを作成
procedure TForm1.StatusBar1DrawPanel(StatusBar: TStatusBar; Panel: TStatusPanel;
  const Rect: TRect);
var
  ARect: TRect;
begin

  if Panel = StatusBar.Panels[0] then
  begin

    //文字色(このPanel内のみ有効となるようだ)
    StatusBar1.Canvas.Font.Color:= clBlack;

    //背景色(このPanel内のみ有効となるようだ)
    StatusBar1.Canvas.Brush.Color:= clBtnFace;

    //矩形を取得
    ARect:= Rect;

    //表示位置(中央寄せ)
    DrawText(StatusBar1.Canvas.Handle, PChar(Panel.Text), -1, ARect,
      DT_CENTER or DT_VCENTER or DT_SINGLELINE);

  end;

  if Panel=StatusBar.Panels[1] then
  begin

    //文字の表示(このPanel内のみ有効となるようだ)
    StatusBar1.Canvas.Font.Color:= clBlack;

    //背景色(このPanel内のみ有効となるようだ)
    StatusBar1.Canvas.Brush.Color:= clBtnFace;

    //矩形を取得
    ARect:= Rect;

    //表示位置(左寄せ)
    DrawText(StatusBar1.Canvas.Handle, PChar(Panel.Text), -1, ARect,
      DT_LEFT or DT_VCENTER or DT_SINGLELINE);

  end;

end;

保存(Ctrl+S)して、実行(F9)。

StatusBar1DrawPanel手続きに描画コードを書いたので文字列が表示される

文字列の色と背景色を変更してみましょう。Panels[0]の方だけ、コードを少し変更します。

  if Panel = StatusBar.Panels[0] then
  begin

    //文字色(このPanel内のみ有効となるようだ)
    //StatusBar1.Canvas.Font.Color:= clBlack;
    StatusBar1.Canvas.Font.Color:= clBlue;

    //背景色(このPanel内のみ有効となるようだ)
    //StatusBar1.Canvas.Brush.Color:= clBtnFace;
    StatusBar1.Canvas.Brush.Color:= clAqua;

    //矩形を取得
    ARect:= Rect;

    //表示位置(中央寄せ)
    DrawText(StatusBar1.Canvas.Handle, PChar(Panel.Text), -1, ARect,
      DT_CENTER or DT_VCENTER or DT_SINGLELINE);

  end;

保存(Ctrl+S)して、実行(F9)。

ボタンをクリックするとPanel[1]の文字列が変わるようにしてみます。
FormにButtonを1つ、追加して次のコードを書きこます。

procedure TForm1.Button1Click(Sender: TObject);
begin
  StatusBar1.Panels[1].Text:= '2番目の表示';
end;

保存(Ctrl+S)して、実行(F9)。

描画更新(StatusBar1.Refresh等)の命令は必要ないようだ

最初に紹介したように、進捗状況の表示に使うのであれば、Panel[1]の方にProgressBarを設定すればOKです。やり方は、次の通り(FormにProgressBarとLabelとButtonを1つずつ、追加してからコードを書きます)。

まず、ボタンをクリックした時の処理。こちらは全部新規に書きます。

procedure TForm1.Button2Click(Sender: TObject);
var
  i, j: integer;
  w: integer;
begin

  //進捗状況を表示するLabelの設定
  Label1.Visible:= True;
  Label1.Top:= 0;
  Label1.Left:= 0;
  Label1.Width:= ProgressBar1.ClientWidth;
  Label1.Height:= ProgressBar1.ClientHeight;
  Label1.Alignment:= taCenter;
  Label1.Layout:= tlCenter;
  Label1.Font.Color:= clBlue;

  //表示する文字列を設定
  StatusBar1.Panels[0].Text:= '進捗状況:';
  //Panel[0]の幅の調整(表示する予定の文字列で初期化)
  w:=StatusBar1.Canvas.TextWidth('進捗状況:');
  StatusBar1.Panels[0].Width:= w + 10;
  StatusBar1.Refresh;

  //Panels[1]の文字列は初期化
  StatusBar1.Panels[1].Text:= '';

  //ProgressBarの設定
  ProgressBar1.Visible:= True;
  ProgressBar1.Position:= 0;
  ProgressBar1.Max:= 100;
  //初期化
  j:= 0;
  for i:= 0 to 100 do
  begin
    //ProgressBar1.Position:=i; // -> MAXまで表示されない
    //値を増やす時
    j:= j + 1;
    If ProgressBar1.Position < ProgressBar1.Max Then
    begin
      //目的の値より一つ大きくしてから、目的の値にする
      ProgressBar1.Position:= j + 1;
      ProgressBar1.Position:= j;
    end else begin
      //最大値にする時
      //最大値を1つ増やしてから、元に戻す
      ProgressBar1.Max:= 100 + 1;
      ProgressBar1.Position:= j + 1;
      ProgressBar1.Max:= 100;
      ProgressBar1.Position:= j;
    end;
    //処理件数を表示
    Label1.Caption:='( '+IntToStr(j-1)+' %)';
    Sleep(25);
    Application.ProcessMessages;
  end;
  ProgressBar1.Visible:= False;
  StatusBar1.Panels[1].Text:= '終了!';
  ShowMessage('Done!');

end;

FormCreate手続きに処理を追加(赤文字の部分)。

procedure TForm1.FormCreate(Sender: TObject);
var
  w:integer;
begin

  //コードで設定
  //StyleをpsOwnerDrawに変更
  StatusBar1.Panels[0].Style:= psOwnerDraw;
  StatusBar1.Panels[0].Text:= 'Information:';

  //Panel[0]の幅の調整(表示する予定の文字列で初期化)
  w:= StatusBar1.Canvas.TextWidth('Information:');
  StatusBar1.Panels[0].Width:= w + 15;
  StatusBar1.Refresh;

  //StyleをpsOwnerDrawに変更
  StatusBar1.Panels[1].Style:= psOwnerDraw;
  StatusBar1.Panels[1].Text:= '最初の表示';

  //進行状況バーをステータスバーに配置する
  ProgressBar1.Parent:=StatusBar1;
  ProgressBar1.Visible:=False;

  //LabelをProgressBarの中心に表示
  Label1.Parent:=ProgressBar1;
  Label1.AutoSize:=False;
  Label1.Transparent:=True;
  Label1.Caption:='';
  Label1.Visible:=False;

end;

StatusBar1DrawPanel手続きの if Panel=StatusBar.Panels[1] then 部分にもProgressBarに関する処理を追加します(赤文字の部分)。

  if Panel=StatusBar.Panels[1] then
  begin

    //文字の表示(このPanel内のみ有効となるようだ)
    StatusBar1.Canvas.Font.Color:= clBlack;

    //背景色(このPanel内のみ有効となるようだ)
    StatusBar1.Canvas.Brush.Color:= clBtnFace;

    //矩形を取得
    ARect:= Rect;

    //表示位置(左寄せ)
    DrawText(StatusBar1.Canvas.Handle, PChar(Panel.Text), -1, ARect,
      DT_LEFT or DT_VCENTER or DT_SINGLELINE);

    //ProgressBarの位置を設定
    With ProgressBar1 do begin
      Top:=Rect.Top;
      Left:=Rect.Left;
      Width:=Rect.Right - Rect.Left - 15;
      Height:=Rect.Bottom - Rect.Top;
    end;

  end;

保存(Ctrl+S)して、実行(F9)。

進捗状況の表示に関する限り、「これがいちばんイイ」と私は思います。

以上がPanelを複数使用して、各々のPanelに対して文字色や背景色を設定する方法の解説です。

3.こんなこともできた! (その1)

他にも実現方法はたくさんあると思いますが、ユーザーに対して最初の1回だけは「とても重要な案内表示を行いたい」場合もあると思います。で、ユーザーが理解したことを(OKボタンを押すなどして)表明したら、もうその表示はプログラムが終了するまで出さない・・・みたいな処理をStatusBarを使って書いてみました。

ボタン3をクリックすると1回だけ案内を表示(OKするとプログラム終了まで表示されない)

FormにButtonを2つ追加します。ここで解説に使ったFormをそのまま使う場合は、新しく追加したボタンはButton3とButton4になります。Button4のNameは「btnOK」、Captionは「OK」に、Widthは「40」、Visibleは「False」に変更します。で、次のコードを記述します。

  private
    { Private 宣言 }
    boolInfo: boolean;

表示の制御に使用するフラグをグローバル変数として宣言しました。続けてButton3をクリックした時の処理を書きます。

procedure TForm1.Button3Click(Sender: TObject);
var
  w:integer;
begin
  if not boolInfo then
  begin
    //制御用フラグを既読扱いに設定
    boolInfo:=True;
    StatusBar1.Panels[1].Text:= '最初の1回だけは表示したい案内';
    //OKボタンを表示する位置を設定
    w:= StatusBar1.Canvas.TextWidth('最初の1回だけは表示したい案内');
    btnOK.SetBounds(
      StatusBar1.Panels[0].Width + w + 5, 0, 40, StatusBar1.ClientHeight);
    btnOK.Visible:=True;
    StatusBar1.Refresh;
  end;
end;

FormCreate手続きに追加する処理です。

procedure TForm1.FormCreate(Sender: TObject);
var
  w:integer;
begin

  ・・・ 省略 ・・・

  //案内表示の制御用フラグ
  boolInfo:= False;

  //OKボタン
  btnOK.Parent:= StatusBar1;
  btnOK.Visible:= False;

end;

StatusBar1DrawPanel手続きの中の if Panel=StatusBar.Panels[1] then の内容を次のように変更します。

  if Panel=StatusBar.Panels[1] then
  begin

    //文字の表示(このPanel内のみ有効となるようだ)
    if not boolInfo then
    begin
      StatusBar1.Canvas.Font.Color:= clBlack;
    end else begin
      StatusBar1.Canvas.Font.Color:= clBlue;
    end;

    //背景色(このPanel内のみ有効となるようだ)
    if not boolInfo then
    begin
      StatusBar1.Canvas.Brush.Color:= clBtnFace;
    end else begin
      StatusBar1.Canvas.Brush.Color:= clAqua;
    end;

    ・・・ 省略 ・・・

  end;

グローバル変数を使用するので、Button1、2が押された時の処理にも制御用変数の初期化を忘れずに追加します。

procedure TForm1.Button1Click(Sender: TObject);
begin
  //制御用フラグを初期化
  boolInfo:= False;
  //表示
  StatusBar1.Panels[1].Text:= '2番目の表示';
end;
procedure TForm1.Button2Click(Sender: TObject);
begin
  //制御用フラグを初期化
  boolInfo:= False;
  
  ・・・ 省略 ・・・
end;

最後に、ユーザーが表示された案内を読み、内容を理解したことを「OK」ボタンを押したというイベントから受け取ります。そのOKボタンの処理です。

procedure TForm1.btnOKClick(Sender: TObject);
begin
  StatusBar1.Canvas.Brush.Color:= clBtnFace;
  StatusBar1.Canvas.Font.Color:= clBlack;
  StatusBar1.Panels[1].Text:= '';
  btnOK.Visible:= False;
end;

これで他の動作モード(Button1や2をクリック)に切り替えない限り、1回OKをクリックすると、もうButton3を何度クリックしても案内は表示されなくなります。

以上、こんなこともできた!(その1) でした。

3.こんなこともできた(その2)

Panelを1つだけ用意して、次のように設定することで、文字列だけでなく、文字の色や背景色の変更も比較的簡単に実現できます。設定方法は次の通り。

StatusBarのSimplePanelは「False」に設定(デフォルト設定のまま)
StatusBarにPanelを1つ追加、StyleをpsOwnerDrawに設定

重要な設定のポイントは2つ。
(1)StatusBarのSimplePanelは「False」に設定(デフォルト設定のまま)
(2)追加したPanelのStyleはpsOwnerDrawに変更(デフォルト設定はpsText)
さらに、これをコードでも設定しておけば、見た目にもわかりやすいと思います。

procedure TForm1.FormCreate(Sender: TObject);
begin
  //SimplePanelの設定
  StatusBar1.SimplePanel:= False;
  //StyleをpsOwnerDrawに変更
  StatusBar1.Panels[0].Style:= psOwnerDraw;
end;

準備ができたら、次の2つの手続きを作成します。

procedure TForm1.Button1Click(Sender: TObject);
begin
  //描画内容の指定
  StatusBar1.SimpleText:='文字列の表示&文字色及び背景色の変更をSimpleTextで実行';
end;
procedure TForm1.StatusBar1DrawPanel(StatusBar: TStatusBar; Panel: TStatusPanel;
  const Rect: TRect);
begin
  if Panel = StatusBar.Panels[0] then
  begin
    //Fontの色
    StatusBar1.Canvas.Font.Color:=clBlue;
    //背景色
    StatusBar1.Canvas.Brush.Color:=clAqua;
    //描画コード
    StatusBar1.Canvas.TextOut(Rect.left, Rect.top, StatusBar1.SimpleText);
  end;
end;

保存(Crtl+S)して、実行(F9)。

文字だけでは気付いてもらえないかな?・・・みたいな時に使えます!

記述するコードも少なく、比較的簡単に「色の変更」を実装できる方法なのではないか・・・と思います。ただし、さらに色の変更を追加したい場合などには、注意が必要です。それは何かというと、「色の変更の設定はStatusBar1DrawPanel手続きの中で行う」ということです。以下のコードは動作しません。

// 注意:このコードは検証用です。正しく動作しません!
procedure TForm1.Button1Click(Sender: TObject);
begin
  //Fontの色
  StatusBar1.Canvas.Font.Color:=clBlue;
  //背景色
  StatusBar1.Canvas.Brush.Color:=clAqua;
  //描画内容の指定
  StatusBar1.SimpleText:='文字列の表示&文字色及び背景色の変更をSimpleTextで実行';
end;

procedure TForm1.StatusBar1DrawPanel(StatusBar: TStatusBar; Panel: TStatusPanel;
  const Rect: TRect);
begin
  if Panel = StatusBar.Panels[0] then
  begin
    //描画コード
    StatusBar1.Canvas.TextOut(Rect.left, Rect.top, StatusBar1.SimpleText);
    //この手続きの中で更新を実行してはいけない(検証用に追加、すべて誤りです)
    //StatusBar1.Refresh;
    //StatusBar1.Repaint;
    //StatusBar1.Update;
    //StatusBar1.Invalidate;
  end;
end;

【参考】Refreshメソッド

Refresh メソッドを呼び出すと,速やかにコントロールを再描画できます。Refresh は Repaint メソッドを呼び出します。Refresh と Repaint メソッドとは互換性があります。

https://docwiki.embarcadero.com/Libraries/Sydney/ja/Vcl.Controls.TControl.Refresh

保存(Crtl+S)して、実行(F9)。

テキストは表示されるが、文字色も背景色も変更されない

ですから、Buttonをもう一つ追加して、押したボタンによって表示される文字や背景の色を変更したい場合は、グローバル変数「例 ColorMode: boolean;」を準備して、Falseなら青、Trueなら赤にする等、さらなる工夫が必要です。以下、その場合の例です。

  private
    { Private 宣言 }
    ColorMode: boolean;

procedure TForm1.FormCreate(Sender: TObject);
begin
  //SimplePanelの設定
  StatusBar1.SimplePanel:= False;
  //StyleをpsOwnerDrawに変更
  StatusBar1.Panels[0].Style:= psOwnerDraw;
  //デフォルトの文字色は青
  ColorMode:= False;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  //色の設定はSimpleTextへの文字列の代入より先に行うこと!
  //理由:SimpleTextへの文字列の代入即DrawPanel手続きが実行されるため
  //色は青
  ColorMode:= False;
  //描画内容の指定
  StatusBar1.SimpleText:= '文字列の表示&文字色及び背景色の変更をSimpleTextで実行';
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
  //色は赤
  ColorMode:=True;
  //描画内容の指定
  StatusBar1.SimpleText:= 'SimpleTextで文字列の表示&文字色及び背景色の変更を実行';
end;

procedure TForm1.StatusBar1DrawPanel(StatusBar: TStatusBar; Panel: TStatusPanel;
  const Rect: TRect);
begin
  if Panel = StatusBar.Panels[0] then
  begin
    if not ColorMode then
    begin
      //Fontの色
      StatusBar1.Canvas.Font.Color:= clBlue;
      //背景色
      StatusBar1.Canvas.Brush.Color:= clAqua;
    end else begin
      //Fontの色
      StatusBar1.Canvas.Font.Color:= clRed;
      //背景色
      StatusBar1.Canvas.Brush.Color:= clWebViolet;
    end;
    //描画コード
    StatusBar1.Canvas.TextOut(Rect.left, Rect.top, StatusBar1.SimpleText);
  end;
end;

保存(Crtl+S)して、実行(F9)。

Button1をクリックしたところ
Button2をクリックしたところ

さらに複数の色を使いたい場合は、TRadioGroup等を利用して、そこで色をユーザーに選択してもらい、StatusBar1DrawPanel 手続きでは、if文ではなくcase文を利用し、その中で RadioGroup の ItemIndex を参照して色を変更する方法が考えられます。

TRadioGroupを利用する場合は、StatusBar1.Refreshも実行する必要があるようです(実行するタイミングも重要!-> StatusBar1DrawPanel 手続きではなく、ButtonClickの中で行う。Refreshを実行する場所を間違えると、これがエンエンと呼び出され続け、画面の描画が非常に重たくなり、通常1つしか選択できないはずのオプションボタンが同時に複数選択状態で表示されるなど、TRadioGroupの挙動もおかしくなります)。

以下に、TRadioGroupを利用する場合の例を示します。

FormにRadioGroupを2つ、Buttonを1つ追加します

RadioGroup1のCaptionは「文字色を選択」、Columnsは「1」、Itemsは「黒、青、赤」、RadioGroup2のCaptionは「背景色を選択」、Columnsは「1」、Itemsは「clBtnFace、clAqua、clWebViolet」をそれぞれ指定します。

新しく追加したButton3をクリックした時のコードです。

procedure TForm1.Button3Click(Sender: TObject);
begin
  //描画内容の指定
  StatusBar1.SimpleText:='文字列の表示&文字色及び背景色の変更を実行';
  //更新
  StatusBar1.Refresh;
end;

StatusBar1DrawPanel手続きのコードは、次のようになります。

procedure TForm1.StatusBar1DrawPanel(StatusBar: TStatusBar; Panel: TStatusPanel;
  const Rect: TRect);
begin
  if Panel = StatusBar.Panels[0] then
  begin

    { //コメント化 -> 前に使った Button1 も Button2 もクリックしない前提です
    if not ColorMode then
    begin
      //Fontの色
      StatusBar1.Canvas.Font.Color:=clBlue;
      //背景色
      StatusBar1.Canvas.Brush.Color:=clAqua;
    end else begin
      //Fontの色
      StatusBar1.Canvas.Font.Color:=clRed;
      //背景色
      StatusBar1.Canvas.Brush.Color:=clWebViolet;
    end;
    }

    //文字色
    case RadioGroup1.ItemIndex of
      0:StatusBar1.Canvas.Font.Color:= clBlack;
      1:StatusBar1.Canvas.Font.Color:= clBlue;
      2:StatusBar1.Canvas.Font.Color:= clRed;
    end;

    //背景色
    case RadioGroup2.ItemIndex of
      0:StatusBar1.Canvas.Brush.Color:= clBtnFace;
      1:StatusBar1.Canvas.Brush.Color:= clAqua;
      2:StatusBar1.Canvas.Brush.Color:= clWebViolet;
    end;

    //描画コード
    StatusBar1.Canvas.TextOut(Rect.left, Rect.top, StatusBar1.SimpleText);

    //更新(ここに書いてはいけない)
    //StatusBar1.Refresh;
    //StatusBar1.Repaint;
    //StatusBar1.Update;
    //StatusBar1.Invalidate;

  end;
end;

保存(Crtl+S)して、実行(F9)。

文字色は「黒」、背景色は「clBtnFace」を指定
文字色は「青」、背景色は「clAqua」を指定
文字色は「赤」、背景色は「clWebViolet」を指定
文字色は「黒」、背景色は「clAqua」を指定

StatusBarのPanelを利用して、そのStyleプロパティを「psOwnerDraw」に設定し、描画を自前で行う場合には広範囲に(コードの実行順等まで含む)様々な注意が必要です。
以上が「こんなこともできた!(その2)」の内容です。

4.まとめ

StatusBarの使い方はSimplePanel:= True; として、SimpleTextに指定した文字列を表示するというカンタンな方法と、デフォルト設定のSimplePanel:= False; のまま、Panelsプロパティで複数個のPanelを設定して、描画はStatusBar1DrawPanel手続きに自分で記述する方法の2種類があり、文字列の表示と色の変更だけならPanelを1個用紙して、SimpleTextを利用する、中間的な方法で「文字列の表示」と「表示色の変更」の両方を実装できる。

さらに複数の色を使いたい場合はRadioGroup等を利用して設定できるが、この場合は描画をRefreshする必要がある。StatusBar1.Refreshは、StatusBar1DrawPanel 手続きの中で呼び出してはいけない。

5.お願いとお断り

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

Link Image click position with Grid control

「画像のクリック位置とグリッドコントロールの連動」

画像を表示したTImageの任意の1点をクリックしたら、そのY座標に応じてStringGridコントロールの適切なセルを選択するようなプログラムが書けないかなー。・・・と思って、実際に書いてみたら、あまりにも簡単に実現できてしまったお話。

1.やりたかったこと
2.作成したプログラム
3.まとめ
4.お願いとお断り

1.やりたかったこと

次のようなGUIのある採点プログラムを作成した。プログラムはスキャナーで読み込んだ複数枚の答案画像から、設問ごとに解答欄をかき集めて表示するもの(答案画像への書き戻しも可能)。で、採点作業をより一層効率的に行うには、各々の解答欄画像をクリックした際、自動的に採点結果を入力するGridコントロールのセルが選択されたらイイなーと思ったことが、この連動プログラムを書こうとしたきっかけです。

画像をクリックした時に得られるY座標を元に、Gridコントロールの最適なセルを選択したい!

2.作成したプログラム

まず、思ったことはTImageのOnMouseDownイベントを利用すればイイ(画像上でクリックした位置のX, Y座標が取得できる)ということです。早速、imgAnswerという名前をつけたTImageをクリックして選択し、OnMouseDownイベントの右側をダブルクリックして、imgAnswerMouseDown手続きを作成。

次のようなコードを書いてみました!

procedure TFormCollaboration.imgAnswerMouseDown(Sender: TObject;
  Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
  //imgAnswerの高さは、解答欄1つの高さ × 解答用紙数 だから
  //クリックした場所の(Y座標 div 解答欄1つの高さ) + 1 が解答用紙の番号になるはず
  OutPutDebugString(PChar(IntToStr((Y div 解答欄1つの高さ) + 1)));   
end;

実行(F9)して、確認。

No,5の解答欄をクリックすると、確かに「5」と表示されている! やった☆

必要と思われる変数 i, j を宣言して・・・
あとは Gridコントロールへ SetFocus するコードを書くだけ!

procedure TFormCollaboration.imgAnswerMouseDown(Sender: TObject;
  Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
var
  i,j:integer;
begin
  //imgAnswerの高さは、解答欄1つの高さ × 解答用紙数 だから
  //クリックした場所の(Y座標 div 解答欄1つの高さ) + 1 が解答用紙の番号になるはず
  OutPutDebugString(PChar(IntToStr((Y div 解答欄1つの高さ) + 1)));
  i:= (Y div 解答欄1つの高さ) + 1;
  //SetFocus
  j:= StrToInt(現在選択している解答欄の番号);
  StringGrid1.Col:= j;
  StringGrid1.Row:= i;
  StringGrid1.SetFocus;   
end;

上のコードでは、わかりやすさのため変数(記号)ではなく、そのかわりに「解答欄1つの高さ」等のように説明の文字列で記述しています。また、採点ミスを防止する観点から、Gridコントロールに表示する入力可能な列は、現在選択(=画像として表示)している解答欄に対応する1列のみとしています。

これで理想としていたカタチの採点プログラムに一歩近づけたように思います。ユーザーに案内したい採点方法は、サっと見て全員が良く出来ている設問であれば、最初に全員正解として得点を一括自動入力し、誤りの解答のみ、解答欄画像を見ながらチェックしてクリック、自動選択された採点欄のセルにゼロ(得点)を入力する方法です。

3.まとめ

案ずるよりナントカといいますが、ほんとうにその通りで、内心、ずっとできるかなー? なんて思っていたことが、案外、基本的な知識だけで簡単に実現できてしまいました!

//Integer 型で、割り算をしたいときは、整数除算の演算を行う div を使う
A div B  //A ÷ B の商が取得できる

あとは、やりたいことを実現するアルゴリズムを考えるだけですね!
プログラミングしていて、いちばんしあわせで、楽しい時間を過ごせました☆

【注意!】
画像の表示倍率の変更が伴う場合には、表示倍率に合わせてプログラムの変更が必要
です。完璧には動作しませんが、ある程度使える(と思われる)表示倍率の変更に対応するコードは次の通りです(クリック位置が画像の下へ行くほど、誤差が大きくなります)。

procedure TFormCollaboration.imgAnswerMouseDown(Sender: TObject;
  Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
var
  i,j,w:integer;
  k:double;
begin
  //imgAnswerの高さは、解答欄1つの高さ × 解答用紙数 だから
  //クリックした場所の(Y座標 div 解答欄1つの高さ) + 1 が解答用紙の番号になるはず
  OutPutDebugString(PChar(IntToStr((Y div 解答欄1つの高さ) + 1)));
  i:= (Y div 解答欄1つの高さ) + 1;
  //表示倍率が100%の時
  if Edit1.Text='100' then
  begin
    i:=(Y div 解答欄1つの高さ)+1;  //このアルゴリズムは記録として残したい
  end else begin
    //表示倍率が100%ではない時
    k:= 解答欄1つの高さ * (StrToFloat(Edit1.Text)/100);
    i:=Ceil(Float(Y)/k);  // <- とりあえずこれで使えるカモ?
  end;

4.お願いとお断り

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

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

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

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

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

1.最初に結論

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

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

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

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

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

TImage

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

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

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

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

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

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

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

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

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

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

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

なのに、TImageの幅は・・・

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

end;

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

画像が表示される

続けて、button2をクリック。

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

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

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

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

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

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

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

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

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

予期した通りゼロ

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

ShowMessage(GPbmp.GetWidth.ToString);

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

3.まとめ

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

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

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

4.お願いとお断り

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

When The Cancel Button is Pressed

「キャンセルボタンが押された時は?」

JpegファイルをTImageに読み込み、プリンターのプロパティを設定して印刷するとき、設定画面にある「OKボタン」ではなく、「キャンセルボタン」が「押された時」の対応方法がわからずハマった話。

1.つくった練習プログラム
2.キャンセルボタンが押されたら・・・?
3.TOpenDialogなら・・・
4.解決方法を知る
5.まとめ
6.お願いとお断り

1.つくった練習プログラム

Formに最低限必要なVCLコンポーネントを以下のように配置。
(右上にある「Printerを選択」のVCLはTLabel(のCaption)。これはなくてもOK。)

Formの幅は810、高さは480に設定

GUIを作成後、Mr.XRAYさんのWebサイトにあった情報を参照して、プリンタのプロパティのダイアログを表示して印刷設定する方法で、練習プログラムを書いてみた。Mr.XRAYさんのWebサイトにはいつも本当にお世話になっています。詳細かつ丁寧なご説明に心より感謝申し上げます。

013_プリンタのプロパティ設定

http://mrxray.on.coocan.jp/Delphi/plSamples/013_SetPrinterProperty.htm#04

グローバル変数の設定は、{ Private 宣言 } に以下の通り記述。

type
  TForm1 = class(TForm)
    Image1: TImage;
    ListBox1: TListBox;
    ComboBox1: TComboBox;
    Button1: TButton;
    Label1: TLabel;
    CheckBox1: TCheckBox;
    procedure FormCreate(Sender: TObject);
    procedure ComboBox1Select(Sender: TObject);
    procedure Button1Click(Sender: TObject);
  private
    { Private 宣言 }
    ADevice     : array[0..MAX_PATH - 1] of Char;
    ADriver     : array[0..MAX_PATH - 1] of Char;
    APort       : array[0..MAX_PATH - 1] of Char;
    ADeviceMode : THandle;
  public
    { Public 宣言 }
  end;

「プリンタのプロパティ」として表示したいダイアログは、TPrintDialog ダイアログ or TPrinterSetupDialog ダイアログを表示した際に、その画面にある「プロパティ」ボタンをクリックすると表示されるダイアログ。(自分的には、これが「かゆいところに手が届く」いちばんイイ!ダイアログじゃないかなー みたいな気がして・・・)

もっと正直な、ほんとうのところを言うと、コントロールパネル → ハードウェアとサウンドの「デバイスとプリンターの表示」で表示されるプリンタを右クリックして出てくるサブメニューにある「プリンターのプロパティ」をクリックして表示される「詳細設定」タブのある「(プリンター名)のプロパティ」の画面を出したかったんだけど、コレを出す方法がわからなかった。
(もちろん、今でもわかりません。わかる方、Web上のどこか、いつまでも、ずっと残る場所に情報を書いていただけるとすごくうれしいです!)

Printerの選択肢を表示する部分は・・・

procedure TForm1.FormCreate(Sender: TObject);
//プリンタのリストを取得してComboBox1にセット
begin
  ComboBox1.Items.Clear;
  ComboBox1.Items.Assign(Printer.Printers);
  ComboBox1.ItemIndex := Printer.PrinterIndex;
  //プリンタを初期化
  ComboBox1Select(nil);  //このprocedureはまだ作ってないから当然エラーになる!
end;

エラーは無視。そのままForm上のComboBox1をクリックして、表示されるオブジェクトインスペクタの OnSelect の右の空欄をダブルクリックして表示される新しいプロシージャに以下の内容を記述。

procedure TForm1.ComboBox1Select(Sender: TObject);
begin
  //選択したプリンタをアクティブなプリンタに設定
  Printer.PrinterIndex := ComboBox1.ItemIndex;
  //ADeviceModeには変更前のプリンタの情報が格納されている
  //その他の値は現在(変更後)のプリンタの情報
  Printer.GetPrinter(ADevice, ADriver, APort, ADeviceMode);
  //ADeviceMode初期化
  Printer.SetPrinter(ADevice, ADriver, APort, 0);
  //ADeviceModeが新しいプリンタドライバの値になる
  Printer.GetPrinter(ADevice, ADriver, APort, ADeviceMode);
end;

プログラムを上書き保存(ComboBox1Select(nil);のエラーはここで解消される)。

で、最初に書いた(キャンセルボタンが押された場合の処理がない)印刷部分のプログラムは以下の通り(usesへの追加内容とButton1Click)。
EXEのある場所にDataフォルダを作成しておき、そこに印刷したいJpegファイルを入れておく。プログラムはこのJpegファイル1枚1枚へのPathをListBox1にセットして、セットされたItemの数だけ印刷Loopをまわすという内容。
なお、Jpegファイルの読み込みはGDI+を使用している。

implementation

uses
  Winapi.GDIPAPI, Winapi.GDIPOBJ, Winapi.GDIPUTIL,
  System.IOUtils, System.UITypes,
  Printers, Winapi.WinSpool,
  System.Types, Vcl.FileCtrl, System.StrUtils, System.Masks,
  Winapi.CommDlg;

  //Vcl.Imaging.jpeg, Winapi.MMSystem, Vcl.axCtrls,は削除した
  //System.UITypesはMessageDlgを使用するために追加
  //Printersは印刷を実行するために追加
  //Winapi.WinSpoolはPrinterPropertyを表示して印刷するために追加
  //System.IOUtils, System.Types, Vcl.FileCtrl, System.StrUtils, System.Masksは
  //ファイル名取得用の関数を使うために必要
  //Winapi.CommDlgはTPrintDlgの表示に必要

{$R *.dfm}
procedure TForm1.Button1Click(Sender: TObject);
var
  //PrinterプロパティのDialogを表示
  DevMode  : PDeviceMode;
  Mode     : Cardinal;
  hPrinter : THandle;
  //矩形
  rect:TRect;
  //印刷関連
  j:integer;
  //for フォルダの選択
  iStartFolder: string;
  iDirectories: TArray<string>;
  FileNames:TStringDynArray;  // -> ローカル変数でなければならない
  //TStringDynArrayの使用にはusesにTypesが必要
  strFileName, strSavePath:string;
  //for 画像読み込み
  graphics:TGPGraphics;
  bmp:TGPBitmap;

  //拡張子の異なるファイルをそれぞれ検索
  function MyGetFiles(const Path, Masks: string): TStringDynArray;
  var
    MaskArray: TStringDynArray;
    Predicate: TDirectory.TFilterPredicate;
  begin
    MaskArray := SplitString(Masks, ';');
    Predicate :=
      function(const Path: string; const SearchRec: TSearchRec): Boolean
      var
        Mask: string;
      begin
        for Mask in MaskArray do
          if MatchesMask(SearchRec.Name, Mask) then
            exit(True);
        exit(False);
      end;
    Result := TDirectory.GetFiles(Path, Predicate);
  end;

begin

  //プロパティの設定Dialogの表示がチェックされていたら実行
  //これを設定しない場合は、デフォルトの設定で印刷される(A4・縦とか)
  if CheckBox1.Checked then
  begin

    //FDeviceModeのメモリをロックしDEVMODE構造体のポインタを取得
    DevMode:=GlobalLock(ADeviceMode);

    try

      //対象のプリンタのハンドルを取得
      if Winapi.WinSpool.OpenPrinter(ADevice, hPrinter, nil) then begin
        try
          //このLModeの値によって,ダイアログ表示かプロパティの設定かが決まる
          Mode:=DM_IN_PROMPT or DM_OUT_BUFFER or DM_IN_BUFFER;
          //必要な場合は、ここで DevModeを修正する
          //例:A4・横の設定にしてからダイアログを表示する
          with DevMode^ do begin
            dmPaperSize:=DMPAPER_A4;  //A4
            dmOrientation:=DMORIENT_LANDSCAPE;  //横
          end;
          //プリンターのプロパティ設定Dialogを表示
          //キャンセルボタンが押された場合の処理なし(これを何とかしたい
          DocumentProperties(Handle, hPrinter, ADevice, DevMode^, DevMode^, Mode);
        finally
          //閉じる
          ClosePrinter(hPrinter);
        end;
      end;
    finally
      //ロック解除
      GlobalUnlock(ADeviceMode);
    end;

  end;

  //設定を反映させる
  Printer.SetPrinter(ADevice, ADriver, APort, ADeviceMode);

  //印刷実行
  //表示に設定
  Image1.Visible:=True;
  //ListBoxも初期化
  ListBox1.Items.Clear;
  //表示位置を設定
  Image1.Left:=10;
  Image1.Top:=10;

  //フォルダの選択
  MessageDlg('OKをクリックするとフォルダ選択ダイアログが表示されます。'+#13#10+#13#10+
      '採点したいデータのあるフォルダを選択してください。', mtInformation, [mbOk] , 0);

  iStartFolder:=ExtractFilePath(Application.ExeName);
  if SelectDirectory(iStartFolder, iDirectories,
    [sdHidePinnedPlaces, sdNoDereferenceLinks, sdForceShowHidden,
    sdAllowMultiselect], 'フォルダを選択してください', 'Folder', 'Ok') then
  begin

    //開いた時のPathを採点結果の保存先Pathとして取得しておく
    strSavePath:=iDirectories[0];

    //拡張子が'jpg'のファイルの名前を取得してListBoxへ格納
    FileNames:=MyGetFiles(iDirectories[0], '*.jpg');
    for strFileName in FileNames do
    begin
      ListBox1.Items.Add(strFileName);
    end;

    //答案の枚数分Loopする
    for j := 0 to ListBox1.Items.Count-1 do
    begin

      //画像の初期化
      Image1.Picture:=nil;

      //GDI+で読む
      bmp:=TGPBitmap.Create(ListBox1.Items[j]);
      Image1.Picture.Bitmap.Width:=bmp.GetWidth;
      Image1.Picture.Bitmap.Height:=bmp.GetHeight;
      Image1.Picture.Bitmap.PixelFormat:=pf24bit;
      graphics:=TGPGraphics.Create(Image1.Picture.Bitmap.Canvas.Handle);

      try
        Graphics.DrawImage(bmp, 0, 0, bmp.GetWidth, bmp.GetHeight);
        Image1.Align:=alNone;
        Image1.Center:=True;  //Imageの中央に表示
        Image1.AutoSize:=False; //サイズは固定しない
        //StretchとProportionalをセットで指定して、伸縮と縦横比維持を両立
        Image1.Stretch:=False; //Image枠内収まるよう画像の大きさを変更しない
        Image1.Proportional:=True;  //画像の縦横比を維持
      finally
        bmp.Free;
        graphics.Free;
      end;

      //処理の表示を止めないおまじない
      Application.ProcessMessages;

      //印刷実行
      //if MessageDlg('印刷しますか?',mtConfirmation,[mbYes,mbNo],0) = mrYes then
      //begin
        //ページを印刷する
        with Printer do
        begin
          {
          if j=0 then
          begin
            BeginDoc;
          end else begin
            NewPage;
          end;
          }
          BeginDoc;
          //大きさを指定
          rect.Top    := 0;
          rect.Left   := 0;
          rect.Bottom := Trunc(( PageWidth / Image1.Picture.Width) * Image1.Picture.Height);
          rect.Right  := PageWidth;
          //ファイルを描画
          Printer.Canvas.StretchDraw(rect, Image1.Picture.Graphic);
          {
          if j=ListBox1.Items.Count-1 then
          begin
            EndDoc;
          end;
          }
          EndDoc;
        end;
      //end;
    end;
  end;

end;

【20220830 追記】
印刷部分の動作検証は、普段愛用している印刷ユーティリティソフトのFinePrintで行いました(FinePrintは、株式会社NSDさんが販売している多機能プリンタドライバで、ページを縮小してのまとめ印刷等が簡単に行える非常に使いやすいソフトです)。FinePrintに出力している分については、上記コードで全ページがきちんと出力されるのですが、実際に業務で使用しているプリンタを指定して出力すると、ページが「抜ける」大変困った現象が発生することを確認しました。そこで、まとめて印刷せず、1枚毎にプリンタへ出力するようコードを修正してあります。

【20220901追記】
次のWebサイトに紹介されている方法を、上記印刷プログラムに対して試したところ、印刷の「抜け」が発生しなくなりました。貴重な情報をご提供くださいました中村 様、本当にありがとうございました。

ビットマップがプリンタに印刷できない

http://tknakamuri.web.fc2.com/tips004.htm

最後の印刷部分は、メタファイルに流し込む等は行わず、TImageのグラフィックをそのままプリンターのCanvasに大きさを合わせて描画している。

この方法は、次のWebサイトで紹介されていたものを、複数枚印刷用にコードを変更して利用させていただきました。ありがとうございました!

Delphi/400 Tips プログラムからの印刷 の 画像の印刷

https://www.migaro.co.jp/contents/products/delphi400/tips/introduction/4_19/01/02.html

プリンターのプロパティ設定を行わずに印刷すると(当たり前だが)、プリンタのデフォルト設定がそのまま適用される。

プリンターのプロパティ設定を行って印刷すると(当たり前だが)、変更した設定が適用されて印刷が実行される。

何の問題もない。これで出来上がり。OK!・・・でも、よかったんだけど、

2.キャンセルボタンが押されたら・・・?

ふと気になって、プリンターのプロパティ設定画面にある「キャンセルボタン」をクリックしてみた。

設定Dialog画面は確かにキャンセルされるんだけど・・・
プログラムがそのまま先へ進んでしまう・・・。
(気持ち的にはキャンセルした時点でExitして欲しいところだ)

んじゃ、キャンセルボタンが押されたらExitすればイイ。
で、その処理はどう書くの?

えぇっ ボク、知らないよー (T_T)
ここで、またハマりました!!

3.TOpenDialogなら・・・

よく使うTOpenDialogなら、こうするのが定番(・・・だと思う)

begin
  if OpenDialog1.Execute then
  begin
    ShowMessage(OpenDialog1.FileName);
  end else begin
    ShowMessage('キャンセル');
  end;
end;

でも、DocumentProperties( )には .Execute はありませんでした。これで、さらに(T_T)

×:DocumentProperties(Handle, hPrinter, ADevice, DevMode^, DevMode^, Mode).Execute

なんだか、面白いコトになってキタ!

4.解決方法を知る

Google先生しか頼りになるヒトはいませんから、さっそくカタカタきいてみます。
(実際には、最終的な解決に至るまでには、かなりの時間を費やしたのですが・・・)

Delphi DocumentProperties関数 ・・・というキーワードでお伺いをたてると、上から3番目に次の情報がHit!

Googleの検索結果から引用

この解説の下の方に、以下の記事がありました☆

関数がプロパティ シートを表示する場合、戻り値は、ユーザーが選択するボタンに応じて IDOK または IDCANCEL になります。

https://docs.microsoft.com/ja-jp/windows/win32/printdocs/documentproperties

あったー!!

さっそくこの情報を元にプログラムを書き換えます。

//DocumentProperties(Handle, hPrinter, ADevice, DevMode^, DevMode^, Mode);

//キャンセルボタンに対応
if DocumentProperties(
  Handle, hPrinter, ADevice, DevMode^, DevMode^, Mode) = IDCANCEL then Exit;

あっけないくらい、カンタンに解決☆
(やったー! コレでまたBlog書けるー!! ・・・みたいな気も)

5.まとめ

DocumentProperties関数は、キャンセルボタンが押されるとIDCANCELが戻り値として返る。これを利用してExitするには・・・

//キャンセルボタンに対応
if DocumentProperties( 略 ) = IDCANCEL then Exit;

6.お願いとお断り

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

Bitmap Conversion

「ビットマップ変換」

TImageに画像を読み込んで、変更を加え、その画像をGDI+で高速に保存しようとした。
しかし、VCLのTBitmapをGDI+のBitmapへ変換する方法がわからず、半日以上試行錯誤。これはその時の記録。

0.Bitmapを変換したい理由
1.わかったこと
2.まとめ
3.お願いとお断り

0.Bitmapを変換したい理由

GDI+を使うようになる前は、次のようなコードでJpegファイルを保存していた。

var
  Jpeg: TJPEGImage;
  S:string;
begin
  S := ChangeFileExt(画像ファイルへのPath, '.jpg');
  Jpeg := TJPEGImage.Create;
  try
    Jpeg.Assign(Image1.Picture.Bitmap);
    Jpeg.Compress;
    Jpeg.SaveToFile(S);
  finally
    Screen.Cursor := crDefault;
    Jpeg.Free;
  end;
end;

1枚とか、ごくわずかな枚数の画像を保存するだけなら、この方法でも特に問題はないんだが、何十枚もの画像を頻繁に処理することを繰り返すような場合には、やはりGDI+を使って高速に処理したくなる。例えば、次のような場合だ。

マークシートと手書きの解答がセットになった解答用紙をスキャナーで読み取り、その手書きの解答欄を矩形選択して切り取り、(1)なら(1)だけ!以下の画像のように人数分集めて表示 & 採点するプログラムを作りたいとき(実際に今、作っている)。

矩形で切り抜いて表示、採点までは無事にプログラミングできた☆

手書き答案をPCと協働で採点するプロジェクト

ハマったのは、ココからだ。採点したら、各々の解答欄を、解答用紙の元の場所に戻したい。矩形で切り取って集めて1枚の画像に仕立てたアルゴリズムを再利用し、読み込み元Rectと書き込み先Rect、及び読み取り元Imageと書き込み先Imageをそれぞれ逆にすれば、各々の解答欄は「ばっちり元の場所に戻る」ハズだ。・・・と考え、実際にその処理を書いてみた。

ところが赤い丸印(採点済みのマーク)を付加して変更を加えたTImage(VCL)のTBitmapをGDI+のBitmapへ変換する方法がどぉーしてもわからない。走召!困った。。。

        //GDI+で元の答案画像を読み込んでおく
        bmp:=TGPBitmap.Create(答案の画像ファイル);
        Image1.Picture.Bitmap.Width:=bmp.GetWidth;
        Image1.Picture.Bitmap.Height:=bmp.GetHeight;
        Image1.Picture.Bitmap.PixelFormat:=pf24bit;
        graphics:=TGPGraphics.Create(Image1.Picture.Bitmap.Canvas.Handle);

        try  
          //イメージを表示
          graphics.DrawImage(bmp, 0, 0, bmp.GetWidth, bmp.GetHeight);

            ・・・ 採点結果の書き戻し準備処理 ・・・

          //採点した解答欄の矩形を元の答案画像へ書き戻し(これは成功する)
          Image1.Canvas.CopyRect(DestRect, Image2.canvas, SrcRect);
        finally
          bmp.Free;
          graphics.Free;
        end;

        //書き戻した答案画像はできてるけど、まだファイルになってない!
        //・・・ってか、それをファイルにする処理をここに書きたい・・・んだけど
        bmp:=TGPBitmap.Create(ココの書き方がわからない);
        //Graphics:=TGPGraphics.Create(Image1.Picture.Bitmap.Canvas.Handle);
        Graphics:=TGPGraphics.Create(Image1.Canvas.Handle);
        try
          Graphics.DrawImage(bmp,0,0);
          ・・・

上のコードで、TGPBitmap.Create( )の( )の中にナニを入れたらいいのか、この部分がどうしてもわからず、かなり苦しんだ。仕方がないからGDI+での保存はいったんあきらめて、Jpeg.Assign(Image1.Picture.Bitmap)の方法を試してみることにする。すると、思っていた通り、たった10枚の答案でも とぉーっても 遅いのだ。

数十枚(最大100枚程度を想定している)の答案の解答欄一つひとつについて、採点後に元の答案画像への書き戻しを行うことを考えると、Jpeg.SaveToFileメソッドはどうしても使う気になれない。保存方法は何がナンでも高速なGDI+を使いたい。

長くなったけど、これがビットマップを変換したい理由なのです。

1.わかったこと

いろいろ調べたり、思い出したりして、試行錯誤。

Image1.Picture.SaveToFile(画像の保存先Path);

GDI+とは何の関係もないけれど、上のコードも試したが、これはエラーに。

bmp:=TGPBitmap.Create(ココの書き方がわからない);
                  ↓
bmp:=TGPBitmap.Create(画像の保存先Pathを入れてみた);

エラーにはならないけど、変更したImageは当然保存されない。
(あたりまえです。元画像を読み込んで、そのまま上書きコピーしてるだけだもん。)

時間だけがゆっくりと過ぎて行く。
このプログラムの完成を待ってる人もいないけど、教えてくれる人もいない。

Google先生も調子悪そうだ。
何を尋ねても、わかってることしか表示してくれない。

僕は、DelphiのIDEではなく、NanaTreeの画面を「ぼー」っと見つめていた。

NanaTreeは、オープンソースのフリーソフト(階層化テキストエディター)で、僕はこのNanaTreeに、これまでに調べたことや、学んだことを備忘録として記録、資料化している。10年以上前から書き溜めてきた、お金では買えない、僕のたからものだ。

僕とDelphi(Object Pascal)の、これまでのすべてが、ここに詰まっている・・・。

ふと、下から3番目のノードが目に留まった。

VCL TBitmap から

なんだって、その先は? あわててノードをクリックする。
すると、目に飛び込んできたのは・・・

VCL TBitmap から GDI+ Bitmap へ

そのものズバリがあったー!!

「灯台元暗し」とはまさにこのこと。

オレ、調べてたんだ・・・。これまで使ったことがなかっただけで・・・。
少し前の記事になりますが、NanaTreeに記録した資料の元の記事がこちらです。

めもニャンだむ

junkiの日常雑感・日記・プログラミングのランダムメモ

VCL TBitmap から GDI+ Bitmap への転送を試した。

TGPBitmap クラスのコンストラクタのうち
constructor Create(stream:IStream;..);reintroduce;overload;
を使う。

まず、VCL TBitmap オブジェクトの内容を SaveToStream() によって、TMemoryStream にセーブする。次に、TStreamAdapter クラスのインスタンスを作成して、TMemoryStream オブジェクトを IStream に変換し、上記のコンストラクタで作成する。

URL:http://blog.livedoor.jp/junki560/archives/21910595.htmlより引用

junkiさん、ほんとうにありがとうございます。
TMemoryStreamを介してVCLビットマップのデータをGDI+に渡せるのですね!

さっそく書いたのが次のコード。

implementation

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

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

{$R *.dfm}

var
  ・・・
  //VCL TBitmapからGDI+ Bitmapへ変換
  Graphics:TGPGraphics;
  srcBMP:TBitmap;
  dstBMP:TGPBitmap;
  stream:TMemoryStream;
  //拡張子を取得する
  dotExt, strExt:string;
  //GetEncoderClsid関数の利用とTGUIDを使用するには、usesにWinapi.GDIPUTILが必要
  ImgGUID:TGUID;
begin

  //解答欄の矩形を元の答案画像へ書き戻し(内容が変更されたImageができる)
  Image1.Canvas.CopyRect(DestRect, Image2.canvas, SrcRect);

  //保存(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));
  //引数の指定をImage1.Picture.Bitmap.Canvas.Handleに変更
  //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(画像の保存先Path));
    //JPEG & TIFFに対応する
    if dotExt='.jpg' then begin
      strExt:='jpeg';
    end else begin
      if dotExt='.tif' then begin
        strExt:='tiff';
      end else begin
        strExt:=StringReplace(dotExt,'.','',[rfReplaceAll, rfIgnoreCase]);
      end;
    end;
    //指定された拡張子を付けて保存
    if GetEncoderClsid('image/'+strExt, ImgGUID) >= 0 then
    begin
      //20220930訂正 bmp -> dstbmp
      //bmp.Save(ChangeFileExt(画像の保存先Path, dotExt), ImgGUID);
      dstbmp.Save(ChangeFileExt(画像の保存先Path, dotExt), ImgGUID);
    end;
  finally
    Graphics.Free;
    srcbmp.Free;
    dstBMP.Free;
    stream.Free;
  end;

end;

ちなみに uses は・・・(参考まで)

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
  System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs,
  Vcl.ExtCtrls, Vcl.StdCtrls, Vcl.Imaging.Jpeg, Vcl.Grids, Vcl.ComCtrls,
  System.IOUtils, System.Types, Winapi.ShellAPI, System.UITypes,
  System.IniFiles, System.Masks, Vcl.Filectrl, System.StrUtils,
  Ipl, OpenCV, Winapi.GDIPAPI, Winapi.GDIPOBJ, Winapi.GDIPUTIL,
  System.Win.ComObj, PythonEngine, Vcl.PythonGUIInputOutput, System.ImageList,
  Vcl.ImgList;

  //GDIPAPI, GDIPOBJ はGDI+を利用した描画に資料するために必要
  //usesにWinapi.GDIPUTILを宣言すればGetEncoderClsid関数を利用してGUIDを取得できる
  //System.Win.ComObjはExcelのxlsmファイルの読み書きに必要

TMemoryStreamを使えば、間に「ファイル」というかたちを入れないでBMPデータをGDI+のBitmapに渡すことができるなんて知りませんでした。これで扱う画像の数が何十枚あっても超高速!に保存処理を実行できます。

GDI+ の bitmap クラス(TGPBitmap)と graphics クラスを使って、画像ファイルから VCL の TBitmap へイメージを読み込むことはもうできてるから、これで画像ファイルからGDI+を使って高速にデータを読み取り、VCL Imageに表示し、必要な変更を加えたりした後、VCL TBitmapからGDI+のTGPBitmapにデータを移して高速に保存処理を行う、GDI+の利点を最大限に活用したプログラムが書けるようになった。

2.まとめ

(1)VCLのTBitmapのデータをまずTMemoryStreamに保存する。

var
  //VCL TBitmapからGDI+ Bitmapへ変換
  srcBMP:TBitmap;
  dstBMP:TGPBitmap;
  stream:TMemoryStream;
begin
  //保存(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);
  ・・・
end;

(2)GDI+のTGPBitmapに入れるには TStreamAdapter.Create(stream) を使う。

  //GDI+のBMPを生成
  dstbmp:=TGPBitmap.Create(TStreamAdapter.Create(stream));
  //Graphics:=TGPGraphics.Create(Image1.Picture.Bitmap.Canvas.Handle);
  Graphics:=TGPGraphics.Create(Image1.Canvas.Handle);
  try
    Graphics.DrawImage(dstbmp,0,0);

(3)最後に使用したBitmapとMemoryStreamのオブジェクトを解放(Free)する。

  finally
    Graphics.Free;
    srcbmp.Free;
    dstBMP.Free;
    stream.Free;
  end;

3.お願いとお断り

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

Disable left and right arrow keys on StringGrid

「StringGrid上で左右の矢印キーを無効化」

Enterキーでのカーソル移動と違って、矢印キーの制御に大苦労(私だけカモだが)。
これが正しい方法かどうか、わからないが結果としてタイトルにある通り、
StringGrid上で左右の矢印キーの無効化に成功。これはその覚書。

0.左右の矢印キーを無効化したい理由
1.無効化する前に行った設定
2.無効化にチャレンジ
3.まとめ
4.お願いとお断り

0.左右の矢印キーを無効化したい理由

例えば、次のようなGUIで、テストの解答用紙をスキャンした画像から解答欄(1)のみをトリミングして採点したい場合、採点用のGridコントロールの列数は2列として、1列目に解答用紙の番号、2列目に採点結果を入力する仕様とすれば、最も使いやすいのではないかと考え、これを実現するために、Gridコントロールの現在必要ない列を「非表示」にする設定を行った。

画面右側に採点用のStringGridを配置している

ここまでは、全然、難しくなかった。

次に、StringGridにフォーカスがあるとき、Enterキーを押し下げるとカーソルが下のセルに移動するように設定。これも以前やったことがあったので、問題なく実現できた。

で、あとはEnterキーでコントロールを移動させるために、Form上のコンポーネントより先にFormがキーボードイベントを取得できるよう、FormCreateで以下を指定。

  KeyPreview:=True;

で、Enterキーの挙動を設定したのと同じFormKeyPressプロシージャで、

  if Ord(Key)=VK_RIGHT then
  begin
    if ActiveControl is TStringGrid then
    begin
      if TStringGrid(ActiveControl).EditorMode then
      begin
        Key:=#0;
        //Exit;
      end;
    end;
  end;

と、設定してあげれば・・・思った通りのGUIが完成・・・するはずだった。

・・・だったが、実際にコンパイルしてみると・・・

Enterキーを押し下げると、次の採点欄にフォーカスが移動(ここまでは予定通り)し、左右の矢印キーは無効化してあるので、押し下げても反応しないはず・・・が、採点欄1で右矢印キーを押し下げると、「カーソルがきえちゃった!」

で、左矢印キーを押すと、「カーソルが現れた!」

って、コトは。

非表示にしてある採点欄2へ、カーソルが移動している・・・

無効化されてない。

矢印キーは、無効化されてないよー

無駄だと思ったが、いちおう確認のため

  if Ord(Key)=VK_RIGHT then
  begin
    if ActiveControl is TStringGrid then
    begin
      if TStringGrid(ActiveControl).EditorMode then
      begin
        ShowMessage('VK_RIGHT');
        Key:=#0;
        //Exit;
      end;
    end;
  end;

右矢印キーの押し下げをフックできたらメッセージを表示するようにしてみたが、まったく反応なし。つまり、Enterキーの押し下げを検出するのと同じ方法では矢印キーの押し下げを検出できないことが判明(号泣)。なんだか、面白いことになってキタ。

1.無効化する前に行った設定

FormにStringGridを配置し、列数・行数は実行時に動的に指定する仕様でプログラミング。さらに、採点しやすいよう、Enterキーでのフォーカスの移動(下のセルへ)と、不要な列の非表示設定を次のように行った。

【StringGridの設定】

procedure TFormCollaboration.FormCreate(Sender: TObject);
var
  i:integer;
begin

  //以下StringGrid関係
  StringGrid1.DefaultDrawing := True;
  StringGrid1.DrawingStyle   := gdsThemed;
  StringGrid1.Options        := StringGrid1.Options + [goDrawFocusSelected];

  //フォーカスのあるセルを強調表示
  StringGrid1.Options:=StringGrid1.Options + [goDrawFocusSelected];
  //Clickでセル編集を可能にする-> [goEditing]をTrueに設定
  StringGrid1.Options:=StringGrid1.Options + [goEditing];
  //常に編集可能に設定
  StringGrid1.Options:=StringGrid1.Options + [goAlwaysShowEditor];
  //範囲選択を可能に設定
  StringGrid1.Options:=StringGrid1.Options + [goRangeSelect];
  //ドラッグ中グリッド内容のスクロールを「行う」に設定
  StringGrid1.Options:=StringGrid1.Options + [goThumbTracking];

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

end;

【IMEは使用不可に設定】

  type
    ・・・

  //Col毎のIMEの制御
  type
    _TGrid = class(TCustomGrid);

  private
    { Private 宣言 }
    ・・・

procedure TFormCollaboration.StringGrid1GetEditText(Sender: TObject; ACol,
  ARow: Integer; var Value: string);
begin
  //IMEの制御
  with TEdit(_TGrid(Sender).InplaceEditor) do
  begin
    ImeMode := imDisable;   //日本語入力OFFは imDisable
  end;
end;

【Enterキーでカーソルは下のセルへ移動】

procedure TFormCollaboration.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にすると同じ項目の次のレコードへ移動。
        ActiveControl.Perform(WM_KEYDOWN,VK_DOWN,0);
        Key:=#0;
      end;
    end else begin
      SelectNext(ActiveControl,True,True);
      Key:=#0;
    end;
  end;

end;

【列の非表示】※StringGridの初期化時と採点欄の切り替え時の両方に設定

  //列幅の自動調整(やるなら列の非表示設定の前に実行)
  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])+10;
      if MaxColWidth < TmpColWidth then
      begin
        MaxColWidth := TmpColWidth;
      end;
    end;
    StringGrid1.ColWidths[iCOL] := MaxColWidth;
  end;

  //必要な列のみ表示
  for i := 1 to StringGrid1.ColCount-1 do
  begin
    if i<>StrToInt(ComboBox1.Text) then
    begin
      StringGrid1.ColWidths[i]:=-StringGrid1.GridLineWidth;
      //StringGrid1.ColWidths[i]:=-1;
    end else begin
      StringGrid1.ColWidths[i]:=45;
    end;
  end;

2.無効化にチャレンジ

やりたいことはわかってるけど、その方法がわからない。特に今回の場合はまったくわからない。なぜ、右矢印キーの押し下げをキャッチしてくれないのか・・・

if Ord(Key)=VK_RIGHT then

理由はわからないが、とにかくFormKeyPress手続きの中に書く上の方法ではダメらしい。VK_RIGHTはここを素通りしてしまう。

Google先生が教えてくれる様々なヒントを読み漁る(ワタシに理解できない内容大変多し・心が折れそうになる)が、「コレだ!」と叫びたくなるような瞬間は訪れず。

しかし、神は私を見捨てなかった・・・。

「Delphi 矢印キー Ord(Key)」で検索すると、検索結果のTopに表示されたのは

Googleの検索結果より抜粋

運命的な出会いを感じつつ、リンクをクリック。すると・・・リンク先(FDELPHI Delphi users’ forum)の記事には、「KeyPreviewをTrueにするだけでは不十分です」との解説が!

記事へのリンク:[Q] フォームへ仮想キーを送ることでつまずいています。

「あったー!」

どこのどなたか存じませぬが、20年以上前にこの書き込みを行ってくださった方に心より深く御礼申し上げます(回答のみで、回答者ご自身のお名前の書き込みはありませんでした)。自らの知識が後の世のどこかで役に立つ日があることを信じて、回答を寄せられたそのお気持ちに到底及ばないと知りながらも、私も同じ気持ちでこれを書いております。

なぜ、到底及ばないか?

上のサイトの回答者様=理由も、仕組みも、わかった上で回答されている。

ワタシ=理由も、仕組みも、なんにもわかってないけど、これを書いている。

共通点:困ってるひとをたすけたい、その一心。

閑話休題。

上記サイトの回答によれば、次のようなイベントハンドラが別に必要であるとのこと。

procedure TForm1.FormCreate(Sender: TObject);
begin
  Application.OnMessage := AppMessage;
end;

procedure TForm1.AppMessage(var Msg: TMsg; var Handled: Boolean);
begin
  if Msg.message = WM_KEYDOWN then begin

  end;
end;

ここまで用意していただけたなら、あとは「左右の矢印キーだったら、入力を無効化する処理」を書けばイイだけ! それなら、ワタシにもできます。
さっそく、記述しました!!

procedure TFormCollaboration.AppMessage(var Msg: TMsg; var Handled: Boolean);
begin

  //StringGrid上で押された右矢印キーを無効化する
  if Msg.message = WM_KEYDOWN then
  begin
    if ActiveControl is TStringGrid then
    begin
      if TStringGrid(ActiveControl).EditorMode then
      begin
        if Msg.wParam = VK_RIGHT then
        begin
          ShowMessage('捕まえた!');
          Key:=#0;
        end;
      end;
    end;
  end;
end;

うわーん。Keyが「未定義の識別子」エラーに!!! よく見たら・・・

procedure TFormCollaboration.AppMessage(var Msg: TMsg; var Handled: Boolean);

パラメーターにあるはずの(私は「ある」と思い込んでいた)・・・、我が愛しの

var Key: Char

が・・・ない。

ない。ない。ないよー。

どこにもない!!(超号泣)

( やっと、ここまでキタのにー )信じていたものに裏切られた哀しみが、千尋の海のような深まりをもって、胸いっぱいに広がって行くのを感じました。

(天の声:よく確認しないオマエがアホなだけだろー)

それでも(なんでかなー)と思いながら、FormCreate部分に書いた次の一文をよく確認してみると、

Application.OnMessage := AppMessage;

OnMessageをポイントして表示されるヒントを見れば、Keyパラメータが最初から入っていないことは明らかです。そうだったのか・・・

オレの人生は、これまで、いつも、いつだって、七転び八起き じゃねー

七転八倒の人生だったじゃねーか。

ここでもそれを確かめただけだぜー。どぉーってことないよ。

たったひとつだけ残された唯一の取柄、それは「あきらめない」こと☆

ここで私は気がつきます。Keyは未定義の識別子エラーになって使えないけど、

if Msg.wParam = VK_RIGHT then

もしかして、コレは動くんじゃね?

可及的速やかに「実行」

実行してみました

やったー☆ うごいたー☆☆

無効化はまだできませんが、とりあえず今までできなかった「右向きの矢印キーが押し下げられたこと」をキャッチできました。偉大なる前進です。

で、思ったことは、VK_RIGHTって確か仮想キーで、ほんとの姿はただの数字だったはずだよなってことです。試しにMsg.wParamの「wParam」をポイントしてみると表示されるヒントには、wParamは「NativeUInt型」って表示されています。

・・・ってことは、NativeUInt型がナンなのかはまったくわからない(こんな型は初めて見た)けれど、Intが付くから、おそらくInteger型の1種なのでしょう。Google先生に訊くと「プラットフォームに依存する符号なし整数型」と解説にあったので、自然数と言いながらきっと0も使えるのでしょう・・・と、自分に都合よく解釈し、Enterキーの押し下げをキャッチしたときは#0を代入していたから、同じだろうと考え wParam に 0 を投入!(正しくは代入です)

procedure TFormCollaboration.AppMessage(var Msg: TMsg; var Handled: Boolean);
begin

  //StringGrid上で押された右矢印キーを無効化する
  if Msg.message = WM_KEYDOWN then
  begin
    if ActiveControl is TStringGrid then
    begin
      if TStringGrid(ActiveControl).EditorMode then
      begin
        if Msg.wParam=VK_RIGHT then
        begin
          //単に0を代入してwParamを書き換え
          Msg.wParam := 0;
        end;
      end;
    end;
  end;

左矢印キーの押し下げにも、忘れずに対応☆

  //StringGrid上で押された左矢印キーを無効化する
  if Msg.message = WM_KEYDOWN then
  begin
    if ActiveControl is TStringGrid then
    begin
      if TStringGrid(ActiveControl).EditorMode then
      begin
        if Msg.wParam=VK_LEFT then
        begin
          //単に0を代入してwParamを書き換え
          Msg.wParam := 0;
        end;
      end;
    end;
  end;

end;

翌日、左右の制御を合体させた方がイイと気づき、上の2つを合体させて・・・

procedure TFormCollaboration.AppMessage(var Msg: TMsg; var Handled: Boolean);
begin
  //StringGrid上で押された左右の矢印キーを無効化する
  if Msg.message = WM_KEYDOWN then
  begin
    if ActiveControl is TStringGrid then
    begin
      if TStringGrid(ActiveControl).EditorMode then
      begin
        if Msg.wParam=VK_RIGHT then
        begin
          //単に0を代入してwParamを書き換え
          Msg.wParam:=0;
        end;
        if Msg.wParam=VK_LEFT then
        begin
          //単に0を代入してwParamを書き換え
          Msg.wParam:=0;
        end;
      end;
    end;
  end;
end;

で、実行!

StringGridにフォーカスして、Enterキーを押し下げ。
カーソルは期待通り、一つ下のセルへ移動。
次に祈りつつ、右矢印キーを押し下げ。
カーソルは消えません。点滅しています。
なんだか、うれしそうです(おまえだろ)。
期待が確信に変わるのを感じつつ、左矢印キーを押し下げ。
カーソルはやっぱり消えません。

ぎゃはは。為せば成る!

根本的に理解してないけど・・・ *(^_^)*♪

3.まとめ

(1)矢印キーは、Enterキーとは違う手続きで押し下げをキャッチする必要がある。
(2)FormCreate時にKeyPreview を True に設定。
(3)さらにFormCreate時に Application.OnMessage := AppMessage; を記述。
(4)Formのメンバーとして Shift+Ctrl+C でAppMessage 手続きを作成。
(5)AppMessage 手続きの中で矢印キーの押し下げをキャッチする。

4.お願いとお断り

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