How to make a combo box with fulltext search autocomplete support?
Asked Answered
D

6

34

I would like a user to be able to type in the second or third word from a TComboBox item and for that item to appear in the AutoSuggest dropdown options

For example, a combo box contains the items:

  • Mr John Brown
  • Mrs Amanda Brown
  • Mr Brian Jones
  • Mrs Samantha Smith

When the user types "Br" the dropdown displays:

  • Mr John Brown
  • Mrs Amanda Brown
  • Mr Brian Jones

and when the user types "Jo" the dropdown displays:

  • Mr John Brown
  • Mr Brian Jones

The problem is that the AutoSuggest functionality only includes items in the dropdown list that begin with what the user has input and so in the examples above nothing will appear in the dropdown.

Is it possible to use the IAutoComplete interface and/or other related interfaces to get around this issue?

Declivous answered 27/2, 2012 at 14:21 Comment(12)
Good question, I'm planning to use something like that in the future. (For an emailaddress control). But I'm afraid this isn't possible with a standard TComboBox.Piping
what is the difference between "from the start" and "not from the start" in this case (technically)?Hal
You need something like this: #7696575Simmonds
Does the IAutoComplete interface support fulltext search ? - NoTented
I don't think that Jens link answers this question. The answer gives an example of mid string AutoAppend functionality in contrast to AutoSuggest functionality that I needDeclivous
We have resolved this by using a dataset and parsing it, and add the result to another StringList, which is assigned. It needed some work-arounds but is working.Vow
The mid string suggestion feature was in a linked article in the answer @JensMühlenhoff suggested. Unfortunately delphi3000.com (which was the target) seems dead now.Bireme
Doesn't is this similar? #2012708Nonu
@EMBarbosa, it would be, but IAutoComplete interface does not support fulltext search, it works only with the text from the beginning. The only way is IMHO to implement it by yourself. Even messages like CB_FINDSTRING doesn't count with the string matching in elsewhere than from on the beginning.Tented
@Tented Oh. I thought that changing IAutoComplete2 option to include ACO_AUTOSUGGEST, it would do that... thanks.Nonu
I suggest you to investigate TCnProcListComboBox implementation from CnPack. CnPack have a combo that do this, but showing procedures and functions of the opened .pas file. I'm not sure where they implemented that, so I am not putting this as an answer. Try this.Nonu
@EMBarbosa, good catch, but you will see there a custom control, not the TComboBox.Tented
T
35

The following example uses the interposed class of the TComboBox component. The main difference from the original class is that the items are stored in the separate StoredItems property instead of
the Items as usually (used because of simplicity).

The StoredItems are being watched by the OnChange event and whenever you change them (for instance by adding or deleting from this string list), the current filter will reflect it even when the combo
list is dropped down.

The main point here is to catch the WM_COMMAND message notification CBN_EDITUPDATE which is being sent whenever the combo edit text is changed but not rendered yet. When it arrives, you just search through the StoredItems list for what you have typed in your combo edit and fill the Items property with matches.

For text searching is used the ContainsText so the search is case insensitive. Forgot to mention,
the AutoComplete feature has to be turned off because it has its own, unwelcomed, logic for this purpose.

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, StrUtils, ExtCtrls;

type
  TComboBox = class(StdCtrls.TComboBox)
  private
    FStoredItems: TStringList;
    procedure FilterItems;
    procedure StoredItemsChange(Sender: TObject);
    procedure SetStoredItems(const Value: TStringList);
    procedure CNCommand(var AMessage: TWMCommand); message CN_COMMAND;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    property StoredItems: TStringList read FStoredItems write SetStoredItems;
  end;

type
  TForm1 = class(TForm)
    ComboBox1: TComboBox;
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

constructor TComboBox.Create(AOwner: TComponent);
begin
  inherited;
  AutoComplete := False;
  FStoredItems := TStringList.Create;
  FStoredItems.OnChange := StoredItemsChange;
end;

