delphicomponentstlistbox

Listbox Onchange Event, component


I want to make a component based on TListBox.

When the Items.Count changes, I want to update the Caption of a TLabel:

Label1.Caption := IntToStr(ListBox1.Items.Count);

I made the component below, but it does not work:

unit UChangeListBox;

interface

uses
  Classes, StdCtrls, Messages;

type
  TChListBox = class(StdCtrls.TListBox)
  private
    FItemIndex: Integer;
    FOnChange: TNotifyEvent;
    procedure CNCommand(var AMessage: TWMCommand); message CN_COMMAND;
  protected
    procedure DoChange; virtual;
    procedure SetItemIndex(const Value: Integer); override;
  published
    property OnChange: TNotifyEvent read FOnChange write FOnChange;
  end;

procedure Register;

implementation

procedure TChListBox.DoChange;
begin
  if Assigned(FOnChange) then
    FOnChange(Self);
end;

procedure TChListBox.CNCommand(var AMessage: TWMCommand);
begin
  inherited;
  if (AMessage.NotifyCode = LBN_SELCHANGE) and (FItemIndex <> ItemIndex) then
  begin
    FItemIndex := ItemIndex;
    DoChange;
  end;
end;

procedure TChListBox.SetItemIndex(const Value: Integer);
begin
  inherited;
  if FItemIndex <> ItemIndex then
  begin
    FItemIndex := Value;
    DoChange;
  end;
end;

procedure Register;
begin
  RegisterComponents('MyComponents',[TChListBox]);
end;

end.

Solution

  • TListBox inherits a protected Changed() method from TControl, which sends a CM_CHANGED message to derived classes. TListBox calls Changed() in reply to LBN_SELCHANGE. You don't need to define your own Change() method, just handle CM_CHANGED instead.

    TListBox.SetItemIndex() sends a LB_SETCURSEL message to the ListBox HWND. That message does not trigger LBN_SELCHANGE, so you would have to detect the ItemIndex change yourself. Which you attempted to do.

    However, these issues only apply to selection changes, not to Item.Count changes. Changing the ItemIndex does not change the Items.Count. If your goal is simply to display a new Items.Count whenever an item is added or removed from the ListBox, you need to handle the LB_ADDSTRING, LB_INSERTSTRING, LB_DELETESTRING and LB_RESETCONTENT messages instead.

    Try something more like this:

    type
      TChListBox = class(TListBox)
      private
        FOnItemCountChange: TNotifyEvent;
      protected
        procedure WndProc(var Message: TMessage); override;
      published
        property OnItemCountChange: TNotifyEvent read FOnItemCountChange write FOnItemCountChange;
      end;
    
    procedure TChListBox.WndProc(var Message: TMessage);
    var
      OldCount: Integer;
    begin
      case Message.Msg of
        LB_ADDSTRING, LB_INSERTSTRING, LB_DELETESTRING:
        begin
          // for LB_(ADD|INSERT)STRING, Message.Result is the 0-based
          // index of the added string, or a LB_ERR... error code.
          //
          // for LB_DELETESTRING, Message.Result is the number of items
          // remaining in the list, or a LB_ERR... error code.
          //
          inherited;
          if (Message.Result >= 0) and Assigned(FOnItemCountChange) then
            FOnItemCountChange(Self);
        end;
        LB_RESETCONTENT:
        begin
          // the Message.Result is not used in this message.
          //
          OldCount := Items.Count;
          inherited;
          if (OldCount <> Items.Count) and Assigned(FOnItemCountChange) then
            FOnItemCountChange(Self);
        end;
      else
        inherited;
      end;
    end;