Comment charger et afficher beaucoup d'images (WinForms)

Tags: Task

L’idée de cet article me vient d’une question posée sur le forum MSDN de C#. La question était la suivante :

J’ai un répertoire avec beaucoup d’images et je souhaite les afficher dans une Windows Form. Le problème est que j’ai un OutOfMemoryException qui se déclenche. Comment faire ?

Bien entendu, si l’on a beaucoup d’images en haute résolution, vous aurez beau avoir un PC de la mort qui tue, la mémoire n’est pas extensible à l’infini. Donc charger "simplement” les images dans des PictureBox ou dans une DataGrid fera que votre PC chauffera.

Le principe

Nous allons donc créer un projet Windows Form (je crois que ca fait 6 ans que je n’en avais pas fait Clignement d'œil). Il comportera simplement un bouton pour lancer l’analyse et un contrôle Panel dans lequel nous allons ajouter dynamiquement des contrôles PictureBox.

Quand on clique sur le bouton d’analyse, une première méthode fera le ménage dans notre panel (supprimera les picturebox existants) :

private void ClearImageControls()
{
    // on supprime les picturebox existants
    if (pnImages.Controls.Count > 0)
    {
        for (var index = pnImages.Controls.Count - 1; index >= 0; index--)
        {
            var ctrlImage = pnImages.Controls[index] as PictureBox;
            if (ctrlImage != null)
            {
                pnImages.Controls.RemoveAt(index);
                ctrlImage.Image = null;
                ctrlImage.Dispose();
            }
        }
    }
}

 

Et donc nous appellerons cette méthode pour vider notre contrôle Panel.

Maintenant, le cœur de notre code va être pour chaque fichier image contenu dans notre répertoire :

  • Charger l’image,
  • Créer une vignette de limage,
  • Décharger l’image,
  • Ajouter un PictureBox contenant la vignette à notre Panel.

C’est donc, dans un premier temps, une approche synchrone, qui prend du temps et qui gèle l’interface (pas cool) :

private void btnSync_Click(object sender, EventArgs e)
{
    // La madame elle fait le ménache
    ClearImageControls();
    // on mesure le  temps d'exécution
    var sw = Stopwatch.StartNew();
    
    var currentColumn = 0;
    var currentRow = 0;
    // pour toutes les images du répertoire
    foreach (var fileName in Directory.GetFiles(FolderName, "*.jpg"))
    {
        // On charge l'image
        Image img = new Bitmap(fileName);
        // On en fait une vignette
        var newImage = img.GetThumbnailImage(PictureWidth, PictureWidth, null, IntPtr.Zero);
        // on décharge l'image
        img.Dispose();

        // on crée le PictureBox
        var newPic = new PictureBox
                         {
                             Width = PictureWidth,
                             Height = PictureWidth,
                             Location = new Point
                                            {
                                                X = currentColumn*(PictureWidth + PictureMargin),
                                                Y = currentRow*(PictureWidth + PictureMargin)
                                            },
                             Image = newImage,
                         };

        // On ajoute le PictureBox dans le Panel
        pnImages.Controls.Add(newPic);

        // On gère les colonnes, lignes
        currentColumn++;
        if (currentColumn >= ColumnsNumber)
        {
            currentColumn = 0;
            currentRow++;
        }
    }

    // Affichage du temps d'exécution
    lbl.Text = string.Format("{0} ms", sw.ElapsedMilliseconds);
}

 

Résultat sur un répertoire contenant 350 images très haute résolution (1 Go d’images jpeg) :

LoadImages0

Une minute, c’est pas mal, mais j’ai un “beau PC” Clignement d'œil.

L’approche asynchrone

Bien entendu, le mieux est d’avoir une approche asynchrone, ce que nous permet facilement le Framework 4.0. Nous allons donc lancer une tâche qui va itérer sur l’ensemble des fichiers et retourner la liste des vignettes :

// Démarrage en asynchrone
            Task<IEnumerable<Image>>.Factory.StartNew(
                () =>
{
    // chargement des vignettes dans un ConcurrentBag
    var images = new ConcurrentBag<Image>();
    Parallel.ForEach(Directory.GetFiles(FolderName, "*.jpg"),
                     fileName =>
                         {
                             // On charge l'image
                             Image img = new Bitmap(fileName);
                             // On en fait une vignette
                             var newImage = img.GetThumbnailImage(
                                 PictureWidth,
                                 PictureWidth,
                                 null,
                                 IntPtr.Zero);
                             images.Add(newImage);
                             img.Dispose();
                         });
    return images;
}).ContinueWith(...);

 

Donc maintenant, dans ContinueWith, on a notre collection d’images que l’on peut ajouter dans le Panel :

.ContinueWith(
task =>
    {
        // on n'a pas de gestion d'erreur ici, mais ce serait une bonne idée quand même
        var currentColumn = 0;
        var currentRow = 0;
        foreach (var image in task.Result)
        {
            var newPic = new PictureBox
            {
                Width = PictureWidth,
                Height = PictureWidth,
                Location = new Point
                {
                    X = currentColumn * (PictureWidth + PictureMargin),
                    Y = currentRow * (PictureWidth + PictureMargin)
                },
                Image = image,
            };
            pnImages.Controls.Add(newPic);

            // On gère les colonnes, lignes
            currentColumn++;
            if (currentColumn >= ColumnsNumber)
            {
                currentColumn = 0;
                currentRow++;
            }
        }
        lbl.Text = string.Format("{0} ms", sw.ElapsedMilliseconds);
    },
TaskScheduler.FromCurrentSynchronizationContext());

 

Notez l’utiilisation de TaskScheduler.FromCurrentSynchronizationContext() pour s’assurer que le résultat est analyser dans le thread de l’UI.

Moralité :

LoadImages1

Le résultat est le même (bonne nouvelle !) et surtout, le temps d’exécution a été sérieusement diminué (14 secondes au lieu d’une minute) et l’interface de l’utilisateur n’a pas été figée. On pourrait rajouter l’affichage d’une image animée pour signifier que l’on travaille, ajouter des informations sur le déroulement de l’analyser, etc. Mais je vais pas vous mâcher tout le travail quand même Clignement d'œil.

Le code complet en mode asynchrone est donc le suivant :

private void async_btnSync_Click(object sender, EventArgs e)
{
    // La madame elle fait le ménache
    ClearImageControls();
    // on mesure le  temps d'exécution
    var sw = Stopwatch.StartNew();

    // Démarrage en asynchrone
    Task<IEnumerable<Image>>.Factory.StartNew(
        () =>
            {
                // chargement des vignettes dans un ConcurrentBag
                var images = new ConcurrentBag<Image>();
                Parallel.ForEach(Directory.GetFiles(FolderName, "*.jpg"),
                                 fileName =>
                                     {
                                         // On charge l'image
                                         Image img = new Bitmap(fileName);
                                         // On en fait une vignette
                                         var newImage = img.GetThumbnailImage(
                                             PictureWidth,
                                             PictureWidth,
                                             null,
                                             IntPtr.Zero);
                                         images.Add(newImage);
                                         img.Dispose();
                                     });
                return images;
            }).ContinueWith(
                task =>
                    {
                        // on n'a pas de gestion d'erreur ici, mais ce serait une bonne idée quand même
                        var currentColumn = 0;
                        var currentRow = 0;
                        foreach (var image in task.Result)
                        {
                            var newPic = new PictureBox
                            {
                                Width = PictureWidth,
                                Height = PictureWidth,
                                Location = new Point
                                {
                                    X = currentColumn * (PictureWidth + PictureMargin),
                                    Y = currentRow * (PictureWidth + PictureMargin)
                                },
                                Image = image,
                            };
                            pnImages.Controls.Add(newPic);

                            // On gère les colonnes, lignes
                            currentColumn++;
                            if (currentColumn >= ColumnsNumber)
                            {
                                currentColumn = 0;
                                currentRow++;
                            }
                        }
                        lbl.Text = string.Format("{0} ms", sw.ElapsedMilliseconds);
                    },
                TaskScheduler.FromCurrentSynchronizationContext());
}

 

Comme quoi, l’asynchrone peut servir même pour les applications “old fashion”, aka Windows Form.

blog comments powered by Disqus