FMX : Recherche dans un TListView

Utilisation de l’évènement OnFilter

Serge Girard

Le composant TListView permet d’ajouter très facilement une boite de recherche par l’intermédiaire de sa propriété SearchVisible. Sans autre ligne de code, la recherche s’effectue sur tous les textes (même invisibles) de la liste. Toutefois cette recherche est établie selon le principe : « accepter l’affichage si un des textes contient le filtre sans tenir compte de la casse ». L’évènement OnFilter permet de modifier ce principe.

Mon objectif est de montrer comment utiliser cet évènement, d’en démonter le mécanisme et de fournir des moyens supplémentaires pour des recherches encore plus ciblées.

Commentez

- Titre : FMX : Recherche dans un TListView

- Auteur : Serge Girard

- Parution : 9 juillet 2020

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2020 Serge Girard. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Bases de l’utilisation de TListView

- I-A. Les différentes apparences

- I-B. Apparence personnelle (custom)

- I-C. L’apparence dynamique

- I-D. Le remplissage avec les LiveBindings

- I-E. La boite de recherche

- II. L’évènement OnFilter

- II-A. Cas d’utilisation simple, une seule zone texte

- II-B. Cas d’utilisation avec deux zones texte

- II-B-1. Mécanisme de l’évènement

- II-B-2. Ébauche de solution

- II-C. Nombre d’objets de type texte d’un élément de liste

- II-D. Rechercher sur une zone particulière

- III. Rendre plus « générique » la méthode

- III-A. Première ébauche

- III-A-1. Utilisation

- III-B. Le coin de l’ « expert »

- III-B-1. Utilisation

- IV. Conclusion

- V. Remerciements

I - Bases de l’utilisation de TListView

On trouve beaucoup de vidéos et d’aides sur ce composant, mais souvent sans s’attarder sur une utilisation plus poussée. Pour repousser ces manques, j’ai, moi aussi, beaucoup écrit à son propos dans d’autres tutoriels :

- Mettre de la couleur dans un TListView ;

- Personnaliser un TListView : ajouter des pieds de groupes ;

- TListView apparence dynamique et images.

Et ai communiqué sur mon blog :

- Obtenir une liste groupée avec les Livebindings ;

- Obtenir une liste groupée avec les Livebindings. Suite ;

- Obtenir une liste groupée avec les Livebindings. Finalisation ?

- ListView FMX, entêtes et pieds de groupes : Suite et Fin !

- Un problème de ListView avec application de style enfin résolu ;

