Delphi Clinic C++Builder Gate Training & Consultancy Delphi Notes Weblog Dr.Bob's Webshop
Dr.Bob's Delphi Notes Dr.Bob's Delphi Clinics
 Dr.Bob's Speedy Listbox for Delphi
See Also: other Dr.Bob Examines... columns
This article was published a few years ago in VBX/OCX Developer Journal

Speedsearch in Listboxes with Delphi
The article is about writing a Delphi TBListBox component to solve an old problem of searching items in listboxes.

In Search of a good ListBox
If often hear people ask why a listbox has a limitation of the number of items or the total amount of data you can store in it. I cannot imagine why anybody would put more than a handful of items in a listbox when you have a so limited (crippled, I should say) way of searching for items in it.
The standard way to search for items in a listbox is to scroll or type the first letter of the item. The default listbox reacts to a keystroke by trying to find the item for which this is the first letter. Having say 2000 items with about 128 starting with the same letter, that's not really a big help. Vertical scrolling through all these items in the listbox also doesn't satisfy our needs: we want a speedsearch facility!

SpeedSearch
The speedsearch ability uses an extra editbox where the user can enter the first few characters of the item to search for. While the user is typing characters, the listbox is scrolling with each additional character to the new position. It is also possible to move around the editbox with the cursor and insert characters anywhere. The listbox will always scroll to the right position. Since the listbox is scrolling with the speed of typing, this is called speedsearch.
We have to do a few things to implement the speedsearch algorithm. First, we must create a new component, TBListBox, derive it from TWinControl and add a TEdit editbox and a TListBox listbox to the private parts of our new component. The editbox will be used to contain the speedsearch buffer. We have to override the Create constructor of TBListBox to actually create the editbox and listbox and make sure the TBListBox is their parent. Furthermore, we must ensure that they both use the parent's font property, and are resized (the editbox just above the listbox) correctly by overriding the SetBounds method.

  unit TBListBx;
  interface
  uses
    Classes, Controls, StdCtrls, Graphics;

  type
    TBListBox = class(TWinControl)
      protected
        constructor Create(AOwner: TComponent); override;
        procedure SetBounds(ALeft, ATop, AWidth, AHeight: Integer); override;

      private
        ListBox: TListBox;
        EditBox: TEdit;

        function GetItems: TStrings;
        procedure SetItems(NewItems: TStrings);

      published
        property Font;
        property Items: TStrings read GetItems write SetItems;
    end {TBListBox};

  procedure Register;

  implementation

    constructor TBListBox.Create(AOwner: TComponent);
    begin
      inherited Create(AOwner);
      ListBox := TListBox.Create(Self);
      ListBox.Parent := Self;
      ListBox.ParentFont := True;
      EditBox := TEdit.Create(Self);
      EditBox.Parent := Self;
      EditBox.ParentFont := True;
      SetBounds(Left,Top,80,120); { initial size }
    end {Create};

    procedure TBListBox.SetBounds(ALeft, ATop, AWidth, AHeight: Integer);
    const
      Box = 10; { space for the box of editbox }
      Bar =  2; { bar between edit and listbox }
    var
      FontHeight: Integer;
    begin
      inherited SetBounds(ALeft, ATop, AWidth, AHeight);
      FontHeight := abs(Font.Height);
      EditBox.SetBounds(0,0,Width,FontHeight+Box);
      ListBox.SetBounds(0,FontHeight+Box+Bar,Width,Height-(FontHeight+Box+Bar));
    end {SetBounds};

    function TBListBox.GetItems: TStrings;
    begin
      GetItems := ListBox.Items;
    end {GetItems};

    procedure TBListBox.SetItems(NewItems: TStrings);
    begin
      ListBox.Items := NewItems;
    end {SetItems};

    procedure Register;
    begin
      RegisterComponents('Dr.Bob', [TBListBox]);
    end {Register};
  end.
Note that the listbox stored its items in a property called Items of type TStrings. To make sure our TBListBox components behaves similar to a listbox, we have to make our own "Items" property with a GetItems and SetItems read and write method. GetItems just returns the ListBox.Items, while SetItems just sets the ListBox.Items. This makes the Items of the parent TBListBox actually the same as the child ListBox.
If we drop the TBListBox component on a form an watch it with the Object Inspector, we see indeed that the property Items becomes available:

