Dino Esposito
Cliquez ici pour télécharger le code de cet article :
CuttingEdge2006_07.exe (1940 ko)
Les grandes idées sont intemporelles. Il y a longtemps, dans Microsoft Systems Journal, Paul DiLascia a illustré une bonne astuce pour afficher des info-bulles contextuelles au-dessus d'images. Lorsque l'utilisateur déplaçait la souris sur l'image, le contrôle info-bulle mettait à jour son texte afin de refléter le nom de l'image.
Comment un contrôle info-bulle peut-il être suffisamment intelligent pour prendre en charge une zone non rectangulaire qui, par exemple, suit précisément la forme naturelle d'un visage humain dans un tableau ? Les zones actives des images ne sont pas nouvelles, mais vous les définissez généralement par l'intermédiaire de formes courantes, faciles à décrire (rectangles, cercles, polygones).
Paul a résolu ce problème en chargeant deux copies de l'image : l'image d'origine et une carte de zone active. La photo originale s'affiche dans un composant de zone d'image, tandis que l'image de la carte est masquée et utilisée pour mettre en correspondance chaque pixel de l'image d'origine avec une couleur particulière. L'image de la carte est presque identique à l'original, à ceci près qu'elle remplit chaque zone active avec une couleur. De cette façon, chaque zone active correspond à une couleur unique, laquelle peut être liée à un texte d'info-bulle. Avant l'ouverture de l'info-bulle, le pixel situé sous la souris est mis en correspondance avec le pixel correspondant dans la carte des zones actives. La couleur de la copie cachée de l'image est mise en correspondance avec la liste des zones actives et le texte associé s'affiche.
Dans le présent numéro de Cutting Edge, je vais appliquer l'astuce de Paul au contrôle Windows ® Forms PictureBox standard, afin qu'il puisse afficher des info-bulles contextuelles et déclencher les événements appropriés lorsque l'utilisateur clique sur des zones particulières. De cette façon, vous pouvez facilement implémenter des cartes cliquables en utilisant des images réelles, et les agrandir ou les réduire à volonté et très facilement.
La photo de gauche dans la figure 1 est affichée pour l'utilisateur ; celle de droite est utilisée en coulisses pour déterminer rapidement si un pixel sur lequel l'utilisateur a cliqué dans l'image affichée appartient à des zones actives, lesquelles sont en rouge, vert citron, cyan, magenta et jaune dans cette photo. J'ai délibérément choisi une image réelle pour illustrer mon propos. Vous pouvez utiliser n'importe quelle image et créer une carte des régions contextuelles.

Figure 1 Image vue par l'utilisateur et image sous-jacente avec les zones actives
Examinons rapidement un scénario courant. L'utilisateur souhaite cliquer sur une carte du monde afin de sélectionner une région donnée, puis d'examiner les ventes. Comment détecter le clic sur les différentes régions ? Vous pouvez partitionner la carte en polygones créés via une suite de points. Une fois que vous avez une région, une API graphique dans le Microsoft® .NET Framework peut indiquer si un point lui appartient. Cette approche est courante dans les applications ASP.NET, où le contrôle ImageMap facilite son implémentation. Cependant, vous rencontrerez des problèmes si la taille du bitmap final change. Lorsque la taille de la carte change, vous devez recalculer tous les points pour toutes les régions. L'application d'un facteur d'échelle peut aider, mais une solution plus souple serait la bienvenue.
L'utilisation d'une deuxième image présente deux avantages majeurs. Tout d'abord, vous pouvez contrôler plus précisément les limites de chaque région. Ensuite, l'édition des régions est triviale ; il suffit de colorier les zones appropriées avec un simple éditeur graphique, tel que Microsoft Paint (voir figure 2).

