Afficher un catalogue d'images provenant d'Internet avec Delphi et Firemonkey

Suite à la diffusion de ma classe de téléchargement de fichiers je me suis dit que j'allais vous faire un exemple pratique.

Histoire de mettre en place une application multiplateformes utilisant une API JSON piochée sur Internet je suis parti en chasse d'une API ouverte et simple à mettre en place. Tous les services en ligne auxquels j'ai pensé demandent une inscription puis une authentification. C'est trop lourd pour un simple article alors nous allons ruser en faisant tout nous-mêmes.

Si vous n'avez pas de serveur web ni de site Internet sur lequel jouer, installez un serveur en local. EasyPHP ou WampServeur sont très bien pour ça. Une fois opérationnel lisez donc la suite.

Le projet

Le projet réalisé dans cet article est très simple : nous avons pour objectif d'afficher les images stockées dans le dossier d'un serveur web. Pour cela nous interrogerons le serveur qui nous donnera la liste des images, puis nous les chargerons et les afficherons à l'écran.

C'est un fonctionnement classique utilisé dans les applications de rencontre ou de gestion de catalogues (de produits, de contacts, de photos, ...). Les listes d'images sont partout de nos jours et le stockage de lus en plus souvent en ligne ou "dans le cloud".

Et si vous n'avez pas besoin d'afficher des images, vous devrez probablement traiter d'autres formats de fichiers. Des PDF de fiches techniques par exemple. Le principe sera le même. Seul l'affichage final à l'écran change.

Ce projet est donc en deux parties : l'une sur le serveur pour l'API REST JSON, l'autre pour l'interrogation de cette API.

Côté serveur web

Pour fonctionner vous allez donc avoir besoin de faire un peu de PHP, mais d'abord connectez-vous à votre serveur pour créer une arborescence.

Vous devez avoir un dossier racine pour ce projet dans lequel vous placerez le programme php créé. Dans ce dossier ajoutez un sous-dossier "images" où vous placerez des photos en JPG ou PNG. Ne mettez pas de fichiers trop volumineux pour cet exercice. En pratique la taille ne joue que sur le temps de téléchargement et la mémoire utilisée lors de leur stockage puis affichage.

Dans un contexte mobile n'oubliez pas qu'idéalement il faut éviter de télécharger des choses volumineuses inutilement car cela consomme de la batterie et le quota de data de vos utilisateurs. La France a eu beaucoup de chance d'avoir un opérateur comme Free Mobile, mais ce n'est pas le cas partout et les forfaits de données sont souvent couteux. Pensez au porte monnaie de vos utilisateurs / clients.

Une fois l'environnement prêt, créez un fichier texte que vous nommerez index.php. Il contiendra le code suivant :

<?php
	$reponse = new stdClass();
	$reponse->liste = array();
	$fichiers = scandir(__DIR__."/images/");
	foreach($fichiers as $fichier) {
		$extension = strtolower(substr($fichier,strlen($fichier)-4));
		if ((".jpg" == $extension) || (".png" == $extension)) {
			$reponse->liste[] = "http://".$_SERVER["HTTP_HOST"].$_SERVER["REQUEST_URI"]."images/".$fichier;
		}
	}
	
	print(json_encode($reponse));
	exit;
?>

Les fonctions d'import de JSON de Delphi sont basées sur des objets JSON (en tout cas jusqu'à aujourd'hui et la version 10.2 Tokyo).

Pour vous simplifier la vie il est préférable d'encapsuler systématiquement vos API dans un objet puis y placer ensuite ce que vous désirez. C'est la raison pour laquelle je crée la classe $reponse avant de lui attribuer une propriété $reponse->liste de type tableau.

Le programme charge ensuite la liste des fichiers du dossier "./images". __DIR__ contient le chemin d'accès au fichier PHP en cours d'exécution. On parcourt cette liste afin de ne conserver que les fichiers ayant l'extension JPG ou PNG.

Pour chaque fichier conservé, on génére l'URL d'accès à la photo.
$_SERVER["HTTP_HOST"] contient le nom du domaine utilisé.
$_SERVER["REQUEST_URI"] contient la requête ayant abouti à l'exécution de ce programme.

Dans mon cas ce programme est accessible depuis l'URL http://127.0.0.1/edsa-web/blogs/afficher-catalogue-images-web/ et retourne la réponse suivante :

