Apprendre la programmation en VBA pour EXCEL par la pratique - Deuxième partie

Tome 2 - Des bases de la programmation en mode graphique à la programmation d'un jeu d'arcade en VBA et Microsoft Excel

Laurent OTT

Dans le tome 2 de ce mémento, qui est une compilation d'exemples pratiques pour apprendre la programmation en Microsoft Office Excel avec VBA, vous allez découvrir la programmation en mode graphique en VBA. Et au fur et à mesure, vous allez apprendre, de façon ludique, à programmer un jeu d'arcade dans ce mode.
Pour bien appréhender cette formation pratique, il est recommandé de lire, en premier, le tome 1.

Merci pour vos avis.
Commentez

- Titre : Apprendre la programmation en VBA pour EXCEL par la pratique - Deuxième partie

- Auteur : Laurent OTT

- Parution : 14 septembre 2016

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2016 Laurent OTT. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Voir également :

- Mémento sur la programmation en VBA pour EXCEL - Tome 1

- Mémento sur la programmation en VBA pour EXCEL - Tome 3

- Mémento sur la programmation en VBA pour EXCEL - Tome 4

- Mémento sur la programmation en VBA pour EXCEL - Tome 5

- Mémento sur la programmation en VBA pour EXCEL - Tome 6

Sommaire

- I. Introduction

- II. Les API de base pour la programmation en mode graphique

- III. Gérer les mémoires écran

- IV. Écrire un texte en transparence

- V. Gérer une image issue d'un fichier BMP

- VI. Modifier la taille d'une image

- VII. Animation de base : déplacer une image à l'écran

- VIII. Animation d'un Sprite

- IX. Les API pour gérer la souris, le clavier, et le son

- X. Procédures du module Img pour écrire du texte

- XI. Procédures du module Img pour dessiner des formes géométriques

- XII. Procédures du module Img pour gérer les images

- XIII. Exemple d'animation avec le module Img

- XIV. Utiliser un formulaire pour les animations

- XV. Snake - Programmer un jeu d'arcade en mode graphique

- XVI. Compatibilité du module Img avec ACCESS

- XVII. La compilation conditionnelle

- XVIII. Un outil pour définir la couleur de transparence des images

- XIX. Utiliser les images contenues dans un formulaire

- XX. Conclusion

- XXI. Les sources et images des exemples présentés

- XXII. Remerciements

I - Introduction

Dans le tome 1, nous avons étudié les bases de la programmation d'EXCEL en VBA. Ce tome 2 est consacré à une partie plus ludique : la programmation en mode graphique.

Mais pourquoi programmer en mode graphique ? Imaginez que vous puissiez maîtriser chacun des pixels de votre écran, afficher des images, gérer des animations… Vous devinez toute la richesse qu'une programmation en mode graphique peut vous apporter. Certes, pas dans une application ordinaire, mais pour un jeu d'arcade, c'est l'outil idéal qui vous manquait pour laisser libre cours à votre imagination.

Autant le savoir dès maintenant, le VBA n'a pas les fonctions nécessaires pour une programmation en mode graphique. Nous allons donc faire appel aux API que nous avons déjà évoquées dans le tome 1 :

« Les API, Application Programming Interface, sont des ensembles de classes, de méthodes ou de fonctions, mises gratuitement à disposition des développeurs par les éditeurs de services, de logiciels ou d'applications. Les développeurs en feront usage pour intégrer certaines fonctionnalités à leurs applications. »

Nous allons étudier les bases de la programmation en mode graphique en utilisant les principales API dédiées au mode graphique. Ces bases vous permettront de développer des applications simples, voire votre premier jeu d'arcade.

Exercice plus délicat qu'il n'y paraît, car vous devrez à la fois interagir avec les interventions du joueur et afficher un maximum d'images pour une bonne esthétique, le tout en préservant la fluidité du jeu.

De solides notions de programmation en VBA vous seront nécessaires. Si ce n'est pas le cas, je vous invite à approfondir vos connaissances en lisant le tome 1, qui vous apportera le niveau requis.

Nous faisons référence dans ce document aux API Windows compatibles avec la version EXCEL 32 bits. Pour plus d'information sur la compatibilité des API sous EXCEL 64 bits, je vous invite à lire l'excellente documentation de Thierry Gasperment : Développer avec Office 64 bits. Les codes sources ont été testés avec EXCEL 2000 sous XP, EXCEL 2010 sous Windows 7, EXCEL 2016 sous Windows 10.

Vous retrouverez le détail de ces API et de nombreux exemples sur le site « http://allapi.mentalis.org/apilist/apilist.php ».

II - Les API de base pour la programmation en mode graphique

La première API que nous utiliserons est GetWindowDC qui retourne dans un entier long le « contexte de périphérique » (Device Context) de l'écran. Par convention nous le noterons Hdc.

Pour bien comprendre, représentez-vous l'écran comme étant l'équivalent d'une image (Bitmap) composée de pixels de couleurs différentes. Le contexte pointe vers cette image chargée en mémoire que nous pouvons maintenant manipuler pixel par pixel.

Ainsi, pour écrire à l'écran un pixel aux coordonnées X, Y on utilise l'API SetPixel(Hdc, X, Y, Couleur).

Inversement, pour connaître la couleur d'un pixel aux coordonnées X,Y on utilise l'API GetPixel(Hdc, X, Y).

Vous remarquerez que les API que nous utiliserons expriment les coordonnées d'un pixel au format (X, Y), contrairement aux fonctions du VBA qui expriment les coordonnées d'une cellule au format (Y, X). Et l'origine d'une image est le pixel 0, 0 contrairement au VBA où l'origine d'une feuille est la cellule 1, 1.

Vous en savez désormais assez pour programmer en mode graphique cet exemple où l'on trace un trait de la position 0, 100 à la position 300, 100 en utilisant la couleur du point haut gauche de l'écran (0, 0) :

Declare Function GetWindowDC Lib "user32" (ByVal hwnd As Long) As Long
Declare Function GetPixel Lib "gdi32" (ByVal Hdc As Long, ByVal X As Long, ByVal Y As Long) As Long
Declare Function SetPixel Lib "gdi32" (ByVal Hdc As Long, ByVal X As Long, ByVal Y As Long, _
ByVal crColor As Long) As Long
Declare Function ReleaseDC Lib "user32" (ByVal hwnd As Long, ByVal Hdc As Long) As Long

Sub NotionsDeBase()

' Récupère le contexte de l'écran:
Hdc = GetWindowDC(0)

' Recherche la couleur du pixel aux coordonnées 0,0 de l'écran:
Couleur = GetPixel(Hdc, 0, 0)

' Dessine avec cette couleur les 301 premiers pixels de la ligne 100:
For X = 0 To 300
Call SetPixel(Hdc, X, 100, Couleur)
Next X

' Libère le buffer utilisé pour atteindre l'écran:
ReleaseDC 0, Hdc

End Sub

La mémoire utilisée pour gérer le contexte de l'écran est libérée par l'API ReleaseDC.

Il y a plus rapide pour tracer une droite, en utilisant dans un premier temps l'API MoveToEx(Hdc, X, Y) pour indiquer les coordonnées du point d'origine, puis LineTo(Hdc, X, Y) pour tracer une droite de ce point jusqu'au point de destination.

La ligne est tracée avec le style, la taille et la couleur par défaut. Vous pouvez paramétrer ces valeurs, avec l'API CreatePen(Style, Taille, Couleur), puis sélectionner ce nouveau modèle avec l'API SelectObject(Hdc, Objet). Le modèle créé est libéré de la mémoire avec l'API DeleteObject(Objet).

Le style du trait peut prendre les valeurs suivantes : 0 trait continu, 1 pointillé, 2 parsemé, 3 alternant traits et points, 4 alternant traits et points doubles.

Declare Function MoveToEx Lib "gdi32" (ByVal Hdc As Long, ByVal X As Long, _
ByVal Y As Long, lpPoint As POINT) As Long
Declare Function LineTo Lib "gdi32" (ByVal Hdc As Long, ByVal X As Long, ByVal Y As Long) As Long
Declare Function CreatePen Lib "gdi32" (ByVal nPenStyle As Long, ByVal nWidth As Long, _
ByVal crColor As Long) As Long
Declare Function SelectObject Lib "gdi32" (ByVal Hdc As Long, ByVal hObject As Long) As Long
Declare Function DeleteObject Lib "gdi32" (ByVal hObject As Long) As Long

Type POINT
X As Long
Y As Long
End Type
Dim PointXY As POINT

Sub NotionsDeBase()

Dim ObjetPen As Long

' Récupère le contexte de l'écran:
Hdc = GetWindowDC(0)

' Création de l'objet crayon qui détermine le style, l'épaisseur et la couleur d'une ligne
' Ici Style = 0 (trait continu), Taille = 1, Couleur = 150000 (rouge):
ObjetPen = CreatePen(0, 1, 150000)
Call SelectObject(Hdc, ObjetPen)

' Active la position 0, 100 (avec PointXY est null):
MoveToEx Hdc, 0, 100, PointXY

' Trace la ligne de la position active à la position 300, 100:
LineTo Hdc, 300, 100

' Efface l'object Crayon de la mémoire:
DeleteObject ObjetPen

' Libère le buffer utilisé pour atteindre l'écran
ReleaseDC 0, Hdc

End Sub

Le dernier point devenant le nouveau point d'origine, cette technique permet de dessiner un polygone vide, c'est-à-dire sans couleur de fond. Exemple pour dessiner un rectangle vide identifié par les coordonnées des angles opposés X1, Y1 à X2, Y2 :

MoveToEx Hdc, X1, Y1, PointXY
LineTo Hdc, X2, Y1
LineTo Hdc, X2, Y2
LineTo Hdc, X1, Y2
LineTo Hdc, X1, Y1

Pour dessiner un rectangle plein, nous utilisons les API CreateSolidBrush(Couleur) pour définir la couleur de fond de remplissage puis Rectangle(Hdc, X1, Y1, X2, Y2).

Pour tout autre polygone plein, nous utilisons l'API Polygon(Hdc, ListeDesPoints, NombreDePoints).

L'API CreateHatchBrush(Style, Couleur) est plus complète que CreateSolidBrush(Couleur) car elle permet de définir, en plus de la couleur de remplissage, le style de remplissage qui peut avoir les valeurs suivantes : 0 traits horizontaux, 1 traits verticaux, 2 traits inclinés de gauche à droite, 3 traits inclinés de droite à gauche, 4 quadrillage, 5 quadrillage incliné, 6 remplissage plein.

Declare Function CreateSolidBrush Lib "gdi32" (ByVal crColor As Long) As Long
Declare Function Rectangle Lib "gdi32" _
(ByVal Hdc As Long, ByVal X1 As Long, ByVal Y1 As Long, _
ByVal X2 As Long, ByVal Y2 As Long) As Long
Declare Function Polygon Lib "gdi32" _
(ByVal Hdc As Long, lpPoint As POINT, ByVal nCount As Long) As Long
Declare Function CreateHatchBrush Lib "gdi32" _
(ByVal nIndex As Long, ByVal crColor As Long) As Long

Sub xNotionsDeBase()

Dim ObjetBrush As Long
Dim ObjetHatchBrush As Long
Dim ObjetPen As Long
Dim PointXY(1 To 3) As POINT

' Récupère le contexte de l'écran:
Hdc = GetWindowDC(0)

' Création de l'objet Brush qui détermine la couleur de remplissage.
' Ici Couleur = 15000 (rouge)
ObjetBrush = CreateSolidBrush(15000)
Call SelectObject(Hdc, ObjetBrush)

' Dessine un rectangle aux coordonnées X1, Y1, X2, Y2:
X1 = 100: Y1 = 100: X2 = 200: Y2 = 200
Call Rectangle(hdc, X1, Y1, X2, Y2)

' Création de l'objet HatchBrush qui détermine le style et la couleur de remplissage.
' Ici Style = 4 (quadrillage), Couleur = 15000 (rouge)
ObjetHatchBrush = CreateHatchBrush(4, 15000)
Call SelectObject(Hdc, ObjetHatchBrush)

' Création de l'objet Pen qui détermine le style du trait, son épaisseur, et sa couleur.
' Ici Style = 2 (pointillé), Taille = 1 (fine), Couleur = 0 (noir)
ObjetPen = CreatePen(2, 1, 0)
Call SelectObject(Hdc, ObjetPen)

' Dessine un Polygone (ici un triangle) aux coordonnées X1, Y1, X2, Y2, X3, Y3:
X1 = 100: Y1 = 400: X2 = 200: Y2 = 500: X3 = X1: Y3 = Y2
PointXY(1).X = X1: PointXY(1).Y = Y1
PointXY(2).X = X2: PointXY(2).Y = Y2
PointXY(3).X = X3: PointXY(3).Y = Y3
Call Polygon(Hdc, PointXY(1), 3)

' Efface les objets de la mémoire:
DeleteObject ObjetBrush
DeleteObject ObjetHatchBrush
DeleteObject ObjetPen

' Libère le buffer utilisé pour atteindre l'écran:
ReleaseDC 0, Hdc

End Sub

Pour écrire du texte, nous disposons de plusieurs API. La rustique TextOut(Hdc, X, Y, Texte, NombreDeCaractères) me semble la plus rapide d'exécution, c'est pourquoi je l'ai retenue.

La configuration des paramètres par défaut utilisés par TextOut se fait avec les API suivantes :

- L'API SetTextColor(Hdc, Couleur) pour définir la couleur du texte ;

- L'API SetBkColor(Hdc, Couleur) pour définir la couleur de fond ;

- L'API CreateFont permet de définir les 14 paramètres de la police utilisée. Les plus utiles sont : font_height qui détermine la taille de la police, weight pour l'épaisseur (de 100 fin, à 900 gras), italic pour mettre en italique (1 pour vrai), underscore pour souligner (1 pour vrai), face_name pour le nom de la police.

Nous utilisons la fonction QBColor du VBA pour convertir une couleur de l'ancien système 16 couleurs (0 Noir, 1 Bleu, 2 Vert, 3 Cyan, 4 Rouge, 5 Magenta, 6 jaune, 7 Blanc, 8 Gris, 9 Bleu clair, 10 Vert clair, 11 Cyan clair, 12 Rouge clair, 13 Magenta clair, 14 Jaune clair, 15 Blanc brillant) au système RVB de 16 millions de couleurs.

Declare Function SetTextColor Lib "gdi32" (ByVal Hdc As Long, ByVal crColor As Long) As Long
Declare Function SetBkColor Lib "gdi32" (ByVal Hdc As Long, ByVal crColor As Long) As Long
Declare Function CreateFont Lib "gdi32" Alias "CreateFontA" _
(ByVal font_height As Long, ByVal font_width As Long, ByVal escapement As Long, _
ByVal Orientation As Long, ByVal weight As Long, ByVal italic As Long, _
ByVal underscore As Long, ByVal strikeout As Long, ByVal character_set As Long, _
ByVal output_precision As Long, ByVal clipping_precision As Long, ByVal quality As Long, _
ByVal pitch_and_family As Long, ByVal face_name As String) As Long
Declare Function TextOut Lib "gdi32" Alias "TextOutA" _
(ByVal Hdc As Long, ByVal X As Long, ByVal Y As Long, ByVal lpString As String, _
ByVal nCount As Long) As Long

Sub NotionsDeBase()

' Récupère le contexte de l'écran:
Hdc = GetWindowDC(0)

Call SetTextColor(Hdc, QBColor(15)) ' Couleur Basic du texte 15 = Blanc
Call SetBkColor(Hdc, QBColor(0))    ' Couleur Basic du fond 0 = Noir

' Création d'un objet qui détermine les propriétés de l'écriture:
Dim ObjetFont As Long
ObjetFont = CreateFont(20, 0, 0, 0, 900, 0, 1, 0, 0, 0, 0, 0, 0, "Arial")
Call SelectObject(Hdc, ObjetFont)

' Affiche le texte sur 15 caractères en haut à gauche (origine 0, 0):
Call TextOut(Hdc, 0, 0, "Démonstration :", 15)

' Libère le buffer utilisé pour atteindre l'écran:
ReleaseDC 0, Hdc

End Sub

Nous verrons plus tard comment écrire en transparence.

III - Gérer les mémoires écran

Jusqu'à présent nous avons travaillé directement sur l'écran, en utilisant le contexte de l'écran. Mais dans la pratique, il ne faut pas faire comme cela, car les temps de mise à jour de l'écran sont trop longs et provoquent des saccades. Vous comprendrez pourquoi plus tard.

Pour des traitements plus rapides, nous allons générer une mémoire compatible avec le format de l'écran, c'est-à-dire un nouveau contexte, y faire une copie de l'écran, travailler dans cette mémoire, puis afficher à l'écran cette mémoire.

Pour cela nous utilisons les nouvelles API suivantes :

- GetSystemMetrics(Index) pour connaître la largeur et la hauteur de l'écran ;

- CreateCompatibleBitmap(Hdc, Largeur, Hauteur) pour créer une mémoire au format Bitmap de l'écran, de la taille désirée ;

- CreateCompatibleDC(Hdc) pour créer un nouveau contexte compatible avec le format de l'écran ;

- BitBlt(HdcDestination, X, Y, Largeur, Hauteur, HdcSource, XSource, YSource, Mode) pour copier dans le contexte HdcDestination en position X, Y sur une largeur et une hauteur, le contenu du contexte source d'origine XSource, YSource ;

- DeleteDC(Hdc) libère, de la mémoire, le contexte créé.

L'exemple suivant permet d'inverser l'écran. Pour cela, nous mémorisons l'écran dans une première mémoire, puis nous créons une deuxième mémoire, vide, qui sera alimentée des pixels inversés, ceux de gauche allant à droite, ceux d'en haut allant en bas. Cette mémoire sera affichée à l'écran 5 secondes, puis la mémoire d'origine sera affichée.

Declare Function GetWindowDC Lib "user32" (ByVal hwnd As Long) As Long
Declare Function GetSystemMetrics Lib "user32" (ByVal nIndex As Long) As Long
Declare Function CreateCompatibleBitmap Lib "gdi32" (ByVal Hdc As Long, ByVal nWidth As Long, _
ByVal nHeight As Long) As Long
Declare Function CreateCompatibleDC Lib "gdi32" (ByVal Hdc As Long) As Long
Declare Function SelectObject Lib "gdi32" (ByVal Hdc As Long, ByVal hObject As Long) As Long
Declare Function BitBlt Lib "gdi32" (ByVal hDestDC As Long, ByVal X As Long, ByVal Y As Long, _
ByVal nWidth As Long, ByVal nHeight As Long, ByVal hSrcDC As Long, ByVal xSrc As Long, _
ByVal ySrc As Long, ByVal dwRop As Long) As Long
Declare Function GetPixel Lib "gdi32" (ByVal Hdc As Long, ByVal X As Long, ByVal Y As Long) As Long
Declare Function SetPixel Lib "gdi32" (ByVal Hdc As Long, ByVal X As Long, ByVal Y As Long, _
ByVal crColor As Long) As Long
Declare Function DeleteObject Lib "gdi32" (ByVal hObject As Long) As Long
Declare Function DeleteDC Lib "gdi32" (ByVal Hdc As Long) As Long
Declare Function ReleaseDC Lib "user32" (ByVal hwnd As Long, ByVal Hdc As Long) As Long
Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)
Const SRCCOPY = &HCC0020

Sub NotionsDeBase()

' Récupère le contexte de l'écran:
Hdc = GetWindowDC(0)

' Recherche la résolution de l'écran:
ImgMaxiX = GetSystemMetrics(0) ' Taille verticale
ImgMaxiY = GetSystemMetrics(1) ' Taille horizontale

' Création d'une mémoire (compatible écran) de taille de l'écran:
Dim ImgEcran_Img As Variant
Dim ImgEcran_Hdc As Long
Dim ImgEcran_Obj As Long

ImgEcran_Img = CreateCompatibleBitmap(Hdc, ImgMaxiX, ImgMaxiY)
ImgEcran_Hdc = CreateCompatibleDC(Hdc)
ImgEcran_Obj = SelectObject(ImgEcran_Hdc, ImgEcran_Img)

' Copie l'intégralité de l'écran dans cette mémoire:
BitBlt ImgEcran_Hdc, 0, 0, ImgMaxiX, ImgMaxiY, Hdc, 0, 0, SRCCOPY

' Création d'une 2e mémoire (compatible écran) de la taille de l'écran:
Dim Tps_Img As Variant
Dim Tps_Hdc As Long
Dim Tps_Obj As Long

Tps_Img = CreateCompatibleBitmap(Hdc, ImgMaxiX, ImgMaxiY)
Tps_Hdc = CreateCompatibleDC(Hdc)
Tps_Obj = SelectObject(Tps_Hdc, Tps_Img)

' Inverse les pixels horizontaux et verticaux:
For X = 0 To ImgMaxiX
For Y = 0 To ImgMaxiY
Couleur = GetPixel(ImgEcran_Hdc, X, Y)
Call SetPixel(Tps_Hdc, ImgMaxiX - X, ImgMaxiY - Y, Couleur)
Next Y
Next X

' Affiche cette mémoire à l'écran:
BitBlt Hdc, 0, 0, ImgMaxiX, ImgMaxiY, Tps_Hdc, 0, 0, SRCCOPY

' Attente de cinq secondes:
Call Sleep(5000)

' Affiche la mémoire d'origine de l'écran:
BitBlt Hdc, 0, 0, ImgMaxiX, ImgMaxiY, ImgEcran_Hdc, 0, 0, SRCCOPY

' Libère les mémoires:
DeleteObject SelectObject(ImgEcran_Hdc, ImgEcran_Obj)
DeleteDC ImgEcran_Hdc
DeleteObject SelectObject(Tps_Hdc, Tps_Obj)
DeleteDC Tps_Hdc

' Libère le buffer écran:
ReleaseDC 0, Hdc
End Sub

Remarques sur l'utilisation de BitBlt (HdcDestination, X, Y, Largeur, Hauteur, HdcSource, XSource, YSource, Mode) : la taille de l'image source à copier est définie dans les arguments Largeur et Hauteur. Le point d'origine (haut, gauche) de l'image source est défini par les arguments XSource et YSource, qui ne valent pas forcément 0. Vous pouvez donc ne copier qu'une fraction de l'image source. De même, l'image n'est pas forcément copiée dans le contexte de destination au point 0, 0 (haut, gauche). Vous pouvez donc copier l'image source n'importe où dans le contexte de destination grâce aux arguments X et Y. Enfin, il s'agit bien d'une copie et non d'un transfert, la source reste donc inchangée.

En résumé : pour faire une copie d'écran dans une mémoire, HdcSource est le contexte de l'écran et HdcDestination est le contexte de la mémoire. Inversement, pour afficher à l'écran une mémoire, HdcSource est le contexte de la mémoire et HdcDestination est le contexte de l'écran.

Il existe une autre API pour copier une image, semblable à BitBlt, un peu plus longue en temps de traitement, mais qui sait gérer la couleur de transparence. C'est l'API GdiTransparentBlt (HdcDestination, X, Y, Largeur, Hauteur, HdcSource, XSource, YSource, LargeurSource, HauteurSource, CouleurDeTransparence)

Nous allons l'utiliser pour écrire un texte en transparence.

IV - Écrire un texte en transparence

Pour écrire un texte en transparence, nous créons dans un premier temps une mémoire de la taille du texte à écrire. Cette mémoire, vide, est initialisée avec des pixels à 0. Puis nous y déposons le texte désiré après avoir pris soin de définir la couleur 0 comme couleur de fond. Enfin, nous copions cette mémoire à l'écran via l'API GdiTransparentBlt en déclarant la couleur 0 comme couleur de transparence.

Cette méthode donne des résultats plus esthétiques que l'écriture sur fond opaque, mais est plus longue en temps de traitement.

Cet exemple montre comment écrire un texte de 15 caractères en police Arial de taille 20 :

Declare Function GdiTransparentBlt Lib "gdi32.dll" (ByVal Hdc As Long, ByVal X As Long, _
ByVal Y As Long, ByVal nWidth As Long, ByVal nHeight As Long, ByVal hSrcDC As Long, _
ByVal xSrc As Long, ByVal ySrc As Long, ByVal nSrcWidth As Long, _
ByVal nSrcHeight As Long, ByVal crTransparent As Long) As Boolean

Sub NotionsDeBase()

' Récupère le contexte de l'écran:
Hdc = GetWindowDC(0)

' Création d'une mémoire (compatible écran) de taille du texte = 20 pixels x 15 caractères:
Dim ImgEcran_Img As Variant
Dim ImgEcran_Hdc As Long
Dim ImgEcran_Obj As Long

ImgEcran_Img = CreateCompatibleBitmap(Hdc, 20 * 15, 20)
ImgEcran_Hdc = CreateCompatibleDC(Hdc)
ImgEcran_Obj = SelectObject(ImgEcran_Hdc, ImgEcran_Img)

' Définition de la couleur du texte et de la couleur de fond (pour ce nouveau contexte):
Call SetTextColor(ImgEcran_Hdc, 10)  ' Couleur texte 10 = Noir
Call SetBkColor(ImgEcran_Hdc, 0)     ' Couleur du fond 0

' Création d'un objet qui détermine les propriétés de l'écriture (pour ce nouveau contexte):
Dim ObjetFont As Long
ObjetFont = CreateFont(20, 0, 0, 0, 900, 0, 0, 0, 0, 0, 0, 0, 0, "Arial")
Call SelectObject(ImgEcran_Hdc, ObjetFont)

' Écrit le texte sur 15 caractères dans cette mémoire, et non pas à l'écran:
Call TextOut(ImgEcran_Hdc, 0, 0, "Démonstration :", 15)

' Copie cette mémoire dans l'écran en 100, 400, avec 0 en couleur de transparence:
Call GdiTransparentBlt(Hdc, 100, 400, 20 * 15, 20, ImgEcran_Hdc, 0, 0, 20 * 15, 20, 0)

' Libère les mémoires:
DeleteObject SelectObject(ImgEcran_Hdc, ImgEcran_Obj)
DeleteDC ImgEcran_Hdc
DeleteObject ObjetFont

' Libère le buffer écran:
ReleaseDC 0, Hdc
End Sub

V - Gérer une image issue d'un fichier BMP

La fonction VBA LoadPicture permet de charger une image Bitmap BMP, compatible au format écran. Elle se substitue alors à l'API CreateCompatibleBitmap.

L'API GetObjectAPI permet de lire le format du fichier Bitmap BMP et de connaître ses dimensions qui nous seront utiles pour copier l'image.

L'exemple ci-dessus charge en mémoire une image BMP représentant une pomme, et obtient un nouveau contexte de périphérique qui permettra de manipuler cette mémoire (et non pas le fichier).

La couleur du point d'origine de l'image est considérée comme la couleur de transparence à utiliser pour afficher l'image.

Declare Function GetObjectAPI Lib "gdi32" Alias "GetObjectA" _
(ByVal hObject As Long, ByVal nCount As Long, lpObject As Any) As Long

Private Type Bitmap
bmType As Long
bmWidth As Long
bmHeight As Long
bmWidthBytes As Long
bmPlanes As Integer
bmBitsPixel As Integer
bmBits As Long
End Type

Sub NotionsDeBase()

' Récupère le contexte de l'écran:
Hdc = GetWindowDC(0)

' Création d'une mémoire (compatible écran):
Dim ImgFichier_Img As Variant
Dim ImgFichier_Hdc As Long
Dim ImgFichier_Obj As Long

' Charge l'image et crée une mémoire compatible au format de l'écran:
Set ImgFichier_Img = LoadPicture("C:\_Formation_VBA\Pomme.bmp")
ImgFichier_Hdc = CreateCompatibleDC(Hdc)
ImgFichier_Obj = SelectObject(ImgFichier_Hdc, ImgFichier_Img)

' Récupère le bitmap de l'image pour connaître la taille de l'image:
Dim Bitmap_BMP As Bitmap
Call GetObjectAPI(ImgFichier_Img.handle, Len(Bitmap_BMP), Bitmap_BMP)

