I. Introduction

Considérons que nous souhaitons faire la liste des images bmp d'un répertoire, que chaque image soit affichée, que l'on puisse cliquer sur chaque image et savoir sur quelle image l'utilisateur a cliqué. Pour cela, il est nécessaire d'utiliser un composant Image (TImage) pour chacune d'entre elles. Nous ne pouvons pas savoir lors de la conception combien d'images nous allons avoir, donc nous ne pouvons ni placer X composants Image, ni créer un tableau de X images. On pourrait évidemment utiliser un système de tableaux dynamiques. Mais en C++, et particulièrement avec C++Builder, nous avons à notre disposition deux outils simples d'utilisation : la class vector et la class TList. Nous allons utiliser la classe TList, dont l'usage est plus simple à mes yeux, bien que la classe vector ne pose pas de grosses difficultés.

Note aux débutants, plus particulièrement : personnellement, lorsque j'ai fait la découverte de cette création dynamique, je me suis mis à tout comprendre sur les pointeurs qui jusque-là me paraissaient très abstraits et dont l'utilité ne me paraissait pas évidente. Même si cela ne paraît pas très clair, il ne faut pas hésiter à s'y lancer. Pour mieux tirer parti de ce document, n'utilisez pas le Copier/Coller. Retapez les morceaux de code, c'est le seul moyen pour vraiment bien comprendre.

Par souci de simplicité, tous les noms sont gardés par défaut : Unit1.cpp, Unit1.h, Project1.cpp, Edit1, ListBox1, …

II. Listage des fichiers

Commençons donc.

Nous souhaitons tout d'abord lister les fichiers image bmp d'un répertoire, puis nous voulons les afficher. Comme nous ne savons pas exactement combien nous en aurons -cela peut aller d'une ou deux à plusieurs centaines-, nous allons utiliser le composant ScrollBox. Dans une ScrollBox, nous pouvons placer autant de composants que nous le souhaitons, des barres de défilement apparaîtront si nécessaire. Sélectionnez donc un composant TScrollBox dans la palette de composants. Il se situe sous l'onglet Supplément. Placez-le sur la fiche et agrandissez-le comme vous le souhaitez.

Maintenant, nous allons créer la fonction qui va demander un répertoire et en lire les fichiers. Cette fonction doit stocker la liste des fichiers, ce qui va être fait dans une TStringList de la classe TForm1. Donc dans la classe TForm1 du fichier en-tête Unit1.h, dans la section public, placez la déclaration suivante :

 
Sélectionnez
TStringList *ListeFichiers;

Ceci déclare uniquement un pointeur sur une TStringList. Il s'agit maintenant d'instancier (construire) cette liste. Nous allons le faire dans le constructeur de la fiche, qui doit maintenant ressembler à ceci :

 
Sélectionnez
__fastcall TForm1::TForm1(TComponent* Owner)
    : TForm(Owner)
{
    ListeFichiers = new TStringList;
}

Cette liste doit être détruite à la fin de l'éxecution de notre programme. Dans l'événement OnDestroy de la fiche, placez le code delete ListeFichiers :

 
Sélectionnez
void __fastcall TForm1::FormDestroy(TObject *Sender)
{
    delete ListeFichiers;
}

