Comment charger et afficher beaucoup d'images (WinForms)
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 ). 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) :
Une minute, c’est pas mal, mais j’ai un “beau PC” .
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é :
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 .
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.