' Récupére la couleur de transparence de l'image (couleur de son point d'origine 0,0):
CouleurTransparence = GetPixel(ImgFichier_Hdc, 0, 0)

' Copie cette mémoire dans l'écran en 100, 100, en utilisant la couleur de transparence:
Call GdiTransparentBlt(Hdc, 100, 100, Bitmap_BMP.bmWidth, Bitmap_BMP.bmHeight, ImgFichier_Hdc, _
0, 0, Bitmap_BMP.bmWidth, Bitmap_BMP.bmHeight, CouleurTransparence)

' Libère les mémoires:
DeleteObject SelectObject(ImgFichier_Hdc, ImgFichier_Obj)
DeleteDC ImgFichier_Hdc
ReleaseDC 0, Hdc

End Sub

VI - Modifier la taille d'une image

L'API StretchBlt(HdcDestination, X, Y, Largeur, Hauteur, HdcSouce, XSource, YSource, LargeurSource, HauteurSource, Mode) permet de transformer les dimensions d'une image. Une image temporaire est utilisée pour gérer cette transformation et restituer l'image d'origine avec ses nouvelles dimensions.

Declare Function StretchBlt Lib "gdi32" (ByVal Hdc As Long, ByVal X As Long, ByVal Y As Long, _
ByVal nWidth As Long, ByVal nHeight As Long, ByVal hSrcDC As Long, ByVal xSrc As Long, _
ByVal ySrc As Long, ByVal nSrcWidth As Long, ByVal nSrcHeight As Long, ByVal dwRop As Long) As Long

Sub NotionsDeBase()

' Récupère le contexte de l'écran:
Hdc = GetWindowDC(0)

' Création d'une mémoire (compatible écran):
Dim ImgFichier_Img As Variant
Dim ImgFichier_Hdc As Long
Dim ImgFichier_Obj As Long

' Charge l'image et crée une mémoire compatible au format de l'écran:
Set ImgFichier_Img = LoadPicture("C:\_Formation_VBA\Pomme.bmp")
ImgFichier_Hdc = CreateCompatibleDC(Hdc)
ImgFichier_Obj = SelectObject(ImgFichier_Hdc, ImgFichier_Img)

' Récupére la couleur de transparence de l'image (couleur de son point d'origine 0,0):
CouleurTransparence = GetPixel(ImgFichier_Hdc, 0, 0)

' Définition des nouvelles dimensions:
Lg = 50: Ht = 50

' Création d'une mémoire temporaire (compatible écran) de taille 50 x 50:
Dim TpsImg_Img As Variant
Dim TpsImg_Hdc As Long
Dim TpsImg_Obj As Long

TpsImg_Img = CreateCompatibleBitmap(Hdc, Lg, Ht)
TpsImg_Hdc = CreateCompatibleDC(Hdc)
TpsImg_Obj = SelectObject(TpsImg_Hdc, TpsImg_Img)

' Alimente la mémoire temporaire de l'image d'origine 26 x 26 avec sa taille modifiée:
StretchBlt TpsImg_Hdc, 0, 0, Lg, Ht, ImgFichier_Hdc, 0, 0, 26, 26, SRCCOPY

' Efface la mémoire de l'image d'origine:
DeleteObject SelectObject(ImgFichier_Hdc, ImgFichier_Obj)
DeleteDC ImgFichier_Hdc

' Création d'une nouvelle mémoire avec les nouvelles dimensions:
ImgFichier_Img = CreateCompatibleBitmap(Hdc, Lg, Ht)
ImgFichier_Hdc = CreateCompatibleDC(Hdc)
ImgFichier_Obj = SelectObject(ImgFichier_Hdc, ImgFichier_Img)

Call BitBlt(ImgFichier_Hdc, 0, 0, Lg, Ht, TpsImg_Hdc, 0, 0, SRCCOPY)

' Affiche à l'écran la mémoire avec ses nouvelles dimensions:
Call GdiTransparentBlt(Hdc, 100, 100, Lg, Ht, ImgFichier_Hdc, 0, 0, Lg, Ht, CouleurTransparence)

' Libère les mémoires:
DeleteObject SelectObject(ImgFichier_Hdc, ImgFichier_Obj)
DeleteDC ImgFichier_Hdc
ReleaseDC 0, Hdc

End Sub

VII - Animation de base : déplacer une image à l'écran

Nous avons suffisamment d'outils graphiques pour une première animation, qui consiste à déplacer une image à l'écran. Reprenons l'image de la pomme que nous venons d'afficher dans l'exemple précédent. D'après les principes de Newton, cette pomme attirée par la gravité est censée tomber au sol, soit au bas de l'écran. Mais avant de la déplacer, il faudra effacer l'ancienne position, sans quoi il va y avoir plusieurs pommes d'affichées.

Nous allons donc procéder en plusieurs étapes. Mémoriser l'écran où sera affichée la pomme. Afficher la pomme. Faire une pause. Afficher l'écran mémorisé sans la pomme. Déplacer la pomme. Recommencer.

Sub NotionsDeBase()

' Récupère le contexte de l'écran:
Hdc = GetWindowDC(0)

' Création d'une mémoire (compatible écran):
Dim ImgFichier_Img As Variant
Dim ImgFichier_Hdc As Long
Dim ImgFichier_Obj As Long

' Charge l'image et crée une mémoire compatible au format de l'écran:
Set ImgFichier_Img = LoadPicture("C:\_Formation_VBA\Pomme.bmp")
ImgFichier_Hdc = CreateCompatibleDC(Hdc)
ImgFichier_Obj = SelectObject(ImgFichier_Hdc, ImgFichier_Img)

' Récupère le bitmap de l'image pour connaître la taille de l'image:
Dim Bitmap_BMP As Bitmap
Call GetObjectAPI(ImgFichier_Img.handle, Len(Bitmap_BMP), Bitmap_BMP)

' Récupére la couleur de transparence de l'image (couleur de son point d'origine 0,0):
CouleurTransparence = GetPixel(ImgFichier_Hdc, 0, 0)

' Création d'une nouvelle mémoire de la taille de la pomme:
Dim Tps_Img As Variant
Dim Tps_Hdc As Long
Dim Tps_Obj As Long

Tps_Img = CreateCompatibleBitmap(Hdc, Bitmap_BMP.bmWidth, Bitmap_BMP.bmHeight)
Tps_Hdc = CreateCompatibleDC(Hdc)
Tps_Obj = SelectObject(Tps_Hdc, Tps_Img)

' Boucle sur la position Y de la pomme en incrémentant de 10 pixels la descente de la pomme:
For Y = 100 To GetSystemMetrics(1) Step 10

' Mémorise l'écran où sera affichée la pomme:
Call BitBlt(Tps_Hdc, 0, 0, Bitmap_BMP.bmWidth, Bitmap_BMP.bmHeight, Hdc, 100, Y, SRCCOPY)

' Affiche la pomme à l'écran en 100, Y, en utilisant la couleur de transparence:
Call GdiTransparentBlt(Hdc, 100, Y, Bitmap_BMP.bmWidth, Bitmap_BMP.bmHeight, _
ImgFichier_Hdc, 0, 0, Bitmap_BMP.bmWidth, Bitmap_BMP.bmHeight, CouleurTransparence)

' Fait une pause:
Call Sleep(50)

' Affiche l'écran d'origine:
Call BitBlt(Hdc, 100, Y, Bitmap_BMP.bmWidth, Bitmap_BMP.bmHeight, Tps_Hdc, 0, 0, SRCCOPY)

Next Y

' Libère les mémoires:
DeleteObject SelectObject(ImgFichier_Hdc, ImgFichier_Obj)
DeleteDC ImgFichier_Hdc
DeleteObject SelectObject(Tps_Hdc, Tps_Obj)
DeleteDC Tps_Hdc
ReleaseDC 0, Hdc

End Sub

Ça marche, mais déception, l'animation est saccadée, et surtout ça clignote. Ce qui est très désagréable.
C'est normal, je vous avais prévenu qu'il ne fallait pas travailler directement sur le contexte de l'écran, car les temps de traitement sont longs, mais passer par une mémoire de travail, travailler dedans, puis l'afficher.

Une fois l'écran d'origine mémorisé, les étapes deviennent : copier en mémoire de travail cet écran d'origine. Y copier ensuite la pomme. Afficher cette mémoire de travail. Déplacer la pomme. Recommencer.

Reprenons le code. Avant la boucle, nous créons une mémoire de la taille de l'écran et nous y copions l'écran d'origine :

Ecran_Img = CreateCompatibleBitmap(Hdc, GetSystemMetrics(0), GetSystemMetrics(1))
Ecran_Hdc = CreateCompatibleDC(Hdc)
Ecran_Obj = SelectObject(Ecran_Hdc, Ecran_Img)

Call BitBlt(Ecran_Hdc, 0, 0, GetSystemMetrics(0), GetSystemMetrics(1), Hdc, 0, 0, SRCCOPY)

Nous créons aussi une mémoire de travail de la taille de l'écran :

Travail_Img = CreateCompatibleBitmap(Hdc, GetSystemMetrics(0), GetSystemMetrics(0))
Travail_Hdc = CreateCompatibleDC(Hdc)
Travail_Obj = SelectObject(Travail_Hdc, Travail_Img)

Dans la boucle, lorsque nous copierons l'écran d'origine en mémoire de travail, nous utiliserons la mémoire d'origine de l'écran et non plus le contexte de l'écran.
La pomme ne sera plus copiée à l'écran, mais dans la mémoire de travail.
Puis cette mémoire de travail sera affichée à l'écran.

Cela permet de ne faire qu'un appel au contexte de l'écran et non plus trois, d'où un gain de temps dans les traitements qui s'explique facilement, car l'accès aux mémoires est bien plus rapide que l'accès à l'écran. On peut aussi réduire la pause pour éliminer l'effet saccadé.

Le code complet de la boucle devient :

' Boucle sur la position Y de la pomme en incrémentant de 10 pixels la descente de la pomme:
For Y = 100 To GetSystemMetrics(1) Step 10

' Copie l'écran d'origine en mémoire de travail:
Call BitBlt(Travail_Hdc, 0, 0, GetSystemMetrics(0), GetSystemMetrics(1), Ecran_Hdc, 0, 0, SRCCOPY)

' Copie la pomme en mémoire de travail en 100, Y, en utilisant la couleur de transparence:
Call GdiTransparentBlt(Travail_Hdc, 100, Y, Bitmap_BMP.bmWidth, Bitmap_BMP.bmHeight, _
ImgFichier_Hdc, 0, 0, Bitmap_BMP.bmWidth, Bitmap_BMP.bmHeight, CouleurTransparence)

' Affiche à l'écran la mémoire de travail:
Call BitBlt(Hdc, 0, 0, GetSystemMetrics(0), GetSystemMetrics(1), Travail_Hdc, 0, 0, SRCCOPY)

' Fait une pause:
Call Sleep(40)

Next Y

Assimilez bien cette gestion des mémoires des images, car nous en aurons besoin par la suite.

VIII - Animation d'un Sprite

Illustrer une pomme qui tombe ne nécessite qu'une image. Il en est autrement pour le vol d'un oiseau. Comme pour un dessin animé, nous avons besoin d'enchaîner différentes images qui décomposent le battement des ailes. C'est ce que nous appelons un Sprite.

J'ai récupéré sur « http://linuxfr.org/users/julien_jorge/journaux/outils-autour-de-gimp-pack-my-sprites-et-xcftools » celui du vol d'un canard. Après retraitement j'ai obtenu huit fichiers BMP de 75 x 84 pixels nommés Vol1 à Vol8, avec 0 en couleur de fond. Pour un cycle d'animation complet, il faudra afficher les images 1 à 8 puis 7 à 1 :

Le code est semblable à celui étudié pour le déplacement de la pomme, car le principe est le même. Seule évolution, l'image à afficher n'est plus toujours la même et il faut boucler sur la liste des images qui composent le cycle d'animation.

Huit mémoires image devront donc être créées, en plus de la mémoire de l'écran d'origine et de la mémoire de travail. Alors pour simplifier le code, nous allons utiliser un type personnalisé dimensionné de -1 à 8, où les images du Sprite du vol seront notées 1 à 8. L'image 0 sera la mémorisation de l'écran d'origine, et l'image -1 sera notre image de travail.

Pour vérifier la rapidité du traitement, nous allons afficher trois canards en même temps…

Type TypeImage
Img As Variant
Hdc As Long
Obj As Long
End Type

Type TypeCanard
X As Long
Y As Long
IndiceImage As Integer
End Type

Sub NotionsDeBase()

' Récupère le contexte de l'écran:
Hdc = GetWindowDC(0)

' Initisalisation du nombre de mémoire image qui seront utilisées:
Dim Image(-1 To 8) As TypeImage

' Initialise les mémoires -1 (de travail) et 0 (écran d'origine):
For i = -1 To 0
Image(i).Img = CreateCompatibleBitmap(Hdc, GetSystemMetrics(0), GetSystemMetrics(1))
Image(i).Hdc = CreateCompatibleDC(Hdc)
Image(i).Obj = SelectObject(Image(i).Hdc, Image(i).Img)
Next i

' Charge les 8 images des canards dans les mémoires 1 à 8:
For i = 1 To 8
Set Image(i).Img = LoadPicture("C:\_Formation_VBA\Vol" & i & ".bmp")
Image(i).Hdc = CreateCompatibleDC(Hdc)
Image(i).Obj = SelectObject(Image(i).Hdc, Image(i).Img)
Next i

' Création de la liste d'ordre d'affichage des images pour un cycle complet:
ListeImages = Array("1", "2", "3", "4", "5", "6", "7", "8", "7", "6", "5", "4", "3", "2", "1")

' Mémorise l'écran d'origine en mémoire 0:
Call BitBlt(Image(0).Hdc, 0, 0, GetSystemMetrics(0), GetSystemMetrics(1), Hdc, 0, 0, SRCCOPY)

' Initialisation de la position des canards de façon aléatoire:
Dim Canard(1 To 3) As TypeCanard
For i = 1 To 3
Call Randomize(Timer)                 ' Générateur aléatoire
Canard(i).X = -100 + Rnd() * 100      ' Position horizontale
Canard(i).Y = Rnd() * 200             ' Position verticale
Canard(i).IndiceImage = Rnd() * 14    ' Image du vol
Next i

' Boucle tant qu'il reste un canard à l'écran, en incrémentant de 10 pixels vers la droite:
Do

' Copie de la mémorisation de l'écran (mémoire 0) en mémoire de travail (mémoire -1):
Call BitBlt(Image(-1).Hdc, 0, 0, GetSystemMetrics(0), GetSystemMetrics(1), _
Image(0).Hdc, 0, 0, SRCCOPY)

' Déplace les canards et incrémente le numéro de l'image du vol:
For i = 1 To 3

Canard(i).X = Canard(i).X + 10

' Incrément l'indice dans la liste des images. Si fin du cycle alors reprend au début.
Canard(i).IndiceImage = Canard(i).IndiceImage + 1
If Canard(i).IndiceImage > UBound(ListeImages) Then Canard(i).IndiceImage = 0

' NumImage contient le numéro de l'image du vol à utiliser:
NumImage = ListeImages(Canard(i).IndiceImage)

' Copie le canard en mémoire de travail -1 en X, Y, avec couleur de transparence 0:
Call GdiTransparentBlt(Image(-1).Hdc, Canard(i).X, Canard(i).Y, 75, 84, _
Image(NumImage).Hdc, 0, 0, 75, 84, 0)
Next i

' Affiche à l'écran la mémoire de travail -1:
Call BitBlt(Hdc, 0, 0, GetSystemMetrics(0), GetSystemMetrics(1), Image(-1).Hdc, 0, 0, SRCCOPY)

' Fait une pause:
Call Sleep(40)

Loop While Canard(1).X < GetSystemMetrics(0) Or Canard(2).X < GetSystemMetrics(0) _
Or Canard(3).X < GetSystemMetrics(0)
' Libère les mémoires:
For i = -1 To 8
DeleteObject SelectObject(Image(i).Hdc, Image(i).Obj)
DeleteDC Image(i).Hdc
Next i
ReleaseDC 0, Hdc

End Sub

Certains développeurs utilisent les termes de héros, de lutin (Sprite en anglais) ou d'avatar pour désigner ces « personnages » animés qui sont présents dans leurs jeux. J'ai une préférence pour « Avatar ».

Nous avons déjà pratiquement fait le tour des API de base nous permettant de programmer en mode graphique. Avouez que finalement ce n'est pas bien compliqué, surtout quand c'est bien expliqué.

Il nous reste juste à étudier les API qui permettent d'interagir avec l'utilisateur, ou le joueur, en lisant les périphériques d'entrée (souris et clavier) mais aussi en ajoutant des effets sonores.

IX - Les API pour gérer la souris, le clavier, et le son

Vos applications en mode graphique nécessiteront sûrement d'utiliser les API suivantes pour gérer la souris, le clavier, et apporter du son.

API pour modifier la forme de la souris en chargeant un curseur d'après un fichier « .cur » :

Declare Function SetSystemCursor Lib "user32" (ByVal hcur As Long, ByVal Id As Long) As Long
Declare Function LoadCursorFromFile Lib "user32" Alias "LoadCursorFromFileA" (ByVal lpFileName As String) As Long

Exemples :

Call SetSystemCursor(LoadCursorFromFile("C:\WINDOWS\Cursors\aero_unavail.cur"), 32512)

Call SetSystemCursor(LoadCursorFromFile("C:\WINDOWS\Cursors\aero_arrow.cur"), 32512)

API pour masquer ou afficher la souris :

Declare Function ShowCursor Lib "user32" (ByVal bShow As Long) As Long

Exemples :

Call ShowCursor(0)  ' Masque le curseur de la souris
Call ShowCursor(1)  ' Affiche le curseur de la souris

API pour mémoriser et déterminer l'espace de déplacement de la souris :

Declare Function GetClipCursor Lib "user32" (lprc As RECT) As Long
Declare Function ClipCursor Lib "user32" (lpRect As Any) As Long

Exemples :

Dim SourisEspace As RECT
Dim SourisBloque As RECT
Call GetClipCursor(SourisEspace)  ' Alimente SourisEspace des coordonnées de l'espace de déplacement.
SourisBloque.Top = 50             ' Limite haute de la souris
SourisBloque.Left = 50            ' Limite gauche de la souris
SourisBloque.Right = 100          ' Limite droite de la souris
SourisBloque.Bottom = 100         ' Limite basse de la souris.
Call ClipCursor(SourisBloque)     ' Bloque la souris dans l'espace 50, 50 à 100, 100
Call ClipCursor(SourisEspace)     ' Restaure l'espace de déplacement d'origine.

API pour connaître la position de la souris :

Declare Function GetCursorPos Lib "user32" (lpPoint As POINT) As Long

Exemple :

Dim PositionSouris as POINT
Call GetCursorPos(PositionSouris) ' Alimente PositionSouris.X et PositionSouris.Y

API pour savoir si une touche du clavier est enfoncée :

Declare Function GetKeyState Lib "user32" (ByVal nVirtKey As Long) As Integer

Exemple :

Do
DoEvents
If GetKeyState(vbKeyDelete) < 0 Then Exit Do ' Sort si la touche Suppr est enfoncée.
Loop

Retrouvez la liste des codes nVirtKey des touches dans l'aide d'Excel à la rubrique : « Constantes de code de touches ». vbKeyLButton (1) et vbKeyRButton (2) représentent les boutons gauche et droit de la souris.

API pour jouer le son d'un fichier WAV :

Declare Function PlaySound Lib "Winmm.dll" Alias "sndPlaySoundA" (ByVal lpszSoundName As String, ByVal uFlags As Long) As Long
Public Const pcsSYNC = 0      ' Suspend l'exécution du code tant que le son n'est pas fini.
Public Const pcsASYNC = 1     ' Joue le son tout en continuant l'exécution du code.
Public Const pcsLOOP = 8      ' Joue le son en boucle, tant qu'un nouveau son n'est pas à jouer.
Public Const pcsNOSTOP = 16   ' N'interrompe pas le son en cours.

Exemples :

Joue le fichier laser-01.wav, une seule fois, sans interrompre le traitement :

Call PlaySound(ThisWorkbook.Path & "\laser-01.wav", pcsASYNC)

Joue le fichier laser-01.wav, en boucle, sans interrompre le traitement, tant qu'un autre appel à la fonction n'est pas réalisé :

Call PlaySound(ThisWorkbook.Path & "\laser-01.wav", pcsLOOP + pcsASYNC)

Force la fin d'un son joué en boucle :

Call PlaySound(" ", pcsASYNC)

Et pour faire parler votre ordinateur, en anglais :

'---------------------------------------------------------------------------------------
Public Sub Speak(StrTexte As String)
'---------------------------------------------------------------------------------------
Static Voice As Object
If Voice Is Nothing Then Set Voice = CreateObject("SAPI.Spvoice")
Voice.Speak StrTexte
If StrTexte = "" Then Set Voice = Nothing
End Sub

Notre prochaine étape sera de développer des procédures pour utiliser plus simplement toutes les API que nous venons d'étudier.

Ces procédures seront regroupées dans le module Img. Les API et les déclarations publiques utilisées seront regroupées dans un module ordinaire Img_Déclarations.

X - Procédures du module Img pour écrire du texte

Dans un nouveau module que nous appellerons Img, nous allons développer des procédures pour simplifier l'usage des API dédiées à la programmation en mode graphique que nous venons d'étudier.
En commençant par le plus compliqué, écrire du texte.

Nous avons vu deux possibilités : écrire sur un fond opaque, ou écrire en transparence. Avec un point commun, car dans les deux cas il est nécessaire de définir en amont, la couleur du texte, la couleur du fond, et les 14 paramètres du modèle d'écriture, ce qui représente des manipulations fastidieuses.
Deux procédures vont nous simplifier la tâche : TexteConfig pour configurer le modèle d'écriture à utiliser, et Texte qui permet d'écrire soit sur un fond opaque soit en transparence.

La procédure TexteConfig configure un modèle d'écriture en passant en arguments : le numéro du modèle à créer, la couleur du texte, la couleur du fond (-1 pour une transparence), le nom de la police, sa taille, et en option son épaisseur, le mode italique, le mode souligné.
La matrice Police mémorisera les différents modèles que nous créerons.
L'objet du modèle d'écriture créé, mémorisé dans la variable Obj de la matrice Police, sera appelé rapidement par SelectObject(Hdc, Police(NuméroDuModèle).Obj) dans la procédure Texte.

Public Type TypePolice
Obj As Long
Taille As Long
Hauteur As Long
CoulTexte As Long
CoulFond As Long
End Type
Public Police(0 To 9) As TypePolice

'---------------------------------------------------------------------------------------
Public Sub TexteConfig(NumPolice As Byte, CouleurTexte As Long, CouleurFond As Long, _
NomPolice As String, Taille As Byte, Optional Epaisseur As Integer = 500, _
Optional Italique As Boolean = False, Optional Souligné As Boolean = False)
'---------------------------------------------------------------------------------------
' Mémorise la configuration pour le modèle de police:
Police(NumPolice).CoulTexte = CouleurTexte
Police(NumPolice).CoulFond = CouleurFond
Police(NumPolice).Obj = CreateFont(Taille, 0, 0, 0, Epaisseur, _
-Italique, -Souligné, 0, 0, 0, 0, 0, 0, NomPolice)
Police(NumPolice).Taille = Taille

' Création d'une mémoire image de hauteur et largeur de la police:
Dim Tps_Img As Long, Tps_Hdc As Long, Tps_Obj As Long
Tps_Img = CreateCompatibleBitmap(Hdc, Police(NumPolice).Taille, Police(NumPolice).Taille)
Tps_Hdc = CreateCompatibleDC(Hdc)
Tps_Obj = SelectObject(Tps_Hdc, Tps_Img)

' Y copie un caractère avec fond plein:
Call SetTextColor(Tps_Hdc, 255)
Call SetBkColor(Tps_Hdc, 255)
Call SelectObject(Tps_Hdc, Police(NumPolice).Obj)
Call TextOut(Tps_Hdc, 0, 0, " ", 1)

' Recherche la hauteur réelle du texte, c.-à-d. tant qu'un pixel à 0 n'est pas rencontré:
For Police(NumPolice).Hauteur = Police(NumPolice).Taille To 1 Step -1
If GetPixel(Tps_Hdc, 1, Police(NumPolice).Hauteur - 1) <> 0 Then Exit For
Next Police(NumPolice).Hauteur

' Libération des mémoires:
DeleteObject SelectObject(Tps_Hdc, Tps_Obj)
DeleteDC Tps_Hdc
End Sub

Remarques sur la procédure TexteConfig :

- La procédure calcule la hauteur réelle d'un caractère dans la police choisie, de façon à optimiser l'affichage pour un texte écrit en transparence. Pour cela il faut faire un test en écrivant un caractère dans une mémoire temporaire et rechercher dans cette mémoire où est la dernière ligne de la couleur de fond. Ce résultat est mémorisé dans la variable Hauteur de la matrice Police.

- Seuls les cinq principaux paramètres sont configurés par cette procédure : nom de la police, taille, épaisseur, italique, souligné. En cas de besoin, vous pouvez configurer les autres paramètres par un appel à l'API : Police(NumPolice).Obj = CreateFont(…)

La fonction Texte permet d'écrire soit sur un fond opaque soit en transparence. Elle retourne la largeur en pixels du texte, s'il est écrit en transparence.

Quatre arguments sont obligatoires : le contexte, l'abscisse, l'ordonnée, le texte à écrire.

Quatre arguments sont facultatifs : le numéro du modèle de police, créé par TexteConfig (par défaut c'est le modèle 0), l'alignement horizontal (à gauche par défaut, AlignH_Gauche), la couleur du texte, et la couleur du fond (s'il ne faut pas utiliser les couleurs du modèle).

Remarques sur la procédure Texte :

- Dans un jeu où il est nécessaire de réduire les temps de traitement, écrire sur un fond opaque avec TextOut est plus rapide que d'écrire en transparence avec GdiTransparentBlt, comme nous l'avons déjà étudié.

- Utilisez la couleur de fond -1 pour une transparence.

- La couleur de transparence utilisée est la couleur du texte + 1, ceci afin de réduire les effets de contraste sur certaines couleurs que GdiTransparentBlt a du mal à gérer.

Exemple pour définir le modèle 0 afin d'écrire en noir sur fond transparent, avec la police Arial taille 40.

Call Img.TexteConfig(0, 0, -1, "Arial", 40)
Call Img.Texte(Hdc, 300, 440, "Démonstration :")

Et pour écrire avec ce modèle, mais en alignant le texte à droite, en blanc sur fond rouge :

Call Img.Texte(Hdc, 300, 440, "Démonstration :", 0, AlignH_Droite, 15, 12)

Exemple pour écrire avec ce modèle, centré sur l'écran et encadré d'un rectangle noir :

' Écrit le texte centré à l'écran, et récupère la taille en pixels du texte:
Lg = Img.Texte(Hdc, (ImageMaxiX / 2), (ImageMaxiY / 2) - (Police(0).Hauteur / 2), _
"Démonstration :", 0, AlignH_Centre)

' Dessine un rectangle vide noir au centre de l'écran de la taille du texte:
Call Img.RectangleVide(Hdc, (ImageMaxiX / 2) - (Lg / 2), _
(ImageMaxiY / 2) - (Police(0).Hauteur / 2), _
(ImageMaxiX / 2) + (Lg / 2), _
(ImageMaxiY / 2) + (Police(0).Hauteur / 2))

'---------------------------------------------------------------------------------------
Public Function Texte(HdcDest As Long, ByVal Px As Long, Py As Long, StrTexte As Variant, _
Optional NumPolice As Byte = 0, Optional AlignementHorizontal As EnumAlignH = 0, _
Optional ByVal CouleurTexte As Long = -9, Optional ByVal CouleurFond As Long = -9) As Long
'---------------------------------------------------------------------------------------
' Prend la couleur de texte du modèle sauf si une couleur de texte est passée en argument:
If CouleurTexte = -9 Then CouleurTexte = Police(NumPolice).CoulTexte
If CouleurTexte > 0 And CouleurTexte < 12 Then CouleurTexte = QBColor(CouleurTexte)
If CouleurTexte >= 12 And CouleurTexte < 16 Then CouleurTexte = QBColor(CouleurTexte) - 1
If CouleurTexte = 0 Then CouleurTexte = 1
If CouleurTexte > 16777214 Then CouleurTexte = 16777214

' Prend la couleur de fond du modèle sauf si une couleur de fond est passée en argument:
If CouleurFond = -9 Then CouleurFond = Police(NumPolice).CoulFond
If CouleurFond > 0 And CouleurFond < 16 Then CouleurFond = QBColor(CouleurFond)
If CouleurFond = 0 Then CouleurFond = 1

' Si pas de transparence et alignement à Gauche alors utilise le mode rapide:
If CouleurFond <> -1 And AlignementHorizontal = AlignH_Gauche Then
Call SetTextColor(HdcDest, CouleurTexte)
Call SetBkColor(HdcDest, CouleurFond)
Call SelectObject(HdcDest, Police(NumPolice).Obj)
Call TextOut(HdcDest, Px, Py, StrTexte, Len(StrTexte))
Exit Function
End If

' Détermine la largeur du texte:
Dim Largeur As Long
Largeur = Len(StrTexte) * Police(NumPolice).Taille

' Création d'une mémoire vierge (initialisée à 0) qui contiendra le texte:
Dim Tps_Img As Long, Tps_Hdc As Long, Tps_Obj As Long
Tps_Img = CreateCompatibleBitmap(Hdc, Largeur, Police(NumPolice).Hauteur)
Tps_Hdc = CreateCompatibleDC(Hdc)
Tps_Obj = SelectObject(Tps_Hdc, Tps_Img)

' Affectation de la couleur de texte, couleur de fond (ou de transparence) et de la police:
Call SetTextColor(Tps_Hdc, CouleurTexte)
Call SetBkColor(Tps_Hdc, IIf(CouleurFond = -1, CouleurTexte + 1, CouleurFond))
Call SelectObject(Tps_Hdc, Police(NumPolice).Obj)

' Génère le texte dans l'espace mémoire Tps_Hdc. Les pixels non utilisés restent à 0:
Call TextOut(Tps_Hdc, 0, 0, StrTexte, Len(StrTexte))

' Recherche la largeur réelle du texte, c.-à-d., tant qu'un pixel à 0 n'est pas rencontré:
Dim MaxiLg As Long
For MaxiLg = Largeur - 1 To 0 Step -1
If GetPixel(Tps_Hdc, MaxiLg, 1) <> 0 Then Exit For
Next MaxiLg
Texte = MaxiLg ' Retourne le nombre de pixels pour afficher le texte.

' Si alignement à Droite ou Centre alors modifie la position Px:
If AlignementHorizontal = AlignH_Droite Then Px = Px - MaxiLg
If AlignementHorizontal = AlignH_Centre Then Px = Px - (MaxiLg / 2)

' Affiche le texte en transparence ou en utilisant une couleur de fond:
If CouleurFond = -1 Then
Call GdiTransparentBlt(HdcDest, Px, Py, MaxiLg, Police(NumPolice).Hauteur, _
Tps_Hdc, 0, 0, MaxiLg, Police(NumPolice).Hauteur, CouleurTexte + 1)
Else
Call BitBlt(HdcDest, Px, Py, MaxiLg, Police(NumPolice).Hauteur, Tps_Hdc, 0, 0, SRCCOPY)
End If

' Libération des mémoires:
DeleteObject SelectObject(Tps_Hdc, Tps_Obj)
DeleteDC Tps_Hdc

End Function

XI - Procédures du module Img pour dessiner des formes géométriques

Pour dessiner les formes géométriques de bases, trait, rectangle, et autres polygones pleins ou vides, nous allons utiliser une procédure qui sait tout faire, car elle regroupe les API Polygon (pour les formes pleines) et LineTo (pour les formes vides, c'est-à-dire lorsque le fond est déclaré transparent).

C'est la procédure Polygone.

Deux arguments sont obligatoires : le contexte, et la liste des points (abscisse, ordonnée) passée sous forme d'un tableau Array().

Cinq arguments sont facultatifs : la couleur du trait, la couleur de fond, la taille du trait, le style du trait pour CreatePen (qui n'est appliqué que si la taille vaut 1), et le style du fond pour CreateHatchBrush (qui est 7, FondTransparent, pour un fond transparent).

'---------------------------------------------------------------------------------------
Public Sub Polygone(HdcDest As Long, ListePointsXY As Variant, _
Optional ByVal CouleurTrait As Long = 0, Optional ByVal CouleurFond As Long = 0, _
Optional Taille As Long = 1, Optional StyleTrait As EnumPenStyle = PenSolide, _
Optional StyleFond As EnumBrushStyle = FondPlein)
'---------------------------------------------------------------------------------------
Dim Cpt As Long
Dim PolyPoints() As POINT, Pt As POINT

' Redimensionne le tableau de points pour l'API:
ReDim PolyPoints(((UBound(ListePointsXY) + 1) / 2) - 1)

' Boucle sur les coordonnées du tableau passé en paramètre pour charger les éléments:
For Cpt = 0 To (UBound(ListePointsXY) - 1) / 2
PolyPoints(Cpt).X = ListePointsXY(2 * Cpt) ' Abcisse du point
PolyPoints(Cpt).Y = ListePointsXY(2 * Cpt + 1) ' Ordonnée du point
Next

' Transforme la couleur du trait Basic (0 à 15) en couleur RVB:
If CouleurTrait > 0 And CouleurTrait < 16 Then CouleurTrait = QBColor(CouleurTrait)

' Crée et sélectionne les options du trait:
Dim ObjPen As Long
ObjPen = CreatePen(StyleTrait, Taille, CouleurTrait)
Call SelectObject(HdcDest, ObjPen)

' Transforme la couleur du fond Basic (0 à 15) en couleur RVB:
If CouleurFond > 0 And CouleurFond < 16 Then CouleurFond = QBColor(CouleurFond)

' Crée et sélectionne les options du fond:
Dim ObjBrush As Long
ObjBrush = CreateHatchBrush(StyleFond, CouleurFond)
Call SelectObject(HdcDest, ObjBrush)

' Dessine le polygone en plein si StyleFond <> FondTransparent sinon en vide:
If StyleFond <> FondTransparent Then
Call Polygon(HdcDest, PolyPoints(0), UBound(PolyPoints) + 1)
Else
Call MoveToEx(HdcDest, PolyPoints(0).X, PolyPoints(0).Y, Pt)
For Cpt = 1 To UBound(PolyPoints)
Call LineTo(HdcDest, PolyPoints(Cpt).X, PolyPoints(Cpt).Y)
Next Cpt
End If

' Libération des mémoires:
DeleteObject ObjPen
DeleteObject ObjBrush
End Sub

La procédure peut être appelée pour dessiner un rectangle vide, en pointillés :

Call Img.Polygone(Hdc, Array(100, 100, 400, 100, 400, 200, 100, 200, 100, 100), _
0, 0, 1, PenPoint, FondTransparent)

Mais j'ai préféré développer une procédure spécifique RectangleVide, qui fait appel à Polygone, où l'on ne passe en argument que le contexte, les coordonnées des points opposés du rectangle X1, Y1 et X2, Y2, en option la couleur du trait (0 par défaut), la taille (1 par défaut), et enfin le style du trait (0 par défaut) :

'---------------------------------------------------------------------------------------
Public Sub RectangleVide(HdcDest As Long, X1 As Long, Y1 As Long, _
X2 As Long, Y2 As Long, Optional CouleurTrait As Long = 0, _
Optional Taille As Long = 1, Optional StyleTrait As EnumPenStyle = PenSolide)
'---------------------------------------------------------------------------------------
Call Polygone(HdcDest, Array(X1, Y1, X2, Y1, X2, Y2, X1, Y2, X1, Y1), _
CouleurTrait, 0, Taille, StyleTrait, FondTransparent)
End Sub

Pour obtenir le même résultat que précédemment, l'appel est simplifié en :

Call Img.RectangleVide(Hdc, 100, 100, 400, 200, 0, 1, PenPoint)

De même une procédure spécifique permet de dessiner un rectangle plein :

'---------------------------------------------------------------------------------------
Public Sub RectanglePlein(HdcDest As Long, X1 As Long, Y1 As Long, _
X2 As Long, Y2 As Long, Optional CouleurFond As Long = 0, _
Optional StyleFond As EnumBrushStyle = FondPlein)
'---------------------------------------------------------------------------------------
Call Polygone(HdcDest, Array(X1, Y1, X2, Y1, X2, Y2, X1, Y2, X1, Y1), _
CouleurFond, CouleurFond, 1, PenSolide, StyleFond)
End Sub

L'appel est simplifié avec en arguments : le contexte, les coordonnées des points opposés du rectangle X1, Y1 et X2, Y2, en option la couleur du trait (0 par défaut), et le style du trait (0 par défaut) :

Call Img.RectanglePlein(Hdc, 100, 100, 400, 200)

Un trait peut être représenté par un polygone vide ayant deux points. Les arguments nécessaires sont : le contexte, les coordonnées des deux points X1, Y1 et X2, Y2, en option la couleur du trait (0 par défaut), la taille du trait (1 par défaut), et le style du trait (0 par défaut) :

'---------------------------------------------------------------------------------------
Public Sub Trait(HdcDest As Long, X1 As Long, Y1 As Long, _
X2 As Long, Y2 As Long, Optional CouleurTrait As Long = 0, _
Optional Taille As Long = 1, Optional StyleTrait As EnumPenStyle = PenSolide)
'---------------------------------------------------------------------------------------
Call Polygone(HdcDest, Array(X1, Y1, X2, Y2), _
CouleurTrait, CouleurTrait, Taille, StyleTrait, FondPlein)
End Sub

Ce qui permet de dessiner un trait rouge en pointillé par cet appel :

Call Img.Trait(Hdc, 100, 100, 400, 100, 12, , PenPoint)

La dernière procédure de ce chapitre est utilisée pour dessiner un cercle, vide ou plein.

Les arguments de la procédure Cercle sont : le contexte, l'abscisse et l'ordonnée du centre du cercle, le diamètre du cercle, la couleur du cercle, et en option s'il est plein ou vide (vide par défaut) :

'---------------------------------------------------------------------------------------
Public Sub Cercle(HdcDest As Long, Px As Long, Py As Long, _
Taille As Long, ByVal Couleur As Long, Optional Plein As Boolean = False)
'---------------------------------------------------------------------------------------

' Transforme la couleur du cercle Basic (0 à 15) en couleur RVB:
If Couleur < 16 Then Couleur = QBColor(Couleur)
If Couleur = 0 Then Couleur = 1 ' La couleur 0 est réservée pour la transparence.

' Création d'une mémoire travail de la taille du cercle, initialisée avec des pixels à 0:
Dim Tps_Img As Long, Tps_Hdc As Long, Tps_Obj As Long
Tps_Img = CreateCompatibleBitmap(Hdc, Taille * 2, Taille * 2)
Tps_Hdc = CreateCompatibleDC(Hdc)
Tps_Obj = SelectObject(Tps_Hdc, Tps_Img)

' Sélectionne un trait fin de la couleur demandée:
Dim ObjPen As Long
ObjPen = CreatePen(0, 1, Couleur)
Call SelectObject(Tps_Hdc, ObjPen)

' Sélectionne la couleur du fond demandée ou 0 si cercle vide:
Dim ObjBrush As Long
ObjBrush = CreateSolidBrush(IIf(Plein = False, 0, Couleur))
Call SelectObject(Tps_Hdc, ObjBrush)

' Dessine le cercle en mémoire de travail:
Call Ellipse(Tps_Hdc, 0, 0, Taille * 2, Taille * 2)

' Copie le cercle calculé en mémoire sans les pixels à 0:
Call GdiTransparentBlt(HdcDest, Px - Taille, Py - Taille, Taille * 2, Taille * 2, _
Tps_Hdc, 0, 0, Taille * 2, Taille * 2, 0)

' Libération des mémoires:
DeleteObject ObjPen
DeleteObject ObjBrush
DeleteObject SelectObject(Tps_Hdc, Tps_Obj)
DeleteDC Tps_Hdc
End Sub

Exemple d'appel pour dessiner un cercle bleu plein, de 130 pixels de diamètre, dont le centre est aux coordonnées 500, 500 :

Call Img.Cercle(Hdc, 500, 500, 130, 1, True)

XII - Procédures du module Img pour gérer les images

La gestion des images sera simplifiée par l'utilisation de variables publiques et de types personnalisés.

Variables publiques :

Public Hdc As Long
Public ImageMiniX As Long, ImageMiniY As Long
Public ImageMaxiX As Long, ImageMaxiY As Long
Public ImageNum As Integer
Public Pt As POINT
Public Type POINT
x As Long: Y As Long
End Type
Public Enum EnumAlignH
AlignH_Gauche = 0
AlignH_Centre = 1
AlignH_Droite = 2
End Enum
Public Enum EnumAlignV
AlignV_Haut = 0
AlignV_Centre = 1
AlignV_Bas = 2
End Enum
Public Enum EnumTypeAnimation
EnBoucle = 0
UneFoisEtAffiche = 1
UneFoisEtQuitte = 2
End Enum

Type personnalisé pour l'affichage des images :

Public Type TypeImage
Img As Variant
Hdc As Long
Obj As Long
Largeur As Long
Hauteur As Long
Transparence As Long
AlignH As EnumAlignH
AlignV As EnumAlignV
End Type
Public Image(-1 To 999) As TypeImage

Type personnalisé pour l'animation des avatars :

Public Type TypeAvatar
X As Long
Y As Long
MvX As Integer
MvY As Integer
Actif As Boolean
Visible As Boolean
SpriteActif As Integer
ListeImages As Variant
SpriteBloqué As Integer
Animation As EnumTypeAnimation
PosXMini As Long: PosXMaxi As Long
PosYMini As Long: PosYMaxi As Long
End Type
Public Avatar(0 To 99) As TypeAvatar

Et grâce aux procédures qui suivent.

La première chose à faire dans un programme en mode graphique est de récupérer le contexte de l'écran et de mémoriser ses dimensions. On peut alors créer une image de la taille de l'écran pour le mémoriser (image numéro 0), et en faire une copie pour une mémoire de travail (image numéro -1).
Ces actions sont effectuées par la procédure InitialiseModeGraphique :

'---------------------------------------------------------------------------------------
Public Sub InitialiseModeGraphique()
'---------------------------------------------------------------------------------------
' Mémorise le contexte de l'écran:
Hdc = GetWindowDC(0)

' Mémorise la taille de l'écran:
ImageMaxiX = GetSystemMetrics(0): ImageMiniX = 0
ImageMaxiY = GetSystemMetrics(1): ImageMiniY = 0

' Création de l'image 0 pour mémoriser l'écran d'origine
Image(0).Img = CreateCompatibleBitmap(Hdc, ImageMaxiX, ImageMaxiY)
Image(ImgDest).Hdc = CreateCompatibleDC(Hdc)
Image(ImgDest).Obj = SelectObject(Image(0).Hdc, Image(0).Img)
Image(ImgDest).Largeur = ImageMaxiX
Image(ImgDest).Hauteur = ImageMaxiY
Image(ImgDest).Transparence = -1 ' Sans transparence

' Mémorise l'écran en mémoire 0 puis fait une copie en mémoire -1.
Call BilBlt(Image(0).Hdc, 0, 0, ImageMaxiX, ImageMaxiY, Hdc, 0, 0, SRCCOPY)
Call CopieImage(0, -1)

End Sub

Inversement, la procédure FermeModeGraphique permet de libérer les mémoires utilisées par la gestion des images avant de quitter le mode graphique :

'---------------------------------------------------------------------------------------
Public Sub FermeModeGraphique()
'---------------------------------------------------------------------------------------
' Libère les mémoires:
For ImageNum = -1 To Ubound(Image())
DeleteObject SelectObject(Image(ImageNum).Hdc, Image(ImageNum).Obj)
DeleteDC Image(ImageNum).Hdc
Next ImageNum

ReleaseDC 0, Hdc
End Sub

La procédure EffaceImage permet de libérer la mémoire passée en argument. Mémoire image qui pourra ensuite être réutilisée. Cela est utile pour l'usage des mémoires temporaires.

Par convention, les images 1 à 9 seront réservées par les procédures du module Img pour servir de mémoires temporaires, en plus des mémoires -1 et 0. Utilisez les mémoires 10 à 999 pour vos applications.

'---------------------------------------------------------------------------------------
Public Sub EffaceImage(NumImage)
'---------------------------------------------------------------------------------------
DeleteObject SelectObject(Image(NumImage).Hdc, Image(NumImage).Obj)
DeleteDC Image(NumImage).Hdc
Image(NumImage).Hdc = 0
End Sub

La procédure CopieImage copie une image source dans une image destination (généralement l'image de travail -1), en utilisant ou non une couleur de transparence, tout dépend de la configuration de l'image source : si Transparence est différent de -1 alors l'image est copiée par l'API GdiTransparentBlt avec la couleur de transparence indiquée, sinon elle est copiée avec l'API BitBlt, donc sans transparence.
Par défaut, c'est l'intégralité de l'image source qui est copiée au point 0, 0 de l'image destination, mais six arguments facultatifs permettent de modifier l'emplacement de la destination (XDest et YDest) ou de définir la fraction de l'image source à copier (XSource, YSource, Largeur, Hauteur).

Notez que si l'image destination n'existe pas encore, alors elle est créée. Dans ce cas la couleur de transparence de l'image source est reprise pour configurer l'image créée. Idem pour son alignement.

Par exemple, la procédure InitialiseModeGraphique fait une copie de l'image écran (mémoire image 0) en image de travail (mémoire image -1) par l'instruction :

Call Img.CopieImage(0, -1)

Il existe une autre façon de créer une mémoire avec cette fonction : en utilisant le même numéro d'image pour la source et la destination. Dans ce cas l'image destination est créée avec la largeur et la hauteur passée en argument.

'---------------------------------------------------------------------------------------
Public Sub CopieImage(ImgSource As Integer, ImgDest As Integer, _
Optional ByVal XDest As Long = 0, Optional ByVal YDest As Long = 0, _
Optional XSource As Long = 0, Optional YSource As Long = 0, _
Optional Largeur As Long = 0, Optional Hauteur As Long = 0)
'---------------------------------------------------------------------------------------
' Définit la largeur et la hauteur de l'image ou prend les valeurs de la source:
Dim Lg As Long, Ht As Long
Lg = IIf(Largeur = 0, Image(ImgSource).Largeur, Largeur)
Ht = IIf(Hauteur = 0, Image(ImgSource).Hauteur, Hauteur)

' Si la destination n'existe pas alors il faut la créer:
If Image(ImgDest).Hdc = 0 Then
Image(ImgDest).Img = CreateCompatibleBitmap(Hdc, Lg, Ht)
Image(ImgDest).Hdc = CreateCompatibleDC(Hdc)
Image(ImgDest).Obj = SelectObject(Image(ImgDest).Hdc, Image(ImgDest).Img)
Image(ImgDest).Largeur = Lg
Image(ImgDest).Hauteur = Ht
Image(ImgDest).Transparence = Image(ImgSource).Transparence
Image(ImgDest).AlignH = Image(ImgSource).AlignH
Image(ImgDest).AlignV = Image(ImgSource).AlignV
If ImgSource = ImgDest Then Exit Sub
End If

' S'il faut appliquer un alignement horizontal ou vertical:
If Image(ImgSource).AlignH = AlignH_Centre Then XDest = XDest - (Lg / 2)
If Image(ImgSource).AlignH = AlignH_Droite Then XDest = XDest - Lg
If Image(ImgSource).AlignV = AlignV_Centre Then YDest = YDest - (Ht / 2)
If Image(ImgSource).AlignV = AlignV_Bas Then YDest = YDest - Ht

' Copie l'image en transparence ou sans transparence:
If Image(ImgSource).Transparence <> -1 Then
Call GdiTransparentBlt(Image(ImgDest).Hdc, XDest, YDest, _
Lg, Ht, Image(ImgSource).Hdc, XSource, YSource, _
Lg, Ht, Image(ImgSource).Transparence)
Else
Call BitBlt(Image(ImgDest).Hdc, XDest, YDest, Lg, Ht, _
Image(ImgSource).Hdc, XSource, YSource, SRCCOPY)
End If

End Sub

La procédure CopieEcran sera parfois nécessaire pour faire une nouvelle mémorisation de l'écran avant d'y faire vos animations. L'image numéro 0 est utilisée comme modèle afin de créer une nouvelle image de la dimension de l'écran.

'---------------------------------------------------------------------------------------
Public Sub CopieEcran(NumImage As Integer)
'---------------------------------------------------------------------------------------
Call EffaceImage(NumImage)
Call CopieImage(0, NumImage)
Call BitBlt(Image(NumImage).Hdc, 0, 0, ImageMaxiX, ImageMaxiY, Hdc, 0, 0, SRCCOPY)

End Sub

La procédure ModifieTailleImage modifie la taille d'une image ImgSource (Lg est la nouvelle largeur, Ht la nouvelle hauteur) avec l'API StretchBlt. Si le numéro de l'image destination imgDest est différent de l'image source, alors une nouvelle image est créée, en récupérant la configuration de l'image source.

'---------------------------------------------------------------------------------------
Public Sub ModifieTailleImage(ImgSource As Integer, Lg As Long, Ht As Long, _
Optional ByVal ImgDest As Integer = -9)
'---------------------------------------------------------------------------------------
Call EffaceImage(1) ' Création d'une mémoire 1 de la nouvelle taille.
Call CopieImage(1, 1, , , , , Lg, Ht)

' Modifie la taille de l'image en mémoire 1:
Call StretchBlt(Image(1).Hdc, 0, 0, Lg, Ht, Image(ImgSource).Hdc, _
0, 0, Image(ImgSource).Largeur, Image(ImgSource).Hauteur, SRCCOPY)

' Soit il faut garder cette image:
If ImgDest = -9 Or ImgDest = ImgSource Then
Call EffaceImage(ImgSource) ' Création d'une mémoire de la nouvelle taille.
Call CopieImage(ImgSource, ImgSource, , , , , Lg, Ht)
Call BitBlt(Image(ImgSource).Hdc, 0, 0, Lg, Ht, Image(1).Hdc, 0, 0, SRCCOPY)
Else ' Soit il faut générer une autre image:
Call EffaceImage(ImgDest)   ' Efface l'image destination
Call CopieImage(1, ImgDest) ' Création d'une image de la nouvelle taille.
Call BitBlt(Image(ImgDest).Hdc, 0, 0, Lg, Ht, Image(1).Hdc, 0, 0, SRCCOPY) ' Copie l'image 1.
Image(ImgDest).Transparence = Image(ImgSource).Transparence ' Reprise de la transparence.
Image(ImgDest).AlignH = Image(ImgSource).AlignH             ' Reprise de l'alignement Horizontal.
Image(ImgDest).AlignV = Image(ImgSource).AlignV             ' Reprise de l'alignement vertical.
End If
Call EffaceImage(1)
End Sub

Vous aurez aussi besoin dans vos applications de retourner une image. Un canard pourra ainsi voler aussi bien de gauche à droite que de droite à gauche.

La procédure TourneImage ne nécessite que deux arguments, le numéro de l'image source, ImgSource, et le numéro de la nouvelle image à créer, ImgDest.

'---------------------------------------------------------------------------------------
Public Sub TourneImage(ImgSource As Integer, ImgDest As Integer)
'---------------------------------------------------------------------------------------
' Création de la nouvelle image d'après le modèle de l'image d'origine:
Call CopieImage(ImgSource, ImgDest)

' Retourne l'image d'origine:
Dim x As Long, Y As Long
For Y = 0 To Image(ImgSource).Hauteur - 1
For x = 0 To Image(ImgSource).Largeur - 1
Call SetPixel(Image(ImgDest).Hdc, Image(ImgDest).Largeur - 1 - x, Y, _
GetPixel(Image(ImgSource).Hdc, x, Y))
Next x
Next Y

End Sub

La procédure AfficheImage affiche à l'écran l'intégralité d'une image (généralement l'image de travail numéro -1), là aussi en utilisant ou non une couleur de transparence, tout dépend de la configuration de l'image passée en argument : si Transparence est différent de -1 alors l'image est affichée par l'API GdiTransparentBlt avec la couleur de transparence indiquée, sinon elle est affichée avec l'API BitBlt, donc sans transparence.

'---------------------------------------------------------------------------------------
Public Sub AfficheImage(ImgSource As Integer)
'---------------------------------------------------------------------------------------
If Image(ImgSource).Transparence <> -1 Then
Call GdiTransparentBlt(Hdc, 0, 0, _
Image(ImgSource).Largeur, Image(ImgSource).Hauteur, _
Image(ImgSource).Hdc, 0, 0, _
Image(ImgSource).Largeur, Image(ImgSource).Hauteur, _
Image(ImgSource).Transparence)
Else
Call BitBlt(Hdc, 0, 0, Image(ImgSource).Largeur, Image(ImgSource).Hauteur, _
Image(ImgSource).Hdc, 0, 0, SRCCOPY)
End If

End Sub

Nous avons déjà étudié comment charger en mémoire une image au format BMP. Rien de particulier donc pour la procédure ChargeImageBMP qui reprend les API LoadPicture pour créer l'image et GetObjectAPI pour connaître ses dimensions et ainsi les mémoriser dans le type personnalisé Image.
Le nom du fichier avec son chemin est le premier argument de la procédure, suivi du numéro d'image à créer.

La couleur de transparence, si elle est connue, peut être passée en option, ou utilisez la valeur -9 pour que cette couleur soit détectée automatiquement d'après le point d'origine 0, 0 de l'image. N'oubliez pas que la valeur -1 indique qu'il ne doit pas être appliqué de transparence dans les procédures de copie et d'affichage de l'image.
L'alignement horizontal (AlignH) et vertical (AlignV) de l'image sont les deux derniers arguments facultatifs.

'---------------------------------------------------------------------------------------
Public Sub ChargeImageBMP(StrFichier As String, NumImage As Integer, _
Optional Transparence As Long = -9, _
Optional AlignH As EnumAlignH = 0, Optional AlignV As EnumAlignV = 0)
'---------------------------------------------------------------------------------------
Dim Tps_Bitmap As Bitmap
Set Image(NumImage).Img = LoadPicture(StrFichier)
Image(NumImage).Hdc = CreateCompatibleDC(Hdc)
Image(NumImage).Obj = SelectObject(Image(NumImage).Hdc, Image(NumImage).Img)
GetObjectAPI Image(NumImage).Img.handle, Len(Tps_Bitmap), Tps_Bitmap

' Mémorise la taille de l'image et son alignement:
Image(NumImage).Largeur = Tps_Bitmap.bmWidth
Image(NumImage).Hauteur = Tps_Bitmap.bmHeight
Image(NumImage).Transparence = Transparence
Image(NumImage).AlignH = AlignH
Image(NumImage).AlignV = AlignV

' Force à détecter la transparence d'après le point d'origine 0,0:
If Transparence = -9 Then Image(NumImage).Transparence = GetPixel(Image(NumImage).Hdc, 0, 0)

End Sub

Il nous reste une dernière procédure à étudier, AnimeAvatar, utilisée pour simplifier la gestion des avatars. Cette fonction gère le déplacement de l'avatar passé en argument en veillant à ne pas déborder des limites définies, et éventuellement, boucle sur la liste des images qui composent le Sprite pour générer une animation.

Revenons sur le type personnalisé Avatar, car la configuration de ses paramètres est très importante dans la gestion des avatars :

- X et Y : définissent les coordonnées de l'avatar. C'est-à-dire où sera copiée l'image de l'avatar dans la mémoire ImgDest passée en argument, en utilisant l'alignement horizontal et vertical défini dans les paramètres de l'image (voir le type personnalisé Image).

- MvX et MvY : définissent la vitesse de déplacement (nombre de pixels) de l'abscisse et de l'ordonnée de l'avatar. Ces valeurs vont alimenter les variables X et Y pour définir les nouvelles coordonnées de l'avatar après son déplacement. Si ces valeurs sont nulles, l'avatar restera donc immobile.

- Actif : indique si l'avatar est actif (True). S'il ne l'est pas (False) la fonction AnimeAvatar ne fera rien.
Attention, par défaut ce paramètre est à False.

- Visible : indique si l'avatar sera affiché après son animation (True) ou non (False). Attention, par défaut ce paramètre est à False.

- SpriteActif : est le numéro de l'élément dans la liste des images du Sprite et non pas le numéro de l'image. La fonction AnimeAvatar retourne le numéro de l'image utilisée.

- ListeImages : contient la liste des numéros d'images qui composent le Sprite.

- SpriteBloqué : si différent de zéro, est le numéro de l'image qu'il faut utiliser quand l'avatar atteint les limites de déplacement autorisées.

- Animation : est le mode d'animation qui peut prendre les valeurs 0 (EnBoucle), pour une lecture en boucle des images du Sprite ; 1 (UneFoisEtAffiche) pour ne pas faire de boucle, la dernière image du Sprite est continuellement affichée ; 2 (UneFoisEtQuitte) pour ne pas faire de boucle, la dernière image du Sprite est affichée une seule fois puis la variable Actif est forcée à False. L'avatar n'est donc plus géré ; si une valeur supérieure à 2 est renseignée, l'animation est gérée en boucle et la variable Animation est décrémentée à chaque passage, jusqu'à atteindre la valeur 2 (UneFoisEtQuitte) ce qui met fin à l'animation. Technique à utiliser pour afficher un avatar un nombre de fois déterminé.

- PosXMini, PosXMaxi, PosYMini, PosYMaxi : si ces valeurs sont différentes de zéro, elles définissent les limites de déplacement autorisées.

Vous comprendrez mieux avec le code de la procédure…

'---------------------------------------------------------------------------------------
Public Function AnimeAvatar(NumAvatar As Integer, Optional ImgDest As Integer = -1) As Integer
'---------------------------------------------------------------------------------------
' Si l'avatar n'est pas déclaré actif alors sort:
If Avatar(NumAvatar).Actif = False Then Exit Function

' Déplace l'avatar:
Avatar(NumAvatar).x = Avatar(NumAvatar).x + Avatar(NumAvatar).MvX
Avatar(NumAvatar).Y = Avatar(NumAvatar).Y + Avatar(NumAvatar).MvY

' Gère les débordements si PosXMini, PosXMaxi, PosYMini, PosXMaxi différents de 0:
If Avatar(NumAvatar).PosXMini <> 0 And Avatar(NumAvatar).x < Avatar(NumAvatar).PosXMini Then _
Avatar(NumAvatar).x = Avatar(NumAvatar).PosXMini
If Avatar(NumAvatar).PosXMaxi <> 0 And Avatar(NumAvatar).x > Avatar(NumAvatar).PosXMaxi Then _
Avatar(NumAvatar).x = Avatar(NumAvatar).PosXMaxi
If Avatar(NumAvatar).PosYMini <> 0 And Avatar(NumAvatar).Y < Avatar(NumAvatar).PosYMini Then _
Avatar(NumAvatar).Y = Avatar(NumAvatar).PosYMini
If Avatar(NumAvatar).PosYMaxi <> 0 And Avatar(NumAvatar).Y > Avatar(NumAvatar).PosYMaxi Then _
Avatar(NumAvatar).Y = Avatar(NumAvatar).PosYMaxi

' Si modification de la liste des images, risque de débordement de capacité:
If Avatar(NumAvatar).SpriteActif > UBound(Avatar(NumAvatar).ListeImages) Then _
Avatar(NumAvatar).SpriteActif = UBound(Avatar(NumAvatar).ListeImages)
' Détermine l'image à afficher:
ImageNum = Avatar(NumAvatar).ListeImages(Avatar(NumAvatar).SpriteActif)

' Gère le Sprite Bloqué en horizontal:
If Avatar(NumAvatar).SpriteBloqué > 0 And Avatar(NumAvatar).MvX <> 0 Then
If Avatar(NumAvatar).x = Avatar(NumAvatar).PosXMini _
Or Avatar(NumAvatar).x = Avatar(NumAvatar).PosXMaxi _
Then
ImageNum = Avatar(NumAvatar).SpriteBloqué
End If
End If

' Gère le Sprite Bloqué en vertical:
If Avatar(NumAvatar).SpriteBloqué > 0 And Avatar(NumAvatar).MvY <> 0 Then
If Avatar(NumAvatar).Y = Avatar(NumAvatar).PosYMini _
Or Avatar(NumAvatar).Y = Avatar(NumAvatar).PosYMaxi _
Then
ImageNum = Avatar(NumAvatar).SpriteBloqué
End If
End If

' Copie l'avatar en mémoire temporaire s'il a été déclaré visible:
If Avatar(NumAvatar).Visible = True Then _
Call CopieImage(ImageNum, ImgDest, Avatar(NumAvatar).x, Avatar(NumAvatar).Y)

' Recherche la prochaine image dans la liste des images du Sprite:
Avatar(NumAvatar).SpriteActif = Avatar(NumAvatar).SpriteActif + 1

' Gère la boucle des Sprite de l'avatar, sauf s'il ne faut pas faire de boucle:
If Avatar(NumAvatar).SpriteActif > UBound(Avatar(NumAvatar).ListeImages) Then
If Avatar(NumAvatar).Animation = EnBoucle Or Avatar(NumAvatar).Animation > UneFoisEtQuitte Then
Avatar(NumAvatar).SpriteActif = LBound(Avatar(NumAvatar).ListeImages)
Else
Avatar(NumAvatar).SpriteActif = UBound(Avatar(NumAvatar).ListeImages)
' Désactive l'avatar s'il faut quitter après la dernière image:
If Avatar(NumAvatar).Animation = UneFoisEtQuitte Then Avatar(NumAvatar).Actif = False
End If
End If

' Éventuellement, décrémente Avatar(NumAvatar).Animation pour atteindre la valeur UneFoisEtQuitte
If Avatar(NumAvatar).Animation > UneFoisEtQuitte Then _
Avatar(NumAvatar).Animation = Avatar(NumAvatar).Animation - 1

' Retourne le numéro de l'image utilisée:
AnimeAvatar = ImageNum
End Function

XIII - Exemple d'animation avec le module Img

Reprenons l'animation du vol des trois canards que nous avons étudiée précédemment, en utilisant cette fois les procédures du module Img développées pour simplifier l'usage des API.
Pour mettre en pratique certaines de ces procédures, les canards volent désormais de droite à gauche et sont deux fois plus gros. J'ai aussi ajouté dans la boucle une condition pour masquer le canard lorsque la souris s'approche à moins de 50 pixels de son centre, et la possibilité de quitter l'animation en appuyant sur une touche. Enfin, le canard numéro 1 descend de 10 pixels toutes les demi-secondes.

Pour exécuter la procédure Descente qui décale l'ordonnée de l'avatar de 10 pixels toutes les demi-secondes, j'ai utilisé l'API SetTimer qui travaille en millisecondes, à la place de la fonction OnTime du VBA. La variable TimerID mémorise le pointeur de la fonction pour la désactiver avec l'API KillTimer.
Les déclarations sont :

Declare Function SetTimer Lib "user32" (ByVal hwnd As Long, ByVal nIDEvent As Long, _
ByVal uElapse As Long, ByVal lpTimerfunc As Long) As Long
Declare Function KillTimer Lib "user32" (ByVal hwnd As Long, ByVal nIDEvent As Long) As Long
Public TimerID As Long

L'instruction qui lance la procédure Descente toutes les 500 millièmes de seconde est :

TimerID = SetTimer(0, 0, 500, AddressOf Descente)

À la fin de la boucle, l'événement est désactivé par l'instruction :

Call KillTimer(0, TimerID)

Pour quitter l'animation en appuyant sur n'importe quelle touche, j'utilise l'instruction :

If Img.ToucheEnfoncée(0) = True Then Call Img.AfficheImage(0): Exit Do

Après avoir ajouté la fonction ToucheEnfoncée dans le module Img :

'---------------------------------------------------------------------------------------
Public Function ToucheEnfoncée(Optional VbKey As Long = 0) As Boolean
'---------------------------------------------------------------------------------------
' Retourne Vrai si la touche VbKey est enfoncée, ou si VbKey = 0 et qu'une touche
' (sauf y compris boutons souris 1 et 2) est enfoncée.
'---------------------------------------------------------------------------------------
Dim i As Integer
DoEvents
If GetKeyState(VbKey) < 0 Then ToucheEnfoncée = True: Exit Function
If VbKey = 0 Then
For i = 3 To 127
If GetKeyState(i) < 0 Then ToucheEnfoncée = True: Exit Function
Next i
End If
End Function

Vous remarquerez dans le code qui suit que les images du Sprite du vol sont chargées avec en paramètre un alignement horizontal et vertical centré, ceci afin que les coordonnées de la souris correspondent au centre de l'image.

'---------------------------------------------------------------------------------------
Sub NotionsDeBase()
'---------------------------------------------------------------------------------------
Call Img.InitialiseModeGraphique

' Chargement des 8 images du Sprite du vol:
For ImageNum = 1 To 8
' Charge l'image en mémoire temporaire numéro 100:
Call Img.ChargeImageBMP(ActiveWorkbook.Path & "\vol" & ImageNum & ".bmp", 10, -9)
' Retourne l'image numéro 10 et la place en mémoire 11 à 18:
Call Img.TourneImage(10, 10 + ImageNum)
' Double la taille des images 11 à 18 qui serviront dans le Sprite:
Call Img.ModifieTailleImage(10 + ImageNum, Image(10).Largeur * 2, Image(10).Hauteur * 2)
' Indique que l'image devra être centrée:
Image(10 + ImageNum).AlignH = AlignH_Centre
Image(10 + ImageNum).AlignV = AlignV_Centre
Next ImageNum

' Initialisation de la position aléatoire des 3 canards:
For i = 1 To 3
Avatar(i).X = ImageMaxiX + Rnd() * 200  ' Position horizontale.
Avatar(i).Y = Rnd() * 300               ' Position verticale.
Avatar(i).SpriteActif = Rnd() * 14      ' Image du vol.
Avatar(i).MvX = -10                     ' Vitesse de déplacement des canards.
' Initialisation de la liste des images qui composent le Sprite de l'avatar:
Avatar(i).ListeImages = Array("11", "12", "13", "14", "15", "16", "17", "18", _
"17", "16", "15", "14", "13", "12", "11")
Avatar(i).Actif = True                  ' Déclare le canard actif.
Avatar(i).Visible = True                ' Déclare le canard visible.
Next i

' Définit la procédure à exécuter dans 1/2 seconde:
TimerID = SetTimer(0, 0, 500, AddressOf Descente)

' Boucle tant qu'il reste un canard à l'écran, en incrémentant de 10 pixels vers la gauche:
Do
' Copie de la mémorisation de l'écran (mémoire 0) en mémoire de travail (mémoire -1):
Call Img.CopieImage(0, -1)

' Déplace les canards en mémoire de travail -1:
For i = 1 To 3: Call Img.AnimeAvatar(i, -1): Next i

' Affiche à l'écran la mémoire de travail -1 et fait une pause:
Call Img.AfficheImage(-1)
Call Sleep(40)

' Si la souris est près du centre du canard alors le canard devient invisible
Call GetCursorPos(Pt)
For i = 1 To 3
Avatar(i).Visible = True
If Abs(Avatar(i).X - Pt.X) < 50 And Abs(Avatar(i).Y - Pt.Y) < 50 Then Avatar(i).Visible = False
Next i

' Si une touche est enfoncée alors remet l'écran d'origine et sort de l'animation:
If Img.ToucheEnfoncée(0) = True Then Call Img.AfficheImage(0): Exit Do

Loop While Avatar(1).X > -100 Or Avatar(2).X > -100 Or Avatar(3).X > -100

' Ferme la procédure qui s'exécute toutes les 1/2 secondes:
Call KillTimer(0, TimerID)

' Fermeture du mode graphique:
Call Img.FermeModeGraphique

End Sub

'---------------------------------------------------------------------------------------
Sub Descente(ByVal hwnd As Long, ByVal uMsg As Long, ByVal IdEvent As Long, ByVal Systime As Long)
'---------------------------------------------------------------------------------------
Avatar(1).Y = Avatar(1).Y + 10
End Sub

Nous allons maintenant étudier l'animation d'un avatar où l'utilisateur intervient pour son déplacement.

Reprenons le Sprite du vol du canard où est ajoutée cette neuvième image :

La touche flèche de droite déplacera l'avatar vers la droite^1, et la flèche de gauche vers la gauche^2. Si ces touches ne sont pas enfoncées, le canard reste immobile et prend une forme d'attente^3. Idem si l'avatar atteint la limite gauche ou droite de l'écran^4.
La touche Fin fera chuter l'avatar^5 (la neuvième image représentée ci-dessus) et mettra fin à l'animation^7 après une explosion du canard^6. Le Sprite de l'explosion est constitué de 7 images de 40 x 40 pixels^8.

Les images du Sprite du vol de gauche vers la droite, « Vol1.bmp » à « Vol8.bmp », sont chargées et alimentent les mémoires 11 à 18^9. Ces mémoires retournées forment le Sprite du vol en sens inverse, mémoires 21 à 28^10. Toutes ces images sont initialisées centrées horizontalement et verticalement^11. L'image du canard qui chute « Vol9.bmp » est chargée en mémoire 20^12.

Le Sprite des 7 images de l'explosion contenu dans le fichier « Sprite_explosion.bmp » est chargé en mémoire 10^8. Une boucle est faite pour copier en mémoire 99 la fraction de l'image de 40 x 40 à récupérer^13. Puis l'image est agrandie et copiée en mémoire 31 à 37^14. Elle peut alors être déclarée centrée^15.

L'avatar est initialisé au centre de l'écran, en ligne 300. L'écran est délimité avec une limite gauche et droite à 50 pixels du bord de l'écran^16.

La boucle d'animation peut commencer^17. Elle se terminera quand la touche Fin sera activée^18, déclenchant en même temps l'animation de la chute de l'avatar et de son explosion.

Durant cette boucle, par défaut, l'avatar est immobile : MvX et MvY valent zéro. Donc l'image de l'avatar est soit la 14 soit la 24 suivant le sens de déplacement en cours^19.

Si la touche flèche de droite est enfoncée^1, alors le sens de déplacement en cours est alimenté à 10. Cela correspondra aussi au déplacement MvX. La liste des images de l'animation de l'avatar est alimentée par les images 11 à 18 et 17 à 11^20. L'image 14 représente l'avatar bloqué dans son déplacement vers la droite^21.

Si la touche flèche de gauche est enfoncée^2, alors le sens de déplacement en cours est alimenté à -10. Cela correspondra aussi au déplacement MvX. La liste des images de l'animation de l'avatar est alimentée par les images 21 à 28 et 27 à 21^22. L'image 24 représente l'avatar bloqué dans son déplacement vers la gauche^23.

Tout est en place pour lancer l'animation de l'avatar.

Si une touche de direction reste enfoncée pendant la boucle, l'avatar sera donc animé normalement.

L'animation de la chute est très simple. Le déplacement MvX est annulé, et par contre MvY est alimenté à 10, pour une chute de 10 pixels par boucle^24. Une seule image, numéro 20, est utilisée^25. La boucle se termine quand l'avatar est à 50 pixels du bas de l'écran^26.

L'explosion nécessite la présence des deux avatars, le canard et l'explosion^27. Pour que le canard reste visible sur les quatre premières images de l'animation afin de simuler son explosion, son Animation est initialisée à 5^28. Ainsi elle sera jouée jusqu'à 2 :

L'animation de l'explosion est déclarée UneFoisEtQuitte^29, donc à sa fin Actif prend la valeur False et indique que la boucle de l'animation peut être quittée^30.

'---------------------------------------------------------------------------------------
Sub NotionsDeBase()
'---------------------------------------------------------------------------------------
Call Img.InitialiseModeGraphique
Dim SensAvatar As Integer

' Chargement des 8 images du Sprite du vol:
For ImageNum = 1 To 8                                                                               ' 9
' Charge l'image en mémoire 11 à 18:
Call Img.ChargeImageBMP(ActiveWorkbook.Path & "\vol" & ImageNum & ".bmp", _
ImageNum + 10, -9, AlignH_Centre, AlignV_Centre)
' Retourne l'image numéro et la place en mémoire 21 à 28:                                       ' 11
Call Img.TourneImage(ImageNum + 10, ImageNum + 20)                                              ' 10
Next ImageNum

' Charge l'image du canard qui chute :                                                              ' 12
Call Img.ChargeImageBMP(ActiveWorkbook.Path & "\vol9.bmp", 20, -9, AlignH_Centre, AlignV_Centre)

' Charge le Sprite de l'explosion (7 images de 40x40) en mémoire 10:
Call Img.ChargeImageBMP(ActiveWorkbook.Path & "\Sprite_explosions.bmp", 10, -9, 0, 0)               ' 8

' Récupère les 7 images du Sprite:
For ImageNum = 0 To 6
' Copie la fraction d'image en mémoire temporaire 99:                                           ' 13
Call Img.CopieImage(10, 99, 0, 0, ImageNum * 40, 0, 40, 40)
' Modifie la taille de l'image 99 et crée l'image 31 à 37:
Call Img.ModifieTailleImage(99, 80, 80, 31 + ImageNum)                                          ' 14
' Déclare que les images 31 à 37 seront centrées:
Image(31 + ImageNum).AlignH = AlignH_Centre                                                     ' 15
Image(31 + ImageNum).AlignV = AlignV_Centre
' Efface la mémoire temporaire 99:
Call Img.EffaceImage(99)
Next ImageNum

' Initialisation de l'avatar 0, qui représente le canard:
Avatar(0).X = ImageMaxiX / 2            ' Position horizontale.                                     ' 16
Avatar(0).Y = 300                       ' Position verticale.
Avatar(0).PosXMini = 50                 ' Limite gauche
Avatar(0).PosXMaxi = ImageMaxiX - 50    ' Limite droite
Avatar(0).Actif = True                  ' Déclare le canard actif.
Avatar(0).Visible = True                ' Déclare le canard visible.

Do                                                                                                  ' 17
' Copie de la mémorisation de l'écran (mémoire 0) en mémoire de travail (mémoire -1):
Call Img.CopieImage(0, -1)

' Si aucune flèche enfoncée alors l'avatar reste immobile :                                     ' 3
Avatar(0).MvX = 0
If SensAvatar < 0 Then                                                                          ' 19
Avatar(0).ListeImages = Array("24") ' Image si immobile vers la gauche.
Else                                                                                            ' 4
Avatar(0).ListeImages = Array("14") ' Image si immobile vers la droite.
End If

' Si flèche droite, alors définit le sprite du vol vers la droite:
If Img.ToucheEnfoncée(vbKeyRight) = True Then                                                   ' 1
SensAvatar = 10             ' Mémorise le sens de déplacement en cours.
Avatar(0).ListeImages = Array("11", "12", "13", "14", "15", "16", "17", "18", _             ' 20
"17", "16", "15", "14", "13", "12", "11")
Avatar(0).SpriteBloqué = 14 ' Image si l'avatar atteint la limite droite.                   ' 21
Avatar(0).MvX = SensAvatar  ' Déplace l'avatar.
End If

' Si flèche gauche, alors définit le sprite du vol vers la gauche:
If Img.ToucheEnfoncée(vbKeyLeft) = True Then                                                    ' 2
SensAvatar = -10            ' Mémorise le sens de déplacement en cours.
Avatar(0).ListeImages = Array("21", "22", "23", "24", "25", "26", "27", "28", _             ' 22
"27", "26", "25", "24", "23", "22", "21")
Avatar(0).SpriteBloqué = 24 ' Image si l'avatar atteint la limite gauche.                   ' 23
Avatar(0).MvX = SensAvatar  ' Déplace l'avatar.
End If

' Déplace l'avatar en mémoire de travail -1:
Call Img.AnimeAvatar(0, -1)

' Si l'avatar est immobile, en cas de déplacement commencera à la 11e image:
If Avatar(0).MvX = 0 Then Avatar(0).SpriteActif = 11

' Affiche à l'écran la mémoire de travail -1 et fait une pause:
Call Img.AfficheImage(-1)
Call Sleep(40)

' Si une touche Fin est enfoncée alors sort de l'animation:
If Img.ToucheEnfoncée(vbKeyEnd) = True Then Call Chute: Call Explosion: Exit Do                ' 7
'  18           5            6
Loop

' Fermeture du mode graphique:
Call Img.FermeModeGraphique

End Sub

'---------------------------------------------------------------------------------------
Sub Chute()
'---------------------------------------------------------------------------------------
Avatar(0).MvX = 0                                   ' 24
Avatar(0).MvY = 10
Avatar(0).ListeImages = Array("20")                 ' 25
Do
Call Img.CopieImage(0, -1)
Call Img.AnimeAvatar(0, -1)
Call Img.AfficheImage(-1)
Call Sleep(40)
Loop While Avatar(0).Y + 50 < ImageMaxiY            ' 26
End Sub

'---------------------------------------------------------------------------------------
Sub Explosion()
'---------------------------------------------------------------------------------------
' Initialisation de l'avatar 1 qui représente l'explosion:
Avatar(1).X = Avatar(0).X
Avatar(1).Y = Avatar(0).Y
Avatar(1).Actif = True                                                      ' 27
Avatar(1).Visible = True
Avatar(1).Animation = UneFoisEtQuitte                                       ' 29
Avatar(1).ListeImages = Array("31", "32", "33", "34", "35", "36", "37")

' Annule le déplacement du canard et l'affichera trois dernières fois:
Avatar(0).MvX = 0
Avatar(0).MvY = 0                                                           ' 27
Avatar(0).SpriteActif = 0
Avatar(0).ListeImages = Array("20")                                         ' 28
Avatar(0).Animation = 5

' Boucle tant que le Sprite de l'explosion n'est pas terminé:
Do
Call Img.CopieImage(0, -1)

Call Img.AnimeAvatar(0, -1) ' Avatar du canard.
Call Img.AnimeAvatar(1, -1) ' Avatar de l'explosion.

Call Img.AfficheImage(-1)
Call Sleep(100)

Loop While Avatar(1).Actif = True                                           ' 30

' Affiche l'écran d'origine:
Call Img.AfficheImage(0)
End Sub

XIV - Utiliser un formulaire pour les animations

En mixant ces deux exemples d'animation, il serait tentant de faire un jeu de tir aux canards en se servant de la souris pour faire exploser les canards en vol.
J'ai bien essayé, mais malheureusement ça ne marche pas, car chaque clic donne la main à la feuille de calcul ouverte, ou à l'application d'arrière-plan, et le clic de la souris n'est pas reconnu par notre code.

Alors que faire ?

La première astuce est d'utiliser un formulaire (voir le tome 1 pour plus d'informations sur la création d'un formulaire), et de bloquer la souris dans l'espace du formulaire pour nous assurer que le clic sera reconnu par notre code.
La seconde astuce est de remplacer le contexte de l'écran par le contexte du formulaire, créant de ce fait un écran bis, et le formulaire, totalement indépendant de l'écran principal, pourra même être déplacé librement, sans incidence sur l'animation.
Cerise sur le gâteau, toutes nos procédures restent compatibles.

Voici un aperçu de ce que nous obtiendrons :

La procédure InitialiseModeGraphique doit être modifiée pour gérer la possibilité de travailler dans un formulaire (passé en argument), et tant que l'on y est, pour éventuellement centrer ce formulaire avec une marge horizontale et verticale désirée…

L'API FindWindowA avec en arguments le type de classe ThunderDFrame et le libellé du formulaire, retourne le numéro de la fenêtre du formulaire. Numéro qui sera converti avec l'API GetDC pour obtenir le numéro du contexte du formulaire.

Declare Function FindWindowA Lib "user32" (ByVal lpClassName As String,
ByVal lpWindowName As String) As Long
Declare Function GetDC Lib "user32" (ByVal hwnd As Long) As Long

Les coordonnées (Top, Left) et les dimensions internes (InsideHeight, InsideWidth) ou externes (Height, Width) d'un formulaire sont exprimées en points et non en pixels. Des conversions seront donc nécessaires pour appliquer une marge à l'affichage du formulaire et modifier sa taille. L'API GetDeviceCaps indique, suivant la résolution utilisée, le nombre de points par pixel et permet ces conversions.

Declare Function GetDeviceCaps Lib "gdi32" (ByVal Hdc As Long, ByVal nIndex As Long) As Long

nIndex = LOGPIXELSX (88) pour la résolution horizontale et LOGPIXELSY (90) pour la résolution verticale.

'---------------------------------------------------------------------------------------
Public Function PointsEnPixelsX(lPoint As Long) As Long
'---------------------------------------------------------------------------------------
Static Mult As Single
If Mult = 0 Then Mult = 72 / GetDeviceCaps(GetWindowDC(0), 88) ' LOGPIXELSX
PointsEnPixelsX = CLng(lPoint / Mult)
End Function

'---------------------------------------------------------------------------------------
Public Function PointsEnPixelsY(lPoint As Long) As Long
'---------------------------------------------------------------------------------------
Static Mult As Single
If Mult = 0 Then Mult = 72 / GetDeviceCaps(GetWindowDC(0), 90) ' LOGPIXELSY
PointsEnPixelsY = CLng(lPoint / Mult)
End Function

'---------------------------------------------------------------------------------------
Public Function PixelsEnPointsX(lPixel As Long) As Long
'---------------------------------------------------------------------------------------
Static Mult As Single
If Mult = 0 Then Mult = 72 / GetDeviceCaps(GetWindowDC(0), 88) ' LOGPIXELSX
PixelsEnPointsX = CLng(lPixel * Mult)
End Function

'---------------------------------------------------------------------------------------
Public Function PixelsEnPointsY(lPixel As Long) As Long
'---------------------------------------------------------------------------------------
Static Mult As Single
If Mult = 0 Then Mult = 72 / GetDeviceCaps(GetWindowDC(0), 90) ' LOGPIXELSY
PixelsEnPointsY = CLng(lPixel * Mult)
End Function

Nous utiliserons aussi ces procédures pour bloquer la souris dans l'espace du formulaire :

'---------------------------------------------------------------------------------------
Public Sub BloqueCurseurDansForm(ObjFormulaire As Object)
'---------------------------------------------------------------------------------------
SourisBloque.Top = PointsEnPixelsX(ObjFormulaire.Top)
SourisBloque.Left = PointsEnPixelsY(ObjFormulaire.Left)
SourisBloque.Bottom = PointsEnPixelsY(ObjFormulaire.Top + ObjFormulaire.Height)
SourisBloque.Right = PointsEnPixelsX(ObjFormulaire.Left + ObjFormulaire.Width)
Call ClipCursor(SourisBloque)
End Sub

Et inversement, pour savoir où se situe la souris dans le formulaire en tenant compte du décalage entre l'origine 0,0 de l'écran et l'origine 0,0 du formulaire :

'---------------------------------------------------------------------------------------
Public Sub LectureCurseurDansForm(Pt As POINT, ObjFormulaire As Object)
'---------------------------------------------------------------------------------------
Call GetCursorPos(Pt)
Pt.x = Pt.x - PointsEnPixelsX(ObjFormulaire.Left)
Pt.Y = Pt.Y - PointsEnPixelsY(ObjFormulaire.Top)
End Sub

Voici donc la nouvelle version de la procédure d'initialisation du mode graphique :

'---------------------------------------------------------------------------------------
Public Sub InitialiseModeGraphique(Optional ObjFormulaire As Object, _
Optional MargeH As Long = -1, Optional MargeV As Long = -1)
'---------------------------------------------------------------------------------------
Call EffaceImage(-1): Call EffaceImage(0)

If ObjFormulaire Is Nothing Then ' Si pas de formulaire, donc utilise l'écran:
Hdc = GetWindowDC(0)             ' Mémorise le contexte de l'écran.
ImageMaxiX = GetSystemMetrics(0) ' Mémorise la taille horizontale de l'écran.
ImageMaxiY = GetSystemMetrics(1) ' Mémorise la taille verticale de l'écran.
Else
' Mémorise le contexte du formulaire (classe Windows suivant version EXCEL):
Hdc = FindWindowA("ThunderDFrame", ObjFormulaire.Caption) ' "ThunderXFrame" si < 2002.
Hdc = GetDC(Hdc)
If MargeH > -1 Then ' S'il faut afficher le formulaire avec une marge horizontale:
ObjFormulaire.Left = PixelsEnPointsX(MargeH)
ObjFormulaire.Width = PixelsEnPointsX(GetSystemMetrics(0) - MargeH * 2)
End If
If MargeV > -1 Then ' S'il faut afficher le formulaire avec une marge verticale:
ObjFormulaire.Top = PixelsEnPointsY(MargeV)
ObjFormulaire.Height = PixelsEnPointsY(GetSystemMetrics(1) - MargeV * 2)
End If
' Mémorise les dimensions maximales du formulaire (en pixels):
ImageMaxiX = PointsEnPixelsX(ObjFormulaire.InsideWidth)  ' Taille horizontale.
ImageMaxiY = PointsEnPixelsY(ObjFormulaire.InsideHeight) ' Taille verticale.
' Redessine le formulaire à l'écran:
ObjFormulaire.Repaint
End If

' Création de l'image 0 pour mémoriser l'écran d'origine:
Call CopieImage(0, 0, , , , , ImageMaxiX, ImageMaxiY)
Image(0).Transparence = -1 ' Sans transparence.

' Mémorise l'écran en mémoire 0 puis fait une copie en mémoire -1:
Call BitBlt(Image(0).Hdc, 0, 0, ImageMaxiX, ImageMaxiY, Hdc, 0, 0, SRCCOPY)
Call CopieImage(0, -1)

' Mémorise l'espace de déplacement de la souris:
Call GetClipCursor(SourisEspace)
End Sub

La procédure de fermeture du mode graphique utilise les API : GetActiveWindow qui retourne la fenêtre active, GetClassName pour vérifier si cette fenêtre est un formulaire, et PostMessage pour la fermer.

Ces API pourront vous être utiles dans vos autres applications, il faut donc les connaître :

Declare Function GetActiveWindow Lib "user32" () As Long
Declare Function GetClassName Lib "user32" Alias "GetClassNameA" (ByVal hwnd As Long, _
ByVal lpClassName As String, ByVal nMaxCount As Long) As Long
Declare Function PostMessage Lib "user32" Alias "PostMessageA" (ByVal hwnd As Long, _
ByVal wMsg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long
'---------------------------------------------------------------------------------------
Public Sub FermeModeGraphique()
'---------------------------------------------------------------------------------------
' Libère les mémoires:
For ImageNum = -1 To UBound(Image()): Call EffaceImage(ImageNum): Next ImageNum
ReleaseDC 0, Hdc
Call ShowCursor(1)              ' Affiche le curseur de la souris.
Call ClipCursor(SourisEspace)   ' Restaure l'espace de déplacement de la souris.

' Ferme la fenêtre active si la fenêtre active est un formulaire:
Dim ClassName As String * 256
Call GetClassName(GetActiveWindow(), ClassName, 256) ' ClassName contient le nom de la classe
If Left(ClassName, 13) = "ThunderDFrame" Then Call PostMessage(GetActiveWindow(), &H10, 0, 0)

' Restaure la résolution graphique d'origine:
Call ChangeScreen_Resolution(0, 0)
End Sub

La dernière instruction de FermeModeGraphique lance la fonction ChangeScreen_Resolution(Largeur, Hauteur), qui mérite quelques explications, car elle aussi peut vous être très utile.
Comme son nom l'indique, cette fonction permet de changer la résolution graphique de l'écran.
Ce peut être nécessaire si votre application est construite pour afficher à l'écran un certain nombre de pixels : une résolution trop petite fera déborder l'affichage de l'écran et inversement une résolution trop grande rendra l'affichage illisible.
Si la résolution passée en arguments est supportée par l'écran, la résolution est modifiée et la fonction retourne True. Sinon rien ne se passe et la fonction retourne False. Les résolutions les plus courantes sont : VGA 640x480, SVGA 800x600, XGA 1024x768, SXGA 1280x1024, UXGA 1600x1200.

Avant de modifier la résolution graphique de l'écran, les valeurs d'origine sont sauvegardées dans les variables publiques Anc_ScrWidth et Anc_ScrHeight.
La résolution d'origine est restituée lorsque les arguments passés à la fonction sont 0, 0, ce que fait la procédure FermeModeGraphique au cas où un changement de résolution ait été fait.

La fonction fait appel aux API EnumDisplaySettings pour mémoriser la configuration de l'écran dans la structure TypDevMODE et ChangeDisplaySettings pour modifier cette configuration.

Declare Function EnumDisplaySettings Lib "user32" Alias "EnumDisplaySettingsA" _
(ByVal lpszDeviceName As Long, ByVal iModeNum As Long, lptypDevMode As Any) As Boolean
Declare Function ChangeDisplaySettings Lib "user32" Alias "ChangeDisplaySettingsA" _
(lptypDevMode As Any, ByVal dwFlags As Long) As Long

La procédure du module Img s'inspire du code que vous trouverez sur la page « www.mrexcel.com/archive/VBA/29971.html », qui détaille aussi la structure TypDevMODE.

'---------------------------------------------------------------------------------------
Public Function ChangeScreen_Resolution(ScrWidth As Long, ScrHeight As Long) As Boolean
'---------------------------------------------------------------------------------------
' Mémorise la résolution avant de la changer:
If Anc_ScrWidth = 0 Then Anc_ScrWidth = GetSystemMetrics(0)
If Anc_ScrHeight = 0 Then Anc_ScrHeight = GetSystemMetrics(1)

' Si pas de valeur indiquée dans les arguments alors restaure la résolution d'origine:
If ScrWidth + ScrHeight = 0 Then
ScrWidth = Anc_ScrWidth
ScrHeight = Anc_ScrHeight
End If

' Si la résolution demandée est déjà celle existante alors rien à faire et quitte:
If ScrWidth = GetSystemMetrics(0) And ScrHeight = GetSystemMetrics(1) Then _
ChangeScreen_Resolution = True: Exit Function

' Mémorise dans TypDevM la configuration de l'écran en utilisant la structure TypDevMODE:
Dim TypDevM As TypDevMODE
Call EnumDisplaySettings(0, 0, TypDevM)
' Change les valeurs de TypDevM:
TypDevM.dmFields = &H80000 Or &H100000
TypDevM.dmPelsWidth = ScrWidth
TypDevM.dmPelsHeight = ScrHeight

' Change la résolution en passant en argument TypDevM:
If ChangeDisplaySettings(TypDevM, &H4) = 0 Then ChangeScreen_Resolution = True
End Function

Exemple pour passer en résolution WXGA 1280x800 et afficher un message en cas d'erreur :

If Img.ChangeScreen_Resolution(1280, 800) = False Then
MsgBox "La résolution 1280 x 800 n'est pas supportée par votre écran" _
& Chr(10) & Chr(13) & "L'affichage risque d'être perturbé."
End If

Nous pouvons maintenant créer ce fameux jeu de tir aux canards, à partir d'un formulaire assez grand, où j'ai incorporé une image de fond. Pour compléter l'animation, j'ai ajouté du son sur le tir « laser-01.wav », et pendant l'explosion « fire-04-loop.wav », ainsi qu'un texte, « BRAVO » ou « ENCORE RATÉ », géré comme un avatar, qui s'affiche lors du clic, et un chronomètre.

Le formulaire possède le bouton « CommandButton1 » qui lance la procédure d'animation ImgDansUnFormulaire contenue dans un module ordinaire. L'objet Formulaire étant passé en argument :

'---------------------------------------------------------------------------------------
Private Sub CommandButton1_Click()
'---------------------------------------------------------------------------------------
CommandButton1.Visible = False      ' Masque le bouton de lancement.
Call ImgDansUnFormulaire(Me)        ' Lance l'animation dans le formulaire.
End Sub

'---------------------------------------------------------------------------------------
Public Sub ImgDansUnFormulaire(MyForm As Object)
'---------------------------------------------------------------------------------------
' Initialise le mode graphique pour travailler dans un formulaire:
Call Img.InitialiseModeGraphique(MyForm, 100, 100) ' Formulaire centré avec une marge de 100

' Chargement des 8 images du Sprite du vol:
For ImageNum = 1 To 8
' Charge l'image en mémoire 11 à 18:
Call Img.ChargeImageBMP(ActiveWorkbook.Path & "\vol" & ImageNum & ".bmp", _
ImageNum + 10, -9, AlignH_Centre, AlignV_Centre)
Next ImageNum

' Charge le Sprite de l'explosion (7 images de 40x40) en mémoire 10:
Call Img.ChargeImageBMP(ActiveWorkbook.Path & "\Sprite_explosions.bmp", 10, -9, 0, 0)

' Récupère les 7 images du Sprite de l'explosion:
For ImageNum = 0 To 6
' Copie la fraction d'image en mémoire temporaire 99:
Call Img.CopieImage(10, 99, 0, 0, ImageNum * 40, 0, 40, 40)
' Modifie la taille de l'image 2 et crée l'image 31 à 37:
Call Img.ModifieTailleImage(99, 80, 80, 31 + ImageNum)
' Déclare que les images 31 à 37 seront centrées:
Image(31 + ImageNum).AlignH = AlignH_Centre
Image(31 + ImageNum).AlignV = AlignV_Centre
' Efface la mémoire temporaire 99:
Call Img.EffaceImage(99)
Next ImageNum

' Charge l'image du viseur, le viseur sera centré à l'affichage:
Call Img.ChargeImageBMP(ActiveWorkbook.Path & "\Viseur.bmp", 99, -1)
Call Img.ModifieTailleImage(99, 30, 30) ' Nouvelle Taille du viseur.
Image(99).AlignH = AlignH_Centre
Image(99).AlignV = AlignV_Centre
Image(99).Transparence = GetPixel(Image(99).Hdc, 0, 0)

' Masque la souris:
Call ShowCursor(0)

' Initialisation de l'avatar 0, qui représente le canard:
Avatar(0).X = -100 * Rnd         ' Position horizontale.
Avatar(0).Y = 100 + Rnd * 300    ' Position verticale.
Avatar(0).MvX = 10               ' Déplacement de l'avatar.
Avatar(0).Actif = True           ' Déclare le canard actif.
Avatar(0).Visible = True         ' Déclare le canard visible.
Avatar(0).ListeImages = Array("11", "12", "13", "14", "15", "16", "17", "18", _
"17", "16", "15", "14", "13", "12", "11")

' Initialise l'avatar 2 qui sera le texte affiché après un tir:
Call Img.TexteConfig(0, 0, -1, "Arial", 30, 900)
Avatar(2).Actif = False
Avatar(2).Visible = True
Avatar(2).ListeImages = Array("10")
Avatar(2).MvY = 5

' Configuration du texte pour l'affichage du chronomètre
Call Img.TexteConfig(1, 15, 0, "Arial", 20)

' Définit la procédure du chronomètre à exécuter toutes les secondes:
TimerID = SetTimer(0, 0, 1000, AddressOf AfficheTemps) ' Procédure à exécuter.
Call AfficheTemps(0, 0, 0, 0) ' Affiche dès maintenant sans attendre une seconde.

On Error GoTo Fin ' Si le formulaire est fermé.
Do
' Bloque la souris dans le cadre du formulaire, qui peut être déplacé à tout moment:
Call Img.BloqueCurseurDansForm(MyForm)

' Copie de la mémorisation de l'écran (mémoire 0) en mémoire de travail (mémoire -1):
Call Img.CopieImage(0, -1)

' Lecture du bouton gauche de la souris:
If Img.ToucheEnfoncée(vbKeyLButton) = True Then
Avatar(2).Actif = True
Avatar(2).X = Pt.X: Avatar(2).Y = Pt.Y + 50:
Avatar(2).Animation = 20
Call Img.EffaceImage(10)
Call Img.CopieImage(10, 10, , , , , Police(0).Taille * 10, Police(0).Taille)

' Son du tir:
Call PlaySound(ThisWorkbook.Path & "\laser-01.wav", pcsASYNC)

' Si l'avatar est touché alors provoque son explosion:
If Abs(Avatar(0).X - Pt.X) < 20 And Abs(Avatar(0).Y - Pt.Y) < 20 Then
Call Img.Texte(Image(10).Hdc, 0, 0, "BRAVO")
Explosion
' Relance un nouvel avatar pour ne pas terminer le jeu:
Avatar(0).X = -100 + Rnd * 100   ' Position horizontale.
Avatar(0).Y = 100 + Rnd * 200    ' Position verticale.
Avatar(0).MvX = 10               ' Déplacement.
Avatar(0).Actif = True           ' Activation.
Else
' Sinon affiche juste un message:
Call Img.Texte(Image(10).Hdc, 0, 0, "ENCORE RATÉ")
End If
End If

' Copie ou non le message suite au clic:
Call Img.AnimeAvatar(2, -1)

' Déplace l'avatar en mémoire de travail -1:
Call Img.AnimeAvatar(0, -1)

' Lecture de la position du curseur et correction d'après l'origine du formulaire:
Call Img.LectureCurseurDansForm(Pt, MyForm)

' Copie le viseur en mémoire de travail:
Call Img.CopieImage(99, -1, Pt.X, Pt.Y)

' Affiche à l'écran la mémoire de travail -1 et fait une pause:
Call Img.AfficheImage(-1)
Call Sleep(40)

' Si la touche Fin est enfoncée alors sort de l'animation:
If Img.ToucheEnfoncée(vbKeyEnd) = True Then Exit Do

Loop While Avatar(0).X < ImageMaxiX + 100 ' Quitte si le canard sort de l'écran.

Fin:
' Ferme la procédure qui s'exécute toutes les secondes:
Call KillTimer(0, TimerID)
' Fermeture du mode graphique:
Call Img.FermeModeGraphique

End Sub

'---------------------------------------------------------------------------------------
Sub Explosion()
'---------------------------------------------------------------------------------------
' Initialisation de l'avatar 1 qui représente l'explosion:
Avatar(1).X = Pt.X
Avatar(1).Y = Pt.Y
Avatar(1).Actif = True
Avatar(1).Visible = True
Avatar(1).SpriteActif = 0
Avatar(1).Animation = UneFoisEtQuitte
Avatar(1).ListeImages = Array("31", "32", "33", "34", "35", "36", "37")

' Annule le déplacement du canard et l'affichera 3 fois:
Avatar(0).MvX = 0
Avatar(0).SpriteActif = 0

' Fait une pause pour entendre la fin du son du tir:
Call Sleep(100)

' Son de l'explosion, joué en boucle:
Call PlaySound(ThisWorkbook.Path & "\fire-04-loop.wav", pcsLOOP + pcsASYNC)

' Boucle tant que le Sprite de l'explosion n'est pas terminé:
Do
Call Img.CopieImage(0, -1)  ' Copie la mémoire de l'écran en mémoire -1.
Call Img.AnimeAvatar(0, -1) ' Anime l'avatar du canard.
Call Img.AnimeAvatar(1, -1) ' Anime l'avatar de l'explosion.
Call Img.AnimeAvatar(2, -1) ' Anime l'avatar du texte affiché.

Call Img.AfficheImage(-1)   ' Affiche la mémoire de travail.
Call Sleep(100)             ' Fait une pause.

If Avatar(0).SpriteActif = 3 Then Avatar(0).Actif = False ' Affiche l'avatar que 3 fois.

Loop While Avatar(1).Actif = True
Call PlaySound(" ", pcsASYNC) ' Fin du son de l'explosion joué en boucle.

End Sub

'---------------------------------------------------------------------------------------
Sub AfficheTemps(ByVal hwnd As Long, ByVal uMsg As Long, ByVal IdEvent As Long, _
ByVal Systime As Long)
'---------------------------------------------------------------------------------------
Static Chrono As Double
If Chrono = 0 Then Chrono = Timer ' Mémorise l'heure de début.

' Affiche le temps écoulé depuis l'heure mémorisée dans la variable Chrono:
Call Img.Texte(Image(0).Hdc, 1, ImageMaxiY - Police(1).Taille, _
Format((Timer - Chrono) / 100000, "nn:ss"), 1) ' Format "minutes:secondes"
End Sub

Nous obtenons ceci.
Il est vrai que c'est sans grand intérêt.
Mais le but était avant tout d'acquérir, progressivement, les notions et les outils nécessaires à la création d'un véritable jeu d'arcade.
Ce sera notre prochaine étape.

XV - Snake - Programmer un jeu d'arcade en mode graphique

Le tome 1 présentait un code pour programmer le jeu d'arcade Snake avec les fonctionnalités du VBA.

Je vous rappelle le principe : avec les touches du clavier « flèche gauche » et « flèche droite », vous déplacez un serpent qui doit manger dix pommes pour passer au niveau suivant. Pour compliquer la tâche du joueur, chaque pomme mangée rallonge le serpent et les niveaux comportent de plus en plus d'obstacles à éviter.

Nous allons reprendre la programmation de ce jeu, cette fois en mode graphique, vous constaterez ainsi la différence.

Voici le rendu de la programmation en mode standard…

Et le rendu que nous allons obtenir en mode graphique.

Cela peut vous surprendre, mais ces deux versions ont beaucoup de points communs. En effet, la version en mode graphique utilise un « masque », pour lire la position des avatars sur le plateau.

Chargé en mémoire Masque(X,Y), il est alimenté de valeurs qui représentent un vide ou un objet : mur, pomme, corps du serpent, porte. C'est la lecture du masque qui permet de déterminer l'image qui doit être affichée sur le plateau et aussi de connaître les objets rencontrés.

Dans le masque ci-dessus, quand la tête du serpent est en position X=5, Y=17, c'est que le serpent est rentré dans un mur. Et la pomme est en position X=22, Y=22. On retrouve les mêmes notions que la programmation en mode standard, sauf que le masque est en mémoire alors qu'en mode standard on utilise directement les informations de la feuille de calcul. Ainsi Masque(X,Y) est l'équivalent de Cells(Y,X).

Dans notre jeu, c'est une feuille de calcul qui permet d'initialiser le masque d'un niveau. Vous pouvez donc facilement créer des niveaux à volonté.

Pour une meilleure compréhension du code, j'ai repris la liste des images utilisées par le jeu :

11 à 30 : le Sprite du serpent avec ses différents sens de déplacement.
Qui comprend aussi la pomme, 26, qui sera l'avatar numéro 10, la porte du bas, 27, qui sera l'avatar numéro 11, et la porte du haut, 28, qui sera l'avatar numéro 12.
Un vide est représenté par l'image 22.

31 : le mur.

41 à 47 : le Sprite des pièces.
C'est l'avatar numéro 8.

51 à 62 : le Sprite du trooper.
C'est l'avatar numéro 9.

81 à 87 : le Sprite de l'explosion.
C'est l'avatar numéro 0.

98 : le fond d'écran d'origine où sera posé le plateau de jeu.
99 : le plateau du jeu avec les murs du niveau.
100 : mémoire de travail du plateau pour poser le serpent.

Le tableau des scores est composé de 7 affichages qui sont gérés comme des avatars (numéros 101 à 107) ne contenant qu'une image, et utilisent les mémoires suivantes :

101 : image où est affiché le niveau.
102 : image où sont affichés les points.
103 : image où sont affichées les vies restantes.
104 : image où est affiché le nombre de pommes mangées dans le niveau.
105 : image où est affichée la durée du jeu.
106 : image où est affiché le compte à rebours du bonus pour gagner des points supplémentaires.
107 : image où est affiché un commentaire.

Quand un score doit être mis à jour, le fond d'origine de l'écran (mémoire 0) où est posé l'avatar est copié dans l'image associée à l'avatar. Puis le texte est écrit dans cette mémoire. L'animation de l'avatar, UneFoisEtAffiche, déclenche d'office l'affichage de la valeur.
C'est la technique que nous avons utilisée dans le tir aux canards. Plus simple que la méthode qui consiste à réécrire à chaque fois le texte de l'intégralité du tableau du score en mémoire de travail.

Pour déplacer le serpent dans le masque, sa position est mémorisée dans une structure :

Type TypeSerpent
X As Integer
Y As Integer
Forme As String * 1
End Type
Public Serpent(0 To 200) As TypeSerpent

X et Y sont les coordonnées, Forme est un code qui est déterminé par la direction de déplacement : H si le serpent se dirige vers le haut (MvY=-1), B vers le bas (MvY=1), G vers la gauche (MvX=-1), D vers la droite (MvX=1).
L'indice 0 représente la tête du serpent, les autres indices représentent le corps.

Le déplacement du serpent se décompose en plusieurs étapes, dont voici les grandes lignes :

1. Le déplacement de la tête, indice 0, suivant la direction active, avec mise à jour du masque.

2. Le déplacement du corps, à rebours en partant de la queue, pour que chaque indice prenne la place de l'indice précédent, avec bien sûr la mise à jour du masque.

3. Le masque de l'ancien emplacement de la queue peut alors être marqué comme libre.

4. Reste maintenant à afficher le serpent, en commençant par la tête.

5. Suivant la direction mémorisée dans Serpent(0).Forme, l'image à afficher sera soit la 14, la 15, la 19, la 20.

6. Même principe pour le corps, qui n'a lui que deux images, 12 et 18, pour les quatre directions possibles.

7. Mais attention, le corps peut prendre un virage. Il faut donc comparer l'indice avec l'indice suivant, et en fonction du sens du virage, utiliser l'image 11, 13, 16, ou 23.

8. Et enfin la queue.

9. Le traitement des éléments du corps du serpent se fait en mémoire 100.

10. Puis cette mémoire est reversée dans la mémoire de travail -1.

11. Mémoire où viendra s'ajouter l'animation des 12 avatars.

12. Avant que le tout soit affiché à l'écran.

13. La lecture des touches enfoncées permet de détecter un changement de direction, et si c'est le cas de modifier la direction active.

Le formulaire de lancement, User_ImgSnake, est non modal, c'est important pour la suite.
Il contient le bouton CommandButton1, qui permet de lancer la procédure publique ImgSnake(MyForm As Object) contenue dans un module classique :

'---------------------------------------------------------------------------------------
Private Sub CommandButton1_Click()
'---------------------------------------------------------------------------------------
CommandButton1.Visible = False  ' Masque le bouton de lancement.
Call ImgSnake(Me)               ' Lance l'animation dans le formulaire.
End Sub

Je vous présente, dans ce qui suit, le code du jeu, en commençant par la déclaration des variables, leur initialisation, l'initialisation des niveaux, avant d'attaquer le cœur de l'animation.

Vous connaissez déjà les procédures du module Img utilisées (sauf une que je vous présenterai après) et le reste ne pose pas de problème, car les commentaires vous guideront.

Bonne lecture.

Code du jeu

'--------------------------------------------------------------------------------------------
Public Const EcranMiniY As Integer = 1          ' Y mini du plateau dans le masque.
Public Const EcranMaxiY As Integer = 29         ' Y maxi du plateau dans le masque.
Public Const EcranMiniX As Integer = 1          ' X mini du plateau dans le masque.
Public Const EcranMaxiX As Integer = 29         ' X maxi du plateau dans le masque.
Public Const TailleImage As Integer = 16        ' Taille en pixel d'une image.
Public Const FormePomme As String * 1 = "@"     ' Forme d'une pomme dans le masque.
Public Const FormePorte As String * 1 = "¤"     ' Forme d'une porte dans le masque.
Public Const NbPommeParNiveau As Byte = 10      ' Nombre de pommes par niveau.
Public Const PointParPomme As Byte = 10         ' Points gagnés par pomme mangée.
Public Const AvanceParPomme As Byte = 7         ' Allonge du serpent par pomme mangée.
Public Const ScoreCouleur = 15                  ' Couleur de fond pour l'affichage des scores.
Public Const ScoreFond = -1                     ' Couleur du texte pour l'affichage des scores.
Public Const ScorePolice = "Arial"              ' Police pour l'affichage des scores.
Public Const ScoreTaille = 36                   ' Taille pour l'affichage des scores.
'--------------------------------------------------------------------------------------------
Public MvX As Integer, MvY As Integer   ' Déplacement en X et Y : -1, 0 ou 1.
Public Direction As Integer             ' Nouvelle direction: -1 = gauche, +1 = droite.
Public i As Integer                     ' Utilisée dans les boucles.
Public TailleSerpent As Byte            ' Taille du corps du serpent.
Public Niveau As Byte                   ' Niveau.
Public Points As Long                   ' Nombre de points gagnés.
Public Vie As Integer                   ' Nombre de vies.
Public NbPomme As Byte                  ' Nombre de pommes mangées dans le niveau.
Public Chrono As Double                 ' Permet de calculer la durée du jeu.
Public Bonus As Double                  ' Compte à rebours pour gagner un bonus.
Public TTimer As Double                 ' Pour la lecture du temps écoulé.
'--------------------------------------------------------------------------------------------
' Numéro des images pour dessiner le serpent suivant sa direction:
Public TeteH As Integer, TeteB As Integer, TeteG As Integer, TeteD As Integer
Public QueueH As Integer, QueueB As Integer, QueueG As Integer, QueueD As Integer
Public CorpsH As Integer, CorpsB As Integer, CorpsG As Integer, CorpsD As Integer
Public VirBD As Integer, VirBG As Integer, VirHG As Integer, VirHD As Integer
'--------------------------------------------------------------------------------------------
' Numéro des images pour dessiner la pomme, la porte haut et bas, un vide, un mur:
Public ImgPomme As Integer, ImgPorteBas As Integer, ImgPorteHaut As Integer
Public ImgVide As Integer, ImgMur As Integer
'--------------------------------------------------------------------------------------------
' Coordonnées d'affichage du plateau, largeur et hauteur.
Public OrX As Long, OrY As Long, OrLg As Long, OrHt As Long
'--------------------------------------------------------------------------------------------
Type TypeMasque      ' Masque de plateau.
X As Long               ' Abscisse.
Y As Long               ' Ordonnée.
V As String * 1         ' Valeur.
End Type
Public Masque(1 To EcranMaxiX, 1 To EcranMaxiY * 2) As TypeMasque

Type TypeScore              ' Tableau des scores.
X As Long               ' Abscisse.
Y As Long               ' Ordonnée.
Lib As String           ' Libellé.
End Type
Public Score(1 To 7) As TypeScore

Type TypeSerpent                            ' Position du Serpent dans le masque du plateau.
X As Integer                            ' Position X.
Y As Integer                            ' Position Y.
Forme As String * 1                     ' Forme de l'élément à afficher.
End Type
Public Serpent(0 To 200) As TypeSerpent     ' Nombre d'éléments, 0 représente la tête.

'--------------------------------------------------------------------------------------------
Public Sub InitialiseVariables()
'--------------------------------------------------------------------------------------------
Niveau = 1
Points = 0
Vie = 3

' Avatar de l'explosion:
Avatar(0).ListeImages = Array("81", "82", "83", "84", "85", "86", "87")
Avatar(0).Visible = True

' Avatar de la pièce:
Avatar(8).X = Avatar(2).X + ScoreTaille * 3
Avatar(8).Y = Avatar(2).Y
Avatar(8).ListeImages = Array("41", "42", "43", "44", "45", "46", "47")
Avatar(8).Visible = True

' Avatar du Trooper:
Avatar(9).X = Avatar(6).X + ScoreTaille * 2
Avatar(9).Y = Avatar(6).Y
Avatar(9).Visible = True

' Avatar pour la pomme:
Avatar(10).ListeImages = Array(ImgPomme)
Avatar(10).Visible = True
Avatar(10).Animation = UneFoisEtAffiche

' Avatar pour la porte du bas:
Avatar(11).ListeImages = Array(CorpsH, CorpsH, CorpsH, CorpsH, CorpsH, QueueH, _
ImgPorteBas, ImgPorteBas, ImgPorteBas, ImgPorteBas)
Avatar(11).X = OrX + Masque((EcranMaxiX + EcranMiniX) / 2, (EcranMaxiX + EcranMiniX) / 2).X
Avatar(11).Y = OrY + Masque(EcranMaxiY, EcranMaxiY).Y
Avatar(11).Visible = True
Call Img.CopieImage(0, ImgPorteBas, 0, 0, Avatar(11).X, Avatar(11).Y, TailleImage, TailleImage)

' Avatar pour la porte du haut:
Avatar(12).ListeImages = Array(81, 82, 83, 84, 85, 86, ImgPorteHaut)
Avatar(12).Animation = UneFoisEtAffiche
Avatar(12).X = OrX + Masque((EcranMaxiX + EcranMiniX) / 2, (EcranMaxiX + EcranMiniX) / 2).X
Avatar(12).Y = OrY + Masque(EcranMiniY, EcranMiniY).Y
Avatar(12).Visible = True
Call Img.CopieImage(0, ImgPorteHaut, 0, 0, Avatar(12).X, Avatar(12).Y, TailleImage, TailleImage)

' Heure de début du jeu:
Chrono = Timer
End Sub

'--------------------------------------------------------------------------------------------
Public Sub InitialiseNiveau()
'--------------------------------------------------------------------------------------------

' Initialise la tête du serpent:
MvX = 0: MvY = -1
Serpent(0).X = (EcranMaxiX + EcranMiniX) / 2
Serpent(0).Y = EcranMaxiY
Serpent(0).Forme = "H"

' Initialise le corps du serpent:
TailleSerpent = AvanceParPomme
For i = 1 To TailleSerpent
Serpent(i).X = Serpent(i - 1).X
Serpent(i).Y = Serpent(i - 1).Y + 1
Serpent(i).Forme = "H"
Next i

Avatar(10).Actif = False
Avatar(12).Actif = False

' Initialise le nombre de pommes:
NbPomme = 0

End Sub

'--------------------------------------------------------------------------------------------
Public Sub DessineNiveau()
'--------------------------------------------------------------------------------------------
Dim X As Integer, Y As Integer

' S'il n'y a plus de niveau dans les feuilles EXCEL:
On Error GoTo FinDesNiveaux

' Copie le plateau vierge en mémoire de travail 99:
Call Img.CopieImage(98, 99)

' Charge le masque du niveau (les murs et les vides):
For X = EcranMiniX To EcranMaxiX
For Y = EcranMiniY To EcranMaxiY
Masque(X, Y).V = " "
Masque(X, Y).V = Sheets("Niveau_" & Niveau).Cells(Y, X)
i = IIf(Masque(X, Y).V = " ", ImgVide, ImgMur)
Call Img.CopieImage(i, 99, Masque(X, Y).X, Masque(X, Y).Y)
Next Y
Next X

' Affiche le plateau à l'écran:
Call Img.CopieImage(99, 0, OrX, OrY)

' Affiche les scores:
Call AfficheScore(0)

' Pose une pomme:
Call PosePomme

' Affichera la porte ouverte un nombre de fois limité:
Avatar(11).Actif = True
Avatar(11).SpriteActif = 0
Avatar(11).Animation = 11

Exit Sub

FinDesNiveaux:
Vie = -1
End Sub

'--------------------------------------------------------------------------------------------
Public Sub PosePomme()
'--------------------------------------------------------------------------------------------
Dim X As Integer, Y As Integer

' Recherche un emplacement vide dans le plateau, grâce au masque:
Do
Do
X = Rnd() * EcranMaxiX
Loop While X < EcranMiniX Or X > EcranMaxiX
Do
Y = Rnd() * EcranMaxiY
Loop While Y < EcranMiniY Or X > EcranMaxiY
Loop While Masque(X, Y).V <> " "

' Initialise les coordonnées graphiques de la pomme:
Masque(X, Y).V = FormePomme
Avatar(10).X = OrX + Masque(X, Y).X
Avatar(10).Y = OrY + Masque(X, Y).Y
Avatar(10).Actif = True

' Relance le compte à rebours pour gagner un bonus:
Bonus = Timer + 10

End Sub

'---------------------------------------------------------------------------------------
Public Sub ImgSnake(MyForm As Object)
'---------------------------------------------------------------------------------------
Dim X As Long, Y As Long

' Change la résolution de l'écran en 1280 x 800:
If Img.ChangeScreen_Resolution(1280, 800) = False Then
MsgBox "La résolution 1280 x 800 n'est pas supportée par votre écran" _
& Chr(10) & Chr(13) & "L'affichage risque d'être perturbé."
End If

' Masque la souris:
Call ShowCursor(0)

' Définit l'origine X,Y du plateau de jeu et calcule la largeur et la hauteur:
OrX = 20: OrY = 20 ' Origine X,Y du plateau de jeu par rapport à l'origine 0,0 du formulaire
OrLg = TailleImage * EcranMaxiX ' Largeur du plateau.
OrHt = TailleImage * EcranMaxiY ' Hauteur du plateau.

' Modifie la hauteur du formulaire (InsideHeight est en lecture seule):
MyForm.Height = Img.PixelsEnPointsY(OrY + OrHt + OrY) + MyForm.Height - MyForm.InsideHeight

' Place le formulaire à 100 pixels du haut de l'écran:
MyForm.Top = 100

' Dessine le formulaire avec ses nouvelles dimensions, et une marge horizontale de 100:
Call Img.InitialiseModeGraphique(MyForm, 100)

' Mémorise les coordonnées graphiques des cellules du masque du plateau:
For X = EcranMiniX To EcranMaxiX
For Y = EcranMiniY To EcranMaxiY + AvanceParPomme
Masque(X, Y).X = (X - 1) * TailleImage
Masque(X, Y).Y = (Y - 1) * TailleImage
Next Y
Next X

' Charge les Sprite:
ChargeLesSprite

' Place le tableau des scores:
InitialiseTableauDesScores

' Initialisation des variables:
InitialiseVariables

' Création d'une image 98 de la taille du plateau:
Call Img.CopieImage(0, 98, 0, 0, OrX, OrY, _
OrX + EcranMaxiX * TailleImage, EcranMaxiY * TailleImage)

' Charge le niveau 1 et l'affiche à l'écran:
InitialiseNiveau
DessineNiveau

' Gestion de l'erreur si le formulaire est fermé:
On Error GoTo Fin

' Boucle tant que le joueur a une vie:
Do
' Bloque la souris dans l'espace du formulaire (erreur si formulaire fermé):
Call Img.BloqueCurseurDansForm(MyForm)

' Copie de la mémorisation de l'écran (mémoire 0) en mémoire de travail (mémoire -1):
Call Img.CopieImage(0, -1)

' Déplace le corps du serpent dans le masque (mais pas la tête):
Call DéplaceSerpent

' Déplace la tête du serpent dans le masque:
Serpent(0).X = Serpent(0).X + MvX
Serpent(0).Y = Serpent(0).Y + MvY

' Mise à jour du plateau du jeu où est le serpent, en mémoire -1:
Call MiseAJourPlateau

' Mise à jour des informations sur le score (durée de jeu et bonus restant):
Call AfficheScore(5)
Call AfficheScore(6)

' Animations des avatars:
For i = 0 To 12
Call Img.AnimeAvatar(i, -1)
Next i

' Contrôle (d'après le masque) si le serpent rencontre ou non un obstacle:
If Masque(Serpent(0).X, Serpent(0).Y).V <> " " Or Serpent(0).Y = EcranMaxiY Then
Call Obstacle ' Analyse le type d'obstacle
End If

' La tête du serpent (puis le corps qui ira à cette position) est un obstacle:
Masque(Serpent(0).X, Serpent(0).Y).V = Serpent(0).Forme

' Affiche à l'écran la mémoire de travail -1:
Call Img.AfficheImage(-1)

' Lecture du clavier:
Direction = 0
TTimer = Timer
Do
If Img.ToucheEnfoncée(vbKeyLeft) = True Then Direction = -1: Exit Do ' Gauche.
If Img.ToucheEnfoncée(vbKeyRight) = True Then Direction = 1: Exit Do ' Droite.
If Img.ToucheEnfoncée(vbKeyControl) = True Then Sleep 50: Exit Do    ' Accélération.
Loop While TTimer + 0.1 > Timer

' Fait l'éventuel changement de direction:
Call ChangeDirection(Direction)

' Fin du jeu si la touche Fin est enfoncée:
If Img.ToucheEnfoncée(vbKeyEnd) = True Then Vie = -9

Loop While Vie > 0

Fin:

' Vérifie si le joueur rentre dans le TOP 10 puis affiche le TOP 10:
Call ImgTop10(Points)

' Active le formulaire du jeu:
MyForm.Show

' Fermeture du mode graphique:
Call Img.FermeModeGraphique

End Sub

'---------------------------------------------------------------------------------------
Private Sub DéplaceSerpent()
'---------------------------------------------------------------------------------------
' Efface le bout de la queue:
Masque(Serpent(TailleSerpent).X, Serpent(TailleSerpent).Y).V = " "

' Affiche le corps (élément précédent):
For i = TailleSerpent To 1 Step -1
Serpent(i).Y = Serpent(i - 1).Y
Serpent(i).X = Serpent(i - 1).X
Serpent(i).Forme = Serpent(i - 1).Forme
Masque(Serpent(i).X, Serpent(i).Y).V = Serpent(i).Forme
Next i

End Sub

'---------------------------------------------------------------------------------------
Private Sub MiseAJourPlateau()
'---------------------------------------------------------------------------------------
Dim X As Long, Y As Long, ik As Integer
Dim XX As Long, YY As Long

' Fait une copie du plateau vierge (mémoire 99) en mémoire 100 de travail:
Call Img.CopieImage(99, 100)

' Affiche le corps suivant la direction (en tenant compte des virages):
For ik = 1 To TailleSerpent - 1

X = Serpent(ik).X
Y = Serpent(ik).Y
XX = Masque(X, Y).X
YY = Masque(X, Y).Y

If Serpent(ik).Forme = "H" Then ImageNum = CorpsH ' 18
If Serpent(ik).Forme = "B" Then ImageNum = CorpsB ' 18
If Serpent(ik).Forme = "G" Then ImageNum = CorpsG ' 12
If Serpent(ik).Forme = "D" Then ImageNum = CorpsD ' 12

If Serpent(ik).Forme <> Serpent(ik + 1).Forme Then
If Serpent(ik).Forme = "H" And Serpent(ik + 1).Forme = "G" Then ImageNum = VirHD ' 16
If Serpent(ik).Forme = "H" And Serpent(ik + 1).Forme = "D" Then ImageNum = VirHG ' 23
If Serpent(ik).Forme = "B" And Serpent(ik + 1).Forme = "G" Then ImageNum = VirBD ' 11
If Serpent(ik).Forme = "B" And Serpent(ik + 1).Forme = "D" Then ImageNum = VirBG ' 13
If Serpent(ik).Forme = "G" And Serpent(ik + 1).Forme = "H" Then ImageNum = VirBG ' 13
If Serpent(ik).Forme = "G" And Serpent(ik + 1).Forme = "B" Then ImageNum = VirHG ' 23
If Serpent(ik).Forme = "D" And Serpent(ik + 1).Forme = "H" Then ImageNum = VirBD ' 11
If Serpent(ik).Forme = "D" And Serpent(ik + 1).Forme = "B" Then ImageNum = VirHD ' 16
End If

Call Img.CopieImage(ImageNum, 100, XX, YY)

Next ik

' Affiche la queue suivant la direction:
If TailleSerpent > 1 Then
X = Serpent(TailleSerpent).X
Y = Serpent(TailleSerpent).Y
XX = Masque(X, Y).X
YY = Masque(X, Y).Y
If Serpent(TailleSerpent).Forme = "H" Then ImageNum = QueueH ' 24
If Serpent(TailleSerpent).Forme = "B" Then ImageNum = QueueB ' 30
If Serpent(TailleSerpent).Forme = "G" Then ImageNum = QueueG ' 29
If Serpent(TailleSerpent).Forme = "D" Then ImageNum = QueueD ' 25
Call Img.CopieImage(ImageNum, 100, XX, YY)
End If

' Affiche la tête suivant la direction:
If TailleSerpent > 0 Then
X = Serpent(0).X
Y = Serpent(0).Y
XX = Masque(X, Y).X
YY = Masque(X, Y).Y
If Serpent(0).Forme = "H" Then ImageNum = TeteH ' 14
If Serpent(0).Forme = "B" Then ImageNum = TeteB ' 20
If Serpent(0).Forme = "G" Then ImageNum = TeteG ' 19
If Serpent(0).Forme = "D" Then ImageNum = TeteD ' 15
Call Img.CopieImage(ImageNum, 100, XX, YY)
End If

' Copie la mémoire de travail du plateau 100 en mémoire de travail écran -1:
Call Img.CopieImage(100, -1, OrX, OrY)

End Sub

'---------------------------------------------------------------------------------------
Private Sub ChangeDirection(Direction As Integer)
'---------------------------------------------------------------------------------------
' Si gauche:
If Direction = -1 Then
' Si haut alors gauche:
If MvX = 0 And MvY = -1 Then MvX = -1: MvY = 0: Serpent(0).Forme = "G": GoTo Fin
' Si gauche alors bas:
If MvX = -1 And MvY = 0 Then MvX = 0: MvY = 1: Serpent(0).Forme = "B": GoTo Fin
' si bas alors droite:
If MvX = 0 And MvY = 1 Then MvX = 1: MvY = 0: Serpent(0).Forme = "D": GoTo Fin
' Si droite alors haut:
If MvX = 1 And MvY = 0 Then MvX = 0: MvY = -1: Serpent(0).Forme = "H": GoTo Fin
End If

' Si droite:
If Direction = 1 Then
' Si haut alors droite:
If MvX = 0 And MvY = -1 Then MvX = 1: MvY = 0: Serpent(0).Forme = "D": GoTo Fin
' Si droite alors bas:
If MvX = 1 And MvY = 0 Then MvX = 0: MvY = 1: Serpent(0).Forme = "B": GoTo Fin
' si bas alors gauche:
If MvX = 0 And MvY = 1 Then MvX = -1: MvY = 0: Serpent(0).Forme = "G": GoTo Fin
' Si gauche alors haut:
If MvX = -1 And MvY = 0 Then MvX = 0: MvY = -1: Serpent(0).Forme = "H": GoTo Fin
End If

Exit Sub

Fin:
' Fait une pause pour éviter une nouvelle lecture trop rapide du clavier:
Sleep 150

End Sub

'---------------------------------------------------------------------------------------
Private Sub Obstacle()
'---------------------------------------------------------------------------------------
Dim ik As Integer

' Si l'obstacle rencontré est une pomme:
' ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If Masque(Serpent(0).X, Serpent(0).Y).V = FormePomme Then

Call PlaySound(ThisWorkbook.Path & "\laser-01.wav", pcsASYNC)

' Fait grandir le serpent (dans le masque donc sans rien afficher):
For i = TailleSerpent + 1 To TailleSerpent + AvanceParPomme
Serpent(i).X = Serpent(TailleSerpent).X
Serpent(i).Y = Serpent(TailleSerpent).Y
Serpent(i).Forme = Serpent(i - 1).Forme
Next i
TailleSerpent = i - 1

' Donne des points et éventuellement un bonus:
Points = Points + PointParPomme
Avatar(8).Actif = True
Avatar(8).Animation = 2 + PointParPomme + (Bonus - Timer)
NbPomme = NbPomme + 1
If Bonus > 0 Then Points = Points + ((Bonus - Timer) * 1)
Call AfficheScore(2)
Call AfficheScore(4)
Call AfficheScore(6)

' Efface la pomme dans le masque et désactive l'avatar:
Masque(Serpent(0).X, Serpent(0).Y).V = " "
Avatar(10).Actif = False

' Si le niveau est terminé, alors ouvre la porte:
If NbPomme = NbPommeParNiveau Then
Call OuvrePorte
Else ' Sinon  pose une nouvelle pomme:
Call PosePomme
End If

Exit Sub
End If

' Si le serpent passe par la porte:
' ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If Masque(Serpent(0).X, Serpent(0).Y).V = FormePorte Then

' Change l'image de la porte par l'image de la tête puis du corps du serpent:
Avatar(12).ListeImages = Array(TeteH, CorpsH)

' Efface le serpent en attribuant des points:
Call FinDuSerpent(True)

' Restaure l'image de la porte du haut:
Avatar(12).ListeImages = Array(ImgPorteHaut)

' Bonus pour les vies restantes:
Score(7).Lib = "Bonus Vie"
Avatar(8).SpriteActif = 0
Avatar(8).Actif = True
For i = 1 To Vie * 10
Points = Points + 1
Call AfficheScore(2)
Call AfficheScore(7)
Call PlaySound(ThisWorkbook.Path & "mouse-doubleclick-01.wav", pcsASYNC)

For ik = 0 To 12
Call Img.AnimeAvatar(ik, -1)
Next ik

Call Img.AfficheImage(-1)
Sleep 50
Next i

Score(7).Lib = ""

' Animation nouveau niveau: Votre code Ici...

' Initialise puis dessine le niveau suivant:
Niveau = Niveau + 1
Vie = Vie + 1
Call InitialiseNiveau
Call DessineNiveau

Exit Sub
End If

' Sinon, le serpent a rencontré un obstacle mortel (mur ou serpent):
' ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Avatar(0).X = OrX + Masque(Serpent(0).X, Serpent(0).Y).X
Avatar(0).Y = OrY + Masque(Serpent(0).X, Serpent(0).Y).Y
Avatar(0).SpriteActif = 0
Avatar(0).Animation = EnBoucle
Avatar(0).Actif = True

Call PlaySound(ThisWorkbook.Path & "\fire-04-loop.wav", pcsLOOP + pcsASYNC)
Vie = Vie - 1                 ' Fait perdre une vie.
Call FinDuSerpent(False)      ' Efface le serpent sans attribuer de point.
Call PlaySound(" ", pcsASYNC) ' Fin du son.
Avatar(0).Actif = False       ' Fin de l'explosion.

Call InitialiseNiveau       ' Efface le plateau.
Call DessineNiveau          ' Redessine le niveau.

End Sub

'---------------------------------------------------------------------------------------
Private Sub OuvrePorte()
'---------------------------------------------------------------------------------------
Bonus = 0

' Met à jour le masque pour indiquer la position de la porte:
Masque((EcranMaxiX + EcranMiniX) / 2, EcranMiniY).V = FormePorte

' Active l'avatar (de la porte) pour générer une explosion, puis affiche la porte (un vide):
Avatar(12).Actif = True
Avatar(12).SpriteActif = 0
Avatar(12).ListeImages = Array(81, 82, 83, 84, 85, 86, ImgPorteHaut)

Call PlaySound(ThisWorkbook.Path & "\fire-04-loop.wav", pcsASYNC)

End Sub

'---------------------------------------------------------------------------------------
Private Sub FinDuSerpent(AvecBonus As Boolean)
'---------------------------------------------------------------------------------------
Bonus = 0

' S'il y a un bonus, initialise l'avatar de la pièce:
If AvecBonus = True Then
Avatar(8).Actif = True
Avatar(8).Animation = EnBoucle
Avatar(8).SpriteActif = 0
End If

' Efface un à un les éléments du corps du serpent:
Do
' Efface dans le masque:
Masque(Serpent(TailleSerpent).X, Serpent(TailleSerpent).Y).V = " "
TailleSerpent = TailleSerpent - 1

' Mise à jour du plateau du jeu où est le serpent, en mémoire -1:
Call MiseAJourPlateau

' Animations des avatars:
For i = 0 To 12
Call Img.AnimeAvatar(i, -1)
Next i

' Affiche la mémoire image de travail et fait une pause:
Call Img.AfficheImage(-1)
Sleep 50

' Attribue un point par élément si c'est un changement de niveau:
If AvecBonus = True Then
Points = Points + 1
Call AfficheScore(2)
Call PlaySound(ThisWorkbook.Path & "mouse-doubleclick-01.wav", pcsASYNC)
End If

Call AfficheScore(6)

Loop While TailleSerpent > 0

' Désactive l'avatar de la pièce:
Avatar(8).Actif = False
End Sub

'---------------------------------------------------------------------------------------
Public Sub ChargeLesSprite()
'---------------------------------------------------------------------------------------
Dim X As Long, Y As Long

' Charge le Sprite du serpent. Sans Transparence:
Call Img.ChargeImageBMP(ThisWorkbook.Path & "\snake-graphics.bmp", 10, -1)

' Boucle pour récupérer les fractions d'images et en modifier la taille:
ImageNum = 11
For Y = 0 To 192 Step 64
For X = 0 To 256 Step 64
Call Img.CopieImage(10, ImageNum, , , X, Y, 64, 64)
Call Img.ModifieTailleImage(ImageNum, TailleImage, TailleImage)
Image(ImageNum).Transparence = GetPixel(Image(10).Hdc, 0, 0)
ImageNum = ImageNum + 1
Next X
Next Y

' Mémorise les images pour chacun des sens de déplacement:
TeteH = 14: TeteB = 20: TeteG = 19: TeteD = 15
QueueH = 24: QueueB = 30: QueueG = 29: QueueD = 25
CorpsH = 18: CorpsB = 18: CorpsG = 12: CorpsD = 12
VirBD = 11: VirBG = 13: VirHD = 16: VirHG = 23
ImgPomme = 26
ImgPorteBas = 27
ImgPorteHaut = 28
ImgVide = 22

Image(ImgPorteBas).Transparence = 0
Image(ImgPorteHaut).Transparence = 0

' Charge l'image du mur et modifie la taille de l'image:
ImgMur = 31
Call Img.ChargeImageBMP(ThisWorkbook.Path & "\Mur.bmp", ImgMur, -1)
Call Img.ModifieTailleImage(ImgMur, TailleImage, TailleImage, ImgMur)

' Charge le Sprite des pièces:
Call Img.ChargeImageBMP(ThisWorkbook.Path & "\Sprite_Pièces.bmp", 10, -9)

Call Img.CopieImage(10, 45, , , 0, 0, 122, 122)
Call Img.CopieImage(10, 46, , , 132, 0, 122, 122)
Call Img.CopieImage(10, 47, , , 279, 0, 122, 122)
Call Img.CopieImage(10, 41, , , 0, 140, 122, 122)
Call Img.CopieImage(10, 42, , , 132, 140, 122, 122)
Call Img.CopieImage(10, 43, , , 261, 140, 122, 122)
Call Img.CopieImage(10, 44, , , 393, 140, 122, 122)

' Modifie les tailles des images des pièces:
For i = 41 To 47
Call Img.ModifieTailleImage(i, ScoreTaille, ScoreTaille)
Image(i).Transparence = 0
Next i

' Charge le Sprite du Trooper:
Call Img.ChargeImageBMP(ThisWorkbook.Path & "\Sprite_Stormtrooper.bmp", 10, -9)
ImageNum = 51
For X = 0 To 150 Step 30
Call Img.CopieImage(10, ImageNum, , , X, 0, 30, 68)
ImageNum = ImageNum + 1
Next X
Call Img.CopieImage(10, 57, , , 0, 204, 34, 68)
Call Img.CopieImage(10, 58, , , 34, 204, 34, 68)
Call Img.CopieImage(10, 59, , , 70, 204, 37, 68)
Call Img.CopieImage(10, 60, , , 111, 204, 41, 68)
Call Img.CopieImage(10, 61, , , 154, 204, 44, 68)
Call Img.CopieImage(10, 62, , , 200, 204, 44, 68)

' Modifie les tailles des images du trooper:
For i = 51 To 62
Call Img.ModifieTailleImage(i, ScoreTaille, ScoreTaille)
Image(i).Transparence = 0
Next i

' Charge le Sprite de l'explosion (7 images de 40x40) en mémoire 10:
Call Img.ChargeImageBMP(ActiveWorkbook.Path & "\Sprite_explosions.bmp", 10, -9, 0, 0)

' Boucle pour récupérer les fractions d'images et en modifier la taille:
ImageNum = 81
For X = 0 To 6
Call Img.CopieImage(10, ImageNum, 0, 0, X * 40, 0, 40, 40)
Call Img.ModifieTailleImage(ImageNum, TailleImage, TailleImage)
Image(ImageNum).Transparence = GetPixel(Image(10).Hdc, 0, 0)
ImageNum = ImageNum + 1
Next X

End Sub

'---------------------------------------------------------------------------------------
Public Sub InitialiseTableauDesScores()
'---------------------------------------------------------------------------------------
Dim X As Long, Y As Long
X = OrX + EcranMaxiX * TailleImage + TailleImage + 170
Y = OrY

' Définition de la police utilisée pour l'affichage des textes:
Call Img.TexteConfig(1, ScoreCouleur, ScoreFond, ScorePolice, ScoreTaille, 900)

' Charge les informations sur le score:
Score(1).Lib = "Niveau : ": Score(1).X = X: Score(1).Y = Y + 0 * ScoreTaille
Score(2).Lib = "Points : ": Score(2).X = X: Score(2).Y = Y + 1 * ScoreTaille
Score(3).Lib = "Vies : ": Score(3).X = X: Score(3).Y = Y + 2 * ScoreTaille
Score(4).Lib = "Pommes : ": Score(4).X = X: Score(4).Y = Y + 3 * ScoreTaille
Score(5).Lib = "Durée : ": Score(5).X = X: Score(5).Y = Y + 4 * ScoreTaille
Score(6).Lib = "Bonus : ": Score(6).X = X: Score(6).Y = Y + 5 * ScoreTaille
Score(7).Lib = "": Score(7).X = X: Score(7).Y = Y + 7 * ScoreTaille
For i = 1 To 7
Call Img.Texte(Image(0).Hdc, Score(i).X, Score(i).Y, Score(i).Lib, 1, AlignH_Droite)
Avatar(i).X = Score(i).X
Avatar(i).Y = Score(i).Y
Avatar(i).Actif = True
Avatar(i).Visible = True
Avatar(i).Animation = UneFoisEtAffiche
Avatar(i).ListeImages = Array(100 + i)
Call Img.CopieImage(0, 100 + i, 0, 0, Avatar(i).X, Avatar(i).Y, 5 * ScoreTaille, ScoreTaille)
Next i

End Sub

'--------------------------------------------------------------------------------------------
Public Sub AfficheScore(NumNiveau As Integer)
'--------------------------------------------------------------------------------------------
Dim Coul As Long

If NumNiveau = 0 Or NumNiveau = 1 Then Call PrépareImageAvatar(1): _
Call Img.Texte(Image(101).Hdc, 0, 0, Niveau, 1)

If NumNiveau = 0 Or NumNiveau = 2 Then Call PrépareImageAvatar(2): _
Call Img.Texte(Image(102).Hdc, 0, 0, Format(Points, "#,0"), 1)

If NumNiveau = 0 Or NumNiveau = 3 Then Call PrépareImageAvatar(3): _
Call Img.Texte(Image(103).Hdc, 0, 0, Vie, 1)

If NumNiveau = 0 Or NumNiveau = 4 Then Call PrépareImageAvatar(4): _
Call Img.Texte(Image(104).Hdc, 0, 0, NbPomme, 1)

If NumNiveau = 0 Or NumNiveau = 5 Then Call PrépareImageAvatar(5): _
Call Img.Texte(Image(105).Hdc, 0, 0, Format((Timer - Chrono) / 100000, "nn:ss"), 1)

If NumNiveau = 0 Or NumNiveau = 6 Then
Call PrépareImageAvatar(6)
If (Bonus - Timer) > 4 Then Coul = -9 Else Coul = 12
If (Bonus - Timer) > 0 Then
Call Img.Texte(Image(106).Hdc, 0, 0, Format((Bonus - Timer) / 100000, "ss"), 1, , Coul)
If Avatar(9).Animation <> EnBoucle Then Avatar(9).SpriteActif = 0 ' 1re initialisation
Avatar(9).Actif = True
Avatar(9).ListeImages = Array("51", "52", "53", "54", "55", "56")
Avatar(9).Animation = EnBoucle
Else
If Avatar(9).Animation = EnBoucle Then ' Si 1re initialisation.
Avatar(9).SpriteActif = 0
Avatar(9).ListeImages = Array("57", "58", "59", "60", "61", "62")
Avatar(9).Animation = UneFoisEtQuitte
End If
End If
End If

If NumNiveau = 0 Or NumNiveau = 7 Then Call PrépareImageAvatar(7): _
Call Img.Texte(Image(107).Hdc, 0, 0, Score(7).Lib, 1, , 11, -9)

End Sub

'--------------------------------------------------------------------------------------------
Private Sub PrépareImageAvatar(NumAvatar As Integer)
'--------------------------------------------------------------------------------------------
' Copie le fond d'origine (du formulaire) dans l'image où sera affiché le texte:
Call Img.CopieImage(0, 100 + NumAvatar, 0, 0, Avatar(NumAvatar).X, Avatar(NumAvatar).Y, _
Image(100 + NumAvatar).Largeur, Image(100 + NumAvatar).Hauteur)
End Sub
'--------------------------------------------------------------------------------------------
'--------------------------------------------------------------------------------------------

'---------------------------------------------------------------------------------------
Public Sub ImgTop10(Points As Long)
'---------------------------------------------------------------------------------------
' Si le score est dans le TOP 10 alors alimente le TOP 10:
Call ImgSaisieTop10(Points)

' Configuration de la taille d'affichage du TOP 10:
Dim X As Long, Y As Long, Lg As Long, Ht As Long, CoulfFond As Long

Lg = 500
Ht = 200
X = (ImageMaxiX / 2) - (Lg / 2)
Y = (ImageMaxiY / 2) - (Ht / 2)
CoulfFond = 0

' Définition des polices à utiliser:
Call Img.TexteConfig(5, 8, CoulfFond, "Arial", 30, 100, True)
Call Img.TexteConfig(6, 12, CoulfFond, "Arial", 30, 500, True)
Call Img.TexteConfig(7, 13, CoulfFond, "Arial", 30, 500, True)
Call Img.TexteConfig(8, 15, CoulfFond, "Arial", 30, 500, True)
Call Img.TexteConfig(9, 10, CoulfFond, "Algerian", 40)

' Chargement des scores depuis la feuille EXCEL:
Dim i As Integer, Top10(0 To 9) As String
For i = 2 To 11
Top10(i - 2) = Format(i - 1, "00") & " : " _
& Format(Sheets("TOP_10").Cells(i, 1), "000000") _
& " : " & Sheets("TOP_10").Cells(i, 2)
Next i

' Lance la procédure de défilement du texte dans un formulaire (User_ImgScores):
Call Img.TexteDéfilement(X, Y, Lg, Ht, _
Titre:="Les 10 meilleurs scores", _
PoliceTitre:=9, _
CouleurFond:=CoulfFond, _
ListeTextes:=Top10, _
ListePolices:=Array("6", "7", "8", "5", "5", "5", "5", "5", "5", "5"), _
DuréePause:=60, _
ObjFormulaire:=User_ImgScores)

End Sub

'---------------------------------------------------------------------------------------
Private Sub ImgSaisieTop10(Points As Long)
'---------------------------------------------------------------------------------------
' Si le score n'est pas dans le TOP 10 alors quitte:
If Points <= Sheets("TOP_10").Cells(12, 1) Then Exit Sub

' Sinon affiche une boîte de dialogue pour saisir le nom du joueur:
Dim Nom As String
Nom = InputBox("BRAVO - VOUS RENTREZ DANS LE TOP 10" _
& Chr(10) & Chr(13) & Chr(13) & Points & " points." _
& Chr(10) & Chr(13) & Chr(13) & "Votre nom : ", "TOP 10")
If Nom = "" Then Nom = "(Vide)"

' Enregistre le score:
Sheets("TOP_10").Cells(12, 1) = Points
Sheets("TOP_10").Cells(12, 2) = Nom
Sheets("TOP_10").Cells(12, 3) = Date

' Trie le tableau des scores (méthode compatible EXCEL 2000):
Nom = ActiveSheet.Name
Sheets("TOP_10").Visible = True
Sheets("TOP_10").Select
Sheets("TOP_10").Range("A1").Select
Selection.Sort Key1:=Range("A2"), Order1:=xlDescending, Header:=xlGuess, _
OrderCustom:=1, MatchCase:=False, Orientation:=xlTopToBottom
Sheets(Nom).Select

End Sub

J'ai créé la procédure TexteDéfilement, que vous trouverez dans le module Img, pour répondre au besoin du jeu, où il fallait afficher les 10 meilleurs scores, voire plus, dans un espace limité en lignes.

La seule façon est de faire défiler les lignes à la manière d'un rouleau qui tourne sur lui-même.

L'affichage peut être fait dans un formulaire, comme ici :
Mais cela nécessite que le formulaire qui lance le jeu soit en mode non modal, pour accepter la présence d'un deuxième formulaire.

Ou bien dans une image, comme ici :

Les arguments de la procédure sont :

- X, Y : coordonnées d'affichage du formulaire ou de l'image ;

- Largeur, Hauteur : dimension de du formulaire ou de l'image ;

- Titre : titre ;

- PoliceTitre : numéro du modèle de police, créé par TexteConfig, à utiliser pour le titre ;

- CouleurFond : couleur de fond de l'affichage ;

- ListeTextes : tableau des lignes de texte à afficher ;

- ListePolices : tableau des numéros de modèle de polices à utiliser pour ces lignes ;

- ObjFormulaire : argument optionnel, est le nom du formulaire à utiliser pour l'affichage ;

- DuréePause : argument optionnel, est la durée, en millisecondes, de la pause entre deux défilements.

Vous avez sur la page précédente un exemple d'appel à cette procédure.

'---------------------------------------------------------------------------------------
Public Sub TexteDéfilement(ByVal X As Long, ByVal Y As Long, _
Largeur As Long, Hauteur As Long, _
Titre As Variant, PoliceTitre As Byte, ByVal CouleurFond As Long, _
ListeTextes As Variant, ListePolices As Variant, _
Optional ByVal ObjFormulaire As Object, Optional DuréePause As Long = 60)
'---------------------------------------------------------------------------------------
' Attention : Utilisation des mémoires 1, 2 et 3
'---------------------------------------------------------------------------------------
Dim YY As Long, i As Integer, Taille As Long, MémoSourisEspace As RECT

' Mémorise l'espace souris actif, et le contexte actif:
Call GetClipCursor(MémoSourisEspace)
Anc_Hdc = Hdc

' Si utilisation d'un formulaire, trouve son contexte et lui donne les dimensions désirées:

If Not ObjFormulaire Is Nothing Then
Hdc = GetDC(FindWindowA("ThunderDFrame", ObjFormulaire.Caption))
Call ObjFormulaire.Show(False)
ObjFormulaire.Left = X: X = 0
ObjFormulaire.Top = Y: Y = 0
ObjFormulaire.Height = Img.PixelsEnPointsY(Hauteur)
ObjFormulaire.Width = Img.PixelsEnPointsX(Largeur)
ObjFormulaire.Repaint
Call Opacité(0)
End If

' Efface les mémoires numéro 1, 2 et 3 qui vont être utilisées par la procédure:
Call EffaceImage(1): Call EffaceImage(2): Call EffaceImage(3)

' Création de 3 grandes mémoires pour y copier plusieurs lignes de texte:
Call CopieImage(1, 1, , , , , Largeur, 1000): Image(1).Transparence = -1
Call CopieImage(2, 2, , , , , Largeur, 1000): Image(2).Transparence = -1
Call CopieImage(3, 3, , , , , Largeur, 1000): Image(3).Transparence = -1

' Pose le titre dans un rectangle plein de la couleur de fond demandée, en mémoire 2:
Call RectanglePlein(Image(2).Hdc, 0, 0, Largeur, Police(PoliceTitre).Hauteur, CouleurFond)
Call Texte(Image(2).Hdc, (Largeur / 2), 0, Titre, PoliceTitre, AlignH_Centre)

' Trace un trait sous le titre, toujours en mémoire 2:
YY = Police(PoliceTitre).Hauteur
Call Trait(Image(2).Hdc, 0, YY, Largeur, YY, Police(PoliceTitre).CoulTexte, 1, Pensolide)

' Copie de la mémoire 2 (qui contient le titre souligné) en mémoire de travail 3:
Call CopieImage(2, 3)

' Dessine un rectangle plein en mémoire 1 pour faire un fond:
Call RectanglePlein(Image(1).Hdc, 0, 0, Largeur, 1000, CouleurFond)

' Pose en mémoire 1 les lignes du texte avec leur numéro de police:
For i = LBound(ListeTextes) To UBound(ListeTextes)
Call Texte(Image(1).Hdc, 3, Taille, ListeTextes(i), Int(ListePolices(i)))
Taille = Taille + Police(Int(ListePolices(i))).Hauteur
Next i

' Fait un double de l'image 1 à sa suite:
Call CopieImage(1, 1, 0, Taille, 0, 0, 0, Taille)

' Copie en mémoire 3 sous le titre, les lignes de texte sur la hauteur maximale:
Call CopieImage(1, 3, 0, YY + 1, 0, 0, Largeur, Hauteur)
Call RectangleVide(Image(3).Hdc, 0, 0, Largeur, Hauteur, Police(PoliceTitre).CoulTexte, 2)

On Error GoTo Fin ' Gestion de l'erreur si le formulaire est fermé.
' Boucle tant qu'une touche n'est pas pressée:
Do
For YY = 1 To Taille Step 2 ' Boucle pour décaler le texte en mémoire 1 à afficher.

' Copie l'image 2 (qui contient le titre souligné) dans l'image 3:
Call CopieImage(2, 3)

' Copie l'image 1 (qui contient le texte) dans l'image 3, en décalant l'affichage:
Call CopieImage(1, 3, 0, Police(PoliceTitre).Hauteur + 1, 0, YY, Largeur, Hauteur)

' Encadre l'affichage avec la couleur du titre si l'on n'est pas dans un formulaire:
If ObjFormulaire Is Nothing Then _
Call RectangleVide(Image(3).Hdc, 0, 0, Largeur, Hauteur, Police(PoliceTitre).CoulTexte, 2)

' Affiche en X,Y cette mémoire 3 (titre souligné + lignes de texte + encradrement):
Call BitBlt(Hdc, X, Y, Largeur, Hauteur, Image(3).Hdc, 0, 0, SRCCOPY)
Call Sleep(DuréePause) ' et fait une pause.
If i > 0 Then
i = 0  ' Au premier passage, fait un fondu en ouverture ou une attente d'une seconde
If Not ObjFormulaire Is Nothing Then Call FonduOuverture(50) Else Call Sleep(1000)
End If

' Quitte si une touche est enfoncée:
If ToucheEnfoncée(0) = True Then Exit Do

' Bloque la souris dans l'espace du formulaire utilisé (erreur si formulaire fermé):
If Not ObjFormulaire Is Nothing Then Call BloqueCurseurDansForm(ObjFormulaire)

Next YY
Loop

' Ferme le formulaire si utilisation d'un formulaire:
If Not ObjFormulaire Is Nothing Then Call FonduFermeture(30): ObjFormulaire.Hide

Fin:
' Efface les mémoires numéro 1, 2 et 3:
Call EffaceImage(1): Call EffaceImage(2): Call EffaceImage(3)

' Restaure l'ancien espace souris, et l'ancien contexte actif:
Call ClipCursor(MémoSourisEspace)
Hdc = Anc_Hdc

End Sub

XVI - Compatibilité du module Img avec ACCESS

Le module Img permet de gérer vos animations soit directement à l'écran, pour simuler une animation sur une feuille de calcul, avec Hdc = GetWindowDC(0), soit dans un formulaire, après avoir retrouvé son contexte avec Hdc = GetDC(FindWindowA("ThunderDFrame", MonFormulaire.Caption)).
Suivant le but recherché, vous opterez pour l'une au l'autre méthode.

Le principe est comparable dans ACCESS, mais avec quelques particularités…
À commencer par le contexte d'un formulaire qui peut être retourné d'après les propriétés du formulaire, ce qui est plus simple : Hdc = GetWindowDC(Form_MonFormulaire.hwnd).

Les choses se compliquent concernant l'origine 0,0 du contexte du formulaire : alors que dans EXCEL l'origine est le point haut gauche à l'intérieur du formulaire, dans ACCESS l'origine est le point haut gauche du formulaire, incluant donc le bandeau supérieur contenant le libellé du formulaire.

Autre spécificité qui laisse penser que ce n'est pas la même équipe qui a développé ces deux applications, dans ACCESS les coordonnées d'un formulaire sont exprimées en Twips, qui valent 1/20e de point. Pour obtenir une équivalence en pixels avec nos procédures PointsEnPixels, il faudra diviser ces valeurs par 20, et les multiplier par 20 avec nos procédures PixelsEnPoints.

Gardez aussi à l'esprit que la configuration des options du formulaire est très importante et influencera l'affichage. Par exemple, si l'option « Fenêtre indépendante » est activée, alors le bandeau supérieur reste en premier plan et vos animations seront masquées par ce bandeau. Certaines options, telles que « Bouton de déplacement », « Afficher Sélecteur », empiètent sur l'intérieur du formulaire.
Optez pour le style de bordure « Double fixe » pour éviter que l'utilisateur modifie la taille du formulaire.

Voici un aperçu de ce que nous obtiendrons dans ACCESS :

Astuce à connaître pour ouvrir une application ACCESS à sa taille maximale, utilisez l'instruction : DoCmd.RunCommand acCmdAppMaximize

Les propriétés WindowLeft et WindowTop du formulaire indiquent ses coordonnées par rapport à l'intérieur de l'application. L'API GetWindowRect sera utilisée pour connaître les coordonnées, en pixels, du formulaire par rapport à l'origine 0,0 de l'écran :

Dim i As RECT
GetWindowRect Form_Menu.hwnd, i

La procédure InitialiseModeGraphique est adaptée pour pouvoir centrer un formulaire à l'écran suivant une marge horizontale ou verticale :

'---------------------------------------------------------------------------------------
Public Sub InitialiseModeGraphique(Optional ObjFormulaire As Object, _
Optional MargeH As Long = -1, Optional MargeV As Long = -1)
'---------------------------------------------------------------------------------------
Dim Irect As RECT
Call EffaceImage(-1): Call EffaceImage(0)

If ObjFormulaire Is Nothing Then ' Si pas de formulaire, donc utilise l'écran:
Hdc = GetWindowDC(0)             ' Mémorise le contexte de l'écran.
ImageMaxiX = GetSystemMetrics(0) ' Mémorise la taille horizontale de l'écran.
ImageMaxiY = GetSystemMetrics(1) ' Mémorise la taille verticale de l'écran.
Else
If MargeH > -1 Then ' S'il faut afficher le formulaire avec une marge horizontale:
ObjFormulaire.Move 0, ObjFormulaire.WindowTop, _
ObjFormulaire.WindowWidth, ObjFormulaire.WindowHeight
' Trouve la position X réelle du formulaire par rapport à l'écran:
Call GetWindowRect(ObjFormulaire.hWnd, Irect)
' Centre le formulaire à l'écran avec une marge horizontale:
ObjFormulaire.Move PixelsEnPointsX(MargeH - Irect.Left) * 20, ObjFormulaire.WindowTop, _
PixelsEnPointsX(GetSystemMetrics(0) - MargeH * 2) * 20, _
ObjFormulaire.WindowHeight
End If
If MargeV > -1 Then ' S'il faut afficher le formulaire avec une marge verticale:
ObjFormulaire.Move ObjFormulaire.WindowLeft, 0, _
ObjFormulaire.WindowWidth, ObjFormulaire.WindowHeight
' Trouve la position Y réelle du formulaire par rapport à l'écran:
Call GetWindowRect(ObjFormulaire.hWnd, Irect)
' Centre le formulaire avec une marge verticale:
ObjFormulaire.Move ObjFormulaire.WindowLeft, PixelsEnPointsY(MargeV - Irect.Top) * 20, _
ObjFormulaire.WindowWidth, PixelsEnPointsY(GetSystemMetrics(1) - MargeV * 2) * 20
End If
' Mémorise les dimensions maximales du formulaire (en pixels):
ImageMaxiX = PointsEnPixelsX(ObjFormulaire.WindowWidth) / 20 ' Taille horizontale.
ImageMaxiY = PointsEnPixelsY(ObjFormulaire.WindowHeight) / 20 ' Taille verticale.
ObjFormulaire.Repaint
' Mémorise le contexte du formulaire:
Hdc = GetWindowDC(ObjFormulaire.hWnd)
End If

' Création de l'image 0 pour mémoriser l'écran d'origine:
Call CopieImage(0, 0, , , , , ImageMaxiX, ImageMaxiY)
Image(0).Transparence = -1 ' Sans transparence.

' Mémorise l'écran en mémoire 0 puis fait une copie en mémoire -1:
Call BitBlt(Image(0).Hdc, 0, 0, ImageMaxiX, ImageMaxiY, Hdc, 0, 0, SRCCOPY)
Call CopieImage(0, -1)

' Mémorise l'espace de déplacement de la souris:
Call GetClipCursor(SourisEspace)
End Sub

Il faut aussi modifier les procédures utilisées par le module Img pour bloquer la souris dans le formulaire et inversement pour lire la position de la souris :

'---------------------------------------------------------------------------------------
Public Sub BloqueCurseurDansForm(ObjFormulaire As Object)
'---------------------------------------------------------------------------------------
Call GetWindowRect(ObjFormulaire.hwnd, SourisBloque)
Hdc = GetWindowDC(ObjFormulaire.hwnd)
End Sub

Dans ACCESS, le contexte Hdc change en cas de déplacement du formulaire, il faut donc le recalculer en permanence au cas où. Cela peut se faire ici, de façon transparente pour le programmeur.

'---------------------------------------------------------------------------------------
Public Sub LectureCurseurDansForm(Pt As POINT, ObjFormulaire As Object)
'---------------------------------------------------------------------------------------
Dim Irect As RECT
Call GetCursorPos(Pt)
Call GetWindowRect(ObjFormulaire.hwnd, Irect)
Pt.X = Pt.X - Irect.Left
Pt.Y = Pt.Y - Irect.Top
End Sub

Nous pouvons récupérer le code du tir au canard dans une application ACCESS. Attention, certaines instructions du VBA EXCEL ne sont pas compatibles avec ACCESS. Dans notre code, il faut remplacer l'instruction ActiveWorkbook.Path par CurrentProject.Path.

Dernier point, la gestion du clic de la souris. Qui une fois de plus pose problème, car le clic sur le formulaire donne la main au formulaire et n'est pas reconnu par notre code.

Pour contourner cette situation, j'ai ajouté la variable publique BoutonSouris, qui est alimentée à 1 lors d'un clic sur le formulaire, par l'événement « Sur clic » du détail :

'---------------------------------------------------------------------------------------
Private Sub Détail_Click()
BoutonSouris = 1 ' Indique que le bouton gauche de la souris est actionné
End Sub

La lecture du clic gauche de la souris, If Img.ToucheEnfoncée(vbKeyLButton) = True Then devient If BoutonSouris = 1 Then.

La variable BoutonSouris est remise à zéro si elle vaut un, pour éviter une lecture en boucle.

Enfin, dernière particularité ACCESS, le bouton de commande du formulaire qui lance le jeu ne peut pas être masqué car il est actif. Pour le rendre non visible, on peut toutefois mettre ses dimensions à zéro :

'---------------------------------------------------------------------------------------
Private Sub CommandButton1_Click()
'---------------------------------------------------------------------------------------
CommandButton1.Height = 0     ' Supprime la hauteur du bouton
CommandButton1.Width = 0      ' Supprime la largeur du bouton
Call ImgDansUnFormulaire(Me)  ' Lance l'animation dans le formulaire.
End Sub

Le reste du code est inchangé.

Le module Img développé pour EXCEL est donc, presque, compatible avec ACCESS. Mais il nous faut maintenir deux versions différentes, ce qui n'est pas pratique.

Heureusement, il existe la compilation conditionnelle.

XVII - La compilation conditionnelle

La compilation conditionnelle permet au VBA de compiler des blocs de code sélectivement. Dit autrement, il est possible de compiler ou non un bloc de code suivant une condition prédéfinie.

La compilation conditionnelle s'applique donc parfaitement à notre situation où un même module doit être utilisé dans EXCEL et dans ACCESS alors que certains blocs de codes sont incompatibles.

Concrètement, nous allons définir, en entête du module Img, une « constante de compilation conditionnelle ». Par exemple :

#Const MonAppli = "Excel" ' Mettre "Excel" pour EXCEL et "Access" pour ACCESS

Les constantes de compilation conditionnelle sont reconnaissables par le dièse qui les précède.

Le nom des variables est libre, mais préférez des noms évocateurs.

Les blocs de code devant être compilés suivant la valeur de MonAppli sont définis d'après les « consignes » de compilation : #If...Then ... #Else ... #ElseIf ... #End If.

Ici, la procédure BloqueCurseurDansForm du module Img utilise la constante de compilation conditionnelle MonAppli pour déterminer les blocs de code à compiler, et ainsi ignorer le code ne correspondant pas notre application hôte.

MonAppli sera déclarée avec la valeur Excel, quand le module Img sera utilisé dans EXCEL, et avec la valeur Access quand elle sera utilisée dans ACCESS :

'---------------------------------------------------------------------------------------
Public Sub BloqueCurseurDansForm(ObjFormulaire As Object)
'---------------------------------------------------------------------------------------
#If MonAppli = "Excel" Then
SourisBloque.Top = PointsEnPixelsX(ObjFormulaire.Top)
SourisBloque.Left = PointsEnPixelsY(ObjFormulaire.Left)
SourisBloque.Bottom = PointsEnPixelsY(ObjFormulaire.Top + ObjFormulaire.Height)
SourisBloque.Right = PointsEnPixelsX(ObjFormulaire.Left + ObjFormulaire.Width)
#End If

#If MonAppli = "Access" Then
Call GetWindowRect(ObjFormulaire.hWnd, SourisBloque)
Hdc = GetWindowDC(ObjFormulaire.hWnd)
#End If

Call ClipCursor(SourisBloque)
End Sub

Il devient aisé maintenant de développer un module Img entièrement compatible EXCEL - ACCESS pour la programmation en mode graphique.

XVIII - Un outil pour définir la couleur de transparence des images

Nous l'avons vu avec Snake, un jeu en mode graphique nécessite de nombreuses images pour les animations. Et pour un meilleur rendu, ces images sont affichées avec un fond transparent. Or les images que l'on déniche sur le web pour les réutiliser dans nos jeux sont souvent composées de dégradés de centaines de couleurs et non pas d'une couleur de fond unique. Pour définir une couleur de transparence, il faut alors recourir à des logiciels de retouche d'image, qui ne sont pas toujours adaptés à notre besoin. Par exemple, si l'outil « Remplissage » de Paint permet de colorier rapidement une portion de l'image, cela n'a d'effet que sur les pixels contigus de même couleur. Impossible donc de se servir de cette fonction sur un fond en dégradé. Sauf à y passer beaucoup de temps, où à convertir l'image en 256 couleurs, ce qui peut nuire au rendu final.

La méduse représentée ici est placée devant un fond dégradé noir et bleu composé de plusieurs milliers de couleurs. Pour comprendre comment unifier ce fond, il faut revenir sur la notion de couleur en informatique.

Chaque couleur est constituée d'un indice rouge, vert, bleu compris entre 0 et 255, l'équivalent de nos couleurs primaires en peinture.

Cela permet de représenter une palette de 16 777 216 couleurs, allant du noir (0, 0, 0) au blanc (255, 255, 255).

Le numéro d'une couleur est obtenu par la formule : rouge + (vert x 256) + (bleu x 256²).

Inversement chaque indice peut être retrouvé d'après le numéro d'une couleur par la formule : rouge = Int(Couleur Mod 256) ; vert = Int((Couleur Mod 65536) / 256) ; bleu = Int(Couleur / 65536).

Ainsi la couleur 525 571 (3, 5, 8) est très proche de la couleur 0 (0, 0, 0) car leurs indices sont proches. Ce n'est pas intuitif lorsque l'on compare l'écart entre leurs numéros de couleur. C'est plus causant en exprimant la couleur 525 571 en pourcentage : elle est composée de 1,18 % de rouge, 1,96 % de vert, 3,14 % de bleu. Pour l'œil humain cela ressemble bigrement à du noir.

Inversement, la couleur 255 (255, 0, 0) est un rouge vif, très différent du noir (0, 0, 0) numéro 0.

Sachant cela, unifier avec une couleur de son choix le fond dégradé de cette image devient un jeu d'enfant : il suffit d'analyser la couleur de chaque pixel de l'image, et de l'échanger si l'écart entre ses 3 indices et les 3 indices de la couleur de référence est faible, par exemple de 10 points. En quelques manipulations vous obtenez ceci :

J'ai développé sous EXCEL un outil basé sur ces principes, destiné aux images de taille réduite utilisées par nos animations.

Deux formulaires sont mis à contribution : le premier contient l'image à modifier, sélectionnée par une boîte de dialogue (voir le module VBO du tome 1). Sa taille est adaptée aux dimensions de l'image pour éviter de la déformer. Attention, une image trop grande risque d'avoir ses couleurs détériorées.
Le second formulaire affiche un zoom de l'image suite à un clic sur l'image du formulaire principal.

Dans la partie gauche du classeur sont affichées des informations sur les couleurs de l'image : numéro de la couleur, rendu de la couleur, nombre de pixels de cette couleur. Seules 10 000 lignes sont affichées pour des problèmes de capacité d'EXCEL.

Volontairement, la souris n'est pas bornée aux limites du formulaire principal, ce qui permet de se déplacer sur le classeur et dans le formulaire du zoom. La couleur pointée par la souris, n'importe où à l'écran, est indiquée dans le libellé des formulaires.

Le classeur est protégé pour éviter qu'il ne soit sélectionné et ainsi faire perdre la main au VBA, mais l'ascenseur reste actif et permet de consulter les informations sur les couleurs de l'image.

La touche « M » mémorise la couleur pointée par la souris. L'information est reportée en ligne 1 du classeur.
La touche « E » échange la couleur pointée par la souris ainsi que ses couleurs proches, par la couleur mémorisée, suivant la marge de tolérance désirée. La touche « + » augmente cette marge (maximum = 50) et la touche « - » la diminue (minimum = 0).
Les touches « 0 » à « 5 » affectent directement une valeur pour cette marge, de 0 à 50.
La touche « R » effectue un remplissage de la couleur pointée par la souris ainsi que ses couleurs proches, par la couleur mémorisée, suivant la marge de tolérance désirée.
La combinaison « Ctrl Z » annule la dernière modification faite.
La combinaison « Ctrl S » sauvegarde l'image modifiée dans un fichier BMP. Le nom du fichier est le nom du fichier source suivi de « _BMP.bmp », puis fait quitter EXCEL.
La touche « Fin » quitte EXCEL.

La procédure utilisée, CopieEnBMP, permet de sauvegarder en fichier BMP une image mémorisée ou un formulaire. Cette procédure est compatible EXCEL - ACCESS :

'---------------------------------------------------------------------------------------
Public Sub CopieEnBMP(Optional StrNomDuFichier As String, Optional ByVal ObjFormulaire As Object, _
Optional ImgSource As Integer = -9)
'---------------------------------------------------------------------------------------
' StrNomDuFichier : Nom du fichier généré ou "CopieEnBMP" si non renseigné
'                   et si l'extension .BMP n'est pas mise alors la rajoute.
' ObjFormulaire : Nom du formulaire s'il faut copier le formulaire et pas une image.
' ImgSource : Numéro de l'image à copier.
'---------------------------------------------------------------------------------------
Dim X1 As Long, Y1 As Long, X2 As Long, Y2 As Long
Dim Irect As RECT
Dim lngLargeur As Long, lngHauteur As Long
Dim lngHdc As Long
Dim lngHBmp As Long
Dim bmiBitmapInfo As BitmapInfo
Dim bmfBitmapFileHeader As BitMapFileHeader
Dim lngFnum As Integer
Dim pixels() As Byte
Dim bolOuvert As Boolean
Dim StrFichier As String

' S'il faut copier le formulaire:
' ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If Not ObjFormulaire Is Nothing Then

#If MonAppli = "Excel" Then
X1 = X1 + PointsEnPixelsX(ObjFormulaire.Left)
Y1 = Y1 + PointsEnPixelsY(ObjFormulaire.Top)
X2 = X1 + PointsEnPixelsX(ObjFormulaire.Width)
Y2 = Y1 + PointsEnPixelsY(ObjFormulaire.Height)
#End If

#If MonAppli = "Access" Then
Call GetWindowRect(ObjFormulaire.hWnd, Irect)
X1 = Irect.Left
Y1 = Irect.Top
X2 = Irect.Right
Y2 = Irect.Bottom
#End If

End If

lngHauteur = Y2 - Y1
lngLargeur = X2 - X1

If ImgSource <> -9 Then
lngHauteur = Image(ImgSource).Hauteur
lngLargeur = Image(ImgSource).Largeur
End If

' Crée un bitmap vierge:
' ~~~~~~~~~~~~~~~~~~~~~~
With bmiBitmapInfo
.biBitCount = 32
.biCompression = BI_RGB
.biPlanes = 1
.biSize = Len(bmiBitmapInfo)
.biHeight = lngHauteur
.biWidth = lngLargeur
.biSizeImage = ((((.biWidth * .biBitCount) + 31) \ 32) * 4 - (((.biWidth * .biBitCount) + 7) \ 8)) * .biHeight
End With

lngHdc = CreateCompatibleDC(0)
lngHBmp = CreateDIBSection(lngHdc, bmiBitmapInfo, DIB_RGB_COLORS, ByVal 0&, ByVal 0&, ByVal 0&)
Call SelectObject(lngHdc, lngHBmp)

' Copie la partie de l'écran demandée:
' ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If ImgSource = -9 Then
Call BitBlt(lngHdc, 0, 0, lngLargeur, lngHauteur, GetDC(GetDesktopWindow()), X1, Y1, SRCCOPY)
Else
Call BitBlt(lngHdc, 0, 0, lngLargeur, lngHauteur, Image(ImgSource).Hdc, 0, 0, SRCCOPY)
End If

' Crée l'entête du fichier bmp:
' ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
With bmfBitmapFileHeader
.bfType = &H4D42&
.bfOffBits = Len(bmfBitmapFileHeader) + Len(bmiBitmapInfo)
.bfSize = .bfOffBits + bmiBitmapInfo.biSizeImage
End With

' Lit les bits du bitmap et les place dans le tableau de pixels:
ReDim pixels(1 To 4, 1 To lngLargeur, 1 To lngHauteur)
Call GetDIBits(lngHdc, lngHBmp, 0, lngHauteur, pixels(1, 1, 1), bmiBitmapInfo, DIB_RGB_COLORS)

' Demande un numéro temporaire de fichier:
lngFnum = FreeFile

' Supprime le fichier s'il existe:
StrFichier = StrNomDuFichier
If StrFichier = "" Then StrFichier = "CopieEnBMP" ' nom du fichier par défaut si non renseigné
If InStr(1, StrFichier, ".") = 0 Then StrFichier = StrFichier & ".BMP" ' ajoute l'extension .BMP si besoin
#If MonAppli = "Excel" Then
If InStr(1, StrFichier, ":") = 0 Then StrFichier = ThisWorkbook.Path & "\" & StrFichier ' répertoire du projet si chemin non renseigné.
#End If
#If MonAppli = "Access" Then
If InStr(1, StrFichier, ":") = 0 Then StrFichier = CurrentProject.Path & "\" & StrFichier ' répertoire du projet si chemin non renseigné.
#End If
On Error Resume Next
Kill StrFichier

' Crée le fichier:
Open StrFichier For Binary As lngFnum
bolOuvert = True

' Écrit l'entête:
Put #lngFnum, , bmfBitmapFileHeader

' Écrit les informations du bitmap:
Put #lngFnum, , bmiBitmapInfo

' Écrit les bits de l'image:
Put #lngFnum, , pixels

' Ferme le fichier si ouvert:
If bolOuvert Then Close lngFnum

' Supprime les objets:
If lngHBmp <> 0 Then DeleteObject lngHBmp
If lngHdc <> 0 Then DeleteDC lngHdc

End Sub

Le code de cette application, lancée depuis le bouton CommandButton1 d'un formulaire :

'---------------------------------------------------------------------------------------
Private Sub CommandButton1_Click()
'---------------------------------------------------------------------------------------
CommandButton1.Visible = False  ' Masque le bouton de lancement.
Call Main(Me)                   ' Lance l'animation dans le formulaire.
End Sub

Option Explicit

Dim i As Long, TabDonnées() As Variant, Quantité() As Long, MAJ_Zoom As Boolean
Dim C0 As Long, ZoomHdc As Long, ZoomMaxiX As Long, ZoomMaxiY As Long, Marge As Long
Dim MiniX As Long, MiniY As Long, MaxiX As Long, MaxiY As Long
Dim PixelTraité() As Byte, InfoAction As Boolean, Nb As Byte

'-------------------------------------------------------------------------------------------
Public Sub Main(MyForm As Object)
'-------------------------------------------------------------------------------------------
Dim x As Long, Y As Long, C As Long, HtImage As Long, LgImage As Long, Coef As Double
Dim MonFichier As String, Zm As POINT

' Pioche le fichier image à traiter:
MonFichier = VBO.BoiteDialogue(msoFileDialogFilePicker, "Choix image", "", "", "", "Image,*.Bmp;*.Jpg")
'MonFichier = ThisWorkbook.Path & "\Sprite_Superman.bmp"
'MonFichier = ThisWorkbook.Path & "\Sprite_Stormtrooper.bmp"
If MonFichier = "" Then Exit Sub

' Initialisation du mode graphique et chargement de l'image source en mémoire 99:
Call Img.InitialiseModeGraphique(MyForm)
Call Img.ChargeImageBMP(MonFichier, 99, -1)

' Calcule la taille du formulaire pour contenir l'image sans trop la déformer:
HtImage = Image(99).Hauteur
LgImage = Image(99).Largeur
Coef = 1
While LgImage > GetSystemMetrics(0) * 0.9 Or HtImage > GetSystemMetrics(1) * 0.9
Coef = Coef - 0.01
LgImage = Image(99).Largeur * Coef
HtImage = Image(99).Hauteur * Coef
Wend
While LgImage < GetSystemMetrics(0) * 0.3 And HtImage < GetSystemMetrics(1) * 0.3
Coef = Coef + 0.01
LgImage = Image(99).Largeur * Coef
HtImage = Image(99).Hauteur * Coef
Wend
MyForm.Width = Img.PixelsEnPointsX(LgImage + 5)
MyForm.Height = Img.PixelsEnPointsY(HtImage + 25)

' Centre le formulaire à l'écran:
MyForm.Left = Img.PixelsEnPointsX(GetSystemMetrics(0) / 2) - (MyForm.Width / 2)
MyForm.Top = Img.PixelsEnPointsY(GetSystemMetrics(1) / 2) - (MyForm.Height / 2)
' Mémorise les dimensions du formulaire:
ImageMaxiX = Img.PointsEnPixelsX(MyForm.InsideWidth)
ImageMaxiY = Img.PointsEnPixelsY(MyForm.InsideHeight)

' Création d'une mémoire 10 correspondant à l'image source 99 de la taille du formulaire:
Call Img.ModifieTailleImage(99, ImageMaxiX, ImageMaxiY, 10)
Call Img.AfficheImage(10)

' Recherche les différentes couleurs de la palette:
MyForm.Caption = "Analyse en cours..."
Application.Cursor = xlWait
Application.ScreenUpdating = False
i = 0
For Y = 0 To Image(10).Hauteur - 1
For x = 0 To Image(10).Largeur - 1
C = GetPixel(Image(10).Hdc, x, Y)
ReDim Preserve TabDonnées(i)
TabDonnées(i) = C
i = i + 1
Next x
Next Y

' Trie les couleurs sans doublon:
Call VBO.QuickRanking(TabDonnées, True, 4)

' Affiche la palette:
Y = 2
For i = 0 To UBound(TabDonnées)
Cells(Y, 1) = i
Cells(Y, 2) = TabDonnées(i)
Cells(Y, 3).Interior.Color = TabDonnées(i)
Y = Y + 1: If Y > 10000 Then Exit For ' Limite à 10000 lignes
If Y Mod 17 = 0 Then MyForm.Caption = "Palette : " & Format(i / UBound(TabDonnées) * 100, "0") & "%"
Next i

' Affiche le nombre de pixels par couleur:
Call ComptePixelParCouleur

' Affiche le formulaire Zoom:
Call User_Zoom.Show(False)
User_Zoom.Left = 0
User_Zoom.Top = 0
ZoomHdc = GetDC(FindWindowA("ThunderDFrame", User_Zoom.Caption))
ZoomMaxiX = Img.PointsEnPixelsX(User_Zoom.InsideWidth)
ZoomMaxiY = Img.PointsEnPixelsY(User_Zoom.InsideHeight)
Zm.x = ZoomMaxiX / 8
Zm.Y = ZoomMaxiY / 8
MAJ_Zoom = True

' Charge l'image du curseur de la souris, image 21, depuis le formulaire User_Image:
Call Img.ChargeImageFormulaire(User_Images, "Image1", 21, -9)
Call Img.ModifieTailleImage(21, 32, 32)

' Création des différentes images:
Call Img.CopieImage(11, 11, 0, 0, 0, 0, ZoomMaxiX / 4, ZoomMaxiY / 4)   ' 11 = Image du zoom
Call Img.CopieImage(12, 12, 0, 0, 0, 0, ZoomMaxiX, ZoomMaxiY)           ' 12 = Partie pointée
Call Img.CopieImage(10, 13)                                             ' 13 = Annulation

Marge = 10
On Error GoTo Fin ' Gestion de l'erreur si le formulaire est fermé.
Do
' Affiche l'image 10 en cas de bug de l'écran:
Call Img.AfficheImage(10)

' Recherche la couleur pointée par la souris à l'écran et pas que dans le formulaire:
Call GetCursorPos(Pt)
C = GetPixel(GetWindowDC(0), Pt.x, Pt.Y)
MyForm.Caption = "Marge : " & Marge & " // Couleur pointée : " & C
User_Zoom.Caption = "Marge : " & Marge & " // Couleur pointée : " & C

' Lecture du clic gauche de la souris:
If Img.ToucheEnfoncée(vbKeyLButton) = True Then
Call Img.LectureCurseurDansForm(Pt, MyForm)
' Si le curseur est dans le formulaire alors lance le zoom:
If Pt.x > 0 And Pt.Y > 0 _
And Pt.x < Img.PointsEnPixelsX(MyForm.Width) _
And Pt.Y < Img.PointsEnPixelsY(MyForm.Height) Then
Zm.x = Pt.x - 2     ' Correction de la marge gauche du formulaire.
Zm.Y = Pt.Y - 25    ' Correction de la marge haute du formulaire.
MAJ_Zoom = True     ' Active la mise à jour du zoom.
End If
End If

' Afficher le zoom de la partie pointée:
If MAJ_Zoom = True Then
' Force l'affichage du formulaire Zoom au cas où il a été fermé:
Call User_Zoom.Show(False)
ZoomHdc = GetDC(FindWindowA("ThunderDFrame", User_Zoom.Caption))
' Copie la partie pointée en mémoire 11 (taille de l'image 11 = taille du formulaire zoom / 4):
Call Img.CopieImage(10, 11, 0, 0, Zm.x - (Image(11).Largeur / 2), _
Zm.Y - (Image(11).Hauteur / 2), ZoomMaxiX, ZoomMaxiY)
' Agrandie cette image = zoom (taille de l'image 12 = taille du formulaire zoom):
Call Img.ModifieTailleImage(11, Image(12).Largeur, Image(12).Hauteur, 12)
' Pose le curseur de la souris:
Call Img.CopieImage(21, 12, ZoomMaxiX / 2, ZoomMaxiY / 2)
' Affiche l'image 12 dans le formulaire zoom:
Call BitBlt(ZoomHdc, 0, 0, Image(12).Largeur, Image(12).Hauteur, Image(12).Hdc, 0, 0, SRCCOPY)
MAJ_Zoom = False
End If

' Lecture de M (mémorise la couleur qui sera utilisée pour échanger les couleurs):
If Img.ToucheEnfoncée(vbKeyM) = True Then C0 = C: Call ComptePixelParCouleur

' Lecture de E (Echange la couleur pointée):
If Img.ToucheEnfoncée(vbKeyE) = True Then
Call Img.CopieImage(10, 13)     ' Image avant modification.
Call RemplaceCouleur(C, Marge)  ' Modifie l'image
Call ComptePixelParCouleur      ' Compte le nombre de pixels par couleur.
End If

' Lecture de R (Remplissage):
If Img.ToucheEnfoncée(vbKeyR) = True Then
' Si le curseur est dans le formulaire:
Call Img.LectureCurseurDansForm(Pt, MyForm)
If Pt.x > 0 And Pt.Y > 0 _
And Pt.x < Img.PointsEnPixelsX(MyForm.Width) _
And Pt.Y < Img.PointsEnPixelsY(MyForm.Height) Then
Zm.x = Pt.x - 2                                 ' Pixel pointé par la souris.
Zm.Y = Pt.Y - 25                                ' Pixel pointé par la souris.
Call Img.CopieImage(10, 13)                     ' Image avant modification.
Call RemplissageCouleur(Zm.x, Zm.Y, C, Marge)   ' Modifie l'image.
Call ComptePixelParCouleur
MAJ_Zoom = True
End If
End If

' Lecture de la touche "+" pour augmenter la marge:
If Img.ToucheEnfoncée(vbKeyAdd) = True Then
Marge = Marge + 1: Call Sleep(80): If Marge > 50 Then Marge = 50
End If

' Lecture de la touche "-" pour diminuer la marge:
If Img.ToucheEnfoncée(vbKeySubtract) = True Then
Marge = Marge - 1: Call Sleep(80): If Marge < 0 Then Marge = 0
End If

' Lecture des touches "0" "1" "2" "3" "4" "5" pour ajuster la marge:
If Img.ToucheEnfoncée(vbKeyNumpad0) = True Then Marge = 0
If Img.ToucheEnfoncée(vbKeyNumpad1) = True Then Marge = 10
If Img.ToucheEnfoncée(vbKeyNumpad2) = True Then Marge = 20
If Img.ToucheEnfoncée(vbKeyNumpad3) = True Then Marge = 30
If Img.ToucheEnfoncée(vbKeyNumpad4) = True Then Marge = 40
If Img.ToucheEnfoncée(vbKeyNumpad5) = True Then Marge = 50

' Lecture de Ctrl Z (annulation de la dernière commande):
If Img.ToucheEnfoncée(vbKeyControl) = True And Img.ToucheEnfoncée(vbKeyZ) = True Then
Call Img.CopieImage(13, 10)
MAJ_Zoom = True
End If

' Lecture de Ctrl S (sauvegarde l'image 10 après lui avoir remis la taille d'origine et quitte):
If Img.ToucheEnfoncée(vbKeyControl) = True And Img.ToucheEnfoncée(vbKeyS) = True Then
Call Img.ModifieTailleImage(10, Image(99).Largeur, Image(99).Hauteur, 10)
' Nom du fichier généré = nom du fichier source + _BMP
MonFichier = Left(MonFichier, InStrRev(MonFichier, ".") - 1) & "_BMP.bmp"
Call Img.CopieEnBMP(MonFichier, , 10)
GoTo Fin
End If

Loop While Img.ToucheEnfoncée(vbKeyEnd) = False

Fin:
' Ferme le mode graphique et quitte EXCEL:
Call Img.FermeModeGraphique
Application.DisplayAlerts = False
Application.Quit
End Sub

'-------------------------------------------------------------------------------------------
Sub ComptePixelParCouleur()
'-------------------------------------------------------------------------------------------
Dim x As Long, Y As Long, Coul As Long, lk As Long
Dim Trouvé As Boolean
ReDim Quantité(0 To UBound(TabDonnées))

' Ôte la protection de la feuille:
Sheets("Info").Unprotect
Sheets("Info").EnableSelection = xlNoSelection

' Compte le nombre de pixels par couleur:
Application.Cursor = xlWait
Application.ScreenUpdating = False
For Y = 0 To Image(10).Hauteur - 1
User_Img.Caption = "Compte les pixels : " & Format(Y / Image(10).Hauteur * 100, "0") & "%"
For x = 0 To Image(10).Largeur - 1
Coul = GetPixel(Image(10).Hdc, x, Y)
lk = VBO.RechercheDichotomique(TabDonnées(), Coul, True, Trouvé)
If Trouvé = True Then Quantité(lk) = Quantité(lk) + 1
Next x
Next Y

' Affiche le résultat trouvé et masque la ligne si la quantité est 0:
Y = 2
For i = 0 To UBound(TabDonnées)
If Quantité(i) > 0 Then Cells(Y, 4) = Quantité(i) Else Rows(Y).RowHeight = 0
Quantité(i) = 0
Y = Y + 1: If Y > 10000 Then Exit For ' Limite à 10000 lignes
Next i

' Informations sur la couleur mémorisée:
Range("Couleur_mémorisée").Interior.Color = C0
Range("Numéro_Couleur") = C0
Range("Rouge") = Int(C0 Mod 256)
Range("Vert") = Int((C0 Mod 65536) / 256)
Range("Bleu") = Int(C0 / 65536)

' Fin de l'analyse:
Application.ScreenUpdating = True
Application.Cursor = xlDefault

' Remet la protection de la feuille pour éviter sa sélection et donc désactiver le formulaire:
Sheets("Info").Protect DrawingObjects:=True, Contents:=True, Scenarios:=True
Sheets("Info").EnableSelection = xlNoSelection
End Sub

'-------------------------------------------------------------------------------------------
Sub RemplaceCouleur(C As Long, Marge As Long)
'-------------------------------------------------------------------------------------------
' Boucle sur les tous les pixels de l'image 10 et les analyse avec TraitementPixel.
'-------------------------------------------------------------------------------------------
Dim x As Long, Y As Long

Application.Cursor = xlWait
For Y = 0 To Image(10).Hauteur - 1
User_Img.Caption = "Remplace par " & C0 & " : " & Format(Y / Image(10).Hauteur * 100, "0") & "%"
For x = 0 To Image(10).Largeur - 1
Call TraitementPixel(x, Y, C, Marge)
Next x
Next Y

' Fin du traitement:
Application.Cursor = xlDefault
MAJ_Zoom = True
End Sub

'-------------------------------------------------------------------------------------------
Function TraitementPixel(x As Long, Y As Long, C As Long, Marge As Long, _
Optional MAJPixel As Boolean = True) As Boolean
'-------------------------------------------------------------------------------------------
' Analyse le pixel X,Y pour éventuellement changer sa couleur en C0, si l'écart entre ses
' indices et les indices de la couleur de référence est conforme à la marge admise.
'-------------------------------------------------------------------------------------------
Dim C_Rouge As Integer, C_Vert As Integer, C_Bleu As Integer
Dim Cl As Long, Cl_Rouge As Integer, Cl_Vert As Integer, Cl_Bleu As Integer

' Si même couleur alors indique que le traitement est ok et quitte:
Cl = GetPixel(Image(10).Hdc, x, Y)
If Cl = C0 Then TraitementPixel = True: Exit Function

' Indice de la couleur du pixel analysé:
Cl_Rouge = Int(Cl Mod 256)
Cl_Vert = Int((Cl Mod 65536) / 256)
Cl_Bleu = Int(Cl / 65536)

' Indice de la couleur à remplacer:
C_Rouge = Int(C Mod 256)
C_Vert = Int((C Mod 65536) / 256)
C_Bleu = Int(C / 65536)

' Si l'écart des indices est dans la marge alors fait l'échange avec la couleur C0:
If Abs(C_Rouge - Cl_Rouge) <= Marge And Abs(C_Vert - Cl_Vert) <= Marge _
And Abs(C_Bleu - Cl_Bleu) <= Marge Then
If MAJPixel = True Then Call SetPixel(Image(10).Hdc, x, Y, C0)
TraitementPixel = True
End If

End Function

Pour réaliser le remplissage d'une portion de l'image, comme le fait Paint, mais ici avec une marge de tolérance dans les indices des couleurs, j'ai testé plusieurs méthodes, et je me suis cassé le nez à plusieurs reprises. Avant d'imaginer la procédure qui suit. Son principe : boucler sur les pixels de l'image et mémoriser leur « état » dans une matrice PixelTraité. S'ils sont potentiellement échangeables, le code 11 leur est attribué. Au point d'origine, j'attribue le code 12. Il reste à boucler sur cette matrice, et comparer chaque pixel de code 11 avec ses 8 pixels adjacents. Si l'un d'eux vaut 12, alors le code du pixel analysé passe à 12. Le traitement se termine quand plus aucune modification n'est faite. Sinon la boucle reprend depuis le début de la matrice.

Pour accélérer les traitements, lorsqu'un pixel à ses 8 pixels adjacents d'un code différent de 11, alors il passe à la valeur 13. Ceci permet d'éviter les analyses inutiles. Inutile aussi de poursuivre l'analyse d'une ligne si tous les pixels suivants ont un code différent de 11.

'-------------------------------------------------------------------------------------------
Function InfoPixelContigue(x As Long, Y As Long, h As Long, V As Long) As Byte
'-------------------------------------------------------------------------------------------
' Si le pixel peut être échangé alors il est contigu:
If PixelTraité(x + h, Y + V) = 11 Then
InfoPixelContigue = True
PixelTraité(x + h, Y + V) = 12
If x + h < MiniX Then MiniX = x + h
If Y + V < MiniY Then MiniY = Y + V
Call SetPixel(Image(10).Hdc, x + h, Y + V, C0)
End If
' S'il a été échangé ou est non échangeable, il est quand même traité:
If PixelTraité(x + h, Y + V) <> 11 Then Nb = Nb + 1
End Function
'-------------------------------------------------------------------------------------------

'-------------------------------------------------------------------------------------------
Sub RemplissageCouleur(Xi As Long, Yi As Long, C As Long, Marge As Long)
'-------------------------------------------------------------------------------------------
' Échange la couleur des pixels adjacents au point Xi,Yi, suivant la marge admise.
'-------------------------------------------------------------------------------------------
Dim x As Long, Y As Long, NbDejàTraité As Long
Dim XX As Long, YY As Long

' Création d'un tableau matrice qui contient la situation des pixels de l'image:
ReDim PixelTraité(-1 To Image(10).Largeur + 1, -1 To Image(10).Hauteur + 1)
Application.Cursor = xlWait

' Boucle sur tous les pixels pour savoir s'il faudra éventuellement les échanger ou non:
MaxiX = 0
For Y = 0 To Image(10).Hauteur - 1
NbDejàTraité = 0
For x = 0 To Image(10).Largeur - 1
If TraitementPixel(x, Y, C, Marge, False) = True Then
PixelTraité(x, Y) = 11 ' Code 11 dans la matrice si un pixel peut être changé.
MaxiY = Y
If x > MaxiX Then MaxiX = x
End If
NbDejàTraité = IIf(PixelTraité(x, Y) <> 11, NbDejàTraité + 1, 0)
Next x
PixelTraité(x - NbDejàTraité, Y) = 1 ' Code 1 pour la fin du traitement de la ligne.
Next Y

' Échange le pixel pointé par la souris = point d'origine du traitement:
MiniX = Xi: MiniY = Yi
PixelTraité(Xi, Yi) = 12 ' Code 12 dans la matrice si un pixel a été changé.
Call SetPixel(Image(10).Hdc, Xi, Yi, C0)

' Boucle sur tous les pixels de l'image pour changer les pixels contigus:
Do
InfoAction = False
For Y = MiniY To MaxiY
NbDejàTraité = 0
For x = MiniX To MaxiX
' Compte le nombre de pixels déjà traités sur la ligne pour ne plus les analyser:
NbDejàTraité = IIf(PixelTraité(x, Y) <> 11, NbDejàTraité + 1, 0)
' Si code 1 alors fin du traitement de la ligne:
If PixelTraité(x, Y) = 1 Then x = x + 1: Exit For
' Si code 12 alors analyse les pixels adjacents:
If PixelTraité(x, Y) = 12 Then
Nb = 0
If InfoPixelContigue(x, Y, -1, -1) = True Then InfoAction = True
If InfoPixelContigue(x, Y, 0, -1) = True Then InfoAction = True
If InfoPixelContigue(x, Y, 1, -1) = True Then InfoAction = True
If InfoPixelContigue(x, Y, -1, 0) = True Then InfoAction = True
If InfoPixelContigue(x, Y, 1, 0) = True Then InfoAction = True
If InfoPixelContigue(x, Y, -1, 1) = True Then InfoAction = True
If InfoPixelContigue(x, Y, 0, 1) = True Then InfoAction = True
If InfoPixelContigue(x, Y, 1, 1) = True Then InfoAction = True
If Nb = 8 Then PixelTraité(x, Y) = 13 ' Code 13 si tous les adjacents sont traités.
End If
Next x
PixelTraité(x - NbDejàTraité, Y) = 1 ' Code 1 pour indiquer la fin du traitement de la ligne.
Call Img.AfficheImage(10)            ' Affiche l'image toutes les lignes traitées.
Next Y
Loop While InfoAction = True ' Boucle tant que le traitement génère une action.
Call Img.AfficheImage(10)    ' Affiche l'image définitive.
End Sub

Vous retrouverez le code des fonctions du module VBO dans le tome 1.

Enfin, vous avez dû remarquer dans le code source, la procédure ChargeImageFormulaire, qui permet de charger en mémoire une image contenue dans un formulaire…

XIX - Utiliser les images contenues dans un formulaire

Lorsque vous diffuserez votre jeu d'arcade, il ne faudra pas oublier d'y joindre les images utilisées. Images qui peuvent être nombreuses. Par exemple, il m'a fallu 58 images pour l'animation d'une fusée dans un feu d'artifice. Sachant qu'il suffit d'un fichier supprimé par erreur pour faire planter votre animation, sans compter les risques de détournement des images qui vous ont pris tant de temps à réaliser, mieux vaut trouver une autre solution.

Pour rester compatible EXCEL - ACCESS, je vous propose de copier vos images dans un formulaire.

Sous ACCESS, configurez un formulaire indépendant, non modal, sans aucun style de bordure, et les images seront « attachées ».

L'ouverture du formulaire pour y récupérer ses images pourrait produire un effet visuel désagréable sans la possibilité de rendre le formulaire invisible, grâce à la procédure Opacité du module Img.

Declare Function GetWindowLong Lib "user32" Alias "GetWindowLongA" _
(ByVal hwnd As Long, ByVal nIndex As Long) As Long
Declare Function SetWindowLong Lib "user32" Alias "SetWindowLongA" _
(ByVal hwnd As Long, ByVal nIndex As Long, ByVal dwNewLong As Long) As Long
Declare Function SetLayeredWindowAttributes Lib "user32" _
(ByVal hwnd As Long, ByVal crey As Byte, ByVal bAlpha As Byte, _
ByVal dwFlags As Long) As Long
Public Const GWL_EXSTYLE = (-20)
Public Const WS_EX_LAYERED = &H80000

'---------------------------------------------------------------------------------------
Public Sub Opacité(PourcentOpacity As Integer)
'---------------------------------------------------------------------------------------
' Définit le pourcentage d'opacité de la fenêtre active (généralement un formulaire).
' PourcentOpacity : Pourcentage de l'opacité, en 0 et 100.
' Cette fonction est, en principe, appelée par FadeIn et FadeOut pour faire un effet graphique.
'-------------------------------------------------------------------------------------------
Call SetWindowLong(GetActiveWindow(), GWL_EXSTYLE, _
GetWindowLong(GetActiveWindow(), GWL_EXSTYLE) Or WS_EX_LAYERED)
Call SetLayeredWindowAttributes(GetActiveWindow(), 0, (PourcentOpacity / 100) * 255, 2)

End Sub

Mêmes invisibles, les pixels sont bel et bien présents, et peuvent être récupérés avec BitBlt pour alimenter une mémoire image.

Cette procédure vous servira pour faire des fondus en ouverture et en fermeture dans vos jeux :

'---------------------------------------------------------------------------------------
Public Sub FonduOuverture(DuréeAttente As Integer)
'---------------------------------------------------------------------------------------
' Fait un fondu en ouverture(Fade In) sur la fenêtre active.
' DuréeAttente : durée d'attente en millisecondes dans la variation, en 0 et 100.
'---------------------------------------------------------------------------------------
Dim ik As Integer
For ik = 0 To 100 Step 2
Call Opacité(ik)
Call Sleep(DuréeAttente)
Next ik
End Sub
'---------------------------------------------------------------------------------------
Public Sub FonduFermeture(DuréeAttente As Integer)
'---------------------------------------------------------------------------------------
' Fait un fondu en fermeture (Fade Out) sur la fenêtre active.
' DuréeAttente : durée d'attente en millisecondes dans la variation, en 0 et 100.
'-------------------------------------------------------------------------------------------
Dim ik As Integer
For ik = 100 To 1 Step -2
Call Opacité(ik)
Call Sleep(DuréeAttente)
Next ik
End Sub

Les arguments de la procédure ChargeImageFormulaire sont proches de ceux de la procédure ChargeImageBMP. Mais à la place du nom du fichier source, il faut indiquer le nom du formulaire source et le nom de l'image que l'on souhaite charger en mémoire.

Peu importe où est située l'image dans le formulaire puisqu'elle est systématiquement déplacée au point d'origine 0,0 par ce traitement. Un formulaire peut donc contenir un grand nombre d'images.

'-------------------------------------------------------------------------------------------
Public Sub ChargeImageFormulaire(MyForm As Object, NomImage As String, _
NumImage As Integer, Optional Transparence As Long = -9, _
Optional AlignH As EnumAlignH = 0, Optional AlignV As EnumAlignV = 0)
'-------------------------------------------------------------------------------------------
' Ouvre le formulaire:
#If MonAppli = "Excel" Then
Call MyForm.Show(False)
#ElseIf MonAppli = "Access" Then
Call DoCmd.OpenForm(MyForm.Name)
#End If

' Rend le formulaire invisible:
Call Img.Opacité(0)

' Recherche le contrôle de l'image demandée:
For ImageNum = 0 To MyForm.Count - 1
If MyForm.Controls(ImageNum).Name = NomImage Then Exit For
Next ImageNum

' Place l'image en haut à gauche du formulaire:
MyForm.Controls(ImageNum).Top = 0
MyForm.Controls(ImageNum).Left = 0
MyForm.Repaint

' Récupère le contexte du formulaire et les coordonnées de l'image:
#If MonAppli = "Excel" Then
Anc_Hdc = GetDC(FindWindowA("ThunderDFrame", MyForm.Caption))
ImageMaxiX = Img.PointsEnPixelsX(MyForm.Controls(ImageNum).Width) - 1
ImageMaxiY = Img.PointsEnPixelsY(MyForm.Controls(ImageNum).Height) - 1
#ElseIf MonAppli = "Access" Then
Anc_Hdc = GetWindowDC(MyForm.hwnd)
ImageMaxiX = Img.PointsEnPixelsX(MyForm.Controls(ImageNum).Width / 20) - 1
ImageMaxiY = Img.PointsEnPixelsY(MyForm.Controls(ImageNum).Height / 20) - 1
#End If

' Copie l'image dans la mémoire avec l'alignement demandé:
Call Img.EffaceImage(NumImage)
Call Img.CopieImage(NumImage, NumImage, 0, 0, 0, 0, ImageMaxiX, ImageMaxiY)
Call BitBlt(Image(NumImage).Hdc, 0, 0, ImageMaxiX, ImageMaxiY, Anc_Hdc, 0, 0, SRCCOPY)
Image(NumImage).Transparence = Transparence
Image(NumImage).AlignH = AlignH
Image(NumImage).AlignV = AlignV

' Force à détecter la transparence d'après le point d'origine 0,0:
If Transparence = -9 Then Image(NumImage).Transparence = GetPixel(Image(NumImage).Hdc, 0, 0)

' Ferme le formulaire:
#If MonAppli = "Excel" Then
Call MyForm.Hide
#ElseIf MonAppli = "Access" Then
DoCmd.Close acForm, MyForm.Name, acSaveNo
#End If

End Sub

XX - Conclusion

Mais pourquoi programmer en mode graphique ? La question était posée dans l'introduction de ce mémento. Je vous laissais entrevoir la possibilité de maîtriser chacun des pixels de l'écran, d'afficher des images et de gérer des animations, et aussi, de vous fournir les fonctionnalités qui vous permettront de programmer un jeu d'arcade.

Je pense que Snake est la démonstration que j'ai tenu mes promesses.

Reste à savoir si j'ai les talents requis pour transmettre mon savoir de façon compréhensible.

Par l'étude progressive des API utilisées, étude que j'espère ne pas être trop fastidieuse, j'ai souhaité vous expliquer la logique de la gestion du mode graphique. Plutôt que de vous livrer des procédures toutes faites dont vous ne comprendriez pas le sens. Armé de ces bases, vous pouvez maintenant développer les fonctions dont vous avez besoin. Pour un jeu d'arcade, pour animer vos applications ou pour impressionner votre entourage.

Si vous désirez vous lancer dans l'aventure des jeux d'arcade, vous trouverez sur Internet de nombreux Sprite en 2D. Que ceux qui ne savent pas dessiner se rassurent. Des sites sont même dédiés aux Sprite des jeux vidéo des premières générations.

Pour les bruitages, vous devrez trouver votre bonheur sur « freesoundeffects.com ».

N'hésitez pas à modifier ou compléter le module Img, comme je l'ai fait en y ajoutant la procédure EffetTransition qui offre 9 effets de transitions pour modifier l'image de fond.

Les plus perspicaces pourront consulter le tutoriel « ACCESS : Tutoriel d'utilisation simple de la classe ClImage » sur « developpez.com ».

Bonne programmation, et peut-être, rendez-vous dans un prochain mémento…

Laurent OTT. 2016

XXI - Les sources et images des exemples présentés

Nous mettons à votre disposition les fichiers sources des exemples présentés dans ce tutoriel.

Vous trouverez le jeu Snake dans cette archive ou dans cette source.

Les images bmp du feu d'artifice sont regroupées en trois parties : partie 1, partie 2 et partie 3. Les fichiers xls et d'autres compléments sont disponibles dans cette quatrième partie.

XXII - Remerciements

Nous tenons à remercier Winjerome pour la mise au gabarit et f-leb pour la relecture orthographique.