上へ

Delphi Tips

  1. StringGrid の幅と高さの調整方法
  2. カーソル移動サンプル
  3. 指定ウィンドウ以下の全てのコントロールの Enabled を設定する
  4. TThread バグについて

StringGrid の幅と高さの調整方法


セル区切り丁度になるように StringGrid の幅と高さを調整する サンプル tip1.lzh
  1. デザイン
    表示したい大きさに ColCount, RowCount を設定(この場合は ColCount=3, RowCount=6)し、 グリッド領域より大きめにレイアウトする。
    デザイン画面
  2. スクロールバーの設定
    ScrollBars プロパティを編集し、スクロールが起こる方向だけ有効にする。 ここでは ssVertical を指定する。
  3. 幅、高さ調整コード
    FormCreate イベントに以下の調整コードを記入する
    procedure TForm1.FormCreate(Sender: TObject);
    begin
      // 調整
      StringGrid1.ClientWidth := StringGrid1.GridWidth;
      StringGrid1.ClientHeight := StringGrid1.GridHeight;
      // 実際に使用する行/列数に設定する
      StringGrid1.RowCount := 20;
    end;
    
  4. 実行画面
    スクロールが起こる方向にはスクロールバー分の領域 垂直スクロールバー幅:GetSystemMetrics(SM_CXHSCROLL) 水平スクロールバー高さ:GetSystemMetrics(SM_CYHSCROLL) が追加されて表示される。 ここでは垂直スクロールバーが追加されて表示される。
    実行画面

カーソル移動サンプル


