FMX : Recherche dans un TListView

Utilisation de l’évènement OnFilter

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.

1 commentaire Donner une note  l'article (5)

Article lu   fois.

L'auteur

Profil ProSite personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

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 :

Et ai communiqué sur mon blog :

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 :

Image non disponible

TAppearanceNames avec une seule zone texte

ListItem, ListItemDelete, ListItemShowCheck

ImageListItemRightButton, ImageListItemRightButtonDelete, ImageListItemRightButtonShowCheck

ImageListItem, ImageListItemDelete, ImageListItemShowCheck

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

Image non disponible

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 :

Image non disponible
Exemple d'apparence dynamique en conception

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.

Image non disponible
Concepteur Livebindings projet RechercheBase

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.

Image non disponible

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.

Image non disponible

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 ».

Image non disponible

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.

 
Sélectionnez
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;
Image non disponible

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 :

Image non disponible

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.

Image non disponible

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
Sélectionnez
var
  MainForm: TMainForm;
  ItemObjectNumber : word;
Initialisation compteur
Sélectionnez
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
Sélectionnez
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.

Image non disponible

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.

 
Sélectionnez
   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.

 
Sélectionnez
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.

Image non disponible

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

Nombre d’objets texte
Sélectionnez
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 !

Image non disponible

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.

 
Sélectionnez
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;
Image non disponible

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.
Sélectionnez
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.
Sélectionnez
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.
Sélectionnez
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.

 
Sélectionnez
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.
Sélectionnez
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.
Sélectionnez
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).

     
    Sélectionnez
    interface
    
    uses
      … 
      SearchInListView;
  2. Déclarer une variable, privée ou publique, de type TSearchInList.

     
    Sélectionnez
     private
        { Déclarations privées }
        Search : TSearchInList;
  3. Créer la variable lors de l’évènement OnCreate de la forme.

    onCreate
    Sélectionnez
    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
Sélectionnez
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

     
    Sélectionnez
    Search.CaseSensitive:=chkCasse.IsChecked;
  • changer les zones de recherche

     
    Sélectionnez
    Search.TestFields.Clear;
    Search.TestFields.Add('company');
    Search.TestFields.Add('country');
  • changer le mode de recherche
 
Sélectionnez
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
Sélectionnez
procedure TMainForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
Search.Free;
end;
Image non disponible

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
Sélectionnez
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
Sélectionnez
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
Sélectionnez
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

 
Sélectionnez
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
Sélectionnez
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).

 
Sélectionnez
// 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
Sélectionnez
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
Sélectionnez
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;

Image non disponible

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
 
Sélectionnez
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.
 
Sélectionnez
// 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
Sélectionnez
// 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']);

Image non disponible

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.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   


Une liste en prévision du besoin d’une recherche sur plus d’un objet texte.
Je vous rappelle qu’il s’agit d’une ébauche, après réflexion l’override me semble inutile.

  

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 ni 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.