Rendu rapide d'images en C#/Winforms
Informations sur l'article
- Auteur : Mod
- Créé : le 05 Juil 2011 à 20:06
- Modifié : le 23 Juil 2011 à 17:52
- Visualisations : 737
Afficher des images à l'aide du framework .Net, n'est a priori pas une tâche complexe. Celui-ci offre en effet tout un panel de classes permettant de gérer des bitmaps, de les découper, d'afficher tout ou portions d'images, effectuer des transformations en tout genre, etc. La liste est longue, et il est difficile de ne pas y trouver son compte en termes de fonctionnalités... Car en dépit de tout cela, utiliser le framework .Net pour l'affichage d'images peut ne pas s'avérer judicieux dans certains cas, et notamment dans celui où l'on a comme principal besoin d'afficher un maximum d'images en un temps minimum. Et c'est là la problématique dont nous allons traiter : l'affichage à haute vitesse d'images, que l'on réalisera à travers l'utilisation de Windows GDI sous .Net.
Avant de commencer...
Pour le replacer dans son contexte, cet article a été pensé à travers la réalisation d'un outil de développement de jeu vidéo, et plus précisément d'un éditeur de map 2D. Un tel outil met en oeuvre des "tilesets", c'est-à-dire des images décomposées en plusieurs "tiles", représentant chacune une portion d'un monde 2D : arbre, maison, sol, eau, etc. Leur juxtaposition permet de représenter des univers bidimensionnel complexes sous une forme optimisée, en réutilisant plusieurs fois les mêmes tiles. L'objectif de cet éditeur est donc simple : extraire les tiles d'un tileset, et les afficher suffisamment rapidement pour pouvoir rafraîchir l'éditeur de map en temps réel (évitant ainsi des ralentissements lors du défilement de la map à l'aide de scrollbars, par exemple).
Cet outil ne met pas en oeuvre de transformations complexes autres que l'agrandissement, et on prendra comme assertion le fait que l'on n'ait pas besoin de davantage de transformations (ce qui par ailleurs sera vrai dans la plupart des cas où l'on peut envisager avoir besoin d'une grande rapidité sur un affichage massif d'images). Autre assertion, la nécessité d'un affichage au pixel près, dit affichage "Pixel perfect".
A titre informatif, cette optique n'est pas purement théorique, puisque la méthode de rendu que je vous présente ici a été appliquée lors de la conception de l'éditeur de map de RPG Engine (cf http://www.game-corp.net/topic-750-editeur-de-jeu-rpg-engine.html).
J'utiliserai au cours de cet article un tileset issu d'un pack de ressources multimédias téléchargeable gratuitement à cette adresse : http://www.famitsu.com/freegame/rtp/xp_rtp.html (site en japonais), et plus précisément celui-ci :

Il s'agit d'une image au format PNG, 32 bits par pixels ARGB, et dont le canal alpha contient des informations de transparence : à la fois de la transparence totale et de la transparence partielle. Il nous faudra en effet nous assurer que ce que nous affichons soit correctement affiché, en prenant en compte toutes les informations possibles, et la transparence en fait partie.
Concernant la technologie employée ici - outre .Net lui-même, bien évidemment - j'ai fait le choix d'utiliser le C#, celui-ci étant à l'heure actuelle le langage .Net le plus utilisé devant le Visual Basic.Net pour les applications lourdes. Notez que le principe reste le même si vous souhaitez transposez ce code en VB.Net.
Pour la mesure du temps, qui sera nécessaire pour contrôler les temps d'affichage, on utilisera ici la variable d'environnement System.Environment.TickCount de .Net, qui contient le nombre de millisecondes écoulées depuis le démarrage du système. Par une simple opération de soustraction après/avant rendu, on obtiendra ainsi une donnée relativement fiable du temps d'affichage.
Enfin, qui dit article traitant de vitesse dit aussi machine hôte à préciser : celle-ci est équipée d'un Core2Duo E7600 à 3Ghz, de 4Go RAM, et est équipée d'une Radeon 4890 HD 1Go. Le tout tournant sous Windows XP SP3 32bits disposant du Framework .Net 3.5 SP1. Le développement sera quant à lui réalisé sous Visual Studio C# 2008 Express Edition SP1.
Cet outil ne met pas en oeuvre de transformations complexes autres que l'agrandissement, et on prendra comme assertion le fait que l'on n'ait pas besoin de davantage de transformations (ce qui par ailleurs sera vrai dans la plupart des cas où l'on peut envisager avoir besoin d'une grande rapidité sur un affichage massif d'images). Autre assertion, la nécessité d'un affichage au pixel près, dit affichage "Pixel perfect".
A titre informatif, cette optique n'est pas purement théorique, puisque la méthode de rendu que je vous présente ici a été appliquée lors de la conception de l'éditeur de map de RPG Engine (cf http://www.game-corp.net/topic-750-editeur-de-jeu-rpg-engine.html).
J'utiliserai au cours de cet article un tileset issu d'un pack de ressources multimédias téléchargeable gratuitement à cette adresse : http://www.famitsu.com/freegame/rtp/xp_rtp.html (site en japonais), et plus précisément celui-ci :

Il s'agit d'une image au format PNG, 32 bits par pixels ARGB, et dont le canal alpha contient des informations de transparence : à la fois de la transparence totale et de la transparence partielle. Il nous faudra en effet nous assurer que ce que nous affichons soit correctement affiché, en prenant en compte toutes les informations possibles, et la transparence en fait partie.
Concernant la technologie employée ici - outre .Net lui-même, bien évidemment - j'ai fait le choix d'utiliser le C#, celui-ci étant à l'heure actuelle le langage .Net le plus utilisé devant le Visual Basic.Net pour les applications lourdes. Notez que le principe reste le même si vous souhaitez transposez ce code en VB.Net.
Pour la mesure du temps, qui sera nécessaire pour contrôler les temps d'affichage, on utilisera ici la variable d'environnement System.Environment.TickCount de .Net, qui contient le nombre de millisecondes écoulées depuis le démarrage du système. Par une simple opération de soustraction après/avant rendu, on obtiendra ainsi une donnée relativement fiable du temps d'affichage.
Enfin, qui dit article traitant de vitesse dit aussi machine hôte à préciser : celle-ci est équipée d'un Core2Duo E7600 à 3Ghz, de 4Go RAM, et est équipée d'une Radeon 4890 HD 1Go. Le tout tournant sous Windows XP SP3 32bits disposant du Framework .Net 3.5 SP1. Le développement sera quant à lui réalisé sous Visual Studio C# 2008 Express Edition SP1.
.Net et les images
Le plus simple est le plus évident lorsque l'on souhaite afficher des images avec le framework .Net, c'est d'utiliser la classe Image. Sa mise en oeuvre est des plus simples, et tiens en deux temps : chargement d'image, affichage d'image. Nous allons pour commencer utiliser cette classe Image, qui nous permettra de récupérer des temps et des rendus qui serviront de base de comparaison.
Avant toute chose, créons un nouveau projet Windows Form, tout ce qu'il y a de plus basique, avec une simple fenêtre qui nous servira de zone de rendu. Celle-ci sera paramétrée à une taille de 1024x 768, afin d'offrir une large zone d'affichage, et sa propriété BackColor paramétrée à 255;192;192, soit une couleur rose pâle. Considérant en effet qu'en cas de problème, les parties transparentes du tileset puisse s'afficher en blanc ou noir, on remplace simplement l'arrière-plan de notre fenêtre par une couleur non neutre. De cette manière, on pourra rapidement apercevoir les problèmes s'il surviennent.
Un label est ensuite ajouté dans le coin haut gauche, sa propriété Text contiendra le temps de rendu calculé en millisecondes.
Le code utilisé pour l'affichage et le rendu se veut le plus basique possible. Commençons par l'ajout des sous-espaces de noms de Drawing dont dépend notre (futur) code :
Code : CSharp
L'évènement Load de la Form charge le tileset :
Code : CSharp
Et l'évènement Paint l'affiche :
Code : CSharp
Notez simplement au passage le calcul du temps, et le rafraîchissement du texte du label.
Concernant ce que l'on doit obtenir à l'écran : on affiche ici la totalité du tileset, en mode d'affichage Pixel. Pour des raisons d'optimisation, on aurait pu instancier les objets Rectangle pour la source et la destination en dehors de la zone de rendu. Toutefois, considérant que l'on puisse en théorie être amené à extraire des portions du tileset à afficher à des positions diverses, j'ai fait ici le choix de conserver l'instanciation de ces deux objets.
Enfin, l'évènement MouseDown nous servira à lancer un rafraîchissement manuel :
Code : CSharp
A l'exécution, nous obtenons ceci :

Résultat à mémoriser, ce rendu nous servira de référence avec la méthode de rendu via GDI...
Et concernant le temps d'affichage : 16ms. Un temps tout à fait honorable puisque l'affichage est instantané. Rien à y redire.
Multiplions maintenant l'affichage par 100 : dans une itération de 0 à 99, on affiche la totalité de l'image, tout en décalant d'un pixel en abscisse pour dire de modifier le rendu :
Code : CSharp
Le résultat :

Aucun coût caché à déplorer, cet affichage est à peu de choses près égal à l'affichage de 100 fois l'image seule. D'un point de vue plus général maintenant, on ne peut en revanche que déplorer le résultat : plus d'une seconde et demi pour l'affichage de "seulement" 100 images, c'est assez énorme, et on pointe là une lacune de .Net : si en terme d'affichage unique, les performances sont intéressantes (ou du moins satisfaisantes), dès lors qu'il s'agit d'afficher un certain nombre de fois une image, on prend en durée et en effets visuels indésirables (en l'occurence, l'affichage se faisant sur une durée humainement perceptible, on voit l'affichage du tileset s'étaler sur une seconde et demi...).
Maintenant que nous avons testé l'affichage total du tileset, nous allons pouvoir triturer l'affichage partiel, qui au regard du postulat de départ prend une certaine ampleur. On se contentera d'afficher dans un premier temps la tile de coordonnées 0,0, c'est-à-dire celle située dans l'extrémité haute gauche du tileset. Celle-ci ne présente pas de transparence, l'utiliser comme base est donc intéressant. On l'affichera 600 fois (en diagonal : on a de la place, profitons-en !) :
Code : CSharp
[Image_4.png]
Cet affichage assez brutal nous laisse sur un résultat mitigé : 875 ms. Relativement au 600 images affichées, cela peut sembler un bon temps. Rapporté à la puissance de la machine hôte et au fait que l'on a un affichage progressif toujours aussi perceptible, c'est à nouveau assez décevant.
Le test suivant concerne la transparence : dans certaines librairies graphiques, il arrive que l'affichage de la transparence alpha fasse perdre un précieux temps. On utilisera ici la tile transparente de coordonnées 0,32 :
Code : CSharp
Le résultat est sans appel :

1218 ms avec de la transparence, contre 875 ms sans. On ne peut que constater une perte de vitesse assez impressionnante, puisque l'on prend pas moins de moitié plus de temps que sur le précédent test.
Voilà tout pour les tests de vitesse sur la méthode DrawImage de .Net. Pour les résumer : si l'affichage unique ne pose aucun soucis, il apparaît très clairement que la multiplication des rendus prend bien trop de temps pour permettre une utilisation en temps réel : on se retrouve avec des temps d'affichage se comptant en centaines de millisecondes.
Avant toute chose, créons un nouveau projet Windows Form, tout ce qu'il y a de plus basique, avec une simple fenêtre qui nous servira de zone de rendu. Celle-ci sera paramétrée à une taille de 1024x 768, afin d'offrir une large zone d'affichage, et sa propriété BackColor paramétrée à 255;192;192, soit une couleur rose pâle. Considérant en effet qu'en cas de problème, les parties transparentes du tileset puisse s'afficher en blanc ou noir, on remplace simplement l'arrière-plan de notre fenêtre par une couleur non neutre. De cette manière, on pourra rapidement apercevoir les problèmes s'il surviennent.
Un label est ensuite ajouté dans le coin haut gauche, sa propriété Text contiendra le temps de rendu calculé en millisecondes.
Le code utilisé pour l'affichage et le rendu se veut le plus basique possible. Commençons par l'ajout des sous-espaces de noms de Drawing dont dépend notre (futur) code :
Code : CSharp
using System.Drawing.Imaging; using System.Drawing.Drawing2D;
L'évènement Load de la Form charge le tileset :
Code : CSharp
image = Image.FromFile("tileset.png");
Et l'évènement Paint l'affiche :
Code : CSharp
int start = System.Environment.TickCount; Graphics g = e.Graphics; g.DrawImage( image, new Rectangle(0, 0, image.Width, image.Height), new Rectangle(0, 0, image.Width, image.Height), GraphicsUnit.Pixel); int end = System.Environment.TickCount; label1.Text = (end - start).ToString();
Notez simplement au passage le calcul du temps, et le rafraîchissement du texte du label.
Concernant ce que l'on doit obtenir à l'écran : on affiche ici la totalité du tileset, en mode d'affichage Pixel. Pour des raisons d'optimisation, on aurait pu instancier les objets Rectangle pour la source et la destination en dehors de la zone de rendu. Toutefois, considérant que l'on puisse en théorie être amené à extraire des portions du tileset à afficher à des positions diverses, j'ai fait ici le choix de conserver l'instanciation de ces deux objets.
Enfin, l'évènement MouseDown nous servira à lancer un rafraîchissement manuel :
Code : CSharp
Refresh();
A l'exécution, nous obtenons ceci :

Résultat à mémoriser, ce rendu nous servira de référence avec la méthode de rendu via GDI...
Et concernant le temps d'affichage : 16ms. Un temps tout à fait honorable puisque l'affichage est instantané. Rien à y redire.
Multiplions maintenant l'affichage par 100 : dans une itération de 0 à 99, on affiche la totalité de l'image, tout en décalant d'un pixel en abscisse pour dire de modifier le rendu :
Code : CSharp
for (int x = 0; x < 99; x++) { g.DrawImage(image, new Rectangle(x, 0, image.Width, image.Height), new Rectangle(0, 0, image.Width, image.Height), GraphicsUnit.Pixel); }
Le résultat :

Aucun coût caché à déplorer, cet affichage est à peu de choses près égal à l'affichage de 100 fois l'image seule. D'un point de vue plus général maintenant, on ne peut en revanche que déplorer le résultat : plus d'une seconde et demi pour l'affichage de "seulement" 100 images, c'est assez énorme, et on pointe là une lacune de .Net : si en terme d'affichage unique, les performances sont intéressantes (ou du moins satisfaisantes), dès lors qu'il s'agit d'afficher un certain nombre de fois une image, on prend en durée et en effets visuels indésirables (en l'occurence, l'affichage se faisant sur une durée humainement perceptible, on voit l'affichage du tileset s'étaler sur une seconde et demi...).
Maintenant que nous avons testé l'affichage total du tileset, nous allons pouvoir triturer l'affichage partiel, qui au regard du postulat de départ prend une certaine ampleur. On se contentera d'afficher dans un premier temps la tile de coordonnées 0,0, c'est-à-dire celle située dans l'extrémité haute gauche du tileset. Celle-ci ne présente pas de transparence, l'utiliser comme base est donc intéressant. On l'affichera 600 fois (en diagonal : on a de la place, profitons-en !) :
Code : CSharp
for (int i = 0; i < 600; i++) { g.DrawImage( image, new Rectangle(i, i, 32, 32), new Rectangle(0, 0, 32, 32), GraphicsUnit.Pixel); }
[Image_4.png]Cet affichage assez brutal nous laisse sur un résultat mitigé : 875 ms. Relativement au 600 images affichées, cela peut sembler un bon temps. Rapporté à la puissance de la machine hôte et au fait que l'on a un affichage progressif toujours aussi perceptible, c'est à nouveau assez décevant.
Le test suivant concerne la transparence : dans certaines librairies graphiques, il arrive que l'affichage de la transparence alpha fasse perdre un précieux temps. On utilisera ici la tile transparente de coordonnées 0,32 :
Code : CSharp
for (int i = 0; i < 600; i++) { g.DrawImage( image, new Rectangle(i, i, 32, 32), new Rectangle(0, 32, 32, 32), GraphicsUnit.Pixel); }
Le résultat est sans appel :

1218 ms avec de la transparence, contre 875 ms sans. On ne peut que constater une perte de vitesse assez impressionnante, puisque l'on prend pas moins de moitié plus de temps que sur le précédent test.
Voilà tout pour les tests de vitesse sur la méthode DrawImage de .Net. Pour les résumer : si l'affichage unique ne pose aucun soucis, il apparaît très clairement que la multiplication des rendus prend bien trop de temps pour permettre une utilisation en temps réel : on se retrouve avec des temps d'affichage se comptant en centaines de millisecondes.
AlphaBlend : GDI à la rescousse
Pas de fumée sans feu : si le problème des performances a ainsi été longuement exposé, ce n'est pas pour rien : il existe en effet une solution dont les performances surpassent de très loin celle du DrawImage de .Net. Et pour trouver cette solution, il faut aller piocher dans l'historique des librairies graphiques liées à Windows : à savoir Windows GDI (introduit par Windows 95, et souvent abregé "GDI"). Et plus précisément sa fonction AlphaBlend.
Pour qui n'a jamais touché à GDI en C ou C++, exploiter la compatibilité de .Net avec celle-ci pourra relever du parcours du combattant. C'est d'un lourd passé de programmation impérative dont on hérite, et la mise en oeuvre de GDI peut s'avérer d'autant plus complexe que la documentation sur le sujet s'avère assez rare et incomplète - d'où cet article spécifiquement dédié à cela.
Comme mentionné précédemment, nous allons exploiter la fonction AlphaBlend de GDI. Celle-ci permet d'afficher une image en prenant en compte les données de la couche alpha, une fonctionnalité quasiment indispensable de nos jours. La source de référence sur AlphaBlend sera bien évidemment le MSDN :
http://msdn.microsoft.com/en-us/library/dd183351%28VS.85%29.aspx
Voici le prototype de ladite fonction :
Code : C
Le premier et le sixième paramètres correspondent respectivement aux handles des device contexts utilisés pour la destination et la source. Un device context correspond à un objet sur lequel il est possible de dessiner. Cela peut être un bitmap chargé en mémoire, l'écran, une imprimante, etc. Notez que pour pouvoir interagir, deux device contexts doivent être compatibles. J'y reviendrai.
Un handle est quant à lui une référence interne à Windows vers un objet donné, et en l'occurrence, un HDC est un handle vers un device context.
Avec ces précisions, les dix premiers paramètres ne devraient pas poser de problème : pour la source on précise donc le handle, les coordonnées de la zone à afficher, et la taille de cette zone. Même chose pour la destination : handle, coordonnées d'affichage, et taille de la zone d'affichage (ça ne sera pas abordé en détail ici, mais cette zone d'affichage permet d'effectuer un redimensionnement, de préférence par un facteur 2 pour des raisons de performances).
Le dernier paramètre pourra s'avérer en revanche un peu plus obscur. Celui-ci attend une structure en entrée, qui pourra être de type BLENDFUNCTION ou EMRALPHABLEND. Nous utiliserons ici BLENDFUNCTION, la structure EMRALPHABLEND étant utilisée pour une fonctionnalité qui ne sera pas abordée ici (à savoir les métafichiers).
Et voici donc la référence vers BLENDFUNCTION :
http://msdn.microsoft.com/en-us/library/dd183393%28VS.85%29.aspx
La structure est la suivante :
Code : C
On retrouve donc ici 4 octets :
- BlendOp correspond à la valeur de l'opération de mélange à effectuer. Le mélange correspond à la méthode d'application de l'image, par exemple par superposition, négatif, etc. Malheureusement, nous n'aurons pas trop à nous soucier de ce paramètre, qui ne peut prendre pour valeur que AC_SRC_OVER (application par superposition). Cette constante a une valeur de 0.
- BlendFlags doit être mis à zéro, guère de difficulté ici.
- SourceConstantAlpha correspond à la modulation alpha à appliquer à l'image à afficher dans sa totalité. Cette modulation s'ajoute à celle déjà existante dans le cas où il y en aurait une. Les zones normalement opaques peuvent ainsi devenir transparentes à un certain degré. Il s'agit d'un octet, la valeur s'étale de 0 (totalement transparent, on ne verra donc rien d'affiché) à 255 (aucune modulation de transparence). Notez que si l'on souhaite utiliser uniquement la transparence native de l'image, on définira ce paramètre à 255.
- Enfin, AlphaFormat vaut 1 si l'image à afficher contient des données de transparence, sinon zéro. Information liée à ce paramètre : GDI+ utilise des données de transparence "prémultipliées", c'est-à-dire que l'on multiplie chaque composante de couleur par la valeur du canal alpha, puis que l'on divise le résultat obtenu par 255 lorsque SourceConstantAlpha est différent de 255. Cela permet d'appliquer à la fois la modulation de transparence et la transparence native de l'image.
C'est tout pour AlphaBlend et BLENDFUNCTION. Ces deux éléments étant au centre de la méthode d'affichage que je vous présente, il était indispensable de les connaître avant tout, d'autant qu'il permettaient d'introduire quelques notions. Nous allons maintenant nous atteler à mettre en oeuvre AlphaBlend, ce qui ne s'avère pas de tout repos.
Pour qui n'a jamais touché à GDI en C ou C++, exploiter la compatibilité de .Net avec celle-ci pourra relever du parcours du combattant. C'est d'un lourd passé de programmation impérative dont on hérite, et la mise en oeuvre de GDI peut s'avérer d'autant plus complexe que la documentation sur le sujet s'avère assez rare et incomplète - d'où cet article spécifiquement dédié à cela.
Comme mentionné précédemment, nous allons exploiter la fonction AlphaBlend de GDI. Celle-ci permet d'afficher une image en prenant en compte les données de la couche alpha, une fonctionnalité quasiment indispensable de nos jours. La source de référence sur AlphaBlend sera bien évidemment le MSDN :
http://msdn.microsoft.com/en-us/library/dd183351%28VS.85%29.aspx
Voici le prototype de ladite fonction :
Code : C
BOOL AlphaBlend( __in HDC hdcDest, __in int xoriginDest, __in int yoriginDest, __in int wDest, __in int hDest, __in HDC hdcSrc, __in int xoriginSrc, __in int yoriginSrc, __in int wSrc, __in int hSrc, __in BLENDFUNCTION ftn );
Le premier et le sixième paramètres correspondent respectivement aux handles des device contexts utilisés pour la destination et la source. Un device context correspond à un objet sur lequel il est possible de dessiner. Cela peut être un bitmap chargé en mémoire, l'écran, une imprimante, etc. Notez que pour pouvoir interagir, deux device contexts doivent être compatibles. J'y reviendrai.
Un handle est quant à lui une référence interne à Windows vers un objet donné, et en l'occurrence, un HDC est un handle vers un device context.
Avec ces précisions, les dix premiers paramètres ne devraient pas poser de problème : pour la source on précise donc le handle, les coordonnées de la zone à afficher, et la taille de cette zone. Même chose pour la destination : handle, coordonnées d'affichage, et taille de la zone d'affichage (ça ne sera pas abordé en détail ici, mais cette zone d'affichage permet d'effectuer un redimensionnement, de préférence par un facteur 2 pour des raisons de performances).
Le dernier paramètre pourra s'avérer en revanche un peu plus obscur. Celui-ci attend une structure en entrée, qui pourra être de type BLENDFUNCTION ou EMRALPHABLEND. Nous utiliserons ici BLENDFUNCTION, la structure EMRALPHABLEND étant utilisée pour une fonctionnalité qui ne sera pas abordée ici (à savoir les métafichiers).
Et voici donc la référence vers BLENDFUNCTION :
http://msdn.microsoft.com/en-us/library/dd183393%28VS.85%29.aspx
La structure est la suivante :
Code : C
typedef struct _BLENDFUNCTION { BYTE BlendOp; BYTE BlendFlags; BYTE SourceConstantAlpha; BYTE AlphaFormat; }BLENDFUNCTION, *PBLENDFUNCTION, *LPBLENDFUNCTION;
On retrouve donc ici 4 octets :
- BlendOp correspond à la valeur de l'opération de mélange à effectuer. Le mélange correspond à la méthode d'application de l'image, par exemple par superposition, négatif, etc. Malheureusement, nous n'aurons pas trop à nous soucier de ce paramètre, qui ne peut prendre pour valeur que AC_SRC_OVER (application par superposition). Cette constante a une valeur de 0.
- BlendFlags doit être mis à zéro, guère de difficulté ici.
- SourceConstantAlpha correspond à la modulation alpha à appliquer à l'image à afficher dans sa totalité. Cette modulation s'ajoute à celle déjà existante dans le cas où il y en aurait une. Les zones normalement opaques peuvent ainsi devenir transparentes à un certain degré. Il s'agit d'un octet, la valeur s'étale de 0 (totalement transparent, on ne verra donc rien d'affiché) à 255 (aucune modulation de transparence). Notez que si l'on souhaite utiliser uniquement la transparence native de l'image, on définira ce paramètre à 255.
- Enfin, AlphaFormat vaut 1 si l'image à afficher contient des données de transparence, sinon zéro. Information liée à ce paramètre : GDI+ utilise des données de transparence "prémultipliées", c'est-à-dire que l'on multiplie chaque composante de couleur par la valeur du canal alpha, puis que l'on divise le résultat obtenu par 255 lorsque SourceConstantAlpha est différent de 255. Cela permet d'appliquer à la fois la modulation de transparence et la transparence native de l'image.
C'est tout pour AlphaBlend et BLENDFUNCTION. Ces deux éléments étant au centre de la méthode d'affichage que je vous présente, il était indispensable de les connaître avant tout, d'autant qu'il permettaient d'introduire quelques notions. Nous allons maintenant nous atteler à mettre en oeuvre AlphaBlend, ce qui ne s'avère pas de tout repos.
GDI vers .Net
Importation
La première chose à faire est de nous rendre capable d'utiliser AlphaBlend dans notre application .Net. Pour ce faire, il va nous falloir importer la fonction de la DLL qui la contient. Un coup d'oeil rapide à sa documentation nous apprendra qu'elle est contenue dans msimg32.dll.
Pour simplifier l'organisation des fonctions qu'il va falloir importer (car vous pouvez vous en douter, on importera bien plus que la seule AlphaBlend...), créons une classe statique "GDI".
La première chose à faire est d'ajouter les using : il n'y en aura que deux :
Code : CSharp
using System; using System.Runtime.InteropServices;
Vient ensuite l'ajout d'AlphaBlend, rien de sorcier là-dedans, voici le prototype de l'import :
Code : CSharp
[DllImport("msimg32.dll")] public static extern bool AlphaBlend( IntPtr hdcDest, // handle to destination DC int nXOriginDest, // x-coord of upper-left corner int nYOriginDest, // y-coord of upper-left corner int nWidthDest, // destination width int nHeightDest, // destination height IntPtr hdcSrc, // handle to source DC int nXOriginSrc, // x-coord of upper-left corner int nYOriginSrc, // y-coord of upper-left corner int nWidthSrc, // source width int nHeightSrc, // source height BlendFunction blendFunction // alpha-blending function );
Seul fait notable : les handle de device contexts auparavant de type HDC sont passés sous le type IntPtr de .Net.
Vient ensuite l'import de BLENDFUNCTION (Encore qu'il ne s'agisse pas à proprement parler d'un import, mais d'un portage), que j'ai au passage renommé BlendFunction pour suivre la casse Pascal des classes et structures du C# :
Code : CSharp
public struct BlendFunction { public byte BlendOp; public byte BlendFlags; public byte SourceConstantAlpha; public byte AlphaFormat; }
Compatibilité : mise en application
Maintenant que nous avons les outils nécessaires à l'affichage, il va falloir rendre compatible notre Image .Net avec GDI. Et c'est bien dans cette opération qu'est le fastidieux de la méthode présentée ici, puisqu'il va nous falloir récupérer un handle de device context qui pourra être exploité par AlphaBlend.
Il va falloir ici faire appel à la connaissances brute de GDI : avec cette librairie, l'affichage suit un schéma bien défini :
- création d'un objet GDI
- création ou récupération d'un device context de dessin compatible avec la cible
- association du device context de dessin et de l'objet GDI
- utilisation du device context de dessin pour effectuer diverses opérations dans un device context d'affichage compatible
La première étape est déjà remplie : nous avons en effet notre image .Net qui permet de récupérer un objet GDI : la classe Image dérive de la classe Bitmap qui contient la méthode GetHBitmap qui retourne un handle d'objet GDI (les choses sont bien faites !).
La deuxième étape fait quant à elle appel à une notion primordiale : celle de compatibilité. Avec GDI, tout objet possède un device context qui lui est propre. Un bitmap possède un device context de bitmap, et un écran un device context d'écran. Pour pouvoir dessiner dans le device context de l'écran, il nous faut utiliser un device context compatible avec celui-ci. Ainsi, bien qu'un bitmap possède un device context, on ne pourra pas l'utiliser pour dessiner dans un écran. Il nous faudra créer un device context compatible avec l'écran, associer le bitmap au nouveau device context, puis, enfin, dessiner.
La question qui se pose alors, c'est : comment créer un device context compatible ?
Il va falloir piocher parmi les fonctions de GDI pour répondre à cette question : CreateCompatibleDC (http://msdn.microsoft.com/en-us/library/aa922550.aspx).
Code : C
HDC CreateCompatibleDC( HDC hdc );
Cette fonction attend pour unique paramètre un device context. Dans notre cas, un device context compatible avec la fenêtre. A nouveau, il va nous falloir faire appel à GDI : GetDC (http://msdn.microsoft.com/en-us/library/aa921543.aspx) permet de récupérer un device context :
Code : C
HDC GetDC( HWND hWnd );
Le seul paramète attendu, hWnd correspond à un handle de fenêtre. Comment récupérer un handle de fenêtre, à présent ? Pour une fois, nul besoin d'aller chercher bien loin : les Windows Forms de .Net possèdent nativement la propriété Handle (là encore de type IntPtr).
Et pour en terminer avec GetDC : les remarques indiquent qu'un device context récupéré avec GetDC doit ensuite être libéré avec ReleaseDC (http://msdn.microsoft.com/en-us/library/aa921334.aspx) :
Code : C
int ReleaseDC( HWND hWnd, HDC hDC );
Un handle de fenêtre, un handle de device context : on a déjà tout cela à portée de main, comme vu précédemment.
Et voilà donc trois nouveaux imports à ajouter à la classe GDI (là encore, pour connaître les DLL utilisées pour l'import, se référer à la documentation MSDN de chaque fonction) :
Code : CSharp
[DllImport("gdi32.dll")] public static extern IntPtr CreateCompatibleDC(IntPtr hdc); [DllImport("user32.dll")] public static extern IntPtr GetDC(IntPtr hWnd); [DllImport("user32.dll")] public static extern bool ReleaseDC(IntPtr hWnd, IntPtr hdc);
Maintenant que nous avons sous la main tous les outils pour créer un device context compatible, on va pouvoir s'intéresser à la mise en application, qui devrait être assez simple avec les explications ci-dessus. Voici le code que l'on retrouve donc, à ajouter dans l'évènement Load de la Form :
Code : CSharp
gdiBitmap = ((Bitmap)image).GetHbitmap(); // Récupère l'objet GDI de l'image. IntPtr hDC = GDI.GetDC(Handle); // Device context de la fenêtre récupéré. dcBitmap = GDI.CreateCompatibleDC(hDC); // Création du device context compatible. GDI.ReleaseDC(Handle, hDC); // Libération du device context de la fenêtre.
Et bien sûr en ajoutant à la classe de la Windows Form, les quelques déclarations d'usage... :
Code : CSharp
IntPtr gdiBitmap; IntPtr dcBitmap;
Dessinons !
Nous sommes maintenant prêt à nous lancer dans le vif du sujet et (enfin) dessiner notre tileset. Il nous suffit maintenant d'associer notre objet GDI représentant le tileset au tout neuf device context compatible avec la Windows Form affichant le rendu. Cela passe par la fonction SelectObject de GDI (http://msdn.microsoft.com/en-us/library/aa932923.aspx) :
Code : C
Sans grande surprise, la fonction attend deux paramètres : le device context et l'objet GDI. L'import ne réserve pas plus de surprise, les deux paramètres étant passés en IntPtr :
Code : CSharp
Nous allons maintenant pouvoir appeler cette fonction :
Code : CSharp
Dans notre exemple, cette ligne est placée dans le Load. Nous n'avons en effet pas besoin de dessiner plusieurs images dans le device context de la fenêtre, une fois chargé, il n'y a que le tileset qui doit être rendu. Si toutefois vous deviez avoir besoin de rendre plusieurs images, sachez que vous pouvez changer dynamiquement le SelectObject, l'objet précédemment sélectionné sera tout simplement remplacé par le nouveau.
Et enfin, nous allons pouvoir remplacer dans le Paint de la Form l'appel à la méthode DrawImage par le code d'appel à AlphaBlend. La première étape est de récupérer le device context de notre fenêtre. Celui-ci est contenu dans l'objet Graphics fournie par l'évènement Paint :
Code : CSharp
Nous pouvons ensuite instancier une nouvelle structure BlendFunction (avec tous les paramètres détaillés) :
Code : CSharp
Notez que l'on n'utilisera donc pas de modulation de transparence (on a SourceConstantAlpha à 255), mais seulement la transparence native. Néanmoins, n'hésitez pas à l'essayer vous-même.
Et pour terminer, l'appel à AlphaBlend :
Code : CSharp
Vous pouvez maintenant vous faire plaisir et lancer l'exécution :

Jackpot, voilà le tileset d'affiché, et l'on peut dès maintenant remarquer le joli "0" dont est orné le label affichant le temps de rendu. Est-ce fini pour autant ?
Regardez-y de plus près... Je vous laisse comparer cette juxtaposition entre le rendu de référence réalisé avec .Net, et le rendu tout juste réalisé avec GDI :

Il y a en effet un pépin : les zones transparentes ne sont pas bien rendues, elles se retrouvent teintées de bleu. Ce problème est récurrent lorsque l'on utilise la méthode GetHBitmap, qui, pour rappel, nous permet de récupérer un bitmap GDI via son handle à partir d'un Bitmap .Net. Opération indispensable si l'on souhaite utiliser AlphaBlend, comme on l'a vu.
C'est en fait une mécanique interne de GetHBitmap qui pose problème : la méthode, lorsqu'elle est appelée, créé un bitmap GDI vierge, le remplit d'une couleur donnée, puis y applique les données du Bitmap .Net source. C'est l'application de la couleur qui - n'ayons pas peur de le dire - est buguée, puisqu'elle n'applique que la composante bleu de la couleur donnée, qui par défaut est le blanc ARGB(255, 255, 255, 255). Au lieu d'avoir un fond blanc, nous avons donc un fond bleu sur lequel se retrouve dessiné notre tileset. Pas de problème là où il n'y a pas de transparence. En revanche, dès que l'on se retrouve à appliquer des zones semi-transparentes, celles-ci s'ajoute au fond bleu du bitmap GDI. D'où cette merveilleuse teinte bleue dont il va falloir se débarasser.
Le problème est connu de Microsoft (cf http://msdn.microsoft.com/en-us/library/ms536295%28VS.85%29.aspx - page de GetHBitmap de GDI+, qui pour information - ou rappel - est à la base du rendu de .Net qui en utilise le code). Dans la section community content, il nous est proposé la solution suivante : ne pas créer de bitmap transparent. Solution comme une autre, mais qui ne nous sied guère, vu que l'on utilise le canal alpha. A cela, on préfèrera tout simplement passer une couleur de fond ne faisant pas appel à de composante bleu. Ainsi, on précisera dans le GetHBitmap la couleur transparente ARGB(0, 0, 0, 0) :
Code : CSharp
Et à l'exécution, vous pourrez constater non sans soulagement un rendu dénué de tout artefact bleuté.
La solution était donc toute simple, mais il était particulièrement intéressant que vous constatiez le genre de problème que peut poser GetHBitmap afin de toujours garder ce bug à l'esprit lorsque vous travaillerez sur des systèmes dérivés de la méthode exposée ici. C'est un petit quelque chose susceptible de faire perdre beaucoup de temps (je parle par expérience).
Code : C
HGDIOBJ SelectObject( HDC hdc, HGDIOBJ hgdiobj );
Sans grande surprise, la fonction attend deux paramètres : le device context et l'objet GDI. L'import ne réserve pas plus de surprise, les deux paramètres étant passés en IntPtr :
Code : CSharp
[DllImport("gdi32.dll")] public static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj);
Nous allons maintenant pouvoir appeler cette fonction :
Code : CSharp
GDI.SelectObject(dcBitmap, gdiBitmap);
Dans notre exemple, cette ligne est placée dans le Load. Nous n'avons en effet pas besoin de dessiner plusieurs images dans le device context de la fenêtre, une fois chargé, il n'y a que le tileset qui doit être rendu. Si toutefois vous deviez avoir besoin de rendre plusieurs images, sachez que vous pouvez changer dynamiquement le SelectObject, l'objet précédemment sélectionné sera tout simplement remplacé par le nouveau.
Et enfin, nous allons pouvoir remplacer dans le Paint de la Form l'appel à la méthode DrawImage par le code d'appel à AlphaBlend. La première étape est de récupérer le device context de notre fenêtre. Celui-ci est contenu dans l'objet Graphics fournie par l'évènement Paint :
Code : CSharp
IntPtr target = g.GetHdc();
Nous pouvons ensuite instancier une nouvelle structure BlendFunction (avec tous les paramètres détaillés) :
Code : CSharp
GDI.BlendFunction bf = new GDI.BlendFunction(); bf.BlendOp = 0; bf.BlendFlags = 0; bf.SourceConstantAlpha = 255; bf.AlphaFormat = 1;
Notez que l'on n'utilisera donc pas de modulation de transparence (on a SourceConstantAlpha à 255), mais seulement la transparence native. Néanmoins, n'hésitez pas à l'essayer vous-même.
Et pour terminer, l'appel à AlphaBlend :
Code : CSharp
GDI.AlphaBlend( target, 0, 0, image.Width, image.Height, dcBitmap, 0, 0, image.Width, image.Height, bf);
Vous pouvez maintenant vous faire plaisir et lancer l'exécution :

Jackpot, voilà le tileset d'affiché, et l'on peut dès maintenant remarquer le joli "0" dont est orné le label affichant le temps de rendu. Est-ce fini pour autant ?
Regardez-y de plus près... Je vous laisse comparer cette juxtaposition entre le rendu de référence réalisé avec .Net, et le rendu tout juste réalisé avec GDI :

Il y a en effet un pépin : les zones transparentes ne sont pas bien rendues, elles se retrouvent teintées de bleu. Ce problème est récurrent lorsque l'on utilise la méthode GetHBitmap, qui, pour rappel, nous permet de récupérer un bitmap GDI via son handle à partir d'un Bitmap .Net. Opération indispensable si l'on souhaite utiliser AlphaBlend, comme on l'a vu.
C'est en fait une mécanique interne de GetHBitmap qui pose problème : la méthode, lorsqu'elle est appelée, créé un bitmap GDI vierge, le remplit d'une couleur donnée, puis y applique les données du Bitmap .Net source. C'est l'application de la couleur qui - n'ayons pas peur de le dire - est buguée, puisqu'elle n'applique que la composante bleu de la couleur donnée, qui par défaut est le blanc ARGB(255, 255, 255, 255). Au lieu d'avoir un fond blanc, nous avons donc un fond bleu sur lequel se retrouve dessiné notre tileset. Pas de problème là où il n'y a pas de transparence. En revanche, dès que l'on se retrouve à appliquer des zones semi-transparentes, celles-ci s'ajoute au fond bleu du bitmap GDI. D'où cette merveilleuse teinte bleue dont il va falloir se débarasser.
Le problème est connu de Microsoft (cf http://msdn.microsoft.com/en-us/library/ms536295%28VS.85%29.aspx - page de GetHBitmap de GDI+, qui pour information - ou rappel - est à la base du rendu de .Net qui en utilise le code). Dans la section community content, il nous est proposé la solution suivante : ne pas créer de bitmap transparent. Solution comme une autre, mais qui ne nous sied guère, vu que l'on utilise le canal alpha. A cela, on préfèrera tout simplement passer une couleur de fond ne faisant pas appel à de composante bleu. Ainsi, on précisera dans le GetHBitmap la couleur transparente ARGB(0, 0, 0, 0) :
Code : CSharp
gdiBitmap = ((Bitmap)image).GetHbitmap(Color.FromArgb(0, 0, 0, 0));
Et à l'exécution, vous pourrez constater non sans soulagement un rendu dénué de tout artefact bleuté.
La solution était donc toute simple, mais il était particulièrement intéressant que vous constatiez le genre de problème que peut poser GetHBitmap afin de toujours garder ce bug à l'esprit lorsque vous travaillerez sur des systèmes dérivés de la méthode exposée ici. C'est un petit quelque chose susceptible de faire perdre beaucoup de temps (je parle par expérience).
Petits tests de vitesse
Maintenant que nous avons notre affichage en bonne et due forme, nous allons pouvoir nous intéresser à la vitesse d'affichage de nos images. Nous avons déjà pu remarquer que l'affichage d'une image seule à coup d'AlphaBlend était instantané (ou plus exactement non mesurable par la précision de TickCount), contre 16 ms avec DrawImage.
Reprenons maintenant notre petite itération pour afficher 100 images. Je vous épargnerai l'image complète identique à la première réalisée via DrawImage, pour me concentrer sur le résultat :

15 ms ! Pas plus !
A comparer aux... 1625 secondes de DrawImage, soit une division du temps d'affichage par plus de 100 !
Testons maintenant l'affichage de 600 fois la tile 1 - sans transparence :
Code : CSharp

16 ms. Qui dit mieux ? Et à nouveau, à comparer aux 875 ms de DrawImage, soit cette fois ci une diminution de plus de 50 fois ! Certes, le gain est moins marqué qu'auparavant, mais reste quand même monumental.
Testons enfin la tile (0,32), avec transparence cette fois-ci :
Code : CSharp

A nouveau, le résultat est sans appel : strictement identique aux 16 ms du rendu ne comportant pas de pixel avec transparence, et à comparer aux 1218 ms de DrawImage. On voit dès lors l'avantage que peut apporter AlphaBlend dans le rendu d'images possédant un canal alpha.
Reprenons maintenant notre petite itération pour afficher 100 images. Je vous épargnerai l'image complète identique à la première réalisée via DrawImage, pour me concentrer sur le résultat :

15 ms ! Pas plus !
A comparer aux... 1625 secondes de DrawImage, soit une division du temps d'affichage par plus de 100 !
Testons maintenant l'affichage de 600 fois la tile 1 - sans transparence :
Code : CSharp
for (int i = 0; i < 600; i++) { GDI.AlphaBlend( target, i, i, 32, 32, dcBitmap, 0, 0, 32, 32, bf); }

16 ms. Qui dit mieux ? Et à nouveau, à comparer aux 875 ms de DrawImage, soit cette fois ci une diminution de plus de 50 fois ! Certes, le gain est moins marqué qu'auparavant, mais reste quand même monumental.
Testons enfin la tile (0,32), avec transparence cette fois-ci :
Code : CSharp
for (int i = 0; i < 600; i++) { GDI.AlphaBlend( target, i, i, 32, 32, dcBitmap, 0, 32, 32, 32, bf); }

A nouveau, le résultat est sans appel : strictement identique aux 16 ms du rendu ne comportant pas de pixel avec transparence, et à comparer aux 1218 ms de DrawImage. On voit dès lors l'avantage que peut apporter AlphaBlend dans le rendu d'images possédant un canal alpha.
Pour récapituler, j'aurais pu parler des performances offertes par la méthode de rendu via GDI, et la vanter longuement. Mais je pense que les résultats parlent d'eux-même tant ils sont écrasants, et prouvent que les technologies à première vue dépassées peuvent encore rendre de sacrés services. Alors au lieu de cela, je vais plutôt parler des inconvénients : outre la mise en oeuvre plus lourde que celle de DrawImage, on rencontre l'impossibilité d'effectuer des transformations complexes autres que le redimensionnement, ou encore l'absence de filtres d'affichage : on travaille ici en mode Pixel perfect uniquement. Est-ce pour autant un tort ? Compte tenu de l'application - un éditeur de map 2D - présentée à la base comme exploitant le rendu GDI, j'aurais tendance à dire que non, c'est de l'affichage haute vitesse de simples images à différents niveaux de zooms, nul besoin de transformations complexes. Si GDI est très intéressant pour de l'application simple d'images dans des Windows Forms, il y a fort à parier qu'une application qui nécessiterait une grande vélocité associée aux transformations se tournerait judicieusement vers d'autres solutions, certes plus complexes, mais aussi plus performantes, telles le délaissé Managed DirectX ou encore XNA.