1

(使用:Delphi XE)

ListViewのすべての行にTButtonを追加しています。ボタンのOnClickハンドラーはSender.Freeです。ただし(リストビューにデータセットを設定するデータセットが更新されたためにリスト行が消える間)、ボタンはリストビューに表示されたままになります。私は何が間違っているのですか?

これがボタンの作成を示す私のコードであり、ボタンが解放されるOnClickは次のとおりです。

(別の注意点として、イベントハンドラーでコンポーネントを破棄するのは適切ではないことを知っています。これはここで何が問題なのですか?ボタンを削除する別の方法を提案できますか?)

procedure TfMain.actWaitListExecute(Sender: TObject);
var
  li: TListItem;
  s:  string;
  btRect: TRect;
  p:  PInteger;
begin
  lstWaitList.Items.Clear;
  lstWaitList.Clear;

  with uqWaitList do
  begin
    if State = dsInactive then
      Open
    else
      Refresh;

    First;
    while not EOF do
    begin
      li := lstWaitList.Items.Add;
      s  := MyDateFormat(FieldByName('VisitDate').AsString);
      li.Caption := s;

      New(p);
      p^ := FieldByName('ROWID').AsInteger;
      li.Data := p;
      s  := MyTimeFormat(FieldByName('InTime').AsString);
      li.SubItems.Add(s);
      li.SubItems.Add(FieldByName('FirstName').AsString + ' ' +
        FieldByName('LastName').AsString);
      //  li.SubItems.Add(FieldByName('LastName').AsString);

      with TButton.Create(lstWaitList) do
      begin
        Parent  := lstWaitList;
        btRect  := li.DisplayRect(drBounds);
        btRect.Left := btRect.Left + lstWaitList.Column[0].Width +
          lstWaitList.Column[1].Width + lstWaitList.Column[2].Width;
        btRect.Right := btRect.Left + lstWaitList.Column[3].Width;
        BoundsRect := btRect;
        Caption := 'Check Out';
        OnClick := WaitingListCheckOutBtnClick;
      end;

      Next;
    end;
  end;


end;


procedure TfMain.lstWaitListDeletion(Sender: TObject; Item: TListItem);
begin
  Dispose(Item.Data);
end;

procedure TfMain.WaitingListCheckOutBtnClick(Sender: TObject);
var
  SelROWID, outtime: integer;
  x: longword;
  y: TPoint;

  h, mm, s, ms: word;

begin
  y := lstWaitList.ScreenToClient(Mouse.CursorPos);
  //  Label23.Caption := Format('%d %d', [y.X, y.y]);
  x := (y.y shl 16) + y.X;
  PostMessage(lstWaitList.Handle, WM_LBUTTONDOWN, 0, x);
  PostMessage(lstWaitList.Handle, WM_LBUTTONUP, 0, x);
  Application.ProcessMessages;

  SelROWID := integer(lstWaitList.Selected.Data^);
  //  ShowMessage(IntToStr(SelROWID));

  with TfCheckOut.Create(Application) do
  begin
    try
      if ShowModal = mrOk then
      begin
        decodetime(teTimeOut.Time, h, mm, s, ms);
        outtime := h * 100 + mm;

        uqSetOutTime.ParamByName('ROWID').Value := SelROWID;
        uqSetOutTime.ParamByName('OT').Value := outtime;
        uqSetOutTime.Prepare;
        uqSetOutTime.ExecSQL;

        (TButton(Sender)).Visible := False;
        (TButton(Sender)).Free;

        actWaitListExecute(Self);
      end;
    finally
      Free;
    end;
  end;

end;

画像:

ここに画像の説明を入力してください

4

4 に答える 4

3

そうですね、2 つの潜在的な問題があると思います。まず、withブロックを使用しているため、コンパイラが一部の識別子を、解決すると思われるものとは異なる方法で解決する可能性があります。たとえば、TfCheckOut に Sender というメンバーがある場合、ローカル Sender の代わりにそれを解放することになります。

