Search code examples
delphiautocompletelistboxdelphi-7

tListBox using AutoComplete navigation from other ListBox


Is there a way to use native tListBox AutoComplete navigation system but based on the items of other ListBox? So when ListBox1 focused when i type some chars items should be selected according to data from ListBox2. Both of them have same amount of items.


Solution

  • Well i tried to make it native but as far as i get it - it's impossible to combine different item-height functionality (Style=lbOwnerDrawVariable) with DataFind events handling. Ofcourse one can edit TCustomListBox.KeyPress procedure in "VCL\StdCtrls", but i dont like to dig into vcl sources. So i decided to implement AutoComplete feature by myself from a scratch. First of all i decided to index item-strings for faster search, but there are no need to make full graph (octotree... whatever...) cuz search in my case (and i have about 1k items) anyways performed nearly instantly - so cuz my list is lexicographically sorted i made indexing only for first letters...

    //...
    const
        acceptCharz=[' '..'z'];
    //...
    type
        twoWords=packed record a,b:word; end;
    //...
    var
        alphs:array[' '..'z']of twoWords;// indexes of first letters
        fs:tStringList;// list for searching in
        fnd:string;// charbuffer for search string
        tk:cardinal;// tickCounter for timing
    //...
    procedure Tform1.button1Click(Sender: TObject);// procedure where list filled with items
    var
        k,l:integer;
        h,q:char;
    begin
        //... here list-content formed ...
        if(fs<>nil)then fs.Free;
        fs:=tStringList.Create;
        // fltr is tListBox which data should be source of AutoCompletion (ListBox2)
        fs.AddStrings(fltr.Items);
        for h:=low(alphs)to high(alphs)do alphs[h].a:=$FFFF;// resetting index
        h:=#0;
        l:=fs.Count-1;
        if(l<0)then exit;
        for k:=0 to l do begin
            s:=AnsiLowerCase(fs.Strings[k]);// for case-insensetivity
            fs.Strings[k]:=s;
            if(length(s)<0)then continue;
            q:=s[1];
            if(h<>q)and(q in acceptCharz)then begin
                if(k>0)then alphs[h].b:=k-1;
                h:=q;
                alphs[h].a:=k;// this will work only with sorted data!
            end;
        end;
        if(h<>#0)then alphs[h].b:=l;
    end;
    //...
    // fl is tListBox with custom drawing and OwnerDrawVariable style (ListBox1)
    // also fl has same amount of items as fltr
    procedure Tform1.flKeyPress(Sender: TObject; var Key: Char);
    var
        n,i,k,e,l,u,m,a:integer;
        s:string;
        h:char;
        function CharLowerr(h:char):char;// make char LowerCase
        begin
            Result:=char(LoWord(CharLower(Pointer(h))));
        end;
    begin
        if(getTickCount-tk>=800)then fnd:='';// AutoComplete timeout
        tk:=getTickCount;
        h:=CharLowerr(key);// for case-insensetivity
        if(h in ['a'..'z','0'..'9',' ','-','.'])then fnd:=fnd+h;// u can add all needed chars
        if(fnd='')then exit;// if no string to find
        h:=fnd[1];// obtain first letter of search-string
        a:=alphs[h].a;// get index of first item starting with letter
        l:=alphs[h].b;// get index of last item starting with letter
        if(a=$FFFF)then exit;// if no such items
        e:=length(fnd);// get length of search-string
        u:=1;// current length of overlap
        m:=1;// max overlap
        i:=a;// index to select
        if(e>1)then for k:=a to l do begin
            s:=fs.Strings[k];
            if(length(s)<e)then continue;
            for n:=2 to e do begin// compare strings char-by-char
                if(s[n]<>fnd[n])then begin// compare failed
                    u:=n-1;
                    break;
                end else u:=n;
                if(u>m)then begin// current overlap is max
                    m:=u;
                    i:=k;
                end;
            end;
            if(u=e)or(u<m)then break;// if end of search reached
        end;
        // select needed index:
        fl.ClearSelection;
        SendMessage(fl.Handle, LB_SELITEMRANGE, 1, MakeLParam(i, i));
        fl.ItemIndex:=i;
        inherited;
    end;
    //...
    

    Yeah this code is kinda ugly, but it works fine, as i said - nearly instantly, i just can see how selection jumps thru items while i type, and i type pretty fast...

    So it was code which i wrote yesterday and all this could end here, but today i realized that it was absolutely dumb decision at whole: in my case, as i mentioned above, i have listbox with OwnerDrawVariable style, so i have custom MeasureItem and DrawItem procedures and the best decision in this kind of situation will be to turn AutoComplete property true and to fill ListBox1 with items from ListBox2. And strings needed to display could be shown anyways in DrawItem procedure. Also possible to remove ListBox2 and keep strings for displaying inside tStringList variable. So morale is - dont rush into writing boilerplates and try to think more before acting =))

    P.S. but one can use this code for some custom AutoComplete handling...