Bob Swart (aka Dr.Bob)
Een slimme listbox voor Delphi (en Kylix?)

In dit artikel zal ik laten zien hoe je de TListBox component kunt uitbreiden met een editbox die kan helpen bij het snel zoeken (quick search) binnen de - gesorteerde - lijst van items in de listbox. De code die ik deze keer zal schrijven zal ik proberen zo netjes (lees: cross-platform) mogelijk te houden, waarbij we de volgende keer zullen zien of het gelukt is en of het nieuwe component zomaar van Delphi (VCL) naar Kylix (CLX) te porten is.

Maar laten we eerst Het Probleem van deze keer nader bekijken. Waar ik op dit moment een probleem mee heb is dat de normale TListBox component wel gesorteerd kan worden, maar niet gebruikt kan worden om snel en slim in de lijst te zoeken. Er wordt alleen maar op de eerste letter gezocht (en aangezien er maar zo'n 26 letters in ons alfabet zijn, en hooguit een honderd of wat unieke ascii tekens, is dat niet zo zinvol als je een lijst van 2000 namen hebt). Wat ik eigenlijk zou willen is dat ik meer dan alleen maar de eerste letter kan intikken. En de meest logische plek om dat te doen (en te blijven zien wat ik tot nu heb ingetikt) is een editbox, die zich vlak boven de listbox bevindt. Bij iedere toetsaanslag zou de editbox dan naar het meest overeenkomende item in de listbox moeten gaan, zodat ik snel en efficient kan zoeken. Ik kan dit natuurlijk gewoon bouwen door een TEdit component vlak boven een TListBox component neer te zetten en in de OnKeyPress van de TEdit te zoeken naar het meest lijkende item in de TListBox, maar dan moet ik het iedere keer opnieuw doen als ik zoiets nodig heb. Neen, dit roept om een heus nieuw component, dat we deze keer alleen voor Delphi zullen bouwen (en in Delphi zullen testen), maar de volgende keer ook in Kylix zullen testen (en dan zullen we zien hoe makkelijk het is om componenten te porten van Delphi naar Kylix).

TBListBox Component
Laten we een nieuw component bouwen dat bestaat uit twee subcomponenten (dat heet dan ook wel een zgn. SuperComponent). Als basis component kunnen we een panel nemen (dan kan je er nog een randje omheen doen), maar ik neem een TWinControl waarbinnen ik de TEdit en TListBox onder elkaar positioneer. Dat laatste gaat het best door de SetBounds methode van TWinControl te gebruiken, want ik weet toevallig dat die methode wordt aangeroepen bij het verplaatsen of vergroten/verkleinen van de originele component (en dan kunnen we de interne TEdit en TListBox componenten ook meteen verplaatsen of mee vergoten/verkleinen). In de constructor van de TBListBox moet ik natuurlijk meteen de TEdit en TListBox maken, en verder moet ik ervoor zorgen dat de meest interessante properties van de TListBox component "naar buiten" worden doorgegeven. In dit geval natuurlijk in ieder geval de Items, en aan de hand van dat voorbeeld zou je zelf ook andere properties naar buiten kunnen publiceren.

  unit TBListBx;
  interface
  uses
    Classes, Controls, StdCtrls;

  type
    TBListBox = class(TWinControl)
      private
        FListBox: TListBox;
        FEditBox: TEdit;
      protected
        function GetItems: TStrings;
        procedure SetItems(NewItems: TStrings);
      public
        constructor Create(AOwner: TComponent); override;
        procedure SetBounds(ALeft, ATop, AWidth, AHeight: Integer); override;
      published
        property Font;
        property Items: TStrings read GetItems write SetItems;
      end {TBListBox};

  procedure Register;

  implementation

  constructor TBListBox.Create(AOwner: TComponent);
  begin
    inherited Create(AOwner);
    FListBox := TListBox.Create(Self);
    FListBox.Parent := Self;
    FListBox.ParentFont := True;
    FListBox.Sorted := True;
    FEditBox := TEdit.Create(Self);
    FEditBox.Parent := Self;
    FEditBox.ParentFont := True;
    SetBounds(Left,Top,80,120);
  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);
    FEditBox.SetBounds(0,0,Width,FontHeight+Box);
    FListBox.SetBounds(0,FontHeight+Box+Bar,Width,Height-(FontHeight+Box+Bar));
  end {SetBounds};

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

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

  procedure Register;
  begin
    RegisterComponents('DrBob42', [TBListBox]);
  end {Register};

  end.
In de eerste listing heb ik ook meteen een Font property gepubliceerd. Daar hoefde ik verder geen akties aan te verbinden, omdat de subcomponenten (FEditBox en FListBox) allebei de ParentFont property op "true" gezet krijgen, zodat die automatisch een nieuw font zullen gebruiken als we dit toekennen aan ons TBListBox component. En let ook op de Sorted property van de interne FListBox die natuurlijk op True moet staan, anders zijn de items in de listbox niet gesorteerd, en dan hebben we er nog niks aan.