destructor TComboBox.Destroy;
begin
  FStoredItems.Free;
  inherited;
end;

procedure TComboBox.CNCommand(var AMessage: TWMCommand);
begin
  // we have to process everything from our ancestor
  inherited;
  // if we received the CBN_EDITUPDATE notification
  if AMessage.NotifyCode = CBN_EDITUPDATE then
    // fill the items with the matches
    FilterItems;
end;

procedure TComboBox.FilterItems;
var
  I: Integer;
  Selection: TSelection;
begin
  // store the current combo edit selection
  SendMessage(Handle, CB_GETEDITSEL, WPARAM(@Selection.StartPos),
    LPARAM(@Selection.EndPos));
  // begin with the items update
  Items.BeginUpdate;
  try
    // if the combo edit is not empty, then clear the items
    // and search through the FStoredItems
    if Text <> '' then
    begin
      // clear all items
      Items.Clear;
      // iterate through all of them
      for I := 0 to FStoredItems.Count - 1 do
        // check if the current one contains the text in edit
        if ContainsText(FStoredItems[I], Text) then
          // and if so, then add it to the items
          Items.Add(FStoredItems[I]);
    end
    // else the combo edit is empty
    else
      // so then we'll use all what we have in the FStoredItems
      Items.Assign(FStoredItems)
  finally
    // finish the items update
    Items.EndUpdate;
  end;
  // and restore the last combo edit selection
  SendMessage(Handle, CB_SETEDITSEL, 0, MakeLParam(Selection.StartPos,
    Selection.EndPos));
end;

procedure TComboBox.StoredItemsChange(Sender: TObject);
begin
  if Assigned(FStoredItems) then
    FilterItems;
end;

procedure TComboBox.SetStoredItems(const Value: TStringList);
begin
  if Assigned(FStoredItems) then
    FStoredItems.Assign(Value)
  else
    FStoredItems := Value;
end;

procedure TForm1.FormCreate(Sender: TObject);
var
  ComboBox: TComboBox;
begin
  // here's one combo created dynamically
  ComboBox := TComboBox.Create(Self);
  ComboBox.Parent := Self;
  ComboBox.Left := 10;
  ComboBox.Top := 10;
  ComboBox.Text := 'Br';

  // here's how to fill the StoredItems
  ComboBox.StoredItems.BeginUpdate;
  try
    ComboBox.StoredItems.Add('Mr John Brown');
    ComboBox.StoredItems.Add('Mrs Amanda Brown');
    ComboBox.StoredItems.Add('Mr Brian Jones');
    ComboBox.StoredItems.Add('Mrs Samantha Smith');
  finally
    ComboBox.StoredItems.EndUpdate;
  end;

  // and here's how to assign the Items of the combo box from the form 
  // to the StoredItems; note that if you'll use this, you have to do
  // it before you type something into the combo's edit, because typing 
  // may filter the Items, so they would get modified
  ComboBox1.StoredItems.Assign(ComboBox1.Items);
end;    

end.
Tented answered 27/2, 2012 at 20:46 Comment(1)
It seems to me like your code has an ownership issue. TComboBox.SetStoredItems sometimes takes owneship of Value, and sometimes not. That makes it very awkward to use. (But not impossible, since you can determine what will happen in each case.)Marleen
D
4

This code was quite good actually, I just fixed bug with handling messages when combo is dropped down, some minor interactions with TComboBox behavior and made it a little user-friendlier. To use it just invoke InitSmartCombo after filling the Items list.

TSmartComboBox is drop in replacement for TComboBox, if you invoke InitSmartCombo it behaves as smart combo, otherwise it acts as standard TComboBox

unit SmartCombo;

interface

uses stdctrls,classes,messages,controls,windows,sysutils;