Figure 2 Zones actives coloriées définies sur une carte du monde
L'utilisation de deux images, en particulier dans le contexte des applications Windows Forms, consomme des ressources négligeables. Dans ASP.NET, la consommation de mémoire due au chargement d'une image en double est plus critique. Heureusement, des solutions peuvent être conçues pour mettre en cache l'image une seule fois et pour la partager entre plusieurs demandes et utilisateurs.
Le contrôle Windows Forms PictureBox peut afficher les images dans divers formats chargés à partir de différentes sources, notamment des objets Image précédemment chargés, des images distantes d'URL connues, ou des images résidant sur le disque. Dans le .NET Framework 2.0, PictureBox a été encore amélioré afin d'afficher une image temporaire pendant le chargement de l'image principale, et une image d'erreur lorsque l'image sélectionnée n'est pas disponible.
Dans le .NET Framework 1.x, lorsque l'utilisateur clique sur l'image, le contrôle déclenche l'événement Click, mais seule une notification de l'action de l'utilisateur est envoyée ; aucune information complémentaire n'est fournie, notamment sur la position de la souris. Les gestionnaires de l'événement reçoivent un objet EventArgs élémentaire :
Sub PictureBox1_Click(ByVal sender As Object, _
ByVal e As EventArgs) _
Handles PictureBox1.Click
...
End Sub
Dans Windows Forms 2.0, vous pouvez tirer parti du nouvel événement MouseClick, lequel fournit la position de la souris au moment du clic :
Sub PictureBox1_MouseClick(ByVal sender As Object, _
ByVal e As MouseEventArgs) _
Handles PictureBox1.MouseClick
...
End Sub
De la même façon, si vous souhaitez afficher des info-bulles contextuelles basées sur la partie de l'image située sous la souris, vous pouvez gérer l'événement MouseMove à partir du formulaire hôte, traiter la position et déterminer l'action à entreprendre.
Le contrôle PictureBox que je vais implémenter dans cet article gère les deux aspects. Il accepte une collection de zones actives et déclenche des événements client si l'utilisateur clique sur une telle région ou s'y déplace. L'interface de programmation globale imite l'interface de programmation du contrôle ImageMap dans ASP.NET 2.0. La principale différence tient au fait que le contrôle PictureBox définit des régions actives en fonction des couleurs et non des points. Le nouveau contrôle PictureBox est dérivé du contrôle intégré Windows.Forms.PictureBox et présente deux propriétés essentielles : HotSpots et MapImage (voir figure 3).
La propriété MapImage est de type Image et représente l'image qui accompagne l'image affichée, dans laquelle les régions actives sont dessinées avec une palette de couleurs bien connue. L'objet Image est encapsulé par un objet Bitmap (un objet GDI+ standard) qui expose des méthodes permettant d'obtenir la couleur d'un pixel donné.
Examinez la carte de la figure 2. La propriété Image de l'objet PictureBox est liée à l'image supérieure ; la nouvelle propriété MapImage est associée à l'image inférieure. Les deux images doivent satisfaire à certaines exigences. Tout d'abord, les images doivent présenter la même taille. Ensuite, l'image associée doit être enregistrée avec un format sans pertes, tel que BMP ou PNG. Les images JPEG doivent être évitées, car elles utilisent une approximation de la couleur lors de la compression. Si la couleur de certains pixels d'une région active est modifiée, le mécanisme de détection des points échoue. Les fichiers GIF sont adaptés dès lors que les couleurs utilisées pour marquer les régions actives font partie de la palette de couleurs. Sinon, comme avec les images JPEG, il existe un risque d'approximation des couleurs. L'image affichée peut être dans n'importe quel format.
Le contrôle Windows Forms PictureBox présente une propriété SizeMode qui indique la façon dont l'image est affichée. Les valeurs valides de cette propriété proviennent de l'énumération PictureBoxSizeMode. La valeur par défaut est Normal, ce qui signifie que l'image est affichée à partir du coin supérieur gauche du contrôle, et que chaque partie de l'image qui dépasse la zone de PictureBox est rognée. Avec la valeur StretchImage, l'image affichée s'étend ou se réduit afin de s'adapter à la taille de PictureBox. Cela pose un problème important, étant donné le comportement attendu du contrôle PictureBox étendu : l'image d'accompagnement doit être redimensionnée selon le même facteur. Cela peut être fait par programmation avec les classes GDI+. Par exemple, vous pouvez utiliser la méthode GetThumbnailImage de la classe Image ou, encore mieux, la méthode DrawImage de la classe Graphics. Pour calculer le ratio, vous comparez la taille de l'objet PictureBox à la taille de l'image. Notez qu'il existe de bonnes raisons d'éviter l'utilisation de GetThumbnailImage dans ce scénario. Sa fidélité par rapport à l'image d'origine peut être très faible, et dans de rares cas, ce peut être une image totalement différente. Bien que cela soit très peu probable, le problème serait difficile à diagnostiquer. L'exemple de contrôle PictureBox n'offrira pas de fonctionnalités supplémentaires si son mode taille est différent de Normal. Si vos besoins sont différents, vous pouvez compléter le code disponible en téléchargement sur le site Web MSDN®Magazine.
Pour détecter rapidement tout changement de valeur de la propriété SizeMode, et autoriser des fonctionnalités supplémentaires, vous devez gérer l'événement SizeModeChanged dans le contrôle :
Protected Overrides Sub OnSizeModeChanged(ByVal e As EventArgs)
MyBase.OnSizeModeChanged(e)
m_beSmart = (Me.SizeMode = PictureBoxSizeMode.Normal)
End Sub
Un membre privé interne effectue le suivi du mode de travail de PictureBox. Ce membre, à savoir la variable m_beSmart dans l'extrait de code précédent, renvoie false si la propriété SizeMode est différente de Normal.
HotSpots est la deuxième propriété importante du nouveau contrôle PictureBox. Il s'agit d'une collection de types personnalisés, comme le montre la figure 4.
HotSpots est de type HotSpotElementCollection, un type générique obtenu à partir du type Collection (Of T) combiné avec le type HotSpotElement. La classe HotSpotElement définit une région de couleur significative dans une image de carte. Dans ce contexte, une région active est identifiée avec un ID numérique unique, une couleur RVB, un titre et une description. La couleur est utilisée pour identifier la région de manière unique. Si vous êtes intéressé par la capture des clics utilisateur, l'ID fournit une valeur numérique à tester sur le client et pour déterminer l'action à entreprendre. Il est probable que les informations de couleur seront suffisantes pour identifier de manière unique la zone sur laquelle l'utilisateur a cliqué ; de ce point de vue, l'ID est donc redondant. Cependant, il peut être plus utile que les couleurs dans les scénarios de liaison de données, dans lesquels vous souhaitez associer des régions d'une carte à l'ID de la base de données back-end. De cette façon, les informations de couleur ne sont pas associées aux informations de région.
Le titre et la description jouent également un autre rôle ; ils peuvent être utilisés pour les info-bulles contextuelles qui s'affichent lorsque l'utilisateur déplace la souris sur l'image affichée.
Vous pouvez alimenter la propriété HotSpots par programmation ou de manière déclarative à partir du concepteur Visual Studio® 2005. Il est intéressant que Visual Studio 2005 reconnaisse automatiquement les propriétés des collections et les lie à l'éditeur de collection intégré (voir figure 5).