キーリピートのように移動させる、タイマーを使っていないのでリピート間隔が安定している、 OnMouseDown/Up のセットではなく、マウス、キーボード、方向全て1つのイベントで処理するので、可読性とメンテナンスが良い。 ・・・と思う。 ただしリターンキーを押した場合のように OnClick イベントだけが発生する場合は対応できない。 サンプル tip2.lzh
  1. デザイン
    移動させるラベル(lblPos)と 移動用のボタン(btnUp, btnDown, btnLeft, btnRight)を配置する。
    デザイン画面
  2. OnKeyPress イベントハンドラ設定
    btnUp に OnKeyPress イベントハンドラを追加し以下のコードを記入する
    procedure TForm1.btnUpKeyPress(Sender: TObject; var Key: Char);
    begin
      if (Key <> ' ') then
        Exit;
    
      // カーソル移動コード
      if (Sender = btnUp) then
        lblPos.Top := lblPos.Top - 1
      else if (Sender = btnDown) then
        lblPos.Top := lblPos.Top + 1
      else if (Sender = btnLeft) then
        lblPos.Left := lblPos.Left - 1
      else if (Sender = btnRight) then
        lblPos.Left := lblPos.Left + 1
        ;
      // 移動範囲制限
      lblPos.Top := Min(Max(lblPos.Top, 0), 200);
      lblPos.Left := Min(Max(lblPos.Left, 0), 200);
      lblPos.Caption := Format('[%3d,%3d]', [lblPos.Top, lblPos.Left]);
    end;
    
  3. OnMouseDown イベントハンドラ設定
    btnUp に OnMouseDown イベントハンドラを追加し以下のコードを記入する
    procedure TForm1.btnUpMouseDown(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
      function WaitForButtonUp(Timeout: DWORD): Boolean;
      // Timeout[ms] ボタンが離されるのを待つ
      begin
        Timeout := GetTickCount + Timeout;
        repeat
          Application.ProcessMessages;
          Result := not (csLButtonDown in TControl(Sender).ControlState);
        until ((GetTickCount > Timeout) or (Result));
      end;
    var
      Key: Char;
    begin
      Key := ' ';
      btnUpKeyPress(Sender, Key);
      if (WaitForButtonUp(500)) then
        Exit;
      // 押し続けられている
      repeat
        btnUpKeyPress(Sender, Key);
      until (WaitForButtonUp(10));
    end;
    
  4. btnDown, btnLeft, btnRight イベントハンドラの設定
    btnDown, btnLeft, btnRight それぞれの OnKeyPress と OnMouseDown イベントを btnUp のイベントに結びつける。
    イベントハンドラ設定
  5. 実行画面
    マウスのクリック、またはスペースキーでボタンを押すとラベルが移動する。 スペースキーの場合の移動速度はキーリピートの間隔。
    実行画面

指定ウィンドウ以下の全てのコントロールの Enabled を設定する


イベントハンドラ中で Application.ProcessMessage 系の関数を呼んでいて、再入排除などのために 処理が終わるまで中止ボタン以外の全てのコントロールを無効にしたいときに使います。 サンプル tip3.lzh
  1. コード
    procedure SetEnabledAll(AOwner: TWinControl; Enabled: Boolean;
      const IgnoreControls: array of TControl;
      const IgnoreClasses: array of TClass);
    // IgnoreControls に指定したコントロールと
    // IgnoreClasses に指定したクラス以外の
    // AOwner 以下のコントロールを全て Enabled に設定する。
    
      function IsIgnored(Control: TControl): Boolean;
      // Control が IgnoreControls か IgnoreClasses に指定されているか
      // True : 指定されている
      // False: 指定されていない
      var
        I: Integer;
      begin
        Result := True;
        for I := 0 to Length(IgnoreControls) - 1 do
          if (Control = IgnoreControls[I])then
            Exit;
        for I := 0 to Length(IgnoreClasses) - 1 do
          if (Control is IgnoreClasses[I]) then
            Exit;
        Result := False;
      end;
    var
      I: Integer;
      Control: TControl;
    begin
      for I := 0 to AOwner.ControlCount - 1 do
      begin
        Control := AOwner.Controls[I];
        // Ignore に指定されていなければ Enabled 設定
        if (not IsIgnored(Control)) then
          Control.Enabled := Enabled;
        // TWinControl は子コントロールも設定
        if (Control is TWinControl) then
          SetEnabledAll(TWinControl(Control), Enabled, IgnoreControls, IgnoreClasses);
      end;
    end;
    
  2. 呼び出し例
    中止ボタン(btnStop), クリアボタン(btnClear) と ラベル(TLabel, TStaticText) 以外のコントロールを全て無効にする。 処理が終わったら、有効に戻す。
    procedure TForm1.btnStartClick(Sender: TObject);
    begin
      // 中止ボタン、クリアボタン、ログ、と ラベル系一般 以外を無効にする。
      btnStop.Enabled := True;
      SetEnabledAll(Self, False, [btnStop, btnClear, memLog], [TLabel, TStaticText]);
      try
        ...(中略)Application.ProcessMessage 系の呼び出しがある処理...
      finally
        // 中止ボタン以外を有効にする
        btnStop.Enabled := False;
        SetEnabledAll(Self, True, [btnStop], []);
      end;
    end;
    
    実行前 処理中:中止ボタン、クリアボタン以外は無効
    実行前 処理中

TThread バグについて


TThread のバグについて検討する。結論として、有名なバグについては既に fix されている。(Delphi 5)
移植性(Kylix)や、開発環境依存の細かい点(IsMultiThread フラグなど)が実装されていることなどを考慮すると、 自作するより TThread を継承して使ったほうが開発効率は良いと思います。
  1. FreeOnTerminate メモリリークバグ
    概要
    FreeOnTerminate = True でメモリリークが起こる
    ソースレベル
    特に問題無いように見える。
    テスト方法
    以下のコード(中村拓男さんのコードそのままです)を実行し、 メモリを Memory View Ver 1.04 Copyright(c) Sota 氏で監視する。
    type
      MyThread = class(TThread)
      protected
        procedure Execute; override;
      end;
    
    procedure MyThread.Execute;
    begin
    end;
    
    procedure TForm1.Timer1Timer(Sender: TObject);
    var
      th: TThread;
    begin
      th := MyThread.Create(True);
      th.FreeOnTerminate := True;
      th.Resume;
    end;
    
    結果
    Delphi デバッグ環境下ではメモリが減りつづける、実行形式(exeファイル)を実行する場合は メモリは減らない。Delphi デバッグ環境の問題?
  2. WaitFor, Synchronize デッドロックバグ
    概要
    スレッド中で Synchronize 呼び出し中にメインスレッドから WaitFor を呼ぶと スレッドはメッセージループがとまるため、SendMessage が完了しない、 メインスレッドでは WaitFor が完了しないので、メッセージループが停止したまま になり、デッドロックを起こす。
    ソースレベル
    WaitFor の呼び出しスレッドを判別して制御を分けている。多分、対応済み。
    テスト方法
    右のコードを実行し、ボタンを連打する。
    type
      MyThread = class(TThread)
      protected
        procedure Execute; override;
        procedure SyncFunc;
      end;
    
    procedure MyThread.Execute;
    begin
      while (not Terminated) do
        Synchronize(SyncFunc);
    end;
    
    procedure MyThread.SyncFunc;
    begin
      Form1.Label1.Tag := Form1.Label1.Tag + 1;
      Form1.Label1.Caption := IntToStr(Form1.Label1.Tag);
    end;
    
    procedure TForm1.FormCreate(Sender: TObject);
    begin
      th := MyThread.Create(False);  
    end;
    
    procedure TForm1.Button1Click(Sender: TObject);
    begin
      th.Terminate;
      th.WaitFor;
      th.Free;
      FormCreate(Self);
    end;
    
    結果
    デッドロックは起こらなかった。追試で Suspended, Suspend, Resume もテストしてみたが、 サスペンド状態で WaitFor を呼ぶと固まる(当然かも)以外の問題は起こらなかった。
  3. 保護されていない共用変数を使用している
    概要
    TThread 内部で保護されていない変数 ThreadWindow, ThreadCount を使っている。
    ソースレベル
    考慮されている。多分対応済み。(Delphi 5)
    テスト方法
    なし
    結果
    必要十分な保護がされているかは読み取れなかったが、 公開されていてメンテされているソースがバグ付きのまま何年も 放置されているとは考えづらいので、自分で書くよりは信頼できそう。