type
  TSmartComboBox = class(TComboBox)
    // Usage:
    //   Same as TComboBox, just invoke InitSmartCombo after Items list is filled with data.
    //   After InitSmartCombo is invoked, StoredItems is assigned and combo starts to behave as a smart combo.
    //   If InitSmartCombo is not invoked it acts as standard TComboBox, it is safe to bulk replace all TComboBox in application with TSmartComboBox
  private
    FStoredItems: TStringList;
    dofilter:boolean;
    storeditemindex:integer;
    procedure FilterItems;
    procedure StoredItemsChange(Sender: TObject);
    procedure SetStoredItems(const Value: TStringList);
    procedure CNCommand(var AMessage: TWMCommand); message CN_COMMAND;
  protected
    procedure KeyPress(var Key: Char); override;
    procedure CloseUp; override;
    procedure Click; override;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    property StoredItems: TStringList read FStoredItems write SetStoredItems;
    procedure InitSmartCombo;
  end;

implementation

procedure TSmartComboBox.KeyPress(var Key: Char);    // combo dropdown must be done in keypress, if its done on CBN_EDITUPDATE it messes up whole message processing mumbo-jumbo
    begin
      inherited;
      if dofilter and not (ord(key) in [13,27]) then begin
        if (items.Count<>0) and not droppeddown then SendMessage(Handle, CB_SHOWDROPDOWN, 1, 0)   // something matched -> dropdown combo to display results
      end;
    end;

procedure TSmartComboBox.CloseUp;     // ugly workaround for some wierd combobox/modified code interactions
var x:string;
    begin
      if dofilter then begin
        if (items.count=1) and (itemindex=0) then text:=items[itemindex]
        else if ((text<>'') and (itemindex<>-1) and (text<>items[itemindex])) or ((text='') and(itemindex=0)) then begin
          storeditemindex:=itemindex;
          x:=text;
          itemindex:=items.indexof(text);
          if itemindex=-1 then text:=x;
        end
        else storeditemindex:=-1;
      end;
      inherited;
    end;

procedure TSmartComboBox.Click;       // ugly workaround for some weird combobox/modified code interactions
    begin
      if dofilter then begin
        if storeditemindex<>-1 then itemindex:=storeditemindex;
        storeditemindex:=-1;
      end;
      inherited;
    end;

procedure TSmartComboBox.InitSmartCombo;
    begin
      FStoredItems.OnChange:=nil;
      StoredItems.Assign(Items);
      AutoComplete := False;
      FStoredItems.OnChange := StoredItemsChange;
      dofilter:=true;
      storeditemindex:=-1;
    end;

constructor TSmartComboBox.Create(AOwner: TComponent);
    begin
      inherited;
      FStoredItems := TStringList.Create;
      dofilter:=false;
    end;

destructor TSmartComboBox.Destroy;
    begin
      FStoredItems.Free;
      inherited;
    end;

procedure TSmartComboBox.CNCommand(var AMessage: TWMCommand);
    begin
      // we have to process everything from our ancestor
      inherited;
      // if we received the CBN_EDITUPDATE notification
      if (AMessage.NotifyCode = CBN_EDITUPDATE) and dofilter then begin
        // fill the items with the matches
        FilterItems;
      end;
    end;

procedure TSmartComboBox.FilterItems;
var
  I: Integer;
  Selection: TSelection;
    begin
      // store the current combo edit selection
      SendMessage(Handle, CB_GETEDITSEL, WPARAM(@Selection.StartPos), LPARAM(@Selection.EndPos));

      // begin with the items update
      Items.BeginUpdate;
      try
        // if the combo edit is not empty, then clear the items
        // and search through the FStoredItems
       if Text <> '' then begin
          // clear all items
          Items.Clear;
          // iterate through all of them
          for I := 0 to FStoredItems.Count - 1 do begin
            // check if the current one contains the text in edit, case insensitive
            if (Pos( uppercase(Text), uppercase(FStoredItems[I]) )>0) then begin
              // and if so, then add it to the items
              Items.Add(FStoredItems[I]);
            end;
          end;
        end else begin
          // else the combo edit is empty
          // so then we'll use all what we have in the FStoredItems
          Items.Assign(FStoredItems);
        end;
      finally
        // finish the items update
        Items.EndUpdate;
      end;
      // and restore the last combo edit selection

      SendMessage(Handle, CB_SETEDITSEL, 0, MakeLParam(Selection.StartPos, Selection.EndPos));
    end;

