Texel
Win32 | Index | Contact

Les bases de la programmation Windows

Chapitre III
Utilisation de la GDI



GDI (Graphics Device Interface) est une interface de périphérique graphique. C'est lui qui permet l'affichage de tout ce qui est graphique à l'écran et sur imprimante (ou traceur). Il possède de nombreuses fonctions: affichage de pixels, textes, bitmaps …Il peut même être utilisé avec des librairies graphiques tel que DirectX. Nous ne parlerons ici que du tracé sur écran, même si bon nombre des fonctions présentées peuvent être utiles pour d'autres périphériques. Je vous conseil d'avoir à porté de main le code source d'exemple de ce chapitre pour bien comprendre ce qui va suivre. Cliquez ici



I. Le contexte de périphérique

Lorsque vous dessinez sur l'écran ou que vous lancez une impression, vous utilisez un périphérique matériel (carte graphique, imprimante…). Dans vos programmes, vous devez demander un handle de contexte de périphérique (Device context en anglais) qui vous autorise à utiliser ce périphérique. Ce contexte de périphérique contient des attributs qui définissent par exemple la couleur du texte courante. Des fonctions permettent de changer les attributs définis dans le contexte de périphérique. Vous n'avez donc pas à redéfinir à chaque fois les attributs du texte ou du tracé que vous affichez (puisqu'ils sont sauvegardés jusqu'à la fin de votre application).

Pour obtenir un handle de contexte de périphérique, il existe deux méthodes.


1) Première méthode

La première méthode est utilisée lors de la réception du message WM_PAINT. Ce message était traité par notre précédente application par la fonction DefWindowProc. Ce message est envoyé par Windows à notre application lorsque la fenêtre doit être redessinée (Ex: Après un chevauchement de fenêtre ou un agrandissement). Dans notre programme précédent, DefWindowProc gérait ce message en redessinant simplement la fenêtre vide. Si l'on veut afficher quelque chose dans notre fenêtre, il faut profiter de ce rafraîchissement pour appeler à chaques mise à jours les fonctions d'affichage de texte et d'image. Mais il faut pour cela obtenir un HDC.

case WM_PAINT:
	HDC hdc; PAINTSTRUCT ps;
	hdc=BeginPaint(hwnd,&ps);	// obtient un handle de contexte de périphérique	
	// affichage de bitmap,pixel…
	EndPaint(hwnd,&ps); 	// libère un handle de contexte de périphérique
return 0;








Vous devez commencer par déclarer un contexte de périphérique, de type HDC (H pour handle et DC pour Device Context), au début de votre procédure de fenêtre ou juste avant d'appeler BeginPaint.

Déclarez ensuite une structure "d'information de dessin" de type PAINTSTRUCT. Les champs de cette structure sont remplis par défaut par Windows, mais vous pouvez modifier ses trois premiers champs.

typedef struct tagPAINTSTRUCT {
	HDC hdc; 
	BOOL fErase; 
	RECT rcPaint; 
	BOOL fRestore; 
	BOOL fIncUpdate; 
	BYTE rgbReserved[32]; 
} PAINTSTRUCT;










Le champ rcPaint est le plus important. C'est une structure de type RECT contenant les coordonnées des sommets d'un rectangle. Ses champs sont left, top, right, et bottom. Ce rectangle contient les limites de la zone à redessiner, appelée zone invalide, dans la zone client (Windows envoi le message WM_PAINT à chaque fois qu'une zone est invalide, jusqu'à ce que la fonction ValideRect ou les fonctions BeginPaint et EndPaint la valide). Si vous ne touchez pas au champ rcPaint Windows le calcul.

Tout le code permettant d'afficher tous les objets graphiques de votre fenêtre doit être placé entre BeginPaint et EndPaint pour pouvoir être redessiner par Windows. Lorsque c'est DefWindowProc qui gère le message WM_PAINT, il utilise aussi BeginPaint et EndPaint (en interne).