Interactie
De code uit de eerste listing is leuk, en als je dit TBListBox component installeert en op een Form zet dan zul je zien dat je editbox netjes boven de listbox blijft staan. Echter, als we in de editbox tekst intikken, dan gebeurt er verder nog helemaal niks: er wordt niet gezocht in de listbox. Niet zo heel erg gek natuurlijk, want daar hebben we nog helemaal niks aan gedaan. Om te reageren op toetsaanslagen van de interne editbox, kunnen we de OnKeyPress event handler invullen. Maar omdat de inhoud van een editbox op verschillende manieren kan veranderen (ook bijvoorbeeld door er tekst uit te knippen of in te plakken) is het beter om de OnChange event handler te pakken in plaats van de OnKeyPress plus nog een paar. De event handler routine die we daarvoor moeten schrijven is van de volgende vorm:

  procedure (Sender: TObject);
Dat wil zeggen dat we een echte methode van een object moeten maken (het makkelijkst is om dat een member van de TBListBox zelf te laten zijn), die ik EditBoxNotifiesListBox heb genoemd. De implementatie kun je zien in de tweede listing, en daar kom ik straks nog even op terug. In de constructor van de TBListBox component moeten we nu de OnChange event handler laten wijzen naar de EditBoxNotifiesListBox methode. En voor alle zekerheid heb ik dat ook maar met de OnEnter event handler gedaan, zodat de listbox al meteen goed gepositioneerd wordt als de gebruiker in de editbox komt.

EditBoxNotifiesListBox
De implementatie van de methode EditBoxNotifiesListBox bestaat uit een simpele while-loop die allereerst de lengte van de tekst in de editbox ophaalt (omdat we van de items in de listbox alleen maar dat stukje willen vergelijken dat net zolang is als de inhoud van de editbox), en vervolgens door de items in de listbox loopt. Dit is een lineair zoekalgoritme, dat langzaam kan worden als de lijst groot wordt. Gelukkig wordt het zoeken alleen maar uitgevoerd als de inhoud van de editbox veranderd, dus valt het potentieel snelheidprobleem wel mee (maar de volgende keer zullen we zien dat er ook een snellere manier is om de listbox goed te positioneren). De volledige source code van het TBListBox component is te zien in onderstaande listing:

  unit tblistbx;
  interface
  uses
    Classes, Controls, StdCtrls;

  type
    TBListBox = class(TWinControl)
      private
        FListBox: TListBox;
        FEditBox: TEdit;
      private
        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.
       }
      protected
        function GetItems: TStrings;
        procedure SetItems(NewItems: TStrings);
      public
        constructor Create(AOwner: TComponent); override;
        procedure SetBounds(ALeft, ATop, AWidth, AHeight: Integer); override;
      published
        property Font;
        property Items: TStrings read GetItems write SetItems;
      end {TBListBox};

  procedure Register;

  implementation
  uses
    SysUtils;

  constructor TBListBox.Create(AOwner: TComponent);
  begin
    inherited Create(AOwner);
    FListBox := TListBox.Create(Self);
    FListBox.Parent := Self;
    FListBox.ParentFont := True;
    FListBox.Sorted := True;
    FEditBox := TEdit.Create(Self);
    FEditBox.Parent := Self;
    FEditBox.ParentFont := True;
    SetBounds(Left,Top,80,120)
  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);
    FEditBox.SetBounds(0,0,Width,FontHeight+Box);
    FListBox.SetBounds(0,FontHeight+Box+Bar,Width,Height-(FontHeight+Box+Bar));
    FEditBox.OnChange := EditBoxNotifiesListbox;
    FEditBox.OnEnter := EditBoxNotifiesListbox
  end {SetBounds};

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

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

  procedure TBListBox.EditBoxNotifiesListbox(Sender: TObject);
  var
    len,index: Integer;
  begin
    len := Length(FEditBox.Text);
    index := 0;
    while (index < FListBox.Items.Count) and // lineair search = slow...
          (CompareText(FEditBox.Text,
           Copy(FListBox.Items[index],1,len)) > 0) do Inc(index);
    FListBox.ItemIndex := index
  end {EditBoxNotifiesListBox};

  procedure Register;
  begin
    RegisterComponents('DrBob42', [TBListBox])
  end {Register};

  end.
Dit component is eenvoudig te installeren (bijvoorbeeld in de Delphi User Components) en te gebruiken in Delphi toepassingen. Als we dan de listbox vullen met een lijst namen van ontwikkelomgevingen of technieken (zoals C, C#, C++, C++Builder, Delphi, JBuilder, Kylix, MIDAS, Pascal en VisiBroker), dan kan ik door C++B in te tikken eindelijk slimmer zoeken dan met de normale listbox die alleen tot de eerste C kan zoeken:

Kylix
De volgende keer wil ik dit component op een tweetal punten uitbreiden. Allereerst wil ik even kijken naar het zoekalgoritme (de EditBoxNotifiesListBox routine), maar ik wil vooral eens proberen of dit component zomaar zonder problemen in Kylix zal werken. Waarschijnlijk niet, al was het maar vanwege de TWinControl die voor zover ik gehoord heb een andere naam zou hebben in Kylix. Ik zal dan ook aandacht besteden aan enkele praktische zaken betreffende het migreren van custom componenten van Delphi naar Kylix, en het eindresultaat zal in ieder geval een single-source crossplatform TBListBox component voor zowel Delphi and Kylix zijn.
Mocht iemand nog vragen, opmerkingen of suggesties hebben, dan hoor ik die het liefst via .


This webpage © 2001-2006 by webmaster drs. Robert E. Swart (aka - www.drbob42.com). All Rights Reserved.