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 :
|
TAppearanceNames avec une seule zone texte |
|
ListItem, ListItemDelete, ListItemShowCheck |
|
ImageListItemRightButton, ImageListItemRightButtonDelete, ImageListItemRightButtonShowCheck |
|
ImageListItem, ImageListItemDelete, ImageListItemShowCheck |
ou offrent une zone détail en supplément :
|
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 :
|
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.
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.
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.
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 ».
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;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 :
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.
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.
var
MainForm: TMainForm;
ItemObjectNumber : word;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.
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.
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 :
- Le fait d’avoir à renseigner le nombre d’objets maximum ItemObjectNumber=
2; - Connaître l’ordre dans lequel seront affichés les objets de l’élément Accept := (ItemObjectNumber=
1)AND... ; - 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.
Le tout peut être intégré dans une fonction plus générale :
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 !
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;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 :
- 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) :
- Il faut également pouvoir indiquer si la recherche sera sensible ou non à la casse ;
- Intégrer le compteur d’objets tel que j’ai pu le définir au chapitre II.B ;
- Faire en sorte d’obtenir le nombre d’objets de type texte cf. chapitre II.C ;
- 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.
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. |
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.
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.
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 ».
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▲
-
Ajouter l’unité SearchInListView à la liste des unités utilisées (partie interface).
Sélectionnezinterfaceuses… SearchInListView; -
Déclarer une variable, privée ou publique, de type TSearchInList.
Sélectionnezprivate{ Déclarations privées }Search : TSearchInList; -
Créer la variable lors de l’évènement OnCreate de la forme.
onCreateSélectionnezprocedureTForm1.FormCreate(Sender: TObject);beginSearch:=TSearchInList.Create(ListView1);// Case Sensitive False; par défautend; - Codifier l’évènement OnFilter de la liste.
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électionnezSearch.CaseSensitive:=chkCasse.IsChecked; -
changer les zones de recherche
SélectionnezSearch.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.
procedure TMainForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
Search.Free;
end;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é.
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.
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
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.
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.
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).
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▲
- 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.
- 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 :
// 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.