{"liste":["http:\/\/127.0.0.1\/edsa-web\/blogs\/afficher-catalogue-images-web\/index.phpimages\/P0104727.JPG","http:\/\/127.0.0.1\/edsa-web\/blogs\/afficher-catalogue-images-web\/index.phpimages\/P0104782.JPG","http:\/\/127.0.0.1\/edsa-web\/blogs\/afficher-catalogue-images-web\/index.phpimages\/P0104783.JPG","http:\/\/127.0.0.1\/edsa-web\/blogs\/afficher-catalogue-images-web\/index.phpimages\/P0104796.JPG","http:\/\/127.0.0.1\/edsa-web\/blogs\/afficher-catalogue-images-web\/index.phpimages\/P0104801.JPG","http:\/\/127.0.0.1\/edsa-web\/blogs\/afficher-catalogue-images-web\/index.phpimages\/P0104809.JPG","http:\/\/127.0.0.1\/edsa-web\/blogs\/afficher-catalogue-images-web\/index.phpimages\/P0104829.JPG","http:\/\/127.0.0.1\/edsa-web\/blogs\/afficher-catalogue-images-web\/index.phpimages\/P0104841.JPG","http:\/\/127.0.0.1\/edsa-web\/blogs\/afficher-catalogue-images-web\/index.phpimages\/P0104848.JPG","http:\/\/127.0.0.1\/edsa-web\/blogs\/afficher-catalogue-images-web\/index.phpimages\/P0104902.JPG"]}

Attention à ne pas utiliser l'URL en spécifiant le nom du programme car il se retrouverait dans $_SERVER["REQUEST_URI"] et pollurait les URL générées. Vous devriez dans ce cas modifier la ligne 

$reponse->liste[] = "http://".$_SERVER["HTTP_HOST"].$_SERVER["REQUEST_URI"]."images/".$fichier;

pour y mettre une URL en dur ou en extraire le nom du fichier PHP.

Maintenant que les choses sont claires et fonctionnelles côté serveur, on peut passer à l'application qui va interroger tout ça.

Côté client mobile ou desktop

Nous allons afficher les images provenant du site sous forme de liste à l'écran. Dans cette version du programme je ne gère pas de cache de fichiers.
Passer par une unité qui stocke les réponses sous forme de fichiers peut vous servir à gérer un cache et éviter les multiples téléchargements des mêmes images. A vous de voir selon vos besoins.

Ce qui suit fonctionnerait aussi en VCL mais les propriétés des composants et leurs valeurs changent. J'ai préféré vous montrer quelque chose qui s'adapterait aussi à un mobile.

Dans Delphi, créez une nouvelle application multiplateformes. Ajoutez l'unité u_download dont vous trouverez le source sur cet article.

Mettez ensuite ces composants dans la fiche principale de l'application :

  • un TToolBar
  • un TButton dans le TToolBar et mettez son alignement à TAlignLayout.Left
  • un TStatusBar
  • un TLabel dans le TStatusBar que vous appellerez lblStatus
  • un TVerticalScrollBar dont l'alignement sera à TAlignLayout.Client

Les composants étant en place nous pouvons passer au code mais enregistrez votre projet avant de continuer. Toujours penser à enregistrer son projet en cours de travail et régulièrement pour éviter les mauvaises surprises.

La première des choses à faire est de gérer l'événement onClick sur le bouton. C'est lui qui va déclencher le téléchargement de la liste, puis les images se chargeront une fois la liste téléchargée. Le tout de préférence sans bloquer l'écran.

procedure TForm1.btnChargeImagesClick(Sender: TObject);
begin
  btnChargeImages.Enabled := false;
  traiter_liste
    ('http://127.0.0.1/edsa-web/blogs/afficher-catalogue-images-web/');
end;

Comme tout se passera en tache de fond, on commence par désactiver le bouton. Il ne faudrait pas que l'utilisateur clique plusieurs fois dessus pour rien. Cela chargerait plusieurs fois les mêmes images.

On appelle une méthode traiter_liste() à laquelle on passe l'URL de l'API créée plus tôt. Dans mon cas une URL locale gérée sous EasyPHP.

Passons maintenant aux choses compliquées : le traitement de l'import du flux d'informations.

procedure TForm1.traiter_liste(url_json: string);
var
  nom_fichier_json: string;
begin
  lblStatus.Text := 'Chargement de la liste des images.';
  Application.ProcessMessages;
  nom_fichier_json := tdownload_file.temporaryFileName('test-json');

On déclare la procédure traiter_liste() comme méthode privée dans la classe.

Elle utilise une variable locale pour stocker le nom du fichier contenant le JSON reçu depuis l'API.

J'utilise le composant lblStatus pour avoir le détail de la dernière action effectuée. Je force son rafraichissement en appelant Application.ProcessMessages qui va traiter la file d'attente d'événements avant de passer à la liste. Cette technique permet de gérer des changements d'états à l'écran et de ne pas bloquer l'application dans des boucles longues si elles sont faites de façon synchrone (donc dans le processus principal du programme).

