如何使用全文搜索自动完成支持制作组合框?

时间:2012-02-27 14:21:03

标签: delphi autocomplete autosuggest

我希望用户能够输入TComboBox项目中的第二个或第三个字词,并且该项目会显示在AutoSuggest下拉选项中

例如,组合框包含项目:

  • John Brown先生
  • 阿曼达·布朗夫人
  • Brian Jones先生
  • 萨曼莎史密斯夫人

当用户输入“Br”时,下拉列表显示:

  • John Brown先生
  • 阿曼达·布朗夫人
  • Brian Jones先生

当用户输入“Jo”时,下拉列表显示:

  • John Brown先生
  • Brian Jones先生

问题是AutoSuggest功能只包含下拉列表中以用户输入内容开头的项目,因此在上面的示例中,下拉列表中不会显示任何内容。

是否可以使用IAutoComplete接口和/或其他相关接口来解决此问题?

5 个答案:

答案 0 :(得分:32)

以下示例使用TComboBox组件的插入类。与原始类的主要区别在于,项目通常存储在单独的StoredItems属性中而不是Items中(因为简单而使用)。

OnChange事件正在监视StoredItems,每当您更改它们时(例如通过添加或删除此字符串列表),当前过滤器即使在组合时也会反映出来/>列表被删除。

这里的要点是捕获WM_COMMAND消息通知CBN_EDITUPDATE,只要组合编辑文本被更改但尚未呈现,就会发送消息。当它到达时,您只需在StoredItems列表中搜索您在组合编辑中输入的内容,并使用匹配填充Items属性。

对于文本搜索,使用ContainsText,因此搜索不区分大小写。忘了提及,AutoComplete功能必须关闭,因为它有自己的,不受欢迎的逻辑用于此目的。

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.

答案 1 :(得分:3)

这个代码实际上相当不错,我刚修复了当组合被删除时处理消息的bug,与TComboBox行为的一些小的交互,并使它变得有点用户友好。要使用它,只需在填写Items列表后调用InitSmartCombo。

TSmartComboBox正在替代TComboBox,如果你调用InitSmartCombo它就像智能组合一样,否则它就像标准的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.

答案 2 :(得分:2)

感谢你的心!经过一些改造,我认为这是非常正确的。

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.

答案 3 :(得分:0)

在处理的OnDropDown事件设置中,TComboBox项目按以下方式过滤:

来自外部完整字符串列表。或者更好地编写自己的TComboBox(TCustomComboBox)后代。

答案 4 :(得分:0)

Unicode 用户添加了代码。

实际上,它只测试了韩语:(

应用功能

  • 防止在输入 Unicode 时出现 OnExit 或 DropDown 时出现的错误
  • 防止在选择项目后文本更正或额外输入时发生的错误

修改后的代码内容

  • @防止错误 - 在输入 Unicode 时选择列表时附加输入值
  • @Filtering 应用于正在输入的每个 Unicode,如果它是在文本结尾之后输入的。
  • @选择多个文本后输入Unicode时的异常处理
  • @Exception 在编辑中已经有一个字符并且列表框关闭时额外输入 Unicode 的情况下的处理

来源...

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.