Recherche dans un TListView
Bonjour,
La recherche dans un TListView via la SearchBox reste une de mes bêtes noires. En effet, de base, tous les éléments texte d'un item sont pris en considération (visibles ou non) et par défaut, la recherche se fait sur un Contains sans casse. [Aparté] vous ne trouvez pas que Contains devrait avoir, comme StartsWith ou EndsWith, la possibilité d'être "case sensitive" !? [/Aparté]
Vous me rétorquerez qu'il existe un évènement OnFilter qui permet de modifier ce comportement.
Exact mais, problème : OnFilter n'indique que la valeur testée mais aucune indication de l'élément de l'item, qu'en est-il si je ne veux tester un seul élément (pour l'instant je m'arrête à un seul :mouarf:)
Donc, je repars en croisade après cette discussion.
Par rapport au bébé de la discussion (je n'aimais pas trop cette variable booléenne restrictive),
j'ai établi un premier "brouillon" (qui fonctionne) mon idée de départ, ajouter, en quelque sorte, des propriétés (TSearchInlist) à la TListView par l'intermédiaire de son TagObject.
Avantage de cette solution : la recherche devient "paramétrable" puisqu'il me suffit de changer l'objet
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
|
type
TSearchMode = (smContains, smStartwith, smEndwith);
TSearchinList = Class(TObject)
private
CurIndice : UInt8;
FieldIndice :Smallint;
MaxIndice : UInt8;
Mode : TSearchMode;
casesensitive : Boolean;
end;
TForm1 = class(TForm)
ListView1: TListView;
PrototypeBindSource1: TPrototypeBindSource;
BindingsList1: TBindingsList;
LinkFillControlToField1: TLinkFillControlToField;
procedure ListView1Filter(Sender: TObject; const AFilter, AValue: string;
var Accept: Boolean);
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
{ Déclarations privées }
S : TSearchinList;
public
{ Déclarations publiques }
end;
var
Form1: TForm1;
implementation
{$R *.fmx}
procedure TForm1.FormCreate(Sender: TObject);
begin
S:=TSearchinList.Create;
S.CurIndice:=0;
S.FieldIndice:=-1; // -1 tout les éléments
S.MaxIndice:=1; // nombre d'élements avec caption d'un item de liste
S.Mode:=smContains;
S.casesensitive:=False;
ListView1.TagObject:=S;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
S.Free;
end;
procedure TForm1.ListView1Filter(Sender: TObject; const AFilter,
AValue: string; var Accept: Boolean);
var AVal, AFil : String; // à cause de Contains je dois passer par des variables locales pour la casse :-(
begin
Accept:=True;
if AFilter.IsEmpty then Exit;
AVal:=AValue;
AFil:=AFilter;
if not TSearchInList(Listview1.TagObject).casesensitive then
begin
AVal:=LowerCase(AValue);
AFil:=LowerCase(AFilter);
end;
case S.Mode of
smContains : Accept:=((S.FieldIndice=-1) OR (S.CurIndice=S.FieldIndice)) AND AVal.Contains(AFil);
smStartwith : Accept:=((S.FieldIndice=-1) OR (S.CurIndice=S.FieldIndice)) AND AVal.StartsWith(AFil);
smEndwith : Accept:=((S.FieldIndice=-1) OR (S.CurIndice=S.FieldIndice)) AND Aval.EndsWith(AFil);
end;
if Accept then S.CurIndice:=0
else inc(S.CurIndice);
{TODO : à revoir}
if S.CurIndice>S.MaxIndice then S.CurIndice:=0; // Oui mais s'il s'agit d'un Entête ou d'un Pied ?
end;
end. |
Cela va certainement évoluer dans la journée pour avoir quelque chose de plus "portable" :
1 - Ajouter un constructor (avec initialisation des "propriétés"). À ce propos coder ça proprement <?
2 - mettre une partie du "filtre" dans l'objet
3 - retrouver le MaxIndice (nombre d'élements avec caption d'un item de liste), le constructeur me fournirait le nom de la liste
4 - plutôt que d'utiliser le FieldIndice, retrouver les noms des éléments ce qui serait plus facile
Mes autres questions
- tout d'abord ma démarche, qu'en pensez vous ?
- il serait mieux de savoir de quel élément/item il s'agit mais comment ?
- d'autres suggestions ?
Premier jus de l'unité idépendante
Voilà un premier jus
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
|
unit SearchInListView;
interface
uses System.SysUtils, System.Types, System.Classes,
System.Generics.Collections,
FMX.ListView, FMX.ListView.Types, FMX.ListView.Appearances;
type
TSearchMode = (smContains, smStartwith, smEndwith);
TSearchinList = Class(TObject)
strict private
Parent : TListView;
Fields : TList<String>;
CurIndice : smallint;
MaxIndice : SmallInt;
public
TestFields : TList<String>;
Mode : TSearchMode;
CaseSensitive : Boolean;
constructor Create(AOwner : TListView);
destructor Destroy; override;
function Accept(const AFilter, AValue: string) : Boolean;
end;
implementation
uses FMX.ListView.DynamicAppearance;
function TSearchinList.Accept(const AFilter, AValue: string): Boolean;
var AVal, AFil, test : String;
begin
Result:=True;
if AFilter.IsEmpty then Exit;
AVal:=AValue;
AFil:=AFilter;
if not Self.casesensitive then
begin
AVal:=LowerCase(AValue);
AFil:=LowerCase(AFilter);
end;
case Self.Mode of
smContains : Result:=((Self.TestFields.Count=0)
OR Self.TestFields.Contains(Self.Fields.Items[Self.CurIndice]))
AND AVal.Contains(AFil);
smStartwith : Result:=((Self.TestFields.Count=0)
OR Self.TestFields.Contains(Self.Fields.Items[Self.CurIndice]))
AND AVal.StartsWith(AFil);
smEndwith : Result:=((Self.TestFields.Count=0)
OR Self.TestFields.Contains(Self.Fields.Items[Self.CurIndice]))
AND AVal.EndsWith(AFil);
end;
if Result then Self.CurIndice:=0
else inc(Self.CurIndice);
if Self.CurIndice>Self.MaxIndice then self.CurIndice:=0;
end;
constructor TSearchinList.Create(AOwner: TListView);
var DynApp : TDynamicAppearance;
appObj : TCollectionItem;
begin
inherited Create;
Self.Parent:=AOwner;
Self.CurIndice:=0;
// Self.FieldIndice:=-1; // {TODO : plus d'un élément, une liste de numéro ou de noms ce serait mieux}
Self.MaxIndice:=-1; // nombre d'élements
Self.Mode:=smContains;
Self.Fields:=TList<String>.Create;
Self.TestFields:=TList<String>.Create;
if Self.Parent.ItemAppearance.ItemAppearance='DynamicAppearance'
then begin
DynApp:=TDynamicAppearance(Self.Parent.ItemAppearanceObjects.ItemObjects);
for appObj in DynApp.ObjectsCollection do
begin
if appobj is TAppearanceObjectItem then
if TAppearanceObjectItem(AppObj).Appearance is TTextObjectAppearance
then begin
inc(Self.MaxIndice);
Self.Fields.Add(TAppearanceObjectItem(AppObj).AppearanceObjectName);
end;
end;
end
else begin
Self.Fields.Add('text');
if LowerCase(Self.Parent.ItemAppearance.ItemAppearance).Contains('detail')
then Self.Fields.Add('detail');
end;
Self.casesensitive:=False;
end;
destructor TSearchinList.Destroy;
begin
Self.Fields.Free;
Self.TestFields.Free;
inherited Destroy;
end;
end. |
L'utilisation en est donc simplifiée puisque intégrable dans toute unité contenant un TListView
par exemple :
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
procedure TFormPaiement.FormCreate(Sender: TObject);
begin
S:=TSearchinList.Create(ListView1);
S.TestFields.AddRange(['Text4']); // recherche uniquement sur l'élément nommé 'text4'
S.Mode:=smStartWith; // commençant par le filtre
S.casesensitive:=false; // sans tenir compte de la casse
ListView1.TagObject:=S; // association à la liste
end;
procedure TFormPaiement.FormDestroy(Sender: TObject);
begin
FreeAndNil(S);
end; |
Deux points restent en obligatoires :
-
il faut associer l'objet à la liste, donc l'utilisation de TagObject de la liste est interdite pour tout autre chose - il ne faut pas oublier de libérer l'objet
À moins qu'en passant par un objet interface ces points puissent être rayés, mais question interface je ne suis certainement pas aussi bon :roll: un coup de main ?
d'autres idées/demandes sur cette recherche