Hiding items in TListBox while filtering by String

后端 未结 2 555
深忆病人
深忆病人 2021-01-13 06:02

Short Version: Is there any way to control or modify LisBox items individually? for example set their Visible property to False separately. I foun

相关标签:
2条回答
  • 2021-01-13 06:45

    This is something I often do, but with list views instead of list boxes. The basic principles are the same, though.

    I tend to store the individual items as objects, which are reference types in Delphi. And I keep them all in one main unfiltered list, which owns the objects, while I maintain a filtered list (which does not own the objects) for display purposes. Like @Sertac, I combine this with a virtual list view.

    To see how this works in practice, create a new VCL application and drop a list view (lvDisplay) and an edit control (eFilter) on the main form:

    Notice I have added three columns to the list view control: "Name", "Age", and "Colour". I also make it virtual (OwnerData = True).

    Now define the class for the individual data items:

    type
      TDogInfo = class
        Name: string;
        Age: Integer;
        Color: string;
        constructor Create(const AName: string; AAge: Integer; const AColor: string);
        function Matches(const AText: string): Boolean;
      end;
    

    where

    { TDogInfo }
    
    constructor TDogInfo.Create(const AName: string; AAge: Integer;
      const AColor: string);
    begin
      Name := AName;
      Age := AAge;
      Color := AColor;
    end;
    
    function TDogInfo.Matches(const AText: string): Boolean;
    begin
      Result := ContainsText(Name, AText) or ContainsText(Age.ToString, AText) or
        ContainsText(Color, AText);
    end;
    

    And let us create the unfiltered list of dogs:

    TForm1 = class(TForm)
      eFilter: TEdit;
      lvDisplay: TListView;
      procedure FormCreate(Sender: TObject);
      procedure FormDestroy(Sender: TObject);
    private
      FList, FFilteredList: TObjectList<TDogInfo>;
    public
    end;
    

    where

    function GetRandomDogName: string;
    const
      DogNames: array[0..5] of string = ('Buster', 'Fido', 'Pluto', 'Spot', 'Bill', 'Rover');
    begin
      Result := DogNames[Random(Length(DogNames))];
    end;
    
    function GetRandomDogColor: string;
    const
      DogColors: array[0..2] of string = ('Brown', 'Grey', 'Black');
    begin
      Result := DogColors[Random(Length(DogColors))];
    end;
    
    procedure TForm1.FormCreate(Sender: TObject);
    var
      i: Integer;
    begin
    
      FList := TObjectList<TDogInfo>.Create(True); // Owns the objects
    
      // Populate with sample data
      for i := 1 to 1000 do
        FList.Add(
          TDogInfo.Create(GetRandomDogName, Random(15), GetRandomDogColor)
        );
    
      FFilteredList := FList;
    
      lvDisplay.Items.Count := FFilteredList.Count;
      lvDisplay.Invalidate;
    
    end;
    
    procedure TForm1.FormDestroy(Sender: TObject);
    begin
      if FFilteredList <> FList then
        FreeAndNil(FFilteredList);
      FreeAndNil(FList);
    end;
    

    The idea is that the list view control always displays the FFilteredList, which either points to the same object instance as FList, or points to a filtered (or sorted) version of it:

    // The list view's OnData event handler
    procedure TForm1.lvDisplayData(Sender: TObject; Item: TListItem);
    begin
    
      if FFilteredList = nil then
        Exit;
    
      if not InRange(Item.Index, 0, FFilteredList.Count - 1) then
        Exit;
    
      Item.Caption := FFilteredList[Item.Index].Name;
      Item.SubItems.Add(FFilteredList[Item.Index].Age.ToString);
      Item.SubItems.Add(FFilteredList[Item.Index].Color);
    
    end;
    
    // The edit control's OnChange handler
    procedure TForm1.eFilterChange(Sender: TObject);
    var
      i: Integer;
    begin
    
      if string(eFilter.Text).IsEmpty then // no filter, display all items
      begin
        if FFilteredList <> FList then
        begin
          FreeAndNil(FFilteredList);
          FFilteredList := FList;
        end;
      end
      else
      begin
        if (FFilteredList = nil) or (FFilteredList = FList) then
          FFilteredList := TObjectList<TDogInfo>.Create(False); // doesn't own the objects
        FFilteredList.Clear;
        for i := 0 to FList.Count - 1 do
          if FList[i].Matches(eFilter.Text) then
            FFilteredList.Add(FList[i]);
      end;
    
      lvDisplay.Items.Count := FFilteredList.Count;
      lvDisplay.Invalidate;
    
    end;
    

    The result:

    Notice that there always is only one in-memory object for each dog, so if you rename a dog, the changes will reflect in the list view, filtered or not. (But don't forget to invalidate it!)

    0 讨论(0)
  • 2021-01-13 06:50

    The items of a VCL listbox, List Box in the API, does not have any visibility property. The only option for not showing an item is to delete it.

    You can use the control in virtual mode however, where there are no items at all. You decide what data to keep, what to display. That's LBS_NODATA window style in the API. In VCL, set the style property to lbVirtual.

    Extremely simplified example follows.

    Let's keep an array of records, one record per virtual item.

    type
      TListItem = record
        FileName: string;
        Visible: Boolean;
      end;
    
      TListItems = array of TListItem;
    

    You can extend the fields as per your requirements. Visibility is one of the main concerns in the question, I added that. You'd probably add something that represents the original name so that you know what name have been changed, etc..

    Have one array per listbox. This example contains one listbox.

    var
      ListItems: TListItems;
    

    Better make it a field though, this is for demonstration only.

    Required units.

    uses
      ioutils, types;
    

    Some initialization at form creation. Empty the filter edit. Set listbox style accordingly. Fill up some file names. All items will be visible at startup.

    procedure TForm1.FormCreate(Sender: TObject);
    var
      ListFiles: TStringDynArray;
      i: Integer;
    begin
      ListFiles := ioutils.TDirectory.GetFiles(TDirectory.GetCurrentDirectory);
    
      SetLength(ListItems, Length(ListFiles));
      for i := 0 to High(ListItems) do begin
        ListItems[i].FileName := ListFiles[i];
        ListItems[i].Visible := True;
      end;
    
      ListBox1.Style := lbVirtual;
      ListBox1.Count := Length(ListFiles);
    
      Edit1.Text := '';
    end;
    

    In virtual mode the listbox is only interested in the Count property. That will arrange how many items will show, accordingly the scrollable area.

    Here's the filter part, this is case sensitive.

    procedure TForm1.Edit1Change(Sender: TObject);
    var
      Text: string;
      Cnt: Integer;
      i: Integer;
    begin
      Text := Edit1.Text;
      if Text = '' then begin
        for i := 0 to High(ListItems) do
          ListItems[i].Visible := True;
        Cnt := Length(ListItems);
      end else begin
        Cnt := 0;
        for i := 0 to High(ListItems) do begin
          ListItems[i].Visible := Pos(Text, ListItems[i].FileName) > 0;
          if ListItems[i].Visible then
            Inc(Cnt);
        end;
      end;
      ListBox1.Count := Cnt;
    end;
    

    The special case in the edit's OnChange is that when the text is empty. Then all items will show. Otherwise code is from the question. Here we also keep the total number of visible items, so that we can update the listbox accordingly.

    Now the only interesting part, listbox demands data.

    procedure TForm1.ListBox1Data(Control: TWinControl; Index: Integer;
      var Data: string);
    var
      VisibleIndex: Integer;
      i: Integer;
    begin
      VisibleIndex := -1;
      for i := 0 to High(ListItems) do begin
        if ListItems[i].Visible then
          Inc(VisibleIndex);
        if VisibleIndex = Index then begin
          Data := ListItems[i].FileName;
          Break;
        end;
      end;
    end;
    

    What happens here is that the listbox requires an item to show providing its index. We loop through the master list counting visible items to find out which one matches that index, and supply its text.

    0 讨论(0)
提交回复
热议问题