Saturday, March 03, 2012

Incremental search in a combo box

Lately, I have increased the use of combo boxes in my programs and it has come to my attention that I sorely need incremental search. A standard combo box finds strings according to the first letter only - if the user presses 'b', then the combo box finds the first string which starts with a 'b', but should the user then press 'r' (let's assume that he's looking for 'brian'), the combo box jumps to the first string which starts with 'r'. Incremental search would cause the combo box to find the first string which starts 'br'.

Obviously some functionality has to be overloaded, but it's not clear exactly what. Googling doesn't bring up many possibilities; here is one of the more complete versions which I found.

procedure TMainForm.DocEntryCBEnter(Sender: TObject);
begin
 GS := '';
 GCOUNT := 0;
 TComboBox(Sender).ItemIndex := 0;
end;

procedure TMainForm.DocEntryCBKeyPress(Sender: TObject; var Key: Char);
begin
 AutoMatch(TComboBox(Sender), Key);
end;

procedure AutoMatch(cbo: TComboBox; var keyascii: char);
var
 sbuffer: pchar;
 s: string;
 retval: longint;

begin
 getmem (sbuffer, 255);
 if Keyascii = #8 then
  begin
   dec (GCOUNT);
   s := copy (GS, 1, GCOUNT);
  end
 else
  s := copy (GS, 1, GCOUNT) + (Keyascii);

 try
  strpcopy (sbuffer, s);
  retval := sendmessage (cbo.Handle, CB_FINDSTRING, word(-1), longint (sbuffer));
  if retval <> CB_ERR then
   begin
    cbo.ItemIndex := Retval;
    GS := cbo.Text;
    Inc (GCOUNT);
    Keyascii := chr (0);
   end
  else
   begin
    messagebeep (0);
    cbo.Text := GS;
   end;
 finally
  freemem (sbuffer, 255);
 end;
end;
Whilst this code includes some nice ideas and looks impressive, it transpires that there are several lines which are inefficient and a few which are simply wrong. At first, I thought that the AutoMatch procedure ought to be part of the DocEntryCBKeyPress code, but it is written cleverly in such a way that it can handle a multitude of combo boxes - or not, as the accumulated string gs would be common to all the combo boxes, when in fact it should be separate.

The first inefficiency is creating a temporary buffer of 256 characters every time the user presses a key (this is the call to 'getmem'). Why not create a permanent buffer for the combo box in the same way that the variable gs is created? But why even do that? This buffer is only required as a parameter to the SendMessage, and it seems that the writer had only an incomplete knowledge of type casting - whilst he type casts the pchar (sbuffer) to a longint (as required for the fourth parameter of SendMessage), he could also have typecast the gs string to a pchar - thus obviating the need for sbuffer, along with its allocation and deallocation.

It also seems that the writer had never heard of string concatenation - he can simply add the new keystroke to the accumulated string (gs) without having to use the 'copy' function.

There is an error in the line which handles the 'backspace' keystroke (#8): should the first key to be pressed be backspace (however illogical this may be), gcount will logically receive the value -1, which will probably be rounded to 65535. Instant memory problems!

The other mistake which I found only became clear after using the code - let's say the user presses 'b' (whilst looking for 'brian'). The call to SendMessage will succeed; the line cbo.ItemIndex := Retval means the combo box will display the first string which starts with 'b' and then the accumulated string is set to this value. Let's say that the found string was 'Bobby'; now the accumulated string will be 'Bobby'; when the user presses 'r', the accumulated string will now be 'Bobbyr', which presumably will not match anything!

My code looks like this:
Procedure IncCBKeyPress (cb: TComboBox; var key: char; var fsaved: string);
var
 retval: longint;
 len: word;

begin
 len:= length (fsaved);
 if (Key = #8) and (len > 0)
  then setlength (fsaved, len - 1)
  else fsaved:= fsaved + key;
 retval:= sendmessage (cb.Handle, CB_FINDSTRING, -1, Longint(PChar(fsaved)));
 if retval <> CB_ERR then
  begin
   cb.ItemIndex:= Retval;
   key:= #0
  end
 else
  begin
   messagebeep (0);
   cb.itemindex:= 0;
   fsaved:= '';
  end;
end;
I've changed the variable names slightly - what was gs has now become fsaved. This is in preparation of creating a new TIncComboBox component. This code can definitely be shared between several combo boxes as the accumulated string is passed as a parameter.

1 comment:

No'am Newman said...

Raymond Chen, in his 'Old New thing' blog, says that combo boxes has supported incremental search for some time, although he doesn't state when the search was added, nor how (which updated dll does one need?).

Here's the link:
http://blogs.msdn.com/b/oldnewthing/archive/2012/10/11/10358482.aspx