procedure TSmartComboBox.StoredItemsChange(Sender: TObject);
    begin
      if Assigned(FStoredItems) then
      FilterItems;
    end;

procedure TSmartComboBox.SetStoredItems(const Value: TStringList);
    begin
      if Assigned(FStoredItems) then
        FStoredItems.Assign(Value)
      else
        FStoredItems := Value;
    end;

procedure Register;
begin
  RegisterComponents('Standard', [TSmartComboBox]);
end;

end.
During answered 7/5, 2017 at 0:33 Comment(3)
I' using Delphi7. Where I can find TSelection object?Therapsid
There is no TSelection object in Delphi7. Insted of its properties, I use two integer values: wStartPos, wEndPos, whcich I place into SendMessage of FilterItems procedure. Like that: SendMessage(Handle, CB_GETEDITSEL, WPARAM(wStartPos), LPARAM(wEndPos));Therapsid
Note that CB_GETEDITSEL takes the integers by pointer, so you need to use SendMessage(Handle, CB_GETEDITSEL, WPARAM(@wStartPos), LPARAM(@wEndPos)) instead.Incandescent
P
3

Thanks for the heart! With a little reworking, I think that is quite right.

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, StrUtils, ExtCtrls;

type
  TComboBox = class(StdCtrls.TComboBox)
  private
    FStoredItems: TStringList;
    procedure FilterItems;
    procedure StoredItemsChange(Sender: TObject);
    procedure SetStoredItems(const Value: TStringList);
    procedure CNCommand(var AMessage: TWMCommand); message CN_COMMAND;
  protected
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    property StoredItems: TStringList read FStoredItems write SetStoredItems;
  end;

type
  TForm1 = class(TForm)
    procedure FormCreate(Sender: TObject);
  private
  public
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

{}constructor TComboBox.Create(AOwner: TComponent);
    begin
      inherited;
      AutoComplete := False;
      FStoredItems := TStringList.Create;
      FStoredItems.OnChange := StoredItemsChange;
    end;

{}destructor TComboBox.Destroy;
    begin
      FStoredItems.Free;
      inherited;
    end;

{}procedure TComboBox.CNCommand(var AMessage: TWMCommand);
    begin
      // we have to process everything from our ancestor
      inherited;
      // if we received the CBN_EDITUPDATE notification
      if AMessage.NotifyCode = CBN_EDITUPDATE then begin
        // fill the items with the matches
        FilterItems;
      end;
    end;

{}procedure TComboBox.FilterItems;
    type
      TSelection = record
        StartPos, EndPos: Integer;
      end;
    var
      I: Integer;
      Selection: TSelection;
      xText: string;
    begin
      // store the current combo edit selection
      SendMessage(Handle, CB_GETEDITSEL, WPARAM(@Selection.StartPos), LPARAM(@Selection.EndPos));

      // begin with the items update
      Items.BeginUpdate;
      try
        // if the combo edit is not empty, then clear the items
        // and search through the FStoredItems
        if Text <> '' then begin
          // clear all items
          Items.Clear;
          // iterate through all of them
          for I := 0 to FStoredItems.Count - 1 do begin
            // check if the current one contains the text in edit
    //      if ContainsText(FStoredItems[I], Text) then
            if Pos( Text, FStoredItems[I])>0 then begin
              // and if so, then add it to the items
              Items.Add(FStoredItems[I]);
            end;
          end;
        end else begin
          // else the combo edit is empty
          // so then we'll use all what we have in the FStoredItems
          Items.Assign(FStoredItems)
        end;
      finally
        // finish the items update
        Items.EndUpdate;
      end;

      // and restore the last combo edit selection
      xText := Text;
      SendMessage(Handle, CB_SHOWDROPDOWN, Integer(True), 0);
      if (Items<>nil) and (Items.Count>0) then begin
        ItemIndex := 0;
      end else begin
        ItemIndex := -1;
      end;
      Text := xText;
      SendMessage(Handle, CB_SETEDITSEL, 0, MakeLParam(Selection.StartPos, Selection.EndPos));

    end;