Après avoir obtenu un handle de contexte de périphérique avec BeginPaint, il ne faut pas oublier de le libérer immédiatement après avec EndPaint avant de quitter la procédure de fenêtre. Ces deux fonctions prennent en argument l'handle de la fenêtre et une structure PAINTSTRUCT. EndPaint retourne un BOOL pour indiquer une réussite ou un échec.

Presque n'importe où dans votre programme (dans WinMain, procédure de fenêtre ou vos propres fonctions), vous pouvez utiliser la fonction InvalidateRect et une structure RECT pour redessiner la partie de la zone cliente que vous souhaitez (vous invalidez alors une zone). Le message WM_PAINT sera alors appelé et BeginPaint rafraîchira l'intérieur du rectangle.

BOOL InvalidateRect( 
	HWND hWnd,		// handle de la fenêtre
	CONST RECT *lpRect,	// structure RECT 
	BOOL bErase		// effacer cette zone ? Oui/Non
); 







Si vous envoyer la valeur NULL en deuxième argument à la fonction, c'est toute la fenêtre qui sera rafraîchie.



2) Deuxième méthode

Elle utilise deux fonctions:

hdc=GetDC(hwnd);	// obtient un handle de contexte de périphérique
...			// affichage de bitmap,pixel…
ReleaseDC(hwnd,hdc);	// libère l'handle





Ces fonctions peuvent être appelées presque n'importe où dans votre programme (après que votre fenêtre est été crée biensûr) et n'ont pas besoin d'être utilisées avec un message WM_PAINT. Mais dans ce cas ATTENTION: si vous avez écrit un texte ou affiché une image au début de votre programme et si la fenêtre est redessinée par Windows (Ex: à cause d'un chevauchement), les objets graphiques seront effacés. Il vous faut donc trouver un moyen de redessiner vos graphismes en rappelant ces fonctions (en les mettant par exemple dans une boucle qui rappel les fonctions en permanance. cf fin du chapitre 2 ou tutorial sur les compteurs).

GetDC et ReleaseDC reçoivent en argument l'handle d'une fenêtre. La seconde fonction nécessite aussi l'handle de contexte de périphérique.

D'autres variantes comme GetWindowDC accède à l'intégralité de votre fenêtre, même les barres de titres, contrairement à GetDC qui n'accède qu'à la zone cliente. CreateDC est aussi utilisable pour dessiner sur tout l'écran (même en dehors de votre fenêtre).




II. L'affichage de texte.

1) La fonction TextOut

Cette fonction est la plus utiliser pour afficher du texte. Elle s'insère après GetDC ou BeginPaint, comme toutes les fonctions GDI. La voici:

BOOL TextOut( 
	HDC hdc, 			// handle d'un contexte de périphérique
	int XStart,		 	// coordonné du texte en x
	int YStart, 			// coordonné de texte en y 
	LPCTSTR lpString,		// pointeur sur la chaîne de caractère
	int iLength 			// longueur de la chaîne
); 









Toutes les fonctions GDI ont besoin de l'handle du contexte de périphérique. Les coordonnées du texte correspondent par défaut au coin supérieur gauche du premier caractère. Pour le paramètre lpString, vous pouvez mettre la chaîne de caractère entre guillemets ou passer en argument un tableau.

Pour la longueur de la chaîne, utilisez tout simplement la fonction C: strlen.


2) La fonction SetTextAlign

Vous pouvez parfois avoir besoin de placer votre texte sur l'extrémité droite de l'écran. Précédez alors TextOut par la fonction SetTextAlign qui définit, grâce à son deuxième argument, à quel emplacement du texte correspondent les coordonnées x et y de TextOut.

UINT SetTextAlign(  
	HDC hdc,	// handle du contexte de périphérique
	UINT fMode 	// drapeau (constante) définissant l'alignement du texte
);