次に、呼び出しは条件内にあり、ShowModal mrOK`TButton(Sender).Freeへの呼び出しの場合にのみアクティブになります。is returningデバッガーに入って、そのコード ブランチが実行されていることを確認しましたか?

独自のイベントハンドラー内でボタンを解放しないという質問に関しては、そうするのはコード的に完全に合法です。これは良い考えではありません。解放すると、イベント ハンドラーの完了後に例外が発生する可能性がありますが、何もしないでください。Freeこれは、がまったく呼び出されていないことをほぼ確実に示しています。安全に解放する方法が必要な場合は、メッセージングをご覧ください。フォームでメッセージ ID とそのハンドラーを作成し、PostMessage(ではなくSendMessage) コントロールをパラメーターとしてフォームにそのメッセージを作成し、メッセージ ハンドラーがボタンを解放する必要があります。こうすることで、イベント ハンドラーが実行されていないことを確認できます。

編集:Free OK、それが呼び出されていると確信している場合は呼び出さFreeれており、例外を発生させずに終了した場合Free、ボタンは破棄されています。それは本当に簡単です。(このコードが実行された後、ボタンをもう一度クリックしてみてください。非常に奇妙なことが起こっていない限り、何も起こりません。) その後もボタンが表示される場合、それは別の問題です。これは、親 (TListView) 自体が再描画されていないことを意味します。そのメソッドを呼び出してみてくださいInvalidate。これにより、Windows が適切に再描画されます。

于 2011-07-08T16:12:29.950 に答える
2

まず第一に、私はあなたの解決策がうまくいかない理由がわかりません。別々に取られたすべての部分はうまく機能しますが、組み合わせたソリューションは機能しません。おそらく、アプローチは非常に複雑で、いくつかの問題を覆い隠しているのかもしれません。おそらく、自分のコードを見たときに決して見られない、ばかげた「jの代わりにiを書いた」の1つです...

とにかく、これが機能する簡単な実装です。データベースからRawデータを取得するのではなくTObjectList<>、データを格納するためにを使用しましたが、概念は同じです。明確にするために、ListViewは他のコントロールを保持するように設計されていないため、ListViewにボタンを配置するというアイデアはサポートしていません。楽しみのために、リストに十分なrawを追加して、垂直スクロールバーが表示されるようにします。スクロールバーを下に移動します。ボタンは移動しません。もちろん、問題を回避するために何かをハックすることはできますが、それは根本的な事実を変えるものではなく、ハックです。に切り替えてTVirtualTree、リストのように設定し、ボタンの列を自分で描画します。コントロールは実行可能ファイルにコンパイルされるためTVirtualTree、Windowsのアップグレードによってカスタム描画が妨げられる可能性はありません。

PASコード:

unit Unit14;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, ComCtrls, Generics.Collections, StdCtrls;

type

  TItemInfo = class
  public
    DateAndTime: TDateTime;
    CustomerName: string;
  end;

  // Subclass the Button so we can add a bit more info to it, in order
  // to make updating the list-view easier.
  TMyButton = class(TButton)
  public
    ItemInfo: TItemInfo;
    ListItem: TListItem;
  end;

  TForm14 = class(TForm)
    ListView1: TListView;
    procedure FormCreate(Sender: TObject);
  private
    // Items list
    List: TObjectList<TitemInfo>;
    procedure FillListWithDummyData;
    procedure FillListView;
    procedure ClickOnCheckOut(Sender: TObject);
  public
    destructor Destroy;override;
  end;

var
  Form14: TForm14;

implementation

{$R *.dfm}

{ TForm14 }

procedure TForm14.ClickOnCheckOut(Sender: TObject);
var B: TMyButton;
    i: Integer;
    R: TRect;
begin
  B := Sender as TMyButton;
  // My button has a reference to the ListItem it sits on, use that
  // to remove the list item from the list view.
  ListView1.Items.Delete(B.ListItem.Index);
  // Not pretty but it works. Should be replaced with better code
  B.Free;
  // All buttons get there coordinates "fixed"
  for i:=0 to ListView1.ControlCount-1 do
    if ListView1.Controls[i] is TMyButton then
    begin
      B := TMyButton(ListView1.Controls[i]);
      if B.Visible then
      begin
        R := B.ListItem.DisplayRect(drBounds);
        R.Left := R.Right - ListView1.Columns[3].Width;
        B.BoundsRect := R;
      end;
    end;
end;

destructor TForm14.Destroy;
begin
  List.Free;
  inherited;
end;

procedure TForm14.FillListView;
var i:Integer;
    B:TMyButton;
    X:TItemInfo;
    ListItem: TListItem;
    R: TRect;
begin
  ListView1.Items.BeginUpdate;
  try
    // Make sure no Buttons are visible on ListView surface
    i := 0;
    while i < ListView1.ControlCount do
      if ListView1.Controls[i] is TMyButton then
        begin
          B := TMyButton(ListView1.Controls[i]);
          if B.Visible then
            begin
              // Make the button dissapear in two stages: On the first list refresh make it
              // invisible, on the second list refresh actually free it. This way we now for
              // sure we're not freeing the button from it's own OnClick handler.
              B.Visible := False;
              Inc(i);
            end
          else
            B.Free;
        end
      else
        Inc(i);
    // Clear the list-view
    ListView1.Items.Clear;
    // ReFill the list-view
    for X in List do
    begin
      ListItem := ListView1.Items.Add;
      ListItem.Caption := DateToStr(X.DateAndTime);
      Listitem.SubItems.Add(TimeToStr(X.DateAndTime));
      Listitem.SubItems.Add(X.CustomerName);

      B := TMyButton.Create(Self);
      R := ListItem.DisplayRect(drBounds);
      R.Left := R.Right - ListView1.Columns[3].Width;
      B.BoundsRect := R;
      B.Caption := 'CHECK OUT (' + IntToStr(R.Top) + ')';
      B.ItemInfo := x;
      B.ListItem := ListItem;
      B.OnClick := ClickOnCheckOut;
      B.Parent := ListView1;
    end;
  finally ListView1.Items.EndUpdate;
  end;
end;

procedure TForm14.FillListWithDummyData;
var X: TItemInfo;
begin
  X := TItemInfo.Create;
  X.DateAndTime := EncodeDate(2011, 7, 7) + EncodeTime(18, 6, 0, 0);
  X.CustomerName := 'Holmes Sherlok';
  List.Add(X);

  X := TItemInfo.Create;
  X.DateAndTime := EncodeDate(2011, 7, 7) + EncodeTime(18, 55, 0, 0);
  X.CustomerName := 'Glover Dan';
  List.Add(X);

  X := TItemInfo.Create;
  X.DateAndTime := EncodeDate(2011, 7, 8) + EncodeTime(23, 9, 0, 0);
  X.CustomerName := 'Cappas Shirley';
  List.Add(X);

  X := TItemInfo.Create;
  X.DateAndTime := EncodeDate(2011, 7, 8) + EncodeTime(23, 9, 0, 0);
  X.CustomerName := 'Jones Indiana';
  List.Add(X);
end;

procedure TForm14.FormCreate(Sender: TObject);
begin
  List := TObjectList<TitemInfo>.Create;
  FillListWithDummyData;
  FillListView;
end;

end.

フォームのDFM。それらは、aとaが付いた単なるフォームでListViewあり、OnFormcreate派手なものは何もありません。

object Form14: TForm14
  Left = 0
  Top = 0
  Caption = 'Form14'
  ClientHeight = 337
  ClientWidth = 635
  Color = clBtnFace
  Font.Charset = DEFAULT_CHARSET
  Font.Color = clWindowText
  Font.Height = -11
  Font.Name = 'Tahoma'
  Font.Style = []
  OldCreateOrder = False
  OnCreate = FormCreate
  DesignSize = (
    635
    337)
  PixelsPerInch = 96
  TextHeight = 13
  object ListView1: TListView
    Left = 8
    Top = 8
    Width = 465
    Height = 321
    Anchors = [akLeft, akTop, akRight, akBottom]
    Columns = <
      item
        Caption = 'DATE'
        Width = 75
      end
      item
        Caption = 'IN TIME'
        Width = 75
      end
      item
        Caption = 'CUSTOMER NAME'
        Width = 150
      end
      item
        Caption = 'CHECK OUT'
        MaxWidth = 90
        MinWidth = 90
        Width = 90
      end>
    TabOrder = 0
    ViewStyle = vsReport
  end
end
于 2011-07-09T06:05:50.277 に答える
1

TListviewでTButtonを動的にインスタンス化することは、間違ったアプローチです。

まず、TListviewはMicrosoft共通コントロール(ComCtl32)のラッパーであり、実行時にTButtonを動的に配置することは不十分なハックであることを理解する必要があります。たとえば、ユーザーがフォームのサイズを変更して、正確に3.5個のボタンが表示されるようにした場合はどうしますか?ボタンの半分が見えるように、ボタンをどのようにクリップしますか?または、部分的な行にボタンが表示されないようにしますか?ユーザーがマウスホイールでスクロールし、その場で動的にコントロールを再生成する必要がある場合に発生する可能性のあるすべての奇妙な問題に対処できると本当に確信していますか?ペイントルーチン、またはマウスの上下のメッセージで、コントロールを生成して解放することは想定されていません。

本当にそこにボタンが必要な場合、必要なのは2つの画像状態です。つまり、押されていない画像と押された画像です。正しいセルに焦点が合っているときに、正しい場所に所有者が描画します。そして、マウスを下に向けると、その領域でクリックが検出されます。

しかし、あなたが主張するなら、私はこれをします:

  1. プログラムの開始時に1つまたは複数のボタンを動的に作成し、必要に応じて各ボタンを表示または非表示にします。
  2. ボタンが多すぎる場合は、button-or-button-control-array要素を割り当てる代わりに表示または非表示にし、解放する代わりに非表示にします。

画像には行ごとに1つのボタンが表示されているため、実行時に作成され、コントロール配列(TListまたはTButtonの配列)に格納された約30個のボタンの配列が必要であると想定します。

各行に所有者が描画したボタンがあるグリッドの典型的な例。これらのボタンはセル内に描画され、マウスを下に操作すると、必要に応じてボタンが下の状態または上に描画されます。

ここに画像の説明を入力してください

ただし、各アイテムを一度に1行ずつ描画するには、owner-draw-a-buttonコードを取得し、各セルにボタンをペイントします。

所有者の描画コード:

// ExGridView1:TExGridView from https://sites.google.com/site/warrenpostma/
procedure TForm1.ExGridView1DrawCell(Sender: TObject; Cell: TExGridCell;
  var Rect: TRect; var DefaultDrawing: Boolean);
var
   btnRect:TRect;
   ofs:Integer;
   caption:String;
   tx,ty:Integer;
   Flags,Pressed: Integer;
   DC:HDC;
begin
 if Cell.Col = 1 then begin
    DC := GetWindowDC(ExGridView1.Handle);
    with ExGridView1.Canvas do
    begin
      Brush.Color := clWindow;
      Rectangle(Rect);
      caption := 'Button '+IntToStr(cell.Row);
      Pen.Width := 1;
      btnRect.Top := Rect.Top +4;
      btnRect.Bottom := Rect.Bottom -4;
      btnRect.Left := Rect.left+4;
      btnRect.Right := Rect.Right-4;
      Pen.Color := clDkGray;
      if FMouseDown=Cell.Row then
      begin
         Flags := BF_FLAT;
         Pressed := 1;
      end else begin
         Flags := 0;
         Pressed := 0;
      end;
      Brush.Color := clBtnFace;
      DrawEdge(DC, btnRect, EDGE_RAISED, BF_RECT or BF_MIDDLE or Flags);
      Flags := (btnRect.Right - btnRect.Left) div 2 - 1 + Pressed;
      PatBlt(DC, btnRect.Left + Flags, btnRect.Top + Flags, 2, 2, BLACKNESS);
      PatBlt(DC, btnRect.Left + Flags - 3, btnRect.Top + Flags, 2, 2, BLACKNESS);
      PatBlt(DC, btnRect.Left + Flags + 3, btnRect.Top + Flags, 2, 2, BLACKNESS);
      Font.Color := clBtnText;
      Font.Style := [fsBold];
      tx := btnRect.left + ((btnRect.Right-btnRect.Left) div 2) - (TextWidth(Caption) div 2);
      ty := btnRect.Top + 2;
      TextOut(tx,ty,caption);
    end;
    DefaultDrawing := false;
 end;
end;

ボタンが押されたときを把握するために、マウスを下に、マウスを上に処理するための、上に示されていない他のコードがあります。必要に応じて、完全なサンプルコードをどこかにアップロードできます。

于 2011-07-08T19:05:09.987 に答える
1

すべてに:

問題を解決しました。OnClick ハンドラーでボタンを解放しようとすることが問題でした。多くの著者から、これは明らかに悪い習慣であるというアドバイスを読みました。そこで、Free 呼び出しを削除し、ObjectList 内のボタンを追跡します。actWaitListExecute では、オブジェクト リストをクリアするだけで、すべてのボタンがクリアされ、新しいボタンが再び再描画されます。

Form 宣言に以下を追加します。

  private
    { Private declarations }
    FButton : TButton;
    FButtonList : TObjectList;

FormCreate に次を追加します。

  FButtonList := TObjectList.Create;

FormDestroy を追加します。

procedure TfMain.FormDestroy(Sender: TObject);
begin
  FButtonList.Free;
end;

actWaitListExecute を変更して、次に示す最後の行を追加します。

procedure TfMain.actWaitListExecute(Sender: TObject);
var
  li: TListItem;
  s:  string;
  btRect: TRect;
  p:  PInteger;
begin
  lstWaitList.Items.Clear;
  lstWaitList.Clear;
  FButtonList.Clear;

actWaitListExecute のコードも変更します。

  FButton := TButton.Create(lstWaitList);
  FButtonList.Add(FButton);
  with  FButton do
  begin
    Parent := lstWaitList;
    Caption := 'Check Out';
    Tag := integer(li);
    OnClick := WaitingListCheckOutBtnClick;

    btRect := li.DisplayRect(drBounds);
    btRect.Left := btRect.Left + lstWaitList.Column[0].Width +
      lstWaitList.Column[1].Width + lstWaitList.Column[2].Width;
    btRect.Right := btRect.Left + lstWaitList.Column[3].Width;
    BoundsRect := btRect;
  end;

そして、すべてが期待どおりに機能します.....ハッピーエンド:)

于 2011-07-09T14:19:25.540 に答える