{}procedure TComboBox.StoredItemsChange(Sender: TObject);
    begin
      if Assigned(FStoredItems) then
        FilterItems;
    end;

{}procedure TComboBox.SetStoredItems(const Value: TStringList);
    begin
      if Assigned(FStoredItems) then
        FStoredItems.Assign(Value)
      else
        FStoredItems := Value;
    end;

//=====================================================================

{}procedure TForm1.FormCreate(Sender: TObject);
    var
      ComboBox: TComboBox;
      xList:TStringList;
    begin

      // here's one combo created dynamically
      ComboBox := TComboBox.Create(Self);
      ComboBox.Parent := Self;
      ComboBox.Left := 8;
      ComboBox.Top := 8;
      ComboBox.Width := Width-16;
//    ComboBox.Style := csDropDownList;

      // here's how to fill the StoredItems
      ComboBox.StoredItems.BeginUpdate;
      try
        xList:=TStringList.Create;
        xList.LoadFromFile('list.txt');
        ComboBox.StoredItems.Assign( xList);
      finally
        ComboBox.StoredItems.EndUpdate;
      end;

      ComboBox.DropDownCount := 24;

      // and here's how to assign the Items of the combo box from the form
      // to the StoredItems; note that if you'll use this, you have to do
      // it before you type something into the combo's edit, because typing
      // may filter the Items, so they would get modified
      ComboBox.StoredItems.Assign(ComboBox.Items);
    end;

end.
Pily answered 2/12, 2012 at 16:54 Comment(2)
You should probably explain what you changed, and why.Nonu
FilterItems was changed.Therapsid
D
1

Added code for Unicode users.

