IV. Le graphisme▲
IV-A. Introduction▲
IV-A-1. L'interface des périphériques graphiques▲
La GDI (Graphics Devcice Interface) ou Interface des Périphériques Graphiques est une API de niveau moyen (ni trop bas ni trop haut) permettant de dessiner sur n'importe quel périphérique graphique (écran ou imprimante par exemple) de manière standard c'est-à-dire sans avoir à communiquer directement avec le pilote. L'étude un peu plus approfondie de cette API fera l'objet du prochain tutoriel. Pour l'instant, nous nous contenterons de découvrir les bases.
IV-A-2. Zone invalide▲
Une partie de la zone cliente d'une fenêtre est dite invalide lorsqu'elle doit être dessinée ou redessinée, ce qui se produit lorsqu'elle vient tout juste d'apparaître alors qu'elle était auparavant cachée (par une autre fenêtre par exemple). Windows informe une fenêtre qu'une partie de sa zone cliente est invalide en lui envoyant le message WM_PAINT avec bien sûr en paramètre (c'est-à-dire dans wParam et lParam) les informations supplémentaires, parmi lesquels un pointeur vers une structure appelée ps (Paint information Structure) contenant, entre autres, les coordonnées du plus petit rectangle contenant la région invalide, appelé rectangle invalide (en anglais : invalid rectangle ou encore update rectangle). Si la fenêtre possède les styles CS_HREDRAW et/ou CS_VREDRAW, non seulement le message WM_PAINT sera également envoyé chaque fois que celle-ci est redimensionnée (verticalement ou horizontalement, suivant le ou les styles spécifiés), mais en plus la fenêtre tout entière sera redessinée (rafraîchie). Si une autre partie de la zone cliente devient invalide avant qu'un message WM_PAINT en attente n'ait été traité, Windows calcule une nouvelle région invalide (et donc aussi un nouveau rectangle invalide) et modifie en conséquence les informations contenues dans la ps. Il ne peut donc y avoir au plus qu'un message WM_PAINT (et pas plus) dans la file des messages.
On peut invalider un rectangle avec la fonction InvalidateRect que nous verrons plus loin. Si le rectangle invalide est validé avant même qu'on ait eu le temps de traiter le message WM_PAINT, le message WM_PAINT sera supprimé de la queue des messages. On peut à tout moment connaître les coordonnées du rectangle invalide à l'aide de la fonction GetUpdateRect puis valider un rectangle avec la fonction ValidateRect.
Évidemment, rien ne dit que l'on ne doit dessiner que sur réception du message WM_PAINT. On peut dessiner à tout moment, chaque fois qu'on en a envie cependant, placer le code de dessin dans le traitement de ce message permet de mieux structurer le programme. Chaque fois qu'on veut mettre à jour un dessin, il suffit d'invalider tout simplement la partie que l'on veut redessiner.
IV-A-3. Contexte de périphérique▲
Avant toute chose, il faut savoir qu'une fenêtre est un dessin (dessiné sur l'écran). Pour dessiner dans une fenêtre, il faut donc connaître quelle partie de l'écran est actuellement utilisée par la fenêtre et dessiner à l'intérieur de cette surface. Cette surface est connue sous le nom de contexte de périphérique (Device Context ou tout simplement DC) de la fenêtre. La prochaine étape est donc de récupérer un handle de ce contexte de périphérique. Une fois ce handle obtenu, on peut désormais utiliser ce DC. On a des fonctions pour dessiner des points, des lignes, des rectangles, etc., des fonctions pour afficher du texte, pour créer des polices de caractère, etc., mais aussi des fonctions pour récupérer des informations sur le périphérique, etc.
Sachez également qu'il existe plusieurs types de contexte de périphérique. Il y a par exemple des contextes de périphérique d'une fenêtre, des contextes de périphérique d'une imprimante et même des contextes de périphérique en mémoire ! En général, les fonctions de la GDI ne font pas la distinction entre ces types de DC, mais cela n'empêche pas l'existence de certaines fonctions et/ou options utilisables uniquement sur un type de DC particulier.
IV-A-4. Dessiner dans la zone cliente d'une fenêtre▲
IV-A-4-a. Le classique « Hello, world ! »▲
Voici la méthode généralement utilisée pour dessiner dans la zone cliente d'une fenêtre :
Premièrement, il faut avoir au moins deux variables : une de type HDC (handle de DC) et une autre de type PAINTSTRUCT (requis par Windows, rien à discuter là dessus).
HDC hDC;
PAINTSTRUCT ps;
Ensuite, dans le traitement du message WM_PAINT, il faut avoir le HDC de la fenêtre et entourer tout le code dessin par BeginPaint()/EndPaint().
hDC =
BeginPaint
(
hwnd, &
ps);
/* Code de dessin ... */
EndPaint
(
hwnd, &
ps);
Par exemple :
LRESULT CALLBACK WndProc
(
HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
HDC hDC;
PAINTSTRUCT ps;
switch
(
message)
{
case
WM_PAINT:
hDC =
BeginPaint
(
hwnd, &
ps);
TextOut
(
hDC, 0
, 0
, "
Hello, world !
"
, 14
);
EndPaint
(
hwnd, &
ps);
break
;
case
WM_DESTROY:
PostQuitMessage
(
0
);
break
;
default
:
return
DefWindowProc
(
hwnd, message, wParam, lParam);
}
return
0L
;
}
Ce code permet d'afficher « Hello, world ! » à l'intérieur de la fenêtre. Ne vous préoccupez pas avec TextOut pour l'instant, ce n'est pas l'important.
IV-A-4-b. Les fonctions BeginPaint et EndPaint▲
Le rôle de la fonction BeginPaint est, entre autres, de valider le rectangle invalide (ce qui va donc de supprimer le message WM_PAINT de la queue des messages) et de retourner un handle du contexte de périphérique de l'écran qui nous permettra alors de dessiner dans l'ex région invalide zone cliente de la fenêtre. Plus précisément ce handle nous permettra de dessiner uniquement à l'intérieur du rectangle invalide. Depuis Windows 2000, les applications aussi peuvent contrôler la partie dessinable d'un DC à l'aide de la fonction SelectClipRgn qui ne nous intéresse pas pour le moment. Pour pouvoir dessiner en dehors du rectangle invalide pendant le traitement de WM_PAINT, vous pouvez utiliser l'une des techniques suivantes :
- étendre la zone invalide ! Autrement dit, invalider un par un, avant d'appeler BeginPaint bien sûr, tous les rectangles à l'intérieur desquels on veut pouvoir dessiner. On peut invalider un rectangle à l'aide de la fonction InvalidateRect qui sera étudiée plus bas ;
- ignorer le HDC retourné par BeginPaint et récupérer un HDC de la fenêtre en utilisant la fonction GetDC que nous allons également voir tout à l'heure. Généralement, cela se fait dès le traitement du message WM_CREATE. Le HDC ainsi obtenu est ensuite libéré dans le traitement de WM_DESTROY.
La fonction EndPaint quant à elle doit juste être appelée lorsqu'on n'envisage plus de dessiner quoi que ce soit.
IV-A-4-c. La fonction InvalidateRect▲
Cette fonction permet d'invalider un rectangle de la zone cliente. N'oubliez pas cependant qu'à chaque nouveau rectangle invalide, il n'existe pas désormais deux ou plusieurs rectangles invalides, mais un seul : le plus grand rectangle englobant tous les (ex-)rectangles invalides.
BOOL InvalidateRect
(
HWND hWnd, CONST RECT *
lpRect, BOOL bRedrawBackground);
Si lpRect vaut NULL, alors c'est toute la zone cliente qui sera invalidée.
Cette fonction permet également de spécifier, via le paramètre bRedrawBackground, s'il faut remettre ou non le fond de la fenêtre à la place du dessin actuel dans le rectangle invalide avant que celui-ci ne soit validé (pou rappel, la validation est faite par BeginPaint et une fois le rectangle invalide validé, le message WM_PAINT est supprimé de la queue des messages). Si ce paramètre vaut TRUE, alors le message WM_ERASEBKGND (avec dans wParam la valeur du HDC) est envoyé (par BeginPaint) et une fois le traitement du message effectué, BeginPaint valide le rectangle invalide, sinon ce message ne sera pas envoyé (tout ce qui a déjà été dessiné ne sera donc pas effacé).
DefWindowProc traite ce message en peignant le fond du rectangle invalide avec la brosse que vous avez définie pour la fenêtre (avec le membre hbrBackground de la structure WNDCLASS(EX)). Si hbrBackground vaut NULL, alors vous devez vous-même traiter ce message. Si vous traitez ce message et que vous avez vous-même peint le fond du rectangle invalide, alors vous devez retourner VRAI pour indiquer à Windows qu'il n'est plus nécessaire de le faire. Sinon vous devez retourner FAUX (et dans ce cas vous avez intérêt à avoir fourni une valeur non nulle à WNDCLASS::hbrBackground…).
IV-A-5. Comment obtenir un HDC▲
En récupérant le retour de BeginPaint bien sûr, mais il y a aussi d'autres fonctions à connaître parmi lesquelles GetDC qui permet entre autres de récupérer le handle du DC d'une fenêtre spécifiée en argument. Avec ce handle, on pourra dessiner dans toute la zone cliente et non seulement à l'intérieur du rectangle invalide. GetWindowDC retourne un handle permettant de dessiner dans toute la fenêtre et non seulement dans la zone cliente.
HDC GetDC
(
HWND hWnd);
HDC GetDC
(
HWND hWnd);
Si hWnd vaut NULL, GetDC et GetWindowDC retournent le handle du DC du périphérique graphique principal, par défaut l'écran.
Généralement, on appelle GetDC pendant le traitement du message WM_CREATE, BeginPaint et EndPaint (et le code de dessin) dans le traitement du message WM_PAINT et ReleaseDC dans le traitement du message WM_DESTROY.
int
ReleaseDC
(
HWND hWnd, HDC hdc);
Donc voici également une autre manière d'afficher Hello world !
LRESULT CALLBACK WndProc
(
HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static
HDC hDC;
PAINTSTRUCT ps;
switch
(
message)
{
case
WM_CREATE:
hDC =
GetDC
(
hwnd);
break
;
case
WM_PAINT:
BeginPaint
(
hwnd, &
ps);
TextOut
(
hDC, 0
, 0
, "
Hello world !
"
, 13
);
EndPaint
(
hwnd, &
ps);
break
;
case
WM_DESTROY:
ReleaseDC
(
hwnd, hDC);
PostQuitMessage
(
0
);
break
;
default
:
return
DefWindowProc
(
hwnd, message, wParam, lParam);
}
return
0L
;
}
IV-B. Les fonctions de dessin▲
IV-B-1. Tracer des lignes et des points▲
Ces fonctions utilisent généralement la notion de position courante. Par exemple, la fonction LineTo trace une ligne à partir de la position courante jusqu'à une certaine position (non incluse) spécifiée en arguments, qui deviendra ensuite la position courante. Pour changer la position courante, on utilisera la fonction MoveToEx :
BOOL MoveToEx
(
HDC hdc, int
x, int
y, LPPOINT lpPoint);
Cette fonction accepte en dernier argument l'adresse d'une structure de type POINT pour stocker les coordonnées de l'ancienne position courante. On peut évidemment mettre NULL. La fonction GetCurrentPositionEx permet d'obtenir la position courante.
BOOL GetCurrentPositionEx
(
HDC hdc, LPPOINT lpPoint);
Les fonctions permettant de tracer des lignes et des points sont :
COLORREF SetPixel
(
HDC hdc, int
x, int
y, COLORREF crColor);
COLORREF GetPixel
(
HDC hdc, int
x, int
y);
BOOL LineTo
(
HDC hdc, int
x, int
y);
BOOL Polyline
(
HDC hdc, CONST POINT *
lppt, int
cPoints);
BOOL PolylineTo
(
HDC hdc, CONST POINT *
lppt, int
cCount);
Le type COLORREF sert évidemment à représenter une couleur RGB. Chacune des composantes R, G et B est stockée sur un octet. La macro RGB permet facilement de constituer une couleur à partir de composantes R, G et B.
COLORREF RGB
(
BYTE r, BYTE g, BYTE b);
Et on peut extraire les composantes R, G et B d'une couleur avec les macros GetRValue, GetGValue et GetBValue.
On a également des fonctions pour tracer des courbes de Bézier. Les courbes de Bézier sont définies par 4 points (les points de contrôle) pour la fonction PolyBezier et 3 pour la fonction PolyBezierTo. Cette dernière utilise la position courante comme premier point de contrôle.
BOOL PolyBezier
(
HDC hdc, CONST POINT *
lppt, int
cPoints);
BOOL PolyBezierTo
(
HDC hdc, CONST POINT *
lppt, int
cCount);
IV-B-2. Rectangles, ellipses et polygones▲
Ces fonctions sont :
BOOL Rectangle
(
HDC hdc, int
x1, int
y1, int
x2, int
y2);
BOOL Ellipse
(
HDC hdc, int
x1, int
y1, int
x2, int
y2);
BOOL RoundRect
(
HDC hdc, int
x1, int
y1, int
x2, int
y2, int
nWidth, int
nHeight);
BOOL Polygon
(
HDC hdc, CONST POINT *
lppt, int
nCount);
La fonction RoundRect trace un rectangle aux coins arrondis en forme d'ellipse (plus précisément de quart d'ellipse …) dont les dimensions (largeur et hauteur) sont spécifiées via les paramètres nWidth et nHeight.
IV-B-3. Les objets graphiques▲
Les objets permettant de dessiner avec la GDI sont le crayon (pen), la brosse (brush), les polices de caractères (font) et les images bitmaps. Pour illustrer la manière de les utiliser, nous allons dessiner un rectangle avec une bordure rouge d'épaisseur 2 pixels et un fond bleu. Notre crayon sera donc rouge et épais de 2 pixels tandis que la brosse sera tout simplement de couleur bleue.
LRESULT CALLBACK WndProc
(
HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static
HDC hDC;
PAINTSTRUCT ps;
static
HPEN hPen;
static
HBRUSH hBrush;
switch
(
message)
{
case
WM_CREATE:
hDC =
GetDC
(
hwnd);
hPen =
CreatePen
(
PS_SOLID, 2
, RGB
(
255
, 0
, 0
));
SelectObject
(
hDC, hPen);
hBrush =
CreateSolidBrush
(
RGB
(
0
, 0
, 255
));
SelectObject
(
hDC, hBrush);
break
;
case
WM_PAINT:
BeginPaint
(
hwnd, &
ps);
Rectangle
(
hDC, 100
, 100
, 300
, 200
);
EndPaint
(
hwnd, &
ps);
break
;
case
WM_DESTROY:
ReleaseDC
(
hwnd, hDC);
DeleteObject
(
hPen);
DeleteObject
(
hBrush);
PostQuitMessage
(
0
);
break
;
default
:
return
DefWindowProc
(
hwnd, message, wParam, lParam);
}
return
0L
;
}
On commence tout d'abord par créer les objets graphiques dont on a besoin (CreatePen, CreateSolidBrush). Ensuite, on sélectionne ces objets pour que tous les dessins qu'on effectuera par la suite soient dessinés avec (SelectObject). Et bien sûr, lorsqu'on n'a plus besoin des objets qu'on a créés, il faut les détruire (DeleteObject).
La fonction :
HGDIOBJ SelectObject
(
HDC hdc, HGDIOBJ hgdiobj);
Sélectionne un nouvel objet et retourne le handle de l'ancien objet.
Pour créer un objet de dessin, on a toujours le choix entre deux méthodes :
- la méthode dite directe, qui se fait par l'intermédiaire d'une fonction recevant directement tous les paramètres nécessaires à la création de l'objet (ex. : CreatePen, CreateSolidBrush, etc.) ;
- la méthode dite indirecte, qui se fait par l'intermédiaire d'une fonction qui attend en argument l'adresse d'une structure décrivant l'objet que l'on veut créer (CreatePenIndirect, CreateBrushIndirect, etc.).
Ainsi l'exemple précédent aurait pu également s'écrire de la manière suivante :
LRESULT CALLBACK WndProc
(
HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static
HDC hDC;
PAINTSTRUCT ps;
static
HPEN hPen;
static
HBRUSH hBrush;
switch
(
message)
{
case
WM_CREATE:
{
LOGPEN pen;
LOGBRUSH brush;
hDC =
GetDC
(
hwnd);
/* Création et sélection du crayon */
pen.lopnColor =
RGB
(
255
, 0
, 0
);
pen.lopnStyle =
PS_SOLID;
pen.lopnWidth.x =
2
;
hPen =
CreatePenIndirect
(&
pen);
SelectObject
(
hDC, hPen);
/* Création et sélection de la brosse */
brush.lbColor =
RGB
(
0
, 0
, 255
);
brush.lbHatch =
0
;
brush.lbStyle =
BS_SOLID;
hBrush =
CreateBrushIndirect
(&
brush);
SelectObject
(
hDC, hBrush);
break
;
}
case
WM_PAINT:
{
BeginPaint
(
hwnd, &
ps);
Rectangle
(
hDC, 100
, 100
, 300
, 200
);
EndPaint
(
hwnd, &
ps);
break
;
}
case
WM_DESTROY:
{
ReleaseDC
(
hwnd, hDC);
DeleteObject
(
hPen);
DeleteObject
(
hBrush);
PostQuitMessage
(
0
);
break
;
}
default
:
{
return
DefWindowProc
(
hwnd, message, wParam, lParam);
}
}
return
0L
;
}
Contrairement à ce qu'on aurait pu s'attendre, le membre lopnWidth qui spécifie l'épaisseur du crayon n'est pas un entier, mais une structure de type POINT. Cependant, le membre y de cette structure n'est pas utilisé.
Pour les crayons, on a les styles PS_NULL, PS_SOLID, PS_DASH, PS_DOT, PS_DASHDOT et PS_DASHDOTDOT. Seul PS_SOLID accepte une épaisseur autre que 1. Lorsque le style est différent de PS_SOLID, et uniquement dans ce cas, vous pouvez utiliser la fonction SetBkColor pour choisir la couleur de fond (fond uni en dessous du coup de crayon).
Pour les brosses, les plus utilisés sont BS_SOLID et BS_HATCHED. Si le style BS_HATCHED est spécifié, alors on peut utiliser les constantes suivantes pour le paramètre lbHatch : HS_HORIZONTAL, HS_VERTICAL, HS_CROSS, HS_DIAGCROSS, HS_BDIAGONAL et HS_FDIAGONAL. Si vous voulez avoir à la fois une couleur de fond (fond uni en dessous du coup de brosse) et des hachures, et uniquement dans ce cas, alors vous pouvez utiliser la fonction SetBkColor pour choisir la couleur de fond.
La fonction SetBkMode permet de contrôler la couleur de fond des objets dessinés. Si le mode est OPAQUE, le fond sera coloré avec la couleur de fond courante (que l'on peut récupérer à l'aide de GetBkColor). Si le mode est TRANSPARENT, le fond est laissé tel quel (c'est-à-dire le même que celui de la fenêtre).
COLORREF SetBkColor
(
HDC hdc, COLORREF crColor);
COLORREF SetBkMode
(
HDC hdc, int
iBkMode);
Et enfin, sachez que vous pouvez également utiliser des objets prédéfinis (appelés objets en stock) comme la brosse blanche (WHITE_BRUSH), noire (BLACK_BRUSH) ou grise (GRAY_BRUSH), etc. ou encore le crayon noir (BLACK_PEN) ou blanc (WHITE_PEN), etc. à l'aide de la fonction GetStockObject.
HGDIOBJ GetStockObject
(
int
fnObject);
IV-B-4. Afficher du texte▲
La fonction la plus simple qui permette d'afficher du texte à l'intérieur d'une fenêtre est :
BOOL TextOut
(
HDC hdc, int
x, int
y, LPCTSTR lpString, int
cbString);
Le texte n'a donc pas à être une chaîne terminée par zéro puisque le paramètre cbString indique le nombre de caractères à afficher.
La fonction DrawText offre plus de contrôle
int
DrawText
(
HDC hdc, LPCTSTR lpString, int
nCount, LPRECT lpRect, UINT uFormat);
Le texte sera affiché à l'intérieur du rectangle pointé par lpRect. Les valeurs les plus utilisées dans uFormat sont DT_LEFT, DT_RIGHT, DT_CENTER, DT_BOTTOM, DT_TOP, DT_VCENTER et DT_SINGLELINE. On peut bien sûr utiliser un format combiné tel que DT_SINGLELINE | DT_CENTER | DT_VCENTER.
DrawTextEx permet de spécifier encore plus d'options. Et enfin, on a les fonctions SetTextColor, SetBkColor et SetBkMode pour modifier la couleur du texte, modifier la couleur de fond et régler la transparence du fond.
COLORREF SetTextColor
(
HDC hdc, COLORREF crColor);