Как создать комбо-бокс с поддержкой полнотекстового автозаполнения?
Я хотел бы, чтобы пользователь мог вводить второе или третье слово из элемента TComboBox
и для этого элемента отображаться в раскрывающихся списках AutoSuggest
Например, поле со списком содержит элементы:
- Г-н Джон Браун
- Миссис Аманда Браун
- Г-н Брайан Джонс
- Миссис Саманта Смит
Когда пользователь набирает "Br", выпадающий список отображает:
- Г-н Джон Браун
- Миссис Аманда Браун
- Г-н Брайан Джонс
и когда пользователь набирает "Jo", выпадающий список отображает:
- Г-н Джон Браун
- Г-н Брайан Джонс
Проблема заключается в том, что функциональность AutoSuggest
включает только элементы в раскрывающемся списке, которые начинаются с ввода пользователя, и поэтому в приведенных выше примерах ничего не появится в раскрывающемся списке.
Можно ли использовать интерфейс IAutoComplete
и/или другие связанные интерфейсы, чтобы обойти эту проблему?
Ответы
Ответ 1
В следующем примере используется внедренный класс компонента TComboBox
. Основное отличие от исходного класса состоит в том, что элементы хранятся в отдельном свойстве StoredItems
вместо Items
, как обычно (используется из-за простоты).
При просмотре StoredItems
происходит событие OnChange
и всякий раз, когда вы меняете их (например, добавив или удалив из этой строки список), текущий фильтр будет отражать его, даже когда список комбо-списка будет опущен.
Главное здесь - поймать сообщение 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 one combo created dynamically
ComboBox := TComboBox.Create(Self);
ComboBox.Parent := Self;
ComboBox.Left := 10;
ComboBox.Top := 10;
ComboBox.Text := 'Br';
// here 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 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 edit, because typing
// may filter the Items, so they would get modified
ComboBox1.StoredItems.Assign(ComboBox1.Items);
end;
end.
Ответ 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 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 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 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 edit, because typing
// may filter the Items, so they would get modified
ComboBox.StoredItems.Assign(ComboBox.Items);
end;
end.
Ответ 3
В обработанной настройке события OnDropDown элементы TComboBox, отфильтрованные:
из внешнего полного списка строк. Или лучше напишите свой собственный потомок TComboBox (TCustomComboBox).
Ответ 4
Этот код был довольно хорош на самом деле, я просто исправил ошибку с обработкой сообщений, когда комбо было сброшено, некоторые незначительные взаимодействия с поведением TComboBox и сделали его немного более удобным для пользователя. Чтобы использовать его, просто вызовите InitSmartCombo после заполнения списка Items.
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.