Actually, it was only tested in Korean :(

Applied function

  • Prevents bugs that appear when OnExit or DropDown occurs while entering Unicode
  • Prevents bugs that occur when text correction or additional input after selecting an item

Modified code content

  • @Prevents a bug - typing values are appended when selecting a list while typing in Unicode
  • @Filtering is applied to each Unicode being typed if it is being entered after the end of the text.
  • @Exception handling in case of entering Unicode after selecting several texts
  • @Exception handling in case of additional input of Unicode when there is already a character in edit and the listbox is closed

Source...

unit SmartCombo;

interface

uses StdCtrls, Classes, Messages, Controls, Windows, SysUtils, StrUtils;

type
  TSmartComboBox = class(TComboBox)
    // Usage:
    // Same as TComboBox, just invoke InitSmartCombo after Items list is filled with data.
    // After InitSmartCombo is invoked, StoredItems is assigned and combo starts to behave as a smart combo.
    // If InitSmartCombo is not invoked it acts as standard TComboBox, it is safe to bulk replace all TComboBox in application with TSmartComboBox
  private
    FChar: Char; // @for UNICODE Filter
    FIgnoreChar: boolean; // @for UNICODE Edit
    FStoredItems: TStringList;
    doFilter: boolean;
    StoredItemIndex: Integer;

    procedure StoredItemsChange(Sender: TObject);
    procedure SetStoredItems(const Value: TStringList);
    procedure CNCommand(var AMessage: TWMCommand); message CN_COMMAND;
    function GetXText(var Key: Char): string;
    function GetXSelStart: Integer;
  protected
    procedure KeyPress(var Key: Char); override;

    // @Prevents a bug - typing values are appended when selecting a list while typing in Unicode
    procedure EditWndProc(var Message: TMessage); override;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;

    procedure FilterItems;
    procedure InitSmartCombo;

    property StoredItems: TStringList read FStoredItems write SetStoredItems;
  end;

implementation

function TSmartComboBox.GetXText(var Key: Char): string;
var
  tmp: string;
begin
  if (Text = '') then // empty edit box
    result := ''
  else if SelLength > 0 then // has selection
  begin
    tmp := Copy(Text, SelStart + 1, SelLength);
    result := ReplaceStr(Text, tmp, '');
  end
  else // not empty edit box and no selection
  begin
    tmp := Copy(Text, 1, SelStart);
    result := tmp + Key;
    result := result + Copy(Text, SelStart + 1, Length(Text) - SelStart);
    Key := #0;
  end;
end;

function TSmartComboBox.GetXSelStart: Integer;
begin
  // empty edit box or has selection
  if (Text = '') or (SelLength > 0) then
    result := SelStart
  else // not empty edit box and no selection
    result := SelStart + 1;
end;

procedure TSmartComboBox.KeyPress(var Key: Char);
// combo dropdown must be done in keypress, if its done on CBN_EDITUPDATE it messes up whole message processing mumbo-jumbo
var
  xSelStart: Integer;
  xText: string;
begin
  inherited;

  if Ord(Key) = 8 then
    FChar := Key;

  if doFilter and not(Ord(Key) in [8, 13, 27]) then // BackSpace, Enter, ESC
  begin
    FChar := Key;

    if DroppedDown then
      Exit;

    if Items.Count = 0 then
      Exit;

    // backup
    xSelStart := GetXSelStart;
    xText := GetXText(Key);

    // dropdown
    SendMessage(Handle, CB_SHOWDROPDOWN, 1, 0);

    if xText.IsEmpty then
      Exit;

    // restore
    Text := xText;
    SelStart := xSelStart;
  end;
end;

procedure TSmartComboBox.InitSmartCombo;
begin
  FStoredItems.OnChange := nil;
  StoredItems.Assign(Items);
  AutoComplete := False;
  FStoredItems.OnChange := StoredItemsChange;
  doFilter := True;
  StoredItemIndex := -1;
end;

constructor TSmartComboBox.Create(AOwner: TComponent);
begin
  inherited;
  FStoredItems := TStringList.Create;
  FIgnoreChar := False;
  doFilter := False;
end;

destructor TSmartComboBox.Destroy;
begin
  FStoredItems.Free;
  inherited;
end;

procedure TSmartComboBox.EditWndProc(var Message: TMessage);
var
  OldText: string;
begin
  case Message.Msg of
    WM_IME_ENDCOMPOSITION:
      begin
        OldText := Self.Text;
        inherited;
        FIgnoreChar := Self.Text = OldText;
      end;
    WM_CHAR:
      begin
        FIgnoreChar := False;
        inherited;
      end;
    WM_IME_CHAR:
      begin
        if FIgnoreChar then
          FIgnoreChar := False
        else
          inherited;
      end;
  else
    inherited;
  end;
end;

procedure TSmartComboBox.CNCommand(var AMessage: TWMCommand);
begin
  // we have to process everything from our ancestor
  inherited;

  // @Filtering is applied to each Unicode being typed if it is being entered after the end of the text.
  // @If you are typing in the middle of the text, do not apply filtering to the Unicode being typed
  // (filtering is applied in units of completed Unicode characters)
  if (SelStart < Length(Text)) and (FChar = #0) then
    Exit;

  // if we received the CBN_EDITUPDATE notification
  if (AMessage.NotifyCode = CBN_EDITUPDATE) and doFilter then
  begin
    // fill the items with the matches
    FilterItems;
  end;

  FChar := #0;
end;

procedure TSmartComboBox.FilterItems;
var
  I: Integer;
  Selection: TSelection;
begin
  // store the current combo edit selection
  SendMessage(Handle, CB_GETEDITSEL, WPARAM(@Selection.StartPos), LPARAM(@Selection.EndPos));

  // begin with the items update
  Items.BeginUpdate;
  try
    // if the combo edit is not empty, then clear the items
    // and search through the FStoredItems
    if Text <> '' then
    begin
      // clear all items
      Items.Clear;
      // iterate through all of them
      for I := 0 to FStoredItems.Count - 1 do
      begin
        // check if the current one contains the text in edit, case insensitive
        if ContainsText(FStoredItems[I], Text) then
        begin
          // and if so, then add it to the items
          Items.Add(FStoredItems[I]);
        end;
      end;
    end
    else
    begin
      // else the combo edit is empty
      // so then we'll use all what we have in the FStoredItems
      Items.Assign(FStoredItems);
    end;
  finally
    // finish the items update
    Items.EndUpdate;
  end;

  // and restore the last combo edit selection
  SendMessage(Handle, CB_SETEDITSEL, 0, MakeLParam(Selection.StartPos, Selection.EndPos));
end;

procedure TSmartComboBox.StoredItemsChange(Sender: TObject);
begin
  if Assigned(FStoredItems) then
    FilterItems;
end;

procedure TSmartComboBox.SetStoredItems(const Value: TStringList);
begin
  if Assigned(FStoredItems) then
    FStoredItems.Assign(Value)
  else
    FStoredItems := Value;
end;

end.
Dishonest answered 19/12, 2020 at 0:51 Comment(4)
Thank you for sharing the good code. @TentedDishonest
I am getting errors on undeclared identifiers—SelLength, SelStart, Items, AutoComplete, BeginUpdate, Clear, Add, Assign, EndUpdate, DroppedDown, and Count. I am not really sure if these errors pertains to TSelection mentioned by @IvanZ above https://mcmap.net/q/437875/-how-to-make-a-combo-box-with-fulltext-search-autocomplete-support. Help please..Nebiim
I got the errors corrected now. I forgot that this is a unit, not a form actually. So, I created a unit only and save it in the project. Then, I created another form where I put my Combobox with a list coming from my DB. I am yet to try it now but having trouble how to invoke InitSmartCombo from this unit. I already applied use unit. I could not invoke it using this — InitSmartCombo after loading this from DB. Is there a proper way to invoke a unit from another form? Help please...Nebiim
Here's what I did in the form I created with combobox, but no luck: procedure TForm1.FormActivate(Sender: TObject); begin cmb1.Items.Clear; cds1.First; while not cds1.eof do begin cmb1.Items.Add(cds1.FieldByName('Name').DisplayText); cds1.Next; end; InitSmartCombo; end;Nebiim
C
0

In handled OnDropDown event setup TComboBox items filtered by:

from external full string list. Or better write your own TComboBox(TCustomComboBox) descendant.

Cyrenaica answered 27/2, 2012 at 20:29 Comment(1)
How does OnDropDown event has something to do with typing in the combo box ?Jamshid
T
0

I've modified the TLama's component and created one for my specific use case. I'm going to leave the source here if someone has similar needs.

It's basically a TComboBox which only allows it's text to be set to an actual valid value.

It behaves like a normal ComboBox with it's Style set to csDropDownList until you start typing in it. After typing, it'll search the StoredItems values and populate the Items with it.

When the component is loaded, it'll assign the Items defined in design time to the StoredItems property. Further modifications to the list in runtime have to be done using the StoredItems property.

The only issues that've found and was not able to fix is that for some reason it only works if the parent of the ComboBox is the actual form (only tested in XE2, may not be a problem in other versions).
I've added some code that pulls the ComboBox out of any nesting while keeping it's relative position, but it loses any other parenting feature.

unit uSmartCombo;

interface

uses
  Vcl.StdCtrls, Classes, Winapi.Messages, Controls;

type
  TSmartComboBox = class(TComboBox)
  private
    FStoredItems: TStringList;
    procedure FilterItems;
    procedure CNCommand(var AMessage: TWMCommand); message CN_COMMAND;
    procedure RedefineCombo;
    procedure SetStoredItems(const Value: TStringList);
    procedure StoredItemsChange(Sender: TObject);
  protected
    procedure KeyPress(var Key: Char); override;
    procedure CloseUp; override;
    procedure Loaded; override;
    procedure DoExit; override;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    property StoredItems: TStringList read FStoredItems write SetStoredItems;
  end;

procedure Register;

implementation

uses
  SysUtils, Winapi.Windows, Vcl.Forms;

procedure Register;
begin
   RegisterComponents('Standard', [TSmartComboBox]);
end;

constructor TSmartComboBox.Create(AOwner: TComponent);
begin
   inherited;
   FStoredItems := TStringList.Create;
   FStoredItems.OnChange := StoredItemsChange;
end;

destructor TSmartComboBox.Destroy;
begin
   FStoredItems.Free;
   inherited;
end;

procedure TSmartComboBox.DoExit;
begin
   inherited;
   RedefineCombo;
end;

procedure TSmartComboBox.Loaded;
var LParent: TWinControl;
    LPoint: TPoint;
begin
   inherited;
   if Items.Count > 0 then
      FStoredItems.Assign(Items);
   AutoComplete := False;
   Style := csDropDownList;

   // The ComboBox doesn't behave properly if the parent is not the form.
   if not (Parent is TForm) then
   begin
      LParent := Parent;
      while (not (LParent is TForm)) and Assigned(LParent) do
         LParent := LParent.Parent;
      LPoint := ClientToParent(Point(0,0), LParent);
      Parent := LParent;
      Left   := LPoint.X;
      Top    := LPoint.Y;
      BringToFront;
   end;
end;

procedure TSmartComboBox.RedefineCombo;
var S: String;
begin
   if Style = csDropDown then
   begin
      if ItemIndex <> -1 then
         S := Items[ItemIndex];

      Style := csDropDownList;
      Items.Assign(FStoredItems);

      if S <> '' then
         ItemIndex := Items.IndexOf(S);
   end;
end;

procedure TSmartComboBox.SetStoredItems(const Value: TStringList);
begin
   if Assigned(FStoredItems) then
      FStoredItems.Assign(Value)
   else
      FStoredItems := Value;
end;

procedure TSmartComboBox.StoredItemsChange(Sender: TObject);
begin
   if Assigned(FStoredItems) then
   begin
      RedefineCombo;
      Items.Assign(FStoredItems);
   end;
end;

procedure TSmartComboBox.KeyPress(var Key: Char);
begin
   if CharInSet(Key, ['a'..'z']) and not (Style = csDropDown) then
   begin
      DroppedDown := False;
      Style := csDropDown;
   end;
   inherited;
   if not (Ord(Key) in [13,27]) then
      DroppedDown := True;
end;

procedure TSmartComboBox.CloseUp;
begin
   if Style = csDropDown then
      RedefineCombo;
   inherited;
end;

procedure TSmartComboBox.CNCommand(var AMessage: TWMCommand);
begin
   inherited;
   if (AMessage.Ctl = Handle) and (AMessage.NotifyCode = CBN_EDITUPDATE) then
      FilterItems;
end;

procedure TSmartComboBox.FilterItems;
var I: Integer;
    Selection: TSelection;
begin
   SendMessage(Handle, CB_GETEDITSEL, WPARAM(@Selection.StartPos), LPARAM(@Selection.EndPos));

   Items.BeginUpdate;
   Try
      if Text <> '' then
      begin
         Items.Clear;
         for I := 0 to FStoredItems.Count - 1 do
            if (Pos(Uppercase(Text), Uppercase(FStoredItems[I])) > 0) then
               Items.Add(FStoredItems[I]);
      end
      else
         Items.Assign(FStoredItems);
   Finally
      Items.EndUpdate;
   End;

   SendMessage(Handle, CB_SETEDITSEL, 0, MakeLParam(Selection.StartPos, Selection.EndPos));
end;

end.
Talc answered 13/5, 2022 at 20:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.