- Modifier la hauteur (et accessoirement d'autres propriétés) de la boite de recherche d'une liste.

Les différentes sections qui suivent vont surtout mettre en exergue ce qui pourrait influer sur la recherche au sein des éléments de la liste.

Ce chapitre n’est qu’un survol du TListView. Vous trouverez une documentation relativement complète des possibilités de ce composant dans le docwiki d'Embarcadero.

I-A - Les différentes apparences

La plupart des apparences proposées ne contiennent que l’élément texte :

(cliquez ici pour aggrandir l'image)

TAppearanceNames avec une seule zone texte

ListItem, ListItemDelete, ListItemShowCheck

ImageListItemRightButton, ImageListItemRightButtonDelete, ImageListItemRightButtonShowCheck

ImageListItem, ImageListItemDelete, ImageListItemShowCheck

ou offrent une zone détail en supplément :

(cliquez ici pour aggrandir l'image)

TAppearanceNames avec une zone détail en supplément.

ImageListItemBottomDetail, ImageListItemBottomDetailShowCheck

ListItemRightDetail, ListItemRightDetailDelete, ListItemRightDetailShowCheck

ImageListItemBottomDetailRightButton, ImageListItemBottomDetailRightButtonShowCheck

I-B - Apparence personnelle (custom)

Cette apparence prédéfinie est particulière, applicable également aux apparences d’entête (HeaderAppearance) et de pied (FooterAppearance) de groupe. Elle serait équivalente à l’apparence ImageListItemBottomDetailRightButtonShowCheck certains éléments étant non visibles par défaut.

La recherche ne s’effectuera jamais sur les entêtes et pieds de groupes. Si, toutefois, vous vouliez faire une recherche sur le texte contenu dans ceux-ci, la solution consistera à utiliser une apparence dynamique et ajouter un TTextObjectAppearance.

I-C - L’apparence dynamique

Apparue plus tardivement (Delphi Berlin 10.1), cette apparence permet un design beaucoup plus personnalisé et de mettre, ainsi, plus de deux zones de texte.

Pour plus d’informations sur l’apparence dynamique, le plus simple est de commencer par lire ce que contient le Docwiki Embarcadero à ce sujet à cette adresse.

Très succinctement, puisque ce n’est pas le sujet principal, après avoir sélectionné pour la propriété ItemAppearance la valeur DynamicAppearance, il est possible d’ajouter des objets spécifiques (ou plutôt des apparences d’objets) les uns à la suite des autres.

Cinq types d’objets sont possibles :

Exemple d'apparence dynamique en conception

(cliquez ici pour aggrandir l'image)

TTextObjectAppearance

Un objet texte, le seul qui nous intéressera dans le cadre de ce tutoriel

TImageObjectAppearance

Un objet image

TAccessoryObjectAppearance

Un objet accessoire, trois types : More, Checkmark, Detail

TTextButtonObjectAppearance

Un objet bouton avec texte

TGlyphButtonObjectAppearance

Un objet bouton, trois types : Add, Delete, CheckBox

Certains de ces objets ne sont visibles que si la liste est en mode édition. Le terme de ce mode est trompeur, vous ne pourrez pas pour autant, modifier les textes comme il serait possible de le faire dans une grille.

Une fois les objets ajoutés, avec un peu d’adresse, il est possible d’arranger les tailles, positions et alignements de ceux-ci.

I-D - Le remplissage avec les LiveBindings

Qui dit TListView sous-entend le plus souvent un grand nombre de données, contrairement à un TListBox. L’éditeur de liaison nous permet de l’alimenter facilement.

Dans les projets illustrant ce tutoriel, j’utiliserai essentiellement un composant TClientDataSet pour accéder aux données de deux fichiers situés dans le répertoire ($Samples)\Data : Biolife.xml et Customers.xml.

Sélectionnez la liste et utilisez le menu contextuel (option : lier visuellement) pour réaliser les liens entre liste et données. Ce n’est l’affaire que d’une ou deux minutes pour obtenir une liste liée aux données si votre table est ouverte.

Concepteur Livebindings projet RechercheBase

(cliquez ici pour aggrandir l'image)

Cela ne veut pas dire que vous ne pourrez pas faire de recherche dans une liste remplie par code. Juste qu’il est très facile de remplir une liste sans code grâce aux LiveBindings.

Vous retrouverez également de nombreux tutoriels plus orientés Livebindings sur mon site.

I-E - La boite de recherche

Pour illustrer ce chapitre, un projet nommé RechercheBasique : téléchargeable ici.

Ajouter une boite de recherche à un TListView est hyper facile puisqu’il suffit d’activer cette fonctionnalité en cochant la propriété SearchVisible de celle-ci.

(cliquez ici pour aggrandir l'image)

Lorsqu’il n’y a qu’un seul élément, la question de savoir sur quel texte sera faite une recherche ne se pose même pas. Par contre, lorsque ces éléments contiennent plus d’une zone comportant du texte, la recherche, par défaut, sera faite sur l’ensemble de ces objets texte (visibles ou non).

Pratique dans la plupart des cas cet effet peut se révéler gênant dans certains cas.

Deux illustrations à ce propos :

- dans ma liste de clients je veux retrouver les clubs d’un pays (i.e. le Venezuela), taper seulement « ven » (vous noterez au passage que la recherche n’est pas sensible à la casse), me fournit six résultats alors qu’un seul était attendu. Bien sûr taper « vene » me fournira la bonne réponse.

(cliquez ici pour aggrandir l'image)

Mais, si je cache la zone pays (je vous rappelle que la recherche se fait même sur les éléments cachés), vous avouerez que l’utilisateur sera mal renseigné.

- second exemple, j’affiche aussi la description de l’animal. Objectif avoué : obtenir tous les poissons dont le nom contient « blu ».

(cliquez ici pour aggrandir l'image)

Vous admettrez que Cabezon ne rentre pas vraiment dans ces critères.

Bien sûr, encore une fois, augmenter mes critères de recherche, rechercher « blue » fera disparaître Cabezon de la liste.

En conclusion de cette petite démonstration (téléchargeable ici) ce qu’il nous faut, c’est un moyen de pouvoir affiner nos critères de recherche. Par exemple :

- que celle-ci soit sensible à la casse ;

- que celle-ci ne sélectionne l’élément que s’il commence par la valeur recherchée ;

- que la sélection ne soit faite que sur quelques éléments, mais pas tous ;

- et cætera.

C’est là que va intervenir la codification dans l’évènement OnFilter de TListView.

Pour ne pas déroger au principe de Pareto (loi des 80-20) dans au moins 80 % des cas où vous aurez mis une possibilité de recherche dans une liste il n’y aura pas besoin de personnaliser cette recherche. La suite de ce tutoriel tente de répondre aux 20 % restants.

II - L’évènement OnFilter

Note au lecteur

À partir de maintenant, une fois établi que la recherche ne se fait que dans le texte contenu dans les éléments de type TTextObjectAppearance, dans le chapitre qui suit, objet et objet texte seront des synonymes.

Intéressons-nous aux paramètres de cet évènement :

Nom

Type

Sender

TObject

Cela va de soi, il s’agit de l’objet à qui appartient l’évènement, donc un TListView

const AFilter

String

Le texte à rechercher

const AValue

String

Le point délicat ! D’où vient cette valeur et quid en cas de multiples objets contenant du texte ?

var Accept

Boolean

Variable qui va indiquer si l’on accepte ou pas l’élément selon la condition que l’on indiquera dans le code.

II-A - Cas d’utilisation simple, une seule zone texte

Pour illustrer l’utilisation de l’évènement, intéressons-nous à une recherche de début de texte.

procedure TForm1.ListView1Filter(Sender: TObject; const AFilter, AValue: string; var Accept: Boolean);
begin
Accept:=AFilter.IsEmpty OR AValue.StartsWith(AFilter,false);
// le second paramètre de StartsWith permet d’indiquer la sensibilité
// à la casse
end;

(cliquez ici pour aggrandir l'image)

Simplissime non ?

Vous retrouverez ce code dans le projet RechercheOnFilter téléchargeable ici.

II-B - Cas d’utilisation avec deux zones texte

Prenons maintenant le cas d’un affichage de type ImageListItemBottomDetail :

(cliquez ici pour aggrandir l'image)

Le problème principal est de déterminer comment est fourni AValue.

II-B-1 - Mécanisme de l’évènement

Pour étudier ce mécanisme, il suffit de poser un point d’arrêt au sein de la fonction et de contrôler le contenu de la constante.

(cliquez ici pour aggrandir l'image)

Première constatation, l’évènement est fréquemment levé, en fait à chaque lecture d’un objet texte. Ainsi, successivement, le contenu de la constante AValue sera-t-il la valeur de l’objet text puis de l’objet detail.

Successivement ? Non, pas tout à fait, si le premier objet testé satisfait la condition alors le second objet n’est pas testé et le programme passe tout de suite à l’élément de liste suivant.

II-B-2 - Ébauche de solution

A priori, il n’y a aucun moyen de savoir quel objet est en cours de dessin (du moins de manière simple). Introduire une sorte de compteur d’objets testés fut donc ma première approche.

Ajout compteur

var
MainForm: TMainForm;
ItemObjectNumber : word;

Initialisation compteur

initialization
ItemObjectNumber:=0;
finalization

end.

Reste à charge de coder l’incrémentation et la réinitialisation de ce compteur au sein de l’évènement OnFilter.

OnFilter

procedure TMainForm.LV2ObjetsFilter(Sender: TObject; const AFilter,
AValue: string; var Accept: Boolean);
begin
if AFilter.IsEmpty  then Exit; // pas de recherche
inc(ItemObjectNumber);         // incrémentation
// tests en fonction du mode
if RBTexte.IsChecked then Accept := (ItemObjectNumber=1) AND UpperCase(AValue).Contains(UpperCase(AFilter));
if RBDetail.IsChecked then Accept :=(ItemObjectNumber=2) AND UpperCase(AValue).Contains(UpperCase(AFilter));
// réinitialisation si le nombre d’objets texte est atteint
// ou si la condition est réalisée (Accept:=true)
if (ItemObjectNumber=2) OR Accept then ItemObjectNumber:=0;
end;

Dans ce contexte (toute apparence à base de ListItemDetail ou plus exactement ne contenant que deux objets de type texte) cette solution est fonctionnelle.

(cliquez ici pour aggrandir l'image)

Elle peut même être applicable à de plus grands nombres d’objets de type texte, pour peu de bien maîtriser les diverses valeurs de la variable ItemObjectNumber.

En « puriste », je n’y vois que quelques bémols :

1. Le fait d’avoir à renseigner le nombre d’objets maximum ItemObjectNumber=2 ;

2. Connaître l’ordre dans lequel seront affichés les objets de l’élément Accept := (ItemObjectNumber=1) AND ... ;

3. Le fait d’être obligé d’utiliser une variable globale (le compteur d’objets).

II-C - Nombre d’objets de type texte d’un élément de liste

Le premier bémol peut, avec un peu d’expertise, être contourné en introduisant une variable privée (voire une propriété) calculée avant une quelconque opération de recherche sur la liste.

Pour les apparences dites « classiques », cf. I.A, le nombre d’objets est déductible facilement en recherchant dans le type d’apparence ( <liste>.ItemAppearance.ItemAppearance ) s’il contient le texte « detail » ou non.

nombredobjets:=1;
if LowerCase(aList.ItemAppearance.ItemAppearance).Contains('detail')
then Inc(nombredobjets);

Pour une apparence dynamique, il va falloir vérifier au sein de la collection d’objets associée.

nombredobjets:=0;
if aList.ItemAppearance.ItemAppearance='DynamicAppearance'
then begin // apparence dynamique      DynApp:=TDynamicAppearance(aList.ItemAppearanceObjects.ItemObjects);
for appObj in DynApp.ObjectsCollection do
begin
if (appobj is TAppearanceObjectItem) AND
(TAppearanceObjectItem(AppObj).Appearance is TTextObjectAppearance)
then inc(nombredobjets);
end;
end ;

Pour pouvoir accéder à la collection d’objets, il est impératif d’ajouter dans les clauses d’utilisation (uses) l’unité FMX.ListView.DynamicAppearance.

(cliquez ici pour aggrandir l'image)

Le tout peut être intégré dans une fonction plus générale :

Nombre d’objets texte

uses FMX.ListView.DynamicAppearance;
...

function TMainForm.HowManyTextObjects(const aList: TListView): integer;
var DynApp : TDynamicAppearance;
appObj : TCollectionItem;
begin
result:=0;
if aList.ItemAppearance.ItemAppearance='DynamicAppearance'
then begin
DynApp:=TDynamicAppearance(aList.ItemAppearanceObjects.ItemObjects);
for appObj in DynApp.ObjectsCollection do
begin
if (appobj is TAppearanceObjectItem) AND
(TAppearanceObjectItem(AppObj).Appearance is TTextObjectAppearance)
then inc(result);
end;
end
else begin
result:=1;
if LowerCase(aList.ItemAppearance.ItemAppearance).Contains('detail')
then Inc(result);
end;
end;

II-D - Rechercher sur une zone particulière

Par zone, j’entends selon le nom de l’objet texte.

D’où l’importance de nommer ces objets plutôt que de laisser les noms par défaut : Text1…Textn, mais surtout, faites-le avant d’établir les liaisons !

(cliquez ici pour aggrandir l'image)

Je n’ai pas encore trouvé de solution pour utiliser les noms de colonnes de la table liée. Même si cela doit être possible, je gage que cela compliquerait énormément le code.

Une fois l’astuce d’obtention des objets d’une liste d’apparence dynamique connue il est simple de retrouver les noms des objets de la collection, la propriété  AppearanceObjectName.

procedure GetTextObjectNames(aList : TListView);
var DynApp : TDynamicAppearance;
appObj : TCollectionItem;
begin
if aList.ItemAppearance.ItemAppearance='DynamicAppearance'
then begin
DynApp:=TDynamicAppearance(Alist.ItemAppearanceObjects.ItemObjects);
for appObj in DynApp.ObjectsCollection do
begin
if appobj is TAppearanceObjectItem then
if TAppearanceObjectItem(AppObj).Appearance is TTextObjectAppearance
then begin
Memo1.lines.Add(TAppearanceObjectItem(AppObj).AppearanceObjectName);
end;
end;
end
else begin
Memo1.Lines.Add('text');
if LowerCase(aList.ItemAppearance.ItemAppearance).Contains('detail')
then Memo1.Lines.Add('detail');
end;
end;

(cliquez ici pour aggrandir l'image)

Pour peu de stocker ces noms des zones qui doivent être filtrées dans une liste, j’ai ainsi la réponse à mon deuxième bémol du chapitre II.B.2 en remplaçant

Accept := (ItemObjectNumber=1) AND ... par une instruction qui recherchera si l’objet en cours fait partie de cette liste ListeDesZonesATester.Contains(nomdelazoneencours) ;.

Encore un peu nébuleux ? Tout va se dévoiler dans le prochain chapitre.

Passez quand même un peu de temps sur ce second projet exemple, téléchargeable ici, pour voir le comportement des recherches.

III - Rendre plus « générique » la méthode

Pourquoi rendre plus générique la méthode ? Tout simplement parce qu’ainsi stockée dans une unité à part, elle pourra être utilisée dans d’autres formes d’une application ou, même, d’autres applications.

Reprenons les besoins, il nous faut :

1. Pouvoir spécifier un mode de recherche. Par défaut le texte de recherche doit être contenu (Contains). Dans le chapitre II.A j’ai fait la recherche sur le début de valeur (StartsWith). En plus de cette possibilité, d’autres fonctions sont envisageables comme l’égalité (=) ou la recherche en fin de valeur (EndsWith) :

2. Il faut également pouvoir indiquer si la recherche sera sensible ou non à la casse ;

3. Intégrer le compteur d’objets tel que j’ai pu le définir au chapitre II.B ;

4. Faire en sorte d’obtenir le nombre d’objets de type texte cf. chapitre II.C ;

5. Enfin, la possibilité de rechercher sur une ou plusieurs zones de type texte doit faire partie de l’arsenal. Pour cela j’aurais besoin de deux listes, une contenant tous les noms d’objets de type texte d’un élément de liste, l’autre pour contenir le nom des différents objets que l’on veut prendre en compte (chapitreII.D).

III-A - Première ébauche

Ma première approche a été de créer une nouvelle classe que je nommerai TSearchInList pour surclasser la recherche originelle.

Pour répondre au point numéro 1, je vais utiliser une énumération TSearchMode.

Énumération des modes de recherche.

TSearchMode = (smContains, smStartwith, smEndwith, smEqual);

Ma nouvelle classe sera composée de deux parties :

- une partie privée.

Membres

Types

ParentList

TListView

Le parent de la classe, la liste appelante.

Fields

TList<String>

Liste des objets texte

CurIndice

SmallInt

Index de l’objet texte en cours

- une partie publique.

Membres

Types

Mode

TSearchMode

Le mode de recherche

CaseSensitive

Boolean

La sensibilité à la casse

TestFields(1)

TList<String>

La liste des noms des objets texte à tester

Constructor

-

Le constructeur de la classe

Destructor

-

Le destructeur de cette classe

Accept

function

Fonction à deux arguments aValue, aFilter renvoyant le résultat du test.

Déclaration de la classe.

TSearchinList = Class(TObject)
strict private
ParentList : TListView;
Fields : TList<String>;
CurIndice : smallint;
public
TestFields : TList<String>;
Mode : TSearchMode;
CaseSensitive : Boolean;
constructor Create(AOwner : TListView);
destructor  Destroy; override;
function Accept(const aFilter, aValue: string) : Boolean;
end;

L’unité de définitions des apparences dynamiques est nécessaire.

uses FMX.ListView.DynamicAppearance;

constructor Create(AOwner : TListView);

Le constructeur va se charger d’initialiser nombre d’éléments dont :

- le mode de recherche par défaut (smContains) ;

- la création des différentes listes Fields et TestFields ;

- le chargement de la liste (Fields) contenant les noms des objets texte trouvés ;

- et, en dernier lieu, la variable CurIndice sera mise à zéro.

Constructeur.

constructor TSearchinList.Create(AOwner: TListView);
var DynApp : TDynamicAppearance;
appObj : TCollectionItem;
begin
inherited Create;
Parent:=AOwner;
CurIndice:=0;
Mode:=smContains;
TextObjectsNames:=TList<String>.Create;
// TestFields sera une liste insensible à la casse
Objects2Test:=TList<String>.Create(TComparer<String>.Construct(
function(const s1, s2: String): Integer
begin
Result := CompareText(s1, s2) ;
end));
// Remplissage de la liste des noms des TTextObjectAppearance
if Parent.ItemAppearance.ItemAppearance='DynamicAppearance'
then begin
DynApp:=TDynamicAppearance(Parent.ItemAppearanceObjects.ItemObjects);
for appObj in DynApp.ObjectsCollection do
begin
if appobj is TAppearanceObjectItem then
if TAppearanceObjectItem(AppObj).Appearance is TTextObjectAppearance
then TextObjectsNames.Add(TAppearanceObjectItem(AppObj).AppearanceObjectName);
end;
end
else begin
TextObjectsNames.Add('text');
if LowerCase(Parent.ItemAppearance.ItemAppearance).Contains('detail')
then begin
TextObjectsNames.Add('detail');
end;
end;
CaseSensitive:=False;
end;

Notez le petit truc supplémentaire, au niveau de la liste des noms des objets à tester (FObjects2Test), l’ajout d’un comparateur permettant l’insensibilité à la casse des éléments.

Objects2Test:=TList<String>.Create(TComparer<String>.Construct(
function(const s1, s2: String): Integer
begin
Result := CompareText(s1, s2) ;
end));

destructor Destroy; override;

Le destructeur aura pour tâche de détruire les listes créées et, par précaution(2), tout autre objet qui aurait pu être créé auparavant.

Constructeur.

destructor TSearchinList.Destroy;
begin
TextObjectsNames.Free;
Objects2Test.Free;
inherited Destroy;
end;

function Accept(const aFilter, aValue: string) : Boolean;

La fonction Accept est, en quelque sorte, le cœur de la classe puisque cette fonction va surcharger la fonction de base utilisée par le TSearchBox « conventionnel ».

Fonction Accept.

function TSearchinList.Accept(const AFilter, AValue: string): Boolean;
var AVal, AFil, test : String;
begin
if AFilter.IsEmpty then Exit(True);
Result:=False;
// si aucun objet texte spécifique demandé alors tous les objets
if (Objects2Test.Count=0)
OR (Objects2Test.Indexof(TextObjectsNames[CurIndice])>-1)
then begin
AVal:=AValue;
AFil:=AFilter;
// sensibilité à la casse
if not casesensitive then
begin
AVal:=LowerCase(AValue);
AFil:=LowerCase(AFilter);
end;
// mode de recherche
case Self.Mode of
smContains  : Result:= AVal.Contains(AFil);
smStartwith : Result:= AVal.StartsWith(AFil);
smEndwith   : Result:= AVal.EndsWith(AFil);
smEqual     : Result:= AVal=AFil;
end;
end;
if Result then CurIndice:=0      // trouvé
else inc(CurIndice);   // prochain objet
if CurIndice>TextObjectsNames.Count-1 then CurIndice:=0; // tous les objets texte ont été testés
end;

III-A-1 - Utilisation

1. Ajouter l’unité SearchInListView à la liste des unités utilisées (partie interface).

interface

uses
…
SearchInListView;

2. Déclarer une variable, privée ou publique, de type TSearchInList.

private
{ Déclarations privées }
Search : TSearchInList;

3. Créer la variable lors de l’évènement OnCreate de la forme.

onCreate

procedure TForm1.FormCreate(Sender: TObject);
begin
Search:=TSearchInList.Create(ListView1);
// Case Sensitive False; par défaut
end;

4. Codifier l’évènement OnFilter de la liste.

onFilter

procedure TForm1.ListViewFilter(Sender: TObject; const AFilter,
AValue: string; var Accept: Boolean);
begin
Accept:=Search.Accept(AFilter,Avalue);
end;

C’est prêt, il ne reste plus qu’à fournir à l’objet les propriétés en fonction de la demande

Exemples :

- changer la sensibilité à la casse

Search.CaseSensitive:=chkCasse.IsChecked;

- changer les zones de recherche

Search.TestFields.Clear;
Search.TestFields.Add('company');
Search.TestFields.Add('country');

- changer le mode de recherche

Search.Mode:=TSearchMode.smStartwith;

En fin de programme, ne pas oublier de détruire la variable de type TSearchInList créée pour éviter toute fuite de mémoire.

Clôture

procedure TMainForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
Search.Free;
end;

(cliquez ici pour aggrandir l'image)

Vous retrouverez l’application de cette unité dans le projet ListViewGenericSearch1 partie d’un ensemble de sources téléchargeables ici.

Ce programme ne fonctionnera pas pour les mobiles (FormClose non pris en compte) ni pour toute version prenant en charge la gestion de mémoire ARC (free devrait être remplacé par disposeof).

III-B - Le coin de l’ « expert »

Le seul inconvénient, encore que minime, à l’ébauche présentée c’est qu’il faut déclarer une variable de type TSearchInList pour chacune des listes d’une unité pour peu, bien sûr, que nous ayons plusieurs TListView sur une même forme et que nous voulions implémenter cette fonctionnalité pour chacun de ces composants. Sans oublier, par la suite, de supprimer les variables créées.

C’est en reprenant mes divers exemples que j’ai pu constater ce problème. C’est à ce stade que mes autres recherches sur ce composant m’ont fait envisager une solution à base d’interface.

Pour ce faire, je suis reparti de la classe TSearchinList présentée chapitre III.A.

J’en ai extrait la partie interface au sens strict du terme pour en faire une première unité.

SearchListViewInterface

unit SearchListViewInterface;

interface

uses System.SysUtils, System.Types, System.Classes,
System.Generics.Collections;

type
TSearchMode = (smContains, smStartwith, smEndwith, smEqual);

ISearchInListView = interface
['{0AF62378-366E-4AED-9918-28F8BA5859A8}']

function Accept(const AFilter, AValue: string): Boolean;
function GetTextObjectsNames : TList<String>;
function GetTestFields: TList<String>;
procedure SetTestFields(Value: TList<String>);
function GetMode: TSearchMode;
procedure SetMode(Value: TSearchMode);
function GetCaseSensitive: Boolean;
procedure SetCaseSensitive(Value: Boolean);
property TextObjectsNames : TList<String> read GetTextObjectsNames;        property TestObjectsTextName : TList<String> read GetTestFields write SetTestFields;
property Mode: TSearchMode read GetMode write SetMode;
property CaseSensitive: Boolean read GetCaseSensitive write SetCaseSensitive;
end;

implementation

end.

Puis j’ai créé une unité, une sorte d’adaptateur, qui va être utilisée pour surclasser le composant TListView.

Ne vous étonnez pas des similitudes de code au niveau des procédures et fonctions, pour partie déjà ébauchées chapitre III.A.

Cette unité me servira à adjoindre l’interface au composant TListView.

TListView = Class(FMX.ListView.TListView, ISearchInListView)

Et ajouter les propriétés nécessaires.

interface

unit ListViewSearchAdapter;

interface
uses System.SysUtils, System.Types, System.Classes,
System.Generics.Collections, System.Generics.Defaults,
FMX.ListView, SearchListViewInterface;

Type
// surclassement de TListView, ajout de l’interface
TListView = Class(FMX.ListView.TListView, ISearchInListView)
strict private
FCurrent: SmallInt;
FObjectsNames : TList<String>;
private
FMode: TSearchMode;
FCaseSensitive: Boolean;
FObjects2Test : TList<String>;
public
// ajout des éléments de l’interface
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
function Accept(const AFilter, AValue: string): Boolean;
function GetTextObjectsNames : TList<String>;
function GetTestFields: TList<String>;
procedure SetTestFields(Value: TList<String>);
function GetMode: TSearchMode;
procedure SetMode(Value: TSearchMode);
function GetCaseSensitive: Boolean;
procedure SetCaseSensitive(Value: Boolean);
property Objects2Test : TList<String> read GetTestFields write SetTestFields;
procedure InitObjectsName;

end;

C’est à dessein que le nom du composant restera identique à l’original.

Cependant, notez que la déclaration de la classe se fait en indiquant spécifiquement l’unité contenant TListView

TListView = Class(FMX.ListView.TListView, ISearchInListView)

et non, comme vous pourriez en avoir l’habitude quand l’on veut « hacker » un composant pour accéder à des propriétés privées

THackListView = class(TListView)

En préalable il va falloir ajouter quelques unités dans la liste des unités à utiliser

implementation

implementation

uses FMX.ListView.Types,
FMX.ListView.Appearances,
FMX.ListView.DynamicAppearance;

constructor Create(AOwner: TComponent); override;

Toujours le même objectif, initialiser les propriétés c’est-à-dire les deux listes FObjectsNames et FObjects2Test équivalentes des deux listes Fields et TestFields de mon ébauche III.A, ainsi que les propriétés Current, CaseSensitive et Mode

constructor TListView.Create(AOwner: TComponent);
begin
inherited;
FObjectsNames:=TList<String>.Create;
FObjects2Test:=TList<String>.Create(TComparer<String>.Construct(
function(const s1, s2: String): Integer
begin
Result := CompareText(s1, s2) ;
end));
FCurrent:=0;
FMode:=smContains;
FCaseSensitive:=False;
end;

destructor Destroy; override;

Rien de nouveau, il faut supprimer la mémoire allouée par les objets (les listes) créés.

destructeur

destructor TListView.Destroy;
begin
FreeAndNil(FObjectsNames);
FreeAndnil(FObjects2Test);
inherited;
end;

Les propriétés

Toute propriété, définie dans l’unité de l’interface nécessite un lecteur et, si besoin, un scribe (getter, setter).

// CaseSensitive
function TListView.GetCaseSensitive: Boolean;
begin
Result:=FCaseSensitive;
end;

procedure TListView.SetCaseSensitive(Value: Boolean);
begin
FCaseSensitive:=Value;
end;

// Mode
function TListView.GetMode: TSearchMode;
begin
Result:=FMode;
end;

procedure TListView.SetMode(Value: TSearchMode);
begin
FMode := Value;
end;

procedure TListView.SetTestFields(Value: TList<String>);
begin
if not Assigned(FObjects2Test) then
FObjects2Test := TList<string>.Create;
FObjects2Test := Value;
end;

function TListView.GetTestFields: TList<String>;
begin
result := FObjects2Test;
end;

function TListView.GetTextObjectsNames: TList<String>;
begin
result:= FObjectsNames;
end;

procedure InitObjectsName;

C’est la petite nouvelle par rapport à l’ébauche. Son but, recenser les noms des objets texte.

Dans l’ébauche ce code se trouvait dans le constructeur.

InitObjectsNames

procedure TListView.InitObjectsName;
var
DynApp: TDynamicAppearance;
appObj: TCollectionItem;
begin
if not Assigned(FObjectsNames) then
FObjectsNames := TList<String>.Create;
// obtenir les éléments texte
if ItemAppearance.ItemAppearance = 'DynamicAppearance' then
begin
if EditMode
then  DynApp := TDynamicAppearance(ItemAppearanceObjects.ItemEditObjects)
else  DynApp := TDynamicAppearance(ItemAppearanceObjects.ItemObjects);
for appObj in DynApp.ObjectsCollection do
begin
if appObj is TAppearanceObjectItem then
if TAppearanceObjectItem(appObj).Appearance is TTextObjectAppearance
then
FObjectsNames.Add(TAppearanceObjectItem(appObj).AppearanceObjectName);
end;
end
else
begin
FObjectsNames.Add('text');
if LowerCase(ItemAppearance.ItemAppearance).Contains('detail')
then FObjectsNames.Add('detail');
end;
end;

Elle ne sera appelée qu’une seule fois, à la première utilisation de la fonction Accept.

function Accept(const AFilter, AValue: string): Boolean;

Cette fonction ressemble beaucoup à celle de l’ébauche si ce n’est l’initialisation de la liste des noms des objets texte si celle-ci n’a pas été encore remplie et le nom d’une propriété qui a changé (CurIndice est devenue FCurrent).

implémentation

function TListView.Accept(const AFilter, AValue: string): Boolean;
var
AVal, AFil : String;
begin
// récupére le nom des objets TTextObjectAppearance dans la liste
if FObjectsNames.Count=0 then InitObjectsName;
if AFilter.IsEmpty then Exit(True);
Result:=False;
// si aucun objet texte spécifique demandé alors tous les objets
if (Objects2Test.Count=0)
OR (Objects2Test.Indexof(FObjectsNames[FCurrent])>-1)
then begin
AVal := AValue;
AFil := AFilter;
if not FCaseSensitive then
begin
AVal := LowerCase(AValue);
AFil := LowerCase(AFilter);
end;
case FMode of
smContains : Result := AVal.Contains(AFil);
smStartwith: Result := AVal.StartsWith(AFil);
smEndwith  : Result := AVal.EndsWith(AFil);
smEqual    : Result := AVal = AFil;
end;
end;
if Result then
FCurrent := 0
else begin
inc(FCurrent);
if FCurrent = (FObjectsNames.Count) then  FCurrent := 0;
end;
end;

Les sources des deux unités se retrouvent dans le dossier compressé de l’application (ListViewGenericSearch2) téléchargeable ici.

III-B-1 - Utilisation

1. Déclarer l’utilisation des unités

interface

uses
…
FMX.ListView, SearchListViewInterface, ListViewSearchAdapter;

Il est important de déclarer ces unités dans la partie interface, mais surtout, après l’utilisation de l’unité FMX.ListView pour que le surclassement opère.

1. Dans le code, l’obligation restante est de codifier l’évènement OnFilter de la liste.

// Codification de l’évènement OnFilter de la liste
procedure TForm1.ListView1Filter(Sender: TObject; const AFilter,
AValue: string; var Accept: Boolean);
begin
// appel de la fonction Accept de l’interface
Accept:=ListView1.Accept(AFilter,Avalue);
end;

Ensuite, selon les choix utilisateur, on interviendra sur les propriétés. Ci-dessous quelques exemples :

exemples

// Changement de sensibilité à la casse (exemples)
ListView1.SetCaseSensitive(True);
ListView1.SetCaseSensitive(False);

// Changement de mode (exemples)
ListView1.SetMode(TSearchMode.smContains);
ListView1.SetMode(TSearchMode.smStartwith);
ListView1.SetMode(TSearchMode.smEqual);
ListView1.SetMode(TSearchMode.smEndwith);

// Objets à tester (exemples)
ListView1.Objects2Test.Add('text');
ListView1.Objects2Test.Add('detail');
ListView1.Objects2Test.Add('country');

// plusieurs objets (exemple)
ListView1.Objects2Test.AddRange(['city','country']);

Tout ceci est rassemblé dans une petite application (ListViewGenericSearch2) que vous pourrez retrouver ici.

IV - Conclusion

L’objectif de ce tutoriel était de démontrer les mécanismes de la recherche de TListView ainsi que les moyens de customiser celle-ci. Les différentes étapes permettant de comprendre la mise en œuvre et d’en apprendre un peu plus sur le surclassement et les interfaces.

Je le répète, dans la plupart des cas vous n’aurez pas besoin de surclasser ainsi votre TListView.

Perspectives :

- il serait sympathique de pouvoir mettre en exergue le texte recherché au sein des éléments, en utilisant les propriétés de TTextLayout ce devrait être envisageable (voir chapitre VII.C de ce tutoriel). Cependant un gros écueil se trouve sur la route, la recherche ne redessine pas l’élément de la liste, mais, plutôt, l’occulte. Si, toutefois, je réussissais à le faire plus tard je ne manquerais pas de le communiquer sur mon blog ;

- bien que différent de TListView, il doit être possible d’utiliser la même technique pour un TListBox, peut-être le sujet d’un nouveau tutoriel.

V - Remerciements

Je remercie tous les intervenants qui ont pu m’aider à peaufiner chaque partie lors de mes appels à l’aide sur le forum. Ayez, comme moi, une pensée chaleureuse pour l’équipe rédactionnelle en particulier à Malick, les correcteurs techniques gaby277, tourlourou comme les correcteurs grammaticaux ClaudeLeloup sans qui ce tutoriel ne pourrait être sous vos yeux.

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

Retour à l'article

(1)Une liste en prévision du besoin d’une recherche sur plus d’un objet texte.

(2)Je vous rappelle qu’il s’agit d’une ébauche, après réflexion l’override me semble inutile.