If we fill the items, compile the form and run it, we have a new component with an editbox and a listbox. Unfortunately, there is no interaction between the two, yet. We can type anything in the editbox without any reaction of the listbox.

SpeedSearch with TBListBox
All we have to do now, is to make sure the editbox "talks" to the listbox when a character is typed in. Luckily for us, there is something called a notification message. It is send to the editbox every time its contents changes, and it called the OnChange event. We can assign this OnChange event with a procedure of the following type:

  procedure (Sender: TObject);
I've added a procedure EditBoxNotifiesListBox to the TBListBox class, and assigned it to the EditBox.OnChange event handler. Now, every time the contents of the editbox changes (for example when we type something in it), the OnChange event is triggered, i.e. the EditBoxNotifiesListBox procedure is called.

EditBoxNotifiesListBox
As we've seen, the items of a listbox are stored in the field Items, which is a string list. If we type text in the editbox, we must find the place in the string list where the item would be inserted if we were to insert it (i.e. we get the position of an exact match, or the item just before the match). To locate a string in a string list, we can use the IndexOf method, which takes a string parameter and returns either the correct index or -1 if no match was found. Unfortunately, IndexOf works only with complete matches. So, if we type "bo" or "bobby" while the listbox contains "bob", we'd get -1 as a result. In order to match partial strings, we have to walk the list of strings and compare them each to the text in editbox either until we're out of strings, or until the text in the editbox is bigger (i.e. comes after) the current item in the listbox. The implementation of this algorithm can be found in the procedure EditBoxNotifiesListBox of TBListBox:

  unit TBListBx;
  interface
  uses
    Classes, Controls, StdCtrls, Graphics;

  type
    TBListBox = class(TWinControl)
      protected
        constructor Create(AOwner: TComponent); override;
        procedure SetBounds(ALeft, ATop, AWidth, AHeight: Integer); override;

      private
        ListBox: TListBox;
        EditBox: TEdit;

        procedure EditBoxNotifiesListbox(Sender: TObject);
        { The TNotifyEvent type is the type for events that have no parameters.
          These events simply notify the component that a specific event occurred.
          For example, OnChange, which is type TNotifyEvent, notifies the control
          that a change has occurred on the (edit) control.
        }

        function GetItems: TStrings;
        procedure SetItems(NewItems: TStrings);

      published
        property Font; {TFont}
        property Items: TStrings read GetItems write SetItems;
    end {TBListBox};

  implementation
  uses
    SysUtils;

    constructor TBListBox.Create(AOwner: TComponent);
    begin
      inherited Create(AOwner);
      ListBox := TListBox.Create(Self);
      ListBox.Parent := Self;
      ListBox.ParentFont := True;
      ListBox.Sorted := True; { sort listbox!! }
      EditBox := TEdit.Create(Self);
      EditBox.Parent := Self;
      EditBox.ParentFont := True;
      EditBox.OnChange := EditBoxNotifiesListbox;
      EditBox.OnEnter := EditBoxNotifiesListbox; { if you come back to the edit... }
      SetBounds(Left,Top,80,120); { initial size }
    end {Create};

    procedure TBListBox.EditBoxNotifiesListBox(Sender: TObject);
    var
      index: Integer;
    begin
      index := ListBox.Items.Count-1;
      while (index > 0) and
            (CompareText(EditBox.Text,ListBox.Items[index]) < 0) do Dec(index);
      ListBox.ItemIndex := index;
    end {EditBoxNotifiesListBox};
Now, if we fill the listbox with some random names and type the text "bobby" in the editbox, the following situation will be the result:

If we move to the listbox (by either clicking on it with the mouse or hit the tab key) and scroll up and down in the listbox, the text in the editbox will not change. If we return to the editbox, we want the listbox to again scroll to the text we typed in the editbox. To ensure that, we have to assign the OnEnter event of the editbox to the method EditBoxNotifiesListBox as well.

Conclusions
We have developed an extended listbox dialog capable of a speedsearch facility in a few lines of code, using techniques such as the creation of sub-components (TEdit and TListBox) and the assignment of dynamic event handlers (OnChange and OnEnter). This new component can be used in all Delphi applications by installing it on the Component Palette of Delphi.


This webpage © 2000-2017 by Bob Swart (aka Dr.Bob - www.drbob42.com). All Rights Reserved