Je voudrais qu'un utilisateur puisse taper le deuxième ou le troisième mot d'un élément TComboBox
et que cet élément apparaisse dans les options déroulantes AutoSuggest
Par exemple, une zone de liste déroulante contient les éléments:
Lorsque l'utilisateur tape "Br", la liste déroulante s'affiche:
et lorsque l'utilisateur tape "Jo", la liste déroulante s'affiche:
Le problème est que la fonctionnalité AutoSuggest
inclut uniquement des éléments dans la liste déroulante qui commencent par ce que l'utilisateur a entré et donc dans les exemples ci-dessus, rien n'apparaîtra dans la liste déroulante.
Est-il possible d'utiliser l'interface IAutoComplete
et/ou d'autres interfaces connexes pour contourner ce problème?
L'exemple suivant utilise la classe interposée du composant TComboBox
. La principale différence avec la classe d'origine est que les éléments sont stockés dans la propriété StoredItems
distincte au lieu de
le Items
comme d'habitude (utilisé pour des raisons de simplicité).
Les StoredItems
sont surveillés par l'événement OnChange
et chaque fois que vous les modifiez (par exemple en ajoutant ou en supprimant de cette liste de chaînes), le filtre actuel le reflétera même lorsque le combo
La liste est déroulée.
Le point principal ici est d'attraper le WM_COMMAND
notification de message CBN_EDITUPDATE
qui est envoyé chaque fois que le texte d'édition combiné est modifié mais pas encore rendu. Quand il arrive, il vous suffit de rechercher dans la liste StoredItems
ce que vous avez tapé dans votre édition combinée et de remplir la propriété Items
avec des correspondances.
Pour la recherche de texte est utilisé le ContainsText
donc la recherche est insensible à la casse. Oublié de mentionner,
la fonction AutoComplete
doit être désactivée car elle a sa propre logique, indésirable, à cet effet.
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.
Ce code était assez bon en fait, je viens de corriger un bug avec la gestion des messages lorsque le combo est abandonné, quelques interactions mineures avec le comportement de TComboBox et le rend un peu plus convivial. Pour l'utiliser, il suffit d'appeler InitSmartCombo après avoir rempli la liste des éléments.
TSmartComboBox remplace directement TComboBox, si vous appelez InitSmartCombo, il se comporte comme un combo intelligent, sinon il agit comme TComboBox standard
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.
Merci pour le coeur! Avec un peu de retouche, je pense que c'est tout à fait juste.
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.
Dans la configuration des événements OnDropDown gérés, les éléments TComboBox sont filtrés par:
à partir de la liste de chaînes complète externe. Ou mieux, écrivez votre propre descendant TComboBox (TCustomComboBox).