Figure 5 Modification de la propriété Collection des zones actives
L'éditeur de collection par défaut ajoute des membres de collection et les affiche avec n'importe quel texte résultant de la méthode ToString du membre. Dans la grille la plus à droite, vous voyez toutes les propriétés du type membre. Chaque propriété est modifiable, comme dans la grille de propriétés parent de Visual Studio 2005.
Si vous êtes familiarisé avec le développement de contrôles ASP.NET, vous savez que les changements entrés via le concepteur ne sont pas conservés dans le fichier codebehind designer.vb ou designer.cs. Si vous ne me croyez pas, essayez. Placez le nouveau contrôle PictureBox sur un formulaire et alimentez la collection HotSpots. Lorsque vous avez terminé, enregistrez et ouvrez le formulaire. Le contrôle PictureBox se comporte comme si la collection était vide, et le fichier designer.vb file ou le formulaire n'a enregistré aucun élément de zone active. Pourquoi ?
La réponse au problème est que vous devez définir une stratégie de sérialisation de concepteur particulière pour la propriété de collection HotSpots. En général, vous devez faire cela pour toute propriété de collection dans les contrôles personnalisés Windows Forms et ASP.NET. L'attribut suivant de la propriété de collection s'en charge :
<DesignerSerializationVisibility( _
DesignerSerializationVisibility.Content)>
Sans cet attribut, aucun changement apporté à la collection lors de la conception n'est jamais conservé dans le fichier designer.vb de Windows Forms ou dans la page source ASPX dans ASP.NET.
Le type HotSpotElementCollection hérite du type Collection générique et l'étend avec deux méthodes finder : ContainsColor et FindHotSpot. Les deux acceptent une couleur en entrée et renvoient respectivement une valeur booléenne ou un objet HotSpotElement, en fonction de ce qu'elles trouvent. Examinez de nouveau la figure 4. FindHotSpot, en particulier, renvoie l'élément HotSpotElement qui correspond à la couleur spécifiée, si elle existe.
En outre, la classe HotSpotElementCollection remplace deux méthodes (InsertItem et SetItem) afin de s'assurer que la couleur n'entre pas en conflit avec un élément déjà présent dans la collection.
Armé avec une prise en charge complète des régions actives de type image, passons à l'implémentation d'événements pour le formulaire hôte. Je vais définir deux événements, HotSpotFound et HotSpotClicked. HotSpotFound se déclenche lorsque la souris se déplace sur une zone active ; HotSpotClicked se déclenche lorsque l'utilisateur clique sur une zone active.
L'événement HotSpotFound est déclaré avec une nouvelle version générique du type EventHandler, de la façon suivante :
Public Event HotSpotFound As EventHandler(Of HotSpotEventArgs)
Vous avez vu la structure de données HotSpotEventArgs dans la figure 4. Elle étend la classe EventArgs de base avec deux propriétés, HotSpot et CancelTooltip. La propriété HotSpot fait référence à la zone active trouvée ou cliquée. La propriété CancelTooltip indique si, en cas de déplacements de la souris, l'info-bulle correspondante doit être annulée. Cette propriété donne au code client le dernier mot sur l'info-bulle affichée pour la zone active. En gérant l'événement, le formulaire client peut changer, voire annuler, l'info-bulle par programmation.
L'événement HotSpotClicked suit un schéma identique.
Public Event HotSpotClicked As EventHandler(Of HotSpotEventArgs)
Dans ce cas, la propriété CancelTooltip n'est pas utilisée par le contrôle PictureBox une fois que le gestionnaire d'événements a renvoyé le contrôle. Quelle que soit la valeur que vous affectez à la propriété dans le gestionnaire d'événements, elle est ignorée par le contrôle PictureBox.
Le contrôle PictureBox enregistre son propre gestionnaire pour l'événement MouseClick lors de l'instanciation, comme nous l'avons vu dans la figure 3. Le gestionnaire interne, OnClickInternal, est illustré figure 6.
Lorsqu'un point de la zone client du contrôle PictureBox est cliqué, le contrôle reçoit les coordonnées client du point à partir de la structure de données de l'environnement, à savoir la classe MouseEventArgs.
L'étape suivante implique la recherche de la couleur du pixel à la position spécifiée. Notez que l'événement MouseClick interne se déclenche même lorsque l'utilisateur clique en dehors des limites de l'image. Vous devez intercepter ces clics et renvoyer le contrôle sans appeler GetPixel sur l'objet Bitmap. Si vous appelez GetPixel avec des coordonnées en dehors de la taille de l'image, une exception est générée. GetPixel est une méthode de la classe Bitmap qui renvoie la couleur du pixel à l'emplacement donné. Vous recherchez ensuite une région associée à cette couleur. Si une région est trouvée, l'objet HotSpotElement correspondant est renvoyé :
Dim elem As HotSpotElement = HotSpots.FindHotSpot(clr)
Vous utilisez la méthode FindHotSpot de la collection de régions actives afin d'extraire les informations sur la région trouvée.
Mais que se passe-t-il si votre image d'origine contient des pixels avec la même couleur que celle de vos régions actives ? Si l'utilisateur déplace la souris précisément sur ce pixel, un conflit se produit. Pour éviter cela, il est judicieux de choisir pour les régions actives des couleurs qui ne sont utilisées nulle part ailleurs dans l'image. Avec plus de 16 millions de couleurs disponibles, la recherche d'une couleur inutilisée est possible, mais pas facile. Cependant, étant donné la façon dont les pixels colorés sont distribués dans une image réelle, votre couleur sera probablement trouvée dans un pixel ici et là, sans poser de problème.
Une solution plus élégante consisterait à réserver une couleur (blanc ou transparent, par exemple) pour toutes les parties de l'image qui n'ont aucune région définie. De cette façon, vous colorez de manière uniforme toute l'image sous-jacente, à l'exception des régions actives. La couleur réservée peut être exposée en tant que propriété publique afin de permettre aux développeurs de la choisir.
Le contrôle PictureBox vous permet de définir des régions actives dans des images affichées. Chaque région est dessinée sur une copie de l'image (la carte) avec une couleur distincte, si possible une couleur qui ne soit pas utilisée dans le reste de l'image. Une fois que vous avez placé le contrôle PictureBox sur un formulaire, vous définissez la carte et vous alimentez la collection des zones actives. La collection des zones actives indique au contrôle les couleurs « actives » de l'image. Par défaut, lorsque l'utilisateur clique sur une région active, l'événement HotSpotClicked est déclenché. Voici un gestionnaire d'événements typique :
Sub PictureBox1_HotSpotClicked(ByVal sender As Object, _
ByVal e As HotSpotEventArgs) _
Handles PictureBox1.HotSpotClicked
MessageBox.Show(e.HotSpot.Description)
End Sub
Si l'objet PictureBox peut capturer les mouvements de la souris, il enregistre un gestionnaire interne pour l'événement MouseMove. Une fois l'événement déclenché, le contrôle présente une info-bulle. Le titre et le texte de l'info-bulle reflètent généralement les valeurs définies dans l'élément de zone active. Cependant, ces paramètres peuvent être changés dans le gestionnaire client. Comme nous l'avons vu, l'info-bulle peut également être annulée :
RaiseEvent HotSpotFound(Me, args)
If Not args.CancelTooltip Then
m_tooltip.ToolTipTitle = args.HotSpot.Title
m_tooltip.SetToolTip(Me, args.HotSpot.Description)
Else
HideTooltip()
End If
La figure 7 illustre le contrôle en action, ainsi que l'info-bulle standard qu'elle affiche. Le titre de l'info-bulle est changé par programmation et la description de la région active est également affichée sur la bande d'état du formulaire :
Sub PictureBox1_HotSpotFound(ByVal sender As Object, _
ByVal e As HotSpotEventArgs) _
Handles PictureBox1.HotSpotFound
Info.Text = e.HotSpot.Description
e.HotSpot.Title = "EMEA"
End Sub