Les drapeaux TA_RIGHT | TA_TOP indique que les coordonnées x et y correspondent au coin supérieur droit de la chaîne de caractère.


3) Les couleurs

SetTextColor change la couleur de votre texte.

COLORREF SetTextColor( 
	HDC hdc, 		// handle du contexte de périphérique
	COLORREF Color  	// couleur de texte
); 






COLORREF est un entier long non signé de 32 bits qui sert à contenir les valeurs de 0 à 255 des trois couleurs primaires (rouge, bleu, vert) qui forment chaques couleurs. Il est plus simple d'envoyer la macro RGB pour le deuxième argument de SetTextColor. Cette macro à trois arguments pour le rouge, le vert, le bleu: RGB(nombre de 0 à 255, idem, idem). Exemple: SetTextColor(hdc,RGB(0,255,0)) affichera le texte en rouge très saturé.

SetBkColor change la couleur de fond située derrière le texte.

COLORREF SetBkColor(
	HDC hdc, 			// handle du contexte de périphérique 
	COLORREF crColor 		// couleur de fond
); 







SetBkMode permet d'avoir une couleur de fond transparente grâce au drapeau TRANSPARENT. Mais le drapeau OPAQUE est aussi disponible.

int SetBkMode( 
	HDC hdc, 		// handle du contexte de périphérique
	int iBkMode 		// drapeau 
); 








III. L'affichage de pixels.

Pour afficher un pixel à l'écran c'est encore plus simple que d'afficher du texte. Ici aussi une fonction suffit:

COLORREF SetPixel( 
	HDC hdc, 		// handle contexte de périphérique
	int X, 			// coordonnée x du pixel
	int Y,		    	// coordonnée y du pixel
	crColor 		// couleur du pixel
);








Rien de bien nouveau à signaler. Noter tout de même que la fonction retourne la couleur utilisée par Windows pour afficher le pixel. En effet, la couleur souhaitée n'est pas toujours disponible et Windows choisit alors la couleur la plus proche. Si vous n'avez pas besoin de cette information, utilisez donc la fonction SetPixelV. Elle a exactement les mêmes paramètres mais ne retourne rien. Elle est de ce fait un peut plus rapide à l'exécution.

Il est possible que vous ayez besoin de connaître la couleur d'un pixel sur une coordonnée. Utilisez la fonction GetPixel:

COLORREF GetPixel( 
	HDC hdc, 		// handle contexte de périphérique
	int X, 			// coordonnée x à lire 
	int Y 			// coordonnée y à lire 
); 







Il existe bien sûr des fonctions pour tracer des figures toutes faites (Rectangle, RoundRect, Ellipse, Arc, Pie, Chord, Polyline …). LineTo trace une ligne de la position courante (modifiable par MoveToEx) jusqu'aux coordonnées passées à la fonction.


IV. afficher une image bitmap

Pour afficher une image bitmap, c'est un peut plus compliquer que pour afficher un pixel. Voici les différentes étapes:

_ Obtenir un handle de bitmap.
_ Obtenir un handle de contexte de périphérique pour l'écran.
_ Obtenir un autre handle de contexte de périphérique de mémoire. Il servira à avoir une surface d'écran "virtuelle" en mémoire qui ne sera pas affichée.
_ Charger le bitmap dans le contexte de périphérique de mémoire.
_ Transférer l'image du contexte de périphérique de mémoire au contexte de périphérique de l'écran (l'image apparaît).
_ Libérer les handles de contexte de périphérique et de bitmap.

Commencez d'abord par déclarer un handle de bitmap de type HBITMAP. Ensuite, pour l'obtenir, il existe deux fonctions possibles. La plus simple utilise LoadBitmap qui est du même genre que LoadCursor avec les mêmes paramètres. Mais LoadImage est plus intéressante:

HANDLE LoadImage( 
	HINSTANCE hinst, 		// handle d'instance où est l'image 
	LPCTSTR     lpszName,		// nom du fichier ou identificateur de l'image
	UINT uType, 			// type de l'image 
	int cxDesired, 			// largeur voulue
	int cyDesired, 			// hauteur voulue
	UINT fuLoad 			// drapeau pour le chargement
); 










Cette fonction retourne un handle de type HANDLE alors que l'handle de notre bitmap est de type HBITMAP. Il faudra donc faire un forçage de type (en mettant HBITMAP entre parenthèse juste avant la fonction).

Le premier argument doit être fixé à NULL pour charger un ficher bitmap externe à l'application. Si vous chargez votre image depuis un fichier ressource inclus dans l'exécutable, utilisez l'handle de votre application.

L'image à afficher n'est pas forcement un bitmap, ce peut être un curseur de souris ou un icône. Envoyez la constante IMAGE_BITMAP au troisième argument dans notre situation.

Fixer la largeur et la hauteur du bitmap à zero pour avoir la taille d'origine du bitmap.

Pour le dernier paramètre, il existe plusieurs drapeaux dont:

LR_CREATEDIBSECTION : Ce drapeau permet d'obtenir l'image avec ses propres propriétés plutôt que d'utiliser des propriétés définit par le périphérique.
LR_LOADFROMFILE : Ce drapeau est à utiliser lorsque vous charger l'image depuis un fichier externe à votre exécutable.

Maintenant, il faut créer un contexte de périphérique pour l'écran comme vous avez appris à le faire précédemment (cf un peu plus haut).

Puis, pour le contexte de périphérique de mémoire, il faut utiliser la fonction CreateCompatibleDC:

HDC CreateCompatibleDC(HDC hdc); 




Comme son nom l'indique, le contexte de périphérique créé est compatible avec un autre contexte de périphérique qui n'est autre que celui passer en argument à la fonction. Si l'argument est NULL vous créer un contexte de périphérique compatible avec l'écran (et c'est ce que l'on veut).

Maintenant que vous avez une surface mémoire vide, il faut charger le bitmap dans cette surface par la fonction SelectObject:

HGDIOBJ SelectObject( 
	HDC hdc, 			// handle cont. de périph. mémoire
	HGDIOBJ hgdiobj 		// handle de l'objet (le bitmap)
);






Cette fonction n'est pas utilisée uniquement pour afficher des bitmaps, mais pour nous HGDIOBJ est un handle de type HBITMAP. Pas besoin de forçage de type ici.

Pour afficher le bitmap à sur l'écran on utilise la fonction BitBlt ou l'une de ses variantes. Ces fonctions demandent en argument la largeur de l'image bitmap source. Donc avant de parlé de ces fonctions, nous allons nous intéresser à la fonction GetObject qui obtient des informations sur l'image et les copie dans une structure de type…BITMAP.

int GetObject(
	HGDIOBJ hgdiobj,		// handle de l'objet graphique (du bitmap)
	int cbBuffer,			// taille de la structure BITMAP 
	LPVOID lpvObject		// pointer vers une structure BITMAP 
); 







Pour le deuxième argument, il faut utiliser la fonction sizeof sur la structure BITMAP. Voici justement la structure:

typedef struct tagBITMAP { 
   LONG   bmType; 		// type du bitmap (mis à zéro)
   LONG   bmWidth; 		// largeur du bitmap en pixels
   LONG   bmHeight; 		// hauteur
   LONG   bmWidthBytes; 	// nombre de bits par lignes
   WORD   bmPlanes; 		// nombre de plan de couleur
   WORD   bmBitsPixel; 		// nombre de bits par point
   LPVOID bmBits; 		// pointeur sur bits de points
} BITMAP; 










On ne va pas ici donner des explications sur les différents champs de la structure. Ce qui nous intéresse, c'est la largeur et la hauteur du bitmap.

Maintenant, on va transférer l'image de la surface mémoire du contexte de périphérique (qui est la source) vers la surface de l'écran (qui est la destination) pour qu'on puis enfin la voir. Pour cela, on utilise la fonction BitBlt:

BOOL BitBlt(
HDC hdcDest,	// handle du contexte de périphérique de destination 
 int nXDest,		// coordonnée x de destination du rectangle supérieur gauche du bitmap
 int nYDest,		// idem pour y
 int nWidth,		// largeur du bitmap (destination et source)
 int nHeight,		// hauteur du bitmap (destination et source)
 HDC hdcSrc,		// handle du contexte de périphérique source
 int nXSrc,		// coordonnée x source du rectangle supérieur gauche du bitmap
 int nYSrc,		// idem pour y
 DWORD dwRop 	// code d'opération de trame (pour quelques manipulations)
  );












La structure BITMAP est utilisée pour le quatrième et le cinquième argument de la fonction.

Pour le dernier argument il y a de nombreux drapeaux pour par exemple inverser les couleurs (cf. win32.hlp). Le plus simple est SRCCOPY qui effectue un transfert direct entre les surfaces.

Notez que l'image source peut provenir de la capture d'une partie de votre propre écran. Les fonctions LoadImage, SelectObject, GetObject sont alors inutiles et CreateCompatibleDC est remplacer par GetDC ou GetWindowDC.

BitBlt ne peut pas modifier la taille d'une image avant de l'afficher, contrairement à StretchBlt:

BOOL StretchBlt(
	HDC hdcDest,		// handle du contexte de périphérique de destination
	int nXOriginDest,	// coordonnée x de destination (coin supérieur gauche) 
	int nYOriginDest,	// coordonnée y de destination (coin supérieur gauche)
	int nWidthDest,		// largeur voulu du bitmap de destination
	int nHeightDest,	// hauteur voulu du bitmap de dstination 
	HDC hdcSrc,		// handle du contexte de périphérique source
	int nXOriginSrc,	// coordonnée x source du rectangle supérieur gauche du bitmap
	int nYOriginSrc,	// coordonnée y source du rectangle supérieur gauche du bitmap
	int nWidthSrc,		// largeur du bitmap source à capturer à partir de nXOriginSrc
	int nHeightSrc,		// hauteur du bitmap source à capturer à partir de nYOriginSrc
	DWORD dwRop 		// code d'opération de trame   
);














Comme vous le voyez, la fonction reçoit les dimensions d'origine et les dimensions souhaitées. Si le signe d'une de ces dimensions est négatives, l'image sera retournée horizontalement ou verticalement. N'oubliez pas de libérer les contextes de périphériques mémoire avec la fonction DeleteDC après son utilisation (et non pas ReleaseDC puisqu'on à pas d'handle de fenêtre pour celui-ci).

BOOL DeleteDC(HDC hdcmem); 




De même, supprimez l'image bitmap avec DeleteObject avant de quitter le programme:

BOOL DeleteObject(HGDIOBJ hObject);	// avec l'handle du bitmap





Conclusion:

Vous remarquerez rapidement lors de vos tests que vous ne pouvez pas afficher d'objet à l'extrémité d'une fenêtre si une partie de l'objet est en dehors de la fenêtre. Ce n'est pas un bogue. C'est normal. Il s'agit d'un phénomène appelé clipping. Pour régler le problème, il suffit de découper l'objet pour n'afficher de celui-ci que la partie située dans la fenêtre.

Vous pouvez télécharger le code source d'un programme utilisant tous ce que nous avons vu ici. Modifiez le et essayez d'autres fonctions décrites dans les fichiers d'aides.

Vous connaissez assez de chose maintenant sur l'API WIN32 pour envisager de commencer à programmer avec DirectX ou OpenGL. Néanmoins le prochain chapitre sur les ressources risque de vous être très utile dans l'avenir.



Version originale: Fevrier 2001
Dernière mise à jour: Juin 2002
Par Grégory Smialek
www.texel.fr.fm