Delphi Clinic | C++Builder Gate | Training & Consultancy | Delphi Notes Weblog | Dr.Bob's Webshop |
|
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 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.