Nous allons maintenant créer la fonction qui recherchera nos fichiers. Créez une nouvelle fonction que vous nommez RechercheImagesBMP, sans arguments, qui renvoie un type void (pour cela, vous pouvez utiliser l'explorateur de classes). Si vous la créez à la main, n'oubliez pas le prototype dans le fichier Unit1.h. Voici le code de la fonction, qui est commenté en dessous :

 
Sélectionnez
void TForm1::RechercheImagesBMP()
{
    // Sélection du répertoire de recherche
    AnsiString RepertoireRecherche = "";
    SelectDirectory("Sélectionnez le répertoire de recherche des images",
                ExtractFileDrive(Application->ExeName), RepertoireRecherche);

    // On vide la liste des fichiers
    ListeFichiers->Clear();

    // Recherche des fichiers
    TSearchRec SRec;

    if (!FindFirst(RepertoireRecherche+"\\*.bmp", faAnyFile, SRec))
    do
    {
        ListeFichiers->Add(RepertoireRecherche + "\\" + SRec.Name);
    }
    while(!FindNext(SRec));

    // Libère la mémoire allouée par FindFirst pour la recherche
    FindClose(SRec);
}

Tout d'abord, l'utilisateur doit choisir dans quel répertoire il souhaite faire la recherche (notre recherche ne prendra pas en compte les sous-répertoires), ce que nous faisons grâce à la fonction SelectDirectory, son premier argument étant la légende de la boite de dialogue, le second, le répertoire racine présenté dans la boite de dialogue (ExtractFileDrive(Application->ExeName) donnera « c:\\ » normalement. En fait, ce code donne le disque sur lequel se situe notre programme) et enfin, la chaîne AnsiString dans laquelle sera placé le répertoire choisi.

Ensuite, nous vidons la liste des fichiers à l'aide de la méthode Clear de cette même liste (ListeFichiers->Clear()).

Le code TSearchRec SRec déclare une structure qui contiendra les informations sur le fichier trouvé. La recherche s'effectue ensuite grâce à la fonction FindFirst qui trouve un fichier correspondant aux critères : premier argument, répertoire et filtre ; deuxième argument, le type des fichiers recherchés ; troisième argument, la structure (que nous venons de déclarer) dans laquelle seront placés les caractéristiques du fichier. Si la fonction FindFirst à trouver un fichier (renvoie NULL), on ajoute le fichier à la liste ListeFichiers->Add(RepertoireRecherche + "\\" + SRec.Name) et on continue la recherche tant que la fonction FindNext trouve les fichiers suivants. Cette fonction n'a plus besoin que de la structure dans laquelle placer les résultats, elle connaît les autres informations qui ont été stockées par FindFirst (d'où le FindClose à la fin pour libérer la mémoire occupée par ces informations. À noter la structure en do…while et non en while tout court qui permet d'exécuter la boucle une fois si la fonction FindFirst a trouvé un fichier.

III. Affichage des images

Notre fonction est donc prête à fonctionner. Une deuxième fonction est maintenant nécessaire, cette fonction va créer la liste des TImage. C'est ici même le coeur du sujet de cet article : la création dynamique d'un nombre non connu à l'avance d'objets. En effet, lors de la conception, nous ne savons pas combien d'images il y aura dans le répertoire choisi par l'utilisateur. Pour cela, nous allons construire nos TImage une par une, à l'aide de l'opérateur new. Ensuite, nous ajouterons le pointeur sur le TImage dans une liste de pointeurs. Ainsi, nous pourrons toujours accéder à n'importe quelle image.

Déclarons donc la liste de pointeurs. Nous devrions plus exactement dire déclarons le pointeur sur notre liste de pointeurs. Pour cela, placez le code suivant dans la classe TForm1 :

 
Sélectionnez
TList *ListeImages;

Il faut maintenant instancier cette liste, comme pour notre TStringList. Donc dans notre constructeur, veillez à placer le code suivant :

 
Sélectionnez
ListeImages = new TList;

Notre constructeur doit donc ressembler à ceci :

 
Sélectionnez
__fastcall TForm1::TForm1(TComponent* Owner)
    : TForm(Owner)
{
    ListeFichiers = new TStringList;
    ListeImages = new TList;
}

Cette liste doit être détruite à la fin du programme. Pour ce faire, rajoutez l'instruction delete ListeImages dans l'événement OnDestroy de notre fiche, qui doit ressembler à ceci :

 
Sélectionnez
void __fastcall TForm1::FormDestroy(TObject *Sender)
{
    delete ListeFichiers;
    delete ListeImages;
}

Cela fait, nous pouvons nous attaquer à notre fonction. En voici l'implémentation. N'oubliez pas de placer son prototype dans votre fichier en-tête Unit1.h.

 
Sélectionnez
void TForm1::CreationListe()
{
    VideImages();

    int ImagePosition = 10;

    for (int i=0; i<ListeFichiers->Count; i++)
    {
        TImage *Image = new TImage(0);
        Image->Parent = ScrollBox1;

        Image->Top = ImagePosition;
        Image->Left = 10;

        Image->AutoSize = true;
        Image->Picture->LoadFromFile(ListeFichiers->Strings[i]);
        ListeImages->Add((void *)Image);

        ImagePosition = 10 + Image->Top + Image->Height;
    }
}

Tout d'abord, faisons appel à une fonction VideImages() que nous allons écrire plus tard, vous en comprendrez facilement l'utilité.

La variable ImagePosition (int ImagePosition = 0) va donner la position où placer l'image. En effet, nous souhaitons faire une liste visuelle des images, donc les images se succéderont dans la ScrollBox et nous devons savoir où placer une image par rapport à sa précédente.

Ensuite commence une boucle for qui va parcourir la liste des fichiers. La liste des fichiers est indicée de 0 à N-1 où N est le nombre de chaînes (représentant les chemins des fichiers), d'où notre boucle de 0 jusqu'à N-1 (i<ListeFichiers->Count). Pour chaque ligne de notre liste de fichiers (pour chaque fichier), nous créons dynamiquement l'image correspondante : TImage *Image = new TImage(0). Normalement, l'argument passé au constructeur est le Owner de l'image. Or, nous souhaitons la détruire nous-même et non juste quand le programme se terminera. (À chaque fois qu'une nouvelle recherche sera effectuée, nous supprimerons toutes ces images de la mémoire). Si nous avions donné par exemple Form1 comme Owner : TImage *Image = new TImage(Form1), la fiche essaierait de détruire cet objet TImage lorsque le programme se terminerait. Mais si nous l'avons détruite avant, lors d'une nouvelle recherche, elle essaierait de détruire un objet qui n'existe plus, ce qui entrainerait une erreur. Pour éviter cela, nous passons 0 (NULL) comme argument au constructeur. Ainsi, nous gardons la main puisque le Owner n'existe pas.

Ensuite, nous définissons le Parent de l'image, c'est-à-dire le composant dans laquelle elle se situe et qui la contiendra. Attention, sans cette ligne Image->Parent = ScrollBox1, notre Image n'apparaitra pas du tout. Ensuite, nous positionnons l'image, en laissant une marge de 10 pixels en haut et à gauche. L'attribution Image->AutoSize = true permet que quand l'image sera chargée, le composant TImage sera aggrandi au pour l'afficher en entier (des barres de défilement apparaitront dans la ScrollBox si nécessaire). L'instruction d'après charge l'image Image->Picture->Bitmap->LoadFromFile(ListeFichiers->Strings[i]). Enfin, nous ajoutons le pointeur sur l'image dans notre liste grâce à la méthode Add de notre liste ListeImages->Add((void *)Image). Cette liste stocke en fait des pointeurs sans type (ou type void) d'où (void *). La dernière instruction, ImagePosition = 10 + Image->Top + Image->Height donne la position de l'image suivante c'est-à-dire la position de l'image courante à laquelle on ajoute sa hauteur avec une marge de 10 pixels entre les images. Ainsi, notre image se situera 10 pixels en dessous de l'image précédente.

Ainsi, grâce à ce système de liste de pointeurs, nous gardons la liste de toutes les images en cours et nous pouvons les manipuler si nécessaire.

Voici donc maintenant la fonction VideImages que nous avons appelée tout à l'heure, au début de la fonction CreationListe. Cette fonction parcourt la liste des pointeurs sur les TImage et supprime chacune, une par une. Cette fonction fait les delete correspondants aux new de tout à l'heure (TImage *Image = new TImage(0);). On voit donc ici un des premiers intérêts de garder une liste des pointeurs sur les images. Ensuite, elle efface la liste des pointeurs.

 
Sélectionnez
void TForm1::VideImages()
{
    for (int i=0; i<ListeImages->Count; i++)
        delete (TImage *)ListeImages->Items[i];
    ListeImages->Clear();
}

Une boucle for permet donc de parcourir la liste des images. Chaque pointeur de cette liste est converti en un pointeur sur un TImage (n'oublions pas que notre liste contient des pointeurs void !) et l'objet pointé est supprimé. Ce code est plus synthétique que le code suivant, mais équivalent :

 
Sélectionnez
for (int i=0; i<ListeImages->Count; i++)
{
      TImage *Image;
      Image = (TImage *)ListeImages->Items[i];
      delete Image;
}

L'instruction suivante, ListeImages->Clear() vide la liste de pointeurs. Pour bien faire, il faut rajouter un appel à cette fonction dans l'événement OnDestroy de votre fiche ; sinon, les images resteront en mémoire lorsque le programme se terminera, ce qui doit être évité, car à chaque appel du programme, vous perdrez de la mémoire. Notre gestionnaire d'événement OnDestroy doit maintenant ressembler à ceci :

 
Sélectionnez
void __fastcall TForm1::FormDestroy(TObject *Sender)
{
    VideImages();
    delete ListeFichiers;
    delete ListeImages;
}

Attention à bien mettre VideImages() avant delete ListeImages, car la fonction VideImages se sert de ListeImages.

Voilà, le plus dur est fait. Maintenant, il nous suffit d'appeller la fonction RechercheImagesBMP, puis la fonction CreationListe. Placez donc un bouton sur votre fiche. Dans son gestionnaire d'événement OnClick, placez l'appel à ces deux fonctions :

 
Sélectionnez
void __fastcall TForm1::Button1Click(TObject *Sender)
{
    RechercheImagesBMP();
    CreationListe();
}

Maintenant, vous pouvez exécuter l'application. À chaque fois que l'utilisateur cliquera sur le bouton, il lui sera proposé de choisir un répertoire, puis la liste d'images sera construite.

IV. Détection des clics

Notre programme fonctionne bien. Nous pourrions le laisser comme cela. Mais nous pouvons le perfectionner, tout en montrant une nouvelle utilité de cette liste de pointeurs : nous allons détecter les clics sur les images, et lors d'un clic, nous ouvrirons l'image avec Paint. Il s'agit donc de créer le gestionnaire d'événement OnClick de chaque image. En fait, nous allons en créer un seul, commun à toutes les images et dans ce gestionnaire, nous allons être capable de savoir exactement sur quelle image on a cliqué. Ensuite, nous trouverons le fichier correspondant et nous l'ouvrirons avec Paint. Voici donc ce gestionnaire d'événement, vous devez le créer à la main, car l'inspecteur d'objet ne permet de créer que les gestionnaires d'événements de composants placés lors de la conception :

 
Sélectionnez
void __fastcall TForm1::ClicSurImage(TObject * Sender)
{
    AnsiString Fichier = "\"" + ListeFichiers->Strings[ListeImages->IndexOf((void *)Sender)] + "\"";
    ShellExecute(Handle, "open", "c:\\Program Files\\Accessoires\\MSPaint.exe", Fichier.c_str(), 0, SW_SHOW);
}

N'oubliez pas de rajouter le prototype de cette fonction dans la classe TForm1, dans le fichier Unit1.h. Cette fonction se décompose en deux instructions. La première permet de récupérer le nom du fichier correspondant à l'image cliquée. Pour savoir sur quelle image on a cliqué, nous utilisons le pointeur Sender, de type TObject*. Nous convertissons ce pointeur en un pointeur void*. Ensuite, nous l'utilisons comme argument de la méthode IndexOf de la liste d'images ListeImages qui renvoie l'indice du composant TImage sur lequel on a cliqué. La liste des fichiers étant créée dans le même ordre, on peut utiliser cette valeur comme indice du nom de fichier à récupérer, ListeFichiers->Strings[X] renvoyant le fichier d'indice X. Le nom de fichier est alors stocké dans la chaîne Fichier.

Ensuite nous faisons un appel à ShellExecute (cf. la FAQ) pour ouvrir le fichier avec Paint.

Il s'agit maintenant d'associer cette méthode au gestionnaire d'événement OnClick de chaque composant Image que nous créons dynamiquement ; nous faisons cela dans la fonction CreationListe qui doit être modifiée ainsi :

 
Sélectionnez
void TForm1::CreationListe()
{
    VideImages();

    int ImagePosition = 10;

    for (int i=0; i<ListeFichiers->Count; i++)
    {
        TImage *Image = new TImage(0);
        Image->OnClick = ClicSurImage;
        Image->Parent = ScrollBox1;

        Image->Top = ImagePosition;
        Image->Left = 10;

        Image->AutoSize = true;
        Image->Picture->Bitmap->LoadFromFile(ListeFichiers->Strings[i]);
        
        ListeImages->Add((void *)Image);

        ImagePosition = 10 + Image->Top + Image->Height;
    }
}

V. Améliorations

Voici quelques suggestions d'améliorations que vous pourrriez essayer de faire pour vous entraîner :

  • [*] Affiche des informations sur l'image (largeur, hauteur) lorsqu'on passe dessus. Pour cela, créez un gestionnaire d'événement OnMouseMove que vous assignez à la création dynamique de chaque image :
 
Sélectionnez
Image->OnMouseMove = MouseMoveSurImage;
  • Avec la fonction MouseMoveSurImage déclarée comme suit : void __fastcall MouseMoveSurImage(TObject *Sender, TShiftState Shift, int X, int Y);
    [*] Lors d'un clic droit sur l'image, un menu popup permet de choisir plusieurs options (ouvrir avec Paint, ouvrir avec le logiciel de votre choix, enregistrer sous, propriétés, …) :
 
Sélectionnez
Image->PopupMenu = PopupMenu1;
  • Il n'est pas nécessaire de créer autant de menus popup que d'images, un seul suffit que vous n'êtes pas obligé de créer dynamiquement et vous utilisez l'argument Sender pour récupérer l'image pour laquelle le menu est appelé.
    [**] Remplacez-la TList par un vector, conteneur standard de la STL. En effet, la TList est très performante mais ne permet pas le contrôle de type, ni un trop grand nombre d'éléments.

VI. À lire également

VII. Conclusion

Dans ce tutoriel, vous avez appris à rechercher les fichiers d'un répertoire et à construire dynamiquement un nombre indéfini d'objets, grâce à la classe TList. Rappelons qu'il est également possible d'utiliser les conteneurs standards de la STL, en particulier les vector qui auraient très bien fait l'affaire.

VIII. Contact

Toutes critiques constructives sont acceptées. Des fautes se sont sûrement glissées sournoisement dans ce document, n'hésitez pas à les signaler. Geronimo (MP : Geronimo).