Figure 7 Info-bulles contextuelles affichées sur une carte
Les fonctionnalités intégrées du contrôle PictureBox présentent une propriété liable appelée Image. L'ajout de la collection HotSpots implique la nécessité d'une forme plus forte de liaison de données. Par exemple, il serait intéressant que la collection puisse être alimentée à partir d'une base de données, ce qui éviterait aux développeurs d'avoir à modifier les fichiers source si une couleur ou une description a changé à un instant donné. Vous pouvez ajouter une propriété DataSource et mettre en correspondance son contenu avec la collection HotSpots, ou éventuellement transformer la collection HotSpots en une propriété lecture/écriture pouvant être définie par programmation en tant qu'objet. Si vous optez pour la propriété DataSource classique, vous devez également définir quelques propriétés de mise en correspondance afin d'indiquer quels champs de la source de données liée mettre en correspondance avec les propriétés liables des éléments de zone active. Par exemple, vous pouvez utiliser des propriétés DataDescriptionField, DataTitleField, DataColorField, DataValueField afin de mettre en correspondance les champs de la source de données avec des éléments de zone active. Pour DataColorField, vous êtes responsable de l'invention d'un algorithme de conversion qui génère un type de couleur .NET Framework basé sur une chaîne ou sur un nombre stocké dans la source de données liée. Cette opération peut être effectuée facilement avec la classe ColorConverter de l'espace de noms System.Drawing.
La propriété liable Image du contrôle PictureBox est marquée avec l'attribut Bindable et ajoutée à la collection DataBindings. Vous pouvez également étendre ce comportement à la propriété MapImage. Pour cela, vous pouvez ajouter l'attribut Bindable à la propriété MapImage du code source du contrôle :
<Bindable(True)> _ Public Property MapImage() As Image ... End Property
J'ai décrit les contrôles de zone d'image contextuels dans le contexte d'une application Windows Forms ; il est cependant facile de créer un contrôle ASP.NET similaire en partant du contrôle Image ou ImageMap.
Dans ASP.NET, le contrôle ImageMap offre un comportement similaire à celui décrit dans cet article et définit un certain nombre de régions HotSpot. Lorsque l'utilisateur clique sur une zone active, le contrôle effectue un postback ou accède à une URL spécifiée. L'environnement ASP.NET est livré avec un certain nombre d'objets HotSpot prédéfinis, notamment les classes CircleHotSpot, RectangleHotSpot et PolygonHotSpot. Vous pouvez également dériver de la classe HotSpot abstraite afin de définir votre propre objet zone active personnalisé, ce qui peut s'avérer utile dans les situations telles que celles que j'ai décrites ici.
Envoyez vos questions et commentaires pour Dino à cutting@microsoft.com.
Dino Esposito est un pilier de Solid Quality Learning et l'auteur de Programming Microsoft ASP.NET 2.0 (Programmation Microsoft ASP.NET 2.0, de Microsoft Press, 2005). Basé en Italie, Dino participe régulièrement aux différents événements de par le monde. Contactez Dino à l'adresse cutting@microsoft.com ou rejoignez son blog sur weblogs.asp.net/despos.