delphivcltlistbox

Hiding items in TListBox while filtering by String


Short Version: Is there any way to control or modify LisBox items individually? for example set their Visible property to False separately. I found a TListBoxItem class in Fire Monkey when I was searching, but I don't want to use Fire Monkey and want it in VCL.

Detailed Version: I tried to filter my ListBox using two TStringList and an Edit, one StringList is global to keep the original list (list_files_global) and another StringList to help filtering procedure (list_files_filter) and my primary list of files is my ListBox (list_files). I created my global StringList on onCreate event while program is starting to store my original list:

procedure Tfrm_main.FormCreate(Sender: TObject);
Begin
  list_files_global := TStringList.Create;
  list_files_global.Assign(list_files.Items);
End;

and used Edit's onChange event for filtering:

procedure Tfrm_main.edit_files_filterChange(Sender: TObject);
Var
  list_files_filter: TStringList;
  i: Integer;
Begin
  list_files_filter := TStringList.Create;
  list_files_filter.Assign(list_files.Items);

  list_files.Clear;

  for i := 0 to list_files_filter.Count - 1 do 
    if pos(edit_files_filter.text, list_files_filter[i]) > 0 then 
      list_files.Items.Add(list_files_filter[i]);

End;

and for switching off the filter, just recover the list from my global list that I created at first:

list_files.Items := list_files_global;

here so far, everything works just fine, but problem is when I'm trying to edit/rename/delete items from filtered list, for example I change an item:

list_files.Items[i] := '-- Changed Item --';

list will be edited, but when I switch off the filter, the original list will be back and all changes are lost. so I want to know is there any proper way to solve this problem? Something like hiding items individually or change items visibility, etc... so I can change the filtering algorithm and get rid of all this making extra lists. I searched the internet and looked into Delphi's help file for a whole day and nothing useful came up.


Solution

  • 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.