Le gros de traiter_liste() est l'appel à la méthode de téléchargement des données JSON que voici :

  tdownload_file.download(url_json, nom_fichier_json,
    procedure
    var
      jso: TJSONObject;
      liste: TJSONArray;
      i: integer;
    begin
      try
        lblStatus.Text := 'Liste chargée.';
        Application.ProcessMessages;
        try
          jso := TJSONObject.ParseJSONValue(tfile.ReadAllText(nom_fichier_json,
            TEncoding.UTF8)) as TJSONObject;
          if assigned(jso) then
          begin
            liste := jso.GetValue('liste') as TJSONArray;
            for i := 0 to liste.Count - 1 do
              traiter_image(liste.Items[i].Value, i);
          end;
        finally
          if assigned(jso) then
            jso.Free;
        end;
      finally
        // showmessage(nom_fichier_json);
        // tfile.Delete(nom_fichier_json);
      end;
    end,
    procedure
    begin
      lblStatus.Text := 'Erreur au chargement de la liste des images.';
      Application.ProcessMessages;
      // showmessage(nom_fichier_json);
      // tfile.Delete(nom_fichier_json);
    end);

Pour rappel cette méthode de classe prend 4 paramètres : l'URL à charger, le chemin du fichier local dans lequel stocker la réponse, une procédure anonyme appelée en cas de succès du téléchargement, une procédure anonyme appelée en cas d'échec du téléchargement.

Concentrons nous donc sur la procédure appelée lorsque url_json a été rapatrié et stocké dans le fichier nom_fichier_json.

Première chose de faite : le changement du texte sur la barre de status. Notez que les deux procédures sont exécutées dans le thread principal, non dans celui qui a servi au téléchargement. Je peux donc utiliser des composants visuels dedans sans aucun risque de conflit ni de plantage.

Vient ensuite le traitement du fichier JSON lui-même. La méthode de classe TJSONObject.ParseJSONValue() a un grand nombre de surcharges. Celle qui nous intéresse permet d'interpréter une chaine de caractères comme source d'un objet JSON. Cette chaine provient directement de la lecture du fichier JSON grâce à TFile.ReadAllText().

Si le fichier contenait bien un objet JSON, on tente d'en récupérer la liste sous forme de TJSONArray que l'on parcourt pour soumettre le chargement des images une par une, mais en même temps.

Une fois la demande des images terminée on peut nettoyer la mémoire en libérant les objets créés, éventuellement le fichier contenant le JSON.

Faites attention à un point important lorsque vous manipulez du JSON sous Delphi : la plupart des fonctions retournent des TJSONValue mais ces objets ne sont pas utilisables en tant que tel (ou très peu). Vous devez donc passer votre temps à caster les résultats dans le type qui vous intéresse afin de travailler correctement. N'hésitez pas à créer autant de variables des différents types qui vous sont utiles pour éviter de transtyper à répétition les mêmes objets. C'est le compilateur qui est censé s'en débrouiller, mais épargnons lui du travail et sécurisons aussi nos sources au maximum dans un soucis de simplification et de maintenabilité.

Pendant ce temps le téléchargement des images se fait depuis traiter_image().

procedure TForm1.traiter_image(url_image: string; num: integer);
var
  img_name: string;
begin
  img_name := tdownload_file.temporaryFileName('test-photo-' + num.ToString);
  lblStatus.Text := 'Chargement de l''image ' + url_image;
  Application.ProcessMessages;
  tdownload_file.download(url_image, img_name,
    procedure
    var
      img: timage;
    begin
      lblStatus.Text := 'Affichage de l''image ' + url_image;
      Application.ProcessMessages;
      img := timage.Create(Self);
      try
        img.Parent := VertScrollBox1;
        img.Align := TAlignLayout.Top;
        img.Height := 200;
        img.Margins.Top := 5;
        img.Margins.Bottom := 5;
        img.Bitmap.LoadFromFile(img_name);
        // showmessage(img_name);
        // tfile.Delete(img_name);
      except
        if assigned(img) then
          img.Free;
      end;
    end,
    procedure
    begin
      lblStatus.Text := 'Chargement de ' + url_image + ' impossible.';
      Application.ProcessMessages;
      // showmessage(img_name);
      // tfile.Delete(img_name);
    end);
end;

La procédure privée traiter_image() a la même structure que la précédente: changement du message de status puis appel de tdownload_file.download() avec les paramètres locaux.

La procédure appelée lorsque le fichier de l'image est téléchargé se contente de créer un TImage et de l'ajouter sur le TVertScrollBox.

Vous constaterez un phénomène étrange lié au positionnement en TAlignLayout.Top lors de l'ajout : la première image ajoutée se met tout en haut, les suivantes se placent en seconde position en redescendant leurs prédécesseuses. Si vous voulez gérer un ordre à l'affichage, n'utiliser pas de positionnement automatique. Utiliser plutôt les ancres des côtés et positionnez vous-mêmes vos composants visuels.

Voilà pour cet exemple. Vous pouvez désormais appeler des API en JSON, les interpréter dans vos programmes et éventuellement jouer à afficher des images venant d'ailleurs sans interrompre l'application et donc en laissant les utilisateurs continuer à travailler sur le programme durant le chargement de ces éléments.

Retrouvez les sources de ce projet sur mon compte GitHub.


Mug Pascal case in AlexandrieMug Chinese New Year 2023 : year of the rabbit