Concepts avancés de la GDI

Le but de ce tutoriel est de montrer comment utiliser la GDI pour réaliser des applications au graphisme soutenu sans prétendre traiter toutes les possibilités de cette API. Il n'est pas non plus un tutoriel d'initiation à la GDI, cette dernière ayant déjà été présentée dans le tutoriel précédent (les bases de la programmation sous Windows).

Commentez cet article : Commentez Donner une note à l'article (5)

Article lu   fois.

L'auteur

Site personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction

Comme nous l'avons déjà dit dans le tutoriel précédent, la GDI (Graphics Device Interface) est une API (implémentée dans le fichier gdi32.dll) permettant de dessiner sur n'importe quel périphérique graphique de manière standard c'est-à-dire sans avoir à communiquer directement avec le pilote. En fait, GDI est aussi le nom de la composante du système (win32k.sys) qui gérait toutes les tâches liées au graphisme avant l'arrivée de Windows Vista. L'API "GDI", l'API que nous allons étudier ici (et que nous désignerons dans ce paragraphe par Win32 GDI), a été nommée ainsi car elle n'était au départ qu'une interface de programmation située juste au-dessus de la "vraie" (win32k.sys).

Depuis Windows Vista, DirectX (plus précisément le composant dxgkrnl.sys) a pris les responsabilités qui étaient pendant longtemps confiées la GDI en ce qui concerne la gestion de l'environnement graphique. En effet, toutes les APIs graphiques de Windows utilisent désormais DirectX pour effectuer le rendu graphique, pas la GDI. Parmi ces APIs, citons Win32 GDI/GDI+ (GDI+ est une version orientée objets et améliorée de Win32 GDI qui fut introduite avec Windows XP), Direct3D (D3D), Windows Presentation Foundation (WPF - une API essentiellement destinée aux applications managées) et Direct2D (D2D), une encaspulation de D3D introduite avec Windows 7 et destinée à remplacer en partie Win32 GDI/GDI+. La GDI continue et continuera cependant à gérer les contextes de périphériques. Dans le cas d'une fenêtre, un DC n'encapsule plus cependant directement l'écran comme au temps des anciennes versions mais la surface D3D utilisée. DirectX et la GDI communiquent en effet entre-elles pour permettre une certaine interopérabilité entre les différentes APIs de niveau application en faisant, entre-autres, le lien entre contexte de périphérique d'une fenêtre et surface D3D.

Malgré tout cela, la connaissance et la maîtrise de la GDI sont nécessaires à tout programmeur Windows pour au moins les raisons suivantes (les mêmes raisons pour lesquelles Microsoft continuera à la développer ou du moins la maintenir pour ecnore un bon bout de temps ...) :

  • Elle est portable entre les matériels. Contrairement à sa cousine DirectX, la GDI permet de dessiner sur n'importe quel périphérique (comme l'écran, l'imprimante, etc.), pas l'écran uniquement.
  • Elle est portable entres les différentes versions du système. Cette API a existé depuis Windows 1.0 et ce n'est pas encore dans un futur proche qu'elle commencera à être menacée de disparition.
  • Elle est simple à utiliser (très simple même dès qu'on la compare avec D2D/3D ...). Apprendre la GDI améliorera simplement et rapidement votre expérience et votre expertise en matière de programmation Windows.


Dans ce tutoriel, nous allons voir comment manipuler les polices de caractères, les images bitmaps, les régions et la transparence en utilisant la GDI.

II. Mapping Mode, Device Caps et polices de caractères

II-A. Le Mapping Mode

Le Mapping Mode définit les caractéristiques planimétriques du DC. Un mode est caractérisé par une unité de longueur (l'unité logique), l'orientation des axes x et y et le point Origine. Dans le mode par défaut, à savoir MM_TEXT, l'unité logique est le pixel et les axes sont orientés vers la droite pour l'axe des x et vers le bas pour l'axe des y. On peut changer de mode avec la fonction SetMapMode :

 
Sélectionnez
int SetMapMode(HDC hdc, int fnMapMode);

Le tableau suivant liste les différents modes existants.

Mode Unité logique Sens des x croissants Sens des y croissants
MM_TEXT 1 px Vers la droite Vers le bas
MM_LOMETRIC 0.1 mm Vers la droite Vers le haut
MM_HIMETRIC 0.01 mm Vers la droite Vers le haut
MM_LOENGLISH 0.01 '' Vers la droite Vers le haut
MM_HIENGLISH 0.001 '' Vers la droite Vers le haut
MM_TWIPS 1 / 1440 '' Vers la droite Vers le haut
MM_ISOTROPIC A définir A définir A définir
MM_ANISOTROPIC A définir A définir A définir


L'utilisation de cette fonction est un peu délicate. Cela vient notamment du fait que la fenêtre et le repère (qui intervient dans les fonctions de dessin ...) n'utilisent pas nécessairement le même mode. En effet, sauf si on a spécifié MM_ISOTROPIC ou MM_ANISOTROPIC, SetMapMode change uniquement le mode du repère et non celui de la fenêtre. Par défaut (et quel que soit le mode utilisé), le point origine de la fenêtre (Window Origin) est le coin supérieur gauche de la zone dessinable (c'est-à-dire la zone cliente ou la fenêtre toute entière même selon ce que vous permet votre DC ...) de la fenêtre. L'origine du repère (Viewport Origin) est, par défaut, confondu avec l'origine de la fenêtre mais on peut bien sûr la déplacer. Les fonctions permettant de connaître / déplacer l'origine de la fenêtre ou l'origine de du repère sont :

 
Sélectionnez
BOOL GetWindowOrgEx(HDC hdc, LPPOINT lpPoint);
BOOL GetViewportOrgEx(HDC hdc, LPPOINT lpPoint);
BOOL SetWindowOrgEx(HDC hdc, int x, int y, LPPOINT lpOldOrg);
BOOL SetViewportOrgEx(HDC hdc, int x, int y, LPPOINT lpOldOrg);

Toutes les distances sont exprimées en unités logiques de la fenêtre (je rappelle que le mode initial est toujours MM_TEXT). En général, lorsqu'on veut choisir une nouvelle origine, on modifie celle du repère plutôt que celle de la fenêtre, mais ce n'est évidemment pas une règle. L'extrait de code suivant montre un exemple d'utilisation du mode MM_LOMETRIC pour dessiner un cercle de 1 cm de rayon au centre de la fenêtre (plus précisément de la zone cliente).

 
Sélectionnez
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    static HINSTANCE hInstance;
    static HDC hDC;
    static POINT center;
    
    PAINTSTRUCT ps;
    
    switch(message)
    {
    case WM_CREATE:
        hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
        hDC = GetDC(hwnd);
        SetMapMode(hDC, MM_LOMETRIC);
        break;
        
    case WM_SIZE:
        /* Centrer le repère */
        center.x = LOWORD(lParam) / 2;
        center.y = HIWORD(lParam) / 2;
        SetViewportOrgEx(hDC, center.x, center.y, NULL);
        break;
        
    case WM_PAINT:
        BeginPaint(hwnd, &ps);
        /* Dans le mode MM_LOMETRIC, l'unité est le dixième de mm donc 100 correspond à 1 cm */
        Ellipse(hDC, -100, 100, 100, -100);
        EndPaint(hwnd, &ps);
        break;
        
    case WM_DESTROY:
        ReleaseDC(hwnd, hDC);
        PostQuitMessage(0);
        break;
        
    default:
        return DefWindowProc(hwnd, message, wParam, lParam);
    }
    
    return 0L;
}


Les modes MM_ISOTROPIC et MM_ANISOTROPIC permettent au programmeur de définir un mode personnalisé. Dans le mode MM_ISOTROPIC, l'unité de longueur suivant l'axe des x doit égal à l'unité de longueur suivant l'axe des y (comme tel est le cas dans les autres modes) quel que soit le point de vue (fenêtre ou repère) alors que dans le mode MM_ANISOTROPIC, on est libre de personnaliser à volonté. Les fonctions suivantes s'utilisent avec ces deux modes pour définir les unités de longueur et du coup l'orientation des axes (il suffit d'utiliser les nombres négatifs pour inverser l'orientation d'un axe) :

 
Sélectionnez
BOOL SetWindowExtEx(HDC hdc, int nXExtent, int nYExtent, LPSIZE lpOldExtent);
BOOL SetViewportExtEx(HDC hdc, int nXExtent, int nYExtent, LPSIZE lpOldExtent);

II-B. Les Device Caps

La fonction GetDeviceCaps permet d'obtenir des informations (telles que la version du pilote, le type de périphérique, etc.) sur un périphérique graphique.

 
Sélectionnez
int GetDeviceCaps(HDC hdc, int nIndex);
Paramètre (nIndex) Description
HORZRES Résolution horizontale (en pixels)
VERTRES Résolution verticale (en pixels)
LOGPIXELSX Résolution horizontale (en pixels / pouce)
LOGPIXELSY Résolution verticale (en pixels / pouce)


Pour récupérer la résolution de l'écran (en pixels), ou pourra également utiliser la fonction :

 
Sélectionnez
int GetSystemMetrics(int nIndex);

En passant en paramètre SM_CXSCREEN pour obtenir la résolution horizontale et SM_CYSCREEN pour obtenir la résolution verticale.

II-C. Les polices de caractères

Une police de caractères est un jeu de caractères et de symboles partageant un design commun. Par abus du langage, un élément d'une police est également appelé une police. Le paramètre qui différencie toute police de caractères donnée d'une autre est son nom (face name). Après avoir choisi une police, on peut généralement, si on le souhaite, ajuster d'autres paramètres comme sa taille ou son style par exemple. La structure LOGFONT permet de décrire une police de caractères.

 
Sélectionnez
typedef struct tagLOGFONT {
    LONG lfHeight;
    LONG lfWidth;
    LONG lfEscapement;
    LONG lfOrientation;
    LONG lfWeight;
    BYTE lfItalic;
    BYTE lfUnderline;
    BYTE lfStrikeOut;
    BYTE lfCharSet;
    BYTE lfOutPrecision;
    BYTE lfClipPrecision;
    BYTE lfQuality;
    BYTE lfPitchAndFamily;
    TCHAR lfFaceName[LF_FACESIZE];
} LOGFONT;

Le membre lfHeight spécifie la taille, en unités logiques, de la police de caractères. Or généralement, on souhaite plutôt la spécifier en points. Dans ce cas, il faut juste faire un petit calcul. En mode MM_TEXT par exemple, on a la formule :

 
Sélectionnez
nPixels = - (nPoints * GetDeviceCaps(hDC, LOGPIXELSY)) / 72

En effet 1 point = 1 / 72 pouce. Et comme l'axe des y est orienté vers le bas, GetDeviceCaps(hDC, LOGPIXELSY) retourne un nombre négatif, ce qui explique la présence du facteur -1. En fait ce -1 c'est seulement pour les matheux parce qu'en fait, il est tout à fait légitime de passer une valeur négative à lfHeight (auquel cas la valeur absolue sera prise en compte).

Le membre lfWidth spécifie la largeur moyenne des caractères, généralement définie comme étant la largeur du caractère 'x'. Il peut être tout simplement laissé à zéro. Le membre lfWeight spécifie l'épaisseur des caractères, il doit être compris entre 0 et 1000. On a également les constantes FW_DONTCARE (0), FW_THIN (100), FW_NORMAL (400), FW_BOLD (700), FW_HEAVY (900), etc.

Voici un exemple de création et d'utilisation d'une police de caractères

 
Sélectionnez
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    static HDC hDC;
    PAINTSTRUCT ps;
    
    static HFONT hFont;
    
    switch(message)
    {
        case WM_CREATE:
        {
            LOGFONT font;
            
            hDC = GetDC(hwnd);
            
            /* Création et sélection de la police */
            ZeroMemory(&font, sizeof(font));
            strcpy(font.lfFaceName, "Monotype Corsiva");
            font.lfHeight = - (24 * GetDeviceCaps(hDC, LOGPIXELSY)) / 72;
            font.lfUnderline = (BYTE)TRUE;
            
            hFont = CreateFontIndirect(&font);
            SelectObject(hDC, hFont);
            
            /* Couleur du texte */
            SetTextColor(hDC, RGB(255, 0, 0));
            
            break;
        }
        
        case WM_PAINT:
        {
            BeginPaint(hwnd, &ps);
            TextOut(hDC, 30, 30, "Hello world !", 13);
            EndPaint(hwnd, &ps);
            
            break;
        }
        
        case WM_DESTROY:
        {
            ReleaseDC(hwnd, hDC);
            DeleteObject(hFont);
            PostQuitMessage(0);
            
            break;
        }
        
        default:
        {
            return DefWindowProc(hwnd, message, wParam, lParam);
        }
    }
    
    return 0L;
}

Pour calculer la largeur et la hauteur d'une chaîne de caractères dans la police courante, on a la fonction :

 
Sélectionnez
BOOL GetTextEntentPoint32(HDC hdc, LPCTSTR lpString, int cbString, LPSIZE lpSize);

III. Les images bitmaps

III-A. Introduction

Une image bitmap (carte de bits) est une image constituée à partir d'un tableau de n lignes et m colonnes, chaque cellule du tableau contenant une information spécifiant la couleur du point correspondant. Un fichier bitmap (.bmp) est un fichier qui stocke une telle image, en la faisant précéder d'un en-tête indiquant des informations qu'il faut savoir concernant l'image (dimensions, nombre de bits de couleur, etc.) et le fichier lui-même (mot magique, offset à partir duquel commencent les bits de l'image, etc.). Si on connaît donc le format exact des fichiers bitmaps, on peut charger les bits constituant l'image pour en faire ensuite ce qu'on veut, comme afficher l'image par exemple. Mais la GDI possède des fonctions permettant de manipuler les images bitmaps sans avoir à connaître leur format. Dans ce tutoriel, ce sont ces fonctions que nous allons utiliser.

III-B. Charger une image puis l'afficher

Pour charger une image bitmap nous avons le choix entre LoadBitmap qui sait seulement charger une image depuis une ressource et LoadImage qui sait également charger une image depuis un fichier. Dans tous les cas, on obtient toujours en retour un handle d'un bitmap (HBITMAP).

 
Sélectionnez
HANDLE LoadImage( HINSTANCE hInstance, LPCTSTR lpszName, UINT uType,
                  int cxDesired, int cyDesired, UINT fuLoad );

hInstance spécifie évidemment le handle du module contenant la ressource à charger et si on passe une valeur différente de NULL, le paramètre lpszName doit spécifier le nom de l'image que l'on veut charger. Si hInstance vaut NULL et que fuLoad vaut LR_LOADFROMFILE, alors lpszName doit spécifier le nom du fichier à charger. Pour une image bitmap (uType = IMAGE_BITMAP), il est inutile de spécifier les paramètres cxDesired (largeur de l'image) et cyDesired (hauteur de l'image) puisque ces informations peuvent être lues dans le fichier. On peut donc tout simplement mettre 0.

Pour afficher l'image, le plus simple est d'utiliser la fonction DrawState.

 
Sélectionnez
BOOL DrawState( HDC hdc, HBRUSH hBrush, DRAWSTATEPROC lpDstProc,
                LPARAM lData, WPARAM wData, int x, int y, int cx, int cy,
                UINT fuFlags );

Dans le cas d'une image bitmap (fuFlags = DST_BITMAP), le paramètre lData doit spécifier le handle du bitmap qu'on veut afficher. Les paramètres cx et cy permettent de spécifier respectivement la largeur et la hauteur de l'image mais bien entendu, DrawState sait récupérer ces informations sans qu'on ait à les lui fournir nous-mêmes. On peut donc ici aussi passer tout simplement 0.

 
Sélectionnez
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    static HDC hDC;
    PAINTSTRUCT ps;
    
    static HBITMAP hBitmap;
    
    switch(message)
    {
        case WM_CREATE:
        {
            hDC = GetDC(hwnd);
            hBitmap = LoadImage(NULL, "c:\\image.bmp", IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE);
            
            break;
        }
        
        case WM_PAINT:
        {
            BeginPaint(hwnd, &ps);
            DrawState(hDC, NULL, NULL, (LPARAM)hBitmap, 0, 20, 20, 0, 0, DST_BITMAP);
            EndPaint(hwnd, &ps);
            
            break;
        }
        
        case WM_DESTROY:
        {
            ReleaseDC(hwnd, hDC);
            DeleteObject(hBitmap);
            PostQuitMessage(0);
            
            break;
        }
        
        default:
        {
            return DefWindowProc(hwnd, message, wParam, lParam);
        }
    }
    
    return 0L;
}

III-C. Afficher une image par copie de bits

III-C-1. L'astuce

Il s'agit d'une technique simple, élégante et puissante pour afficher une image bitmap. Elle consiste à créer un DC en mémoire dont la seule contrainte est d'avoir les mêmes caractéristiques que le DC de destination, de sélectionner l'image sur ce DC (le DC en mémoire), et de copier tous les bits du DC en mémoire vers le DC de destination à l'aide de la fonction BitBlt (Bit-Block Transfert).

III-C-2. Les fonctions

La fonction :

 
Sélectionnez
HDC CreateCompatibleDC(HDC hdc);

permet de créer un DC en mémoire possédant les mêmes caractéristiques que le DC spécifié en argument. Sa surface sera initialement restreinte à un tout petit bitmap monochrome de 1 px de largeur et de hauteur. Donc avant de dessiner sur un DC en mémoire, il faut lui octroyer une surface assez grande pour contenir tout notre dessin en sélectionnant une image bitmap. Une fois qu'on a sélectionné un bitmap, grâce à la fonction SelectObject, le DC devient aussi grand que l'image sélectionnée. On peut désormais dessiner tout ce qu'on veut à l'intérieur de cette surface, comme avec n'importe quel DC. Il suffit ensuite de copier l'image pixel par pixel, du DC en mémoire vers un autre DC (associé à une fenêtre par exemple), à l'aide de la fonction BitBlt (voir aussi : StretchBlt).

 
Sélectionnez
BOOL BitBlt( HDC hdcDest, int xDest, int yDest, int nWidth, int nHeight,
             HDC hdcSource, int xSource, int ySource, DWORD dwOperation );

Dans le cas d'une opération de copie, la valeur du paramètre dwOperation doit être égale à SRCCOPY. Les paramètres nWidth et nHeight spécifient respectivement la largeur et la hauteur de l'image à copier. La fonction GetObject permet de récupérer des informations sur un objet graphique dont le handle est passé en argument.

 
Sélectionnez
int GetObject(HGDIOBJ hgdiobj, int cbObjectInfo, LPVOID lpObjectInfo);

Si hgdiobj est un handle d'un crayon (HPEN), lpObjectInfo doit pointer sur une structure de type LOGPEN. Si c'est un handle d'une police (HFONT), lpObjectInfo doit pointer sur une structure de type LOGFONT. Et ainsi de suite. Dans tous les cas, cbObjectInfo doit indiquer la taille de l'objet pointé par lpObjectInfo. Pour un bitmap, cet objet doit être une structure de type BITMAP. En ce qui nous concerne, seuls les membres bmWidth et bmHeight de cette structure, qui spécifient respectivement la largeur et la hauteur de l'image, nous intéressent.

Et enfin, il ne faut pas oublier de détruire le DC en mémoire avec DeleteDC lorsqu'on n'en a plus besoin.

III-C-3. Exemple

 
Sélectionnez
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    static HDC hDC, hdcMem;
    PAINTSTRUCT ps;
    
    static HBITMAP hBitmap;
    static BITMAP bitmap;
    
    switch(message)
    {
        case WM_CREATE:
        {
            /* Chargement de l'image */
            hBitmap = LoadImage(NULL, "c:\\image.bmp", IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE);
            GetObject(hBitmap, sizeof(bitmap), &bitmap);
            
            /* DC de la fenêtre */
            hDC = GetDC(hwnd);
            
            /* DC en mémoire */
            hdcMem = CreateCompatibleDC(hDC);
            SelectObject(hdcMem, hBitmap);
            
            break;
        }
        
        case WM_PAINT:
        {
            BeginPaint(hwnd, &ps);
            BitBlt(hDC, 20, 20, bitmap.bmWidth, bitmap.bmHeight, hdcMem, 0, 0, SRCCOPY);
            EndPaint(hwnd, &ps);
            
            break;
        }
        
        case WM_DESTROY:
        {
            ReleaseDC(hwnd, hDC);
            DeleteDC(hdcMem);
            DeleteObject(hBitmap);
            PostQuitMessage(0);
            
            break;
        }
        
        default:
        {
            return DefWindowProc(hwnd, message, wParam, lParam);
        }
    }
    
    return 0L;
}

III-D. Les masques bitmaps

Un masque est un objet qui permet de cacher certaines parties d'un dessin. Par exemple, supposons qu'on ait d'un côté un dessin sur papier et de l'autre côté un carton de même dimension que le papier, comportant un trou au milieu. Si on place le carton au dessus du papier, on ne voit plus que la partie du dessin que nous laisse voir le trou. Le papier représente l'image et le carton le masque. En masquant l'image avec le masque, on obtient une nouvelle image (le résultat du masquage).

La technique de masquage consiste à réaliser des opérations bit à bit avec les bits des couleurs de l'image à masquer et ceux du masque. En RGB, le code de la couleur noir est 0 partout et celui de la couleur blanc des 1 partout. Ces couleurs sont donc très pratiques pour contrôler les parties d'une image qu'on veut cacher ou afficher. La fonction BitBlt nous sera ici encore d'une grande utilité. En spécifiant SRCAND dans le paramètre dwOperation par exemple, cette fonction permet de mélanger les couleurs de la source et de la destination à l'aide de l'opérateur ET. Avec SRCPAINT les couleurs sont mélangées en utilisant l'opérateur OU. SRCCOPY, comme nous l'avons déjà vu un peu plus haut, permet de copier la source en écrasant la destination. On a aussi NOTSRCCOPY qui permet de copier la source avec tous les bits inversés. Et il y en a encore d'autres. Par exemple, pour afficher une image à l'intérieur d'une ellipse, voici comment on fait :

  1. On charge l'image qu'on veut afficher, on crée un DC en mémoire compatible avec le DC de destination puis on sélectionne l'image sur ce DC. Maintenant, l'image n'attend donc plus qu'à être masquée.

  2. On crée un bitmap vierge compatible avec le DC cible (celui de la fenêtre) de même type et de même dimension que l'image à masquer (CreateCompatibleBitmap) ainsi qu'un nouveau DC en mémoire. On sélectionne bien sûr le bitmap sur ce DC, ensuite on dessine un rectangle noir puis une ellipse en blanc. Le masque est alors prêt.

  3. Il ne nous reste plus qu'à masquer l'image avec l'opération ET. On peut maintenant afficher l'image résultante.

Si on affiche l'image, on aura un rectangle noir et une ellipse affichant une partie de l'image qu'on a masquée. Or le plus souvent, on ne veut pas avoir ce rectangle noir (ni un rectangle blanc ...). Ce qu'on veut, c'est une ellipse affichant une partie de l'image masquée et le fond de la fenêtre, et rien de plus. Pour avoir le résultat voulu, on affichera donc l'image à l'aide de la méthode suivante :

  1. On inverse les bits du masque de façon à avoir un rectangle blanc et une ellipse en noir.

  2. On copie l'image ainsi obtenue vers le DC de la fenêtre en appliquant l'opération ET. On a donc maintenant une ellipse en noir dessinée sur le fond de la fenêtre. Tout ce qui était en blanc a disparu grâce au ET.

  3. On copie enfin l'image masquée (le rectangle noir avec une ellipse affichant une partie de l'image originale) vers le DC de la fenêtre à l'aide de l'opération OU.

Voici un exemple de code :

 
Sélectionnez
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    static HDC hDC, hdcImage, hdcMasque;
    static HBITMAP hImage, hMasque;
    static BITMAP bmImage;
    
    PAINTSTRUCT ps;
    
    switch(message)
    {
        case WM_CREATE:
        {
            hDC = GetDC(hwnd);
            
            /* On prépare l'image */
            hdcImage = CreateCompatibleDC(hDC);
            hImage = LoadImage(NULL, "c:\\image.bmp", IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE);
            
            SelectObject(hdcImage, hImage);
            
            GetObject(hImage, sizeof(bmImage), &bmImage);
            
            /* On prépare le masque */
            hdcMasque = CreateCompatibleDC(hDC);
            hMasque = CreateCompatibleBitmap(hDC, bmImage.bmWidth, bmImage.bmHeight);
            SelectObject(hdcMasque, hMasque);
            
            SelectObject(hdcMasque, GetStockObject(BLACK_BRUSH));
            Rectangle(hdcMasque, 0, 0, bmImage.bmWidth - 1, bmImage.bmHeight - 1);
            
            SelectObject(hdcMasque, GetStockObject(WHITE_BRUSH));
            Ellipse(hdcMasque, 0, 0, bmImage.bmWidth - 1, bmImage.bmHeight - 1);
            
            /* On masque l'image */
            BitBlt(hdcImage, 0, 0, bmImage.bmWidth, bmImage.bmHeight, hdcMasque, 0, 0, SRCAND);
            
            /* Rectangle blanc + ellipse noire */
            BitBlt(hdcMasque, 0, 0, bmImage.bmWidth, bmImage.bmHeight, hdcMasque, 0, 0, NOTSRCCOPY);
            
            break;
        }
        
        case WM_PAINT:
        {
            BeginPaint(hwnd, &ps);
            
            BitBlt(hDC, 0, 0, bmImage.bmWidth, bmImage.bmHeight, hdcMasque, 0, 0, SRCAND);
            BitBlt(hDC, 0, 0, bmImage.bmWidth, bmImage.bmHeight, hdcImage, 0, 0, SRCPAINT);
            
            EndPaint(hwnd, &ps);
            
            break;
        }
        
        case WM_DESTROY:
        {
            DeleteObject(hMasque);
            DeleteObject(hImage);
            DeleteDC(hdcMasque);
            DeleteDC(hdcImage);
            ReleaseDC(hwnd, hDC);
            PostQuitMessage(0);
            
            break;
        }
        
        default:
        {
            return DefWindowProc(hwnd, message, wParam, lParam);
        }
    }
    
    return 0L;
}

IV. Les rectangles et les régions

IV-A. Les rectangles

Un rectangle est tout simplement un objet représenté par une structure de type RECT définie comme suit :

 
Sélectionnez
typedef struct _RECT {
    LONG left;
    LONG top;
    LONG right;
    LONG bottom;
} RECT;

Les rectangles sont utilisés dans de nombreuses fonctions comme AdjustWindowRect, InvalidateRect ou encore GetClientRect mais ce qu'il faut également connaître, c'est qu'on peut faire des opérations (intersection, union, etc.) sur les rectangles. Dans ce paragraphe, nous n'allons cependant pas parler de ces fonctions mais plutôt des fonctions permettant de les dessiner.

Nous avons déjà vu la fonction Rectangle qui permet de dessiner un rectangle mais on a également les fonctions FillRect et FrameRect qui acceptent un pointeur vers un rectangle en argument.

 
Sélectionnez
int FillRect(HDC hDC, CONST RECT * lpRect, HBRUSH hBrush);
int FrameRect(HDC hDC, CONST RECT * lpRect, HBRUSH hBrush);

Ces fonctions permettent de dessiner un rectangle en utilisant la brosse spécifiée en argument. FillRect dessine un rectangle plein alors que FrameRect rectangle dessine le périmètre uniquement avec une épaisseur de 1 unité logique. Remarquez bien que FrameRect utilise une brosse et non un crayon pour dessiner le rectangle. A noter également que FillRect n'inclut pas les côtés droit et bas du rectangle spécifié (donc remplir le fond de la zone cliente de votre fenêtre, vous appelez GetClientRect puis passez directement le rectangle retourné à FillRect).

IV-B. Les régions

IV-B-1. Présentation

Une région est un objet (qui peut être un rectangle, une ellipse, etc.) que l'on peut dessiner mais aussi et surtout utiliser à d'autres fins. Une région peut être créée à l'aide d'une variété de fonctions, qui retournent toutes un handle d'une région (HRGN), dont voici les plus utilisées :

 
Sélectionnez
HRGN CreateRectRgn(int left, int top, int right, int bottom);
HRGN CreateEllipticRgn(int left, int top, int right, int bottom);
HRGN CreateRoundRectRgn(int left, int top, int right, int bottom, int nWidthEllipse, int nHeightEllipse);
HRGN CreatePolygonRgn(CONST POINT * lppt, int cPoints, int fnPolyFillMode);
HRGN CreateRectRgnIndirect(CONST RECT * lpRect);
HRGN CreateEllipticRgnIndirect(CONST RECT * lpRect);

Les coordonnées sont relatives au coin supérieur gauche de la fenêtre (il s'agit bien de la fenêtre, pas de la zone cliente). Les rectangles n'incluent pas les côtés droit et bas.

Après avoir créé des régions, on peut les combiner à l'aide de la fonction :

 
Sélectionnez
int CombineRgn(HRGN hrgnDest, HRGN hrgnSrc1, HRGN hrgnSrc2, int fnCombineMode);

Le paramètre fnCombineMode permet de spécifier la manière dont comment on veut combiner les régions. On a le choix entre RGN_COPY (dans ce cas le paramètre hrgnSrc2 est ignoré), RGN_AND, RGN_OR, RGN_DIFF et RGN_XOR.

Et enfin on peut dessiner une région (usuellement avec FillRgn), déplacer une région (OffsetRgn), restreindre la partie dessinable d'un DC à une région (SelectObject), restreindre la partie visible d'une fenêtre à une région (SetWindowRgn), etc.

 
Sélectionnez
BOOL OffsetRgn(HRGN hRgn, int dx, int dy);
int FillRgn(HDC hDC, HRGN hRgn, HBRUSH hBrush);
int SetWindowRgn(HWND hWnd, HRGN hRgn, BOOL bRedraw);

Dans SetWindowRgn, le paramètre bRedraw doit être mis à TRUE pour redessiner la fenêtre après avoir appliqué la nouvelle région. Ce paramètre peut donc être FALSE pour une fenêtre cachée mais est souvent positionné à TRUE pour une fenêtre visible.

Lorsqu'on n'en a plus besoin, il ne faut pas oublier de détruire la région avec DeleteObject. Le paragraphe suivant montre comment utiliser les régions pour créer des fenêtres non-rectangulaires.

IV-B-2. Application à la création de fenêtres non rectangulaires

IV-B-2-a. Création de la fenêtre

Il suffit généralement de créer une fenêtre popup (sans barre de titre et tout ça), de créer une région de la forme qu'on veut donner à la fenêtre et de limiter la partie visible de la fenêtre à cette région. Attention ! comme notre fenêtre ne comportera pas de barre de titre, de bouton fermer, etc., nous devons penser à un système permettant à l'utilisateur de la fermer avant de se lancer dans la programmation. Comme nous n'avons pas encore étudié les contrôles (les boutons et tout ça ...), nous allons tout simplement spécifier le style WS_SYSMENU dans CreateWindow afin qu'on puisse fermer la fenêtre via le menu système, accessible en faisant un clic droit dans le bouton représentant la fenêtre dans la barre des tâches.

Voici un programme qui crée une fenêtre en forme de rectangle aux coins arrondis avec un fond coloré en noir.

 
Sélectionnez
#include <windows.h>

#define CORNER_R 24 /* Rayon du cercle à utiliser pour arrondir les coins de notre rectangle. */

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
void OnCreate(HWND hwnd);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    WNDCLASS wc;
    HWND hWnd;
    MSG msg;
    
    wc.cbClsExtra     = 0;
    wc.cbWndExtra     = 0;
    wc.hbrBackground  = GetStockObject(BLACK_BRUSH);
    wc.hCursor        = LoadCursor(NULL, IDC_ARROW);
    wc.hIcon          = LoadIcon(NULL, IDI_APPLICATION);
    wc.hInstance      = hInstance;
    wc.lpfnWndProc    = WndProc;
    wc.lpszClassName  = "Classe 1";
    wc.lpszMenuName   = NULL;
    wc.style          = CS_HREDRAW | CS_VREDRAW;

    RegisterClass(&wc);

    hWnd = CreateWindow("Classe 1",
                        "Fenêtre non rectangulaire",
                        WS_POPUP | WS_SYSMENU,
                        100, 100, 600, 300,
                        NULL,
                        NULL,
                        hInstance,
                        NULL);

    ShowWindow(hWnd, nCmdShow);

    while (GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return (int)msg.wParam;
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    LRESULT ret = 0L;

    switch(message)
    {
    case WM_CREATE:
        OnCreate(hwnd);
        break;

    case WM_DESTROY:
        PostQuitMessage(0);
        break;

    default:
        ret = DefWindowProc(hwnd, message, wParam, lParam);
    }

    return ret;
}

void OnCreate(HWND hwnd)
{
    RECT r;
    HRGN hRgn;

    GetClientRect(hwnd, &r);
    hRgn = CreateRoundRectRgn(r.left, r.top, r.right, r.bottom, 2 * CORNER_R, 2 * CORNER_R);

    SetWindowRgn(hwnd, hRgn, FALSE); /* On peut mettre bRedraw a FALSE car, */
    /* étant dans le traitement du message WM_CREATE, on est sûr que        */
    /* la fenêtre n'a pas encore été affichée.                              */
    
    DeleteObject(hRgn);
}

IV-B-2-b. Donner à l'utilisateur la possibilité de déplacer la fenêtre

Le problème avec les fenêtres sans barre de titre comme celle qu'on vient de créer, c'est qu'elles doivent implémenter elles-mêmes le support des déplacements demandées par l'utilisateur. En effet, l'utilisateur est censé utiliser la barre de titre pour déplacer une fenêtre or la nôtre n'en a pas. Nous devons donc chercher une solution à ce problème. La plus simple, s'est de s'accorder qu'on va rendre ce déplacement possible en autorisant l'utilisateur à commander le déplacement depuis n'importe quelle position dans la zone cliente. Comment faire ?

Chaque fois qu'un événement provenant de la souris (déplacement, bouton enfoncé, bouton relâché), le message WM_NCHITTEST est envoyé à la fenêtre qui doit traiter l'événement. Lorsqu'on a affaire à ce message, wParam ne contient aucune information et lParam les coordonnées (relatives au point supérieur gauche de l'écran) de la position qu'avait le curseur au moment où l'événement s'est produit. DefWindowProc traite ce message en retournant un entier indiquant quelle partie de la fenêtre était en dessous du curseur au moment de l'événement. Par exemple, HTCAPTION est retourné pour dire que le curseur était dans la barre de titre, HTCLIENT pour dire qu'il était dans la zone cliente, etc.

Habituellement (c'est-à-dire lorsqu'on utilise des fenêtres "normales"), on traite ce message en retournant tout simplement la valeur retournée par DefWindowProc. Windows utilise cette valeur pour effectuer une action particulière sur la fenêtre si besoin est. Par exemple, si la valeur retournée par le dernier traitement de WM_NCHITTEST est HTCAPTION et qu'ensuite l'utilisateur appuie sur le bouton gauche de la souris puis déplace la souris pendant que le bouton gauche est encore maintenu enfoncé, Windows conclut que l'utilisateur veut déplacer et la fenêtre et va donc satisfaire la requête. Donc si voulons que l'utilisateur puisse commander le déplacement de la fenêtre depuis n'importe quelle position dans la zone cliente, il suffit de retourner HTCAPTION (pour dire que la souris est dans la barre de trire) chaque fois que DefWindowProc retourne HTCLIENT dans le traitement du message WM_NCHITTEST. Voici donc notre nouvelle procédure de fenêtre :

 
Sélectionnez
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    LRESULT ret = 0L;

    switch(message)
    {
    case WM_CREATE:
        OnCreate(hwnd);
        break;

    case WM_NCHITTEST:
        ret = DefWindowProc(hwnd, message, wParam, lParam);

        if (ret == HTCLIENT)
            ret = HTCAPTION;

        break;

    case WM_DESTROY:
        PostQuitMessage(0);
        break;

    default:
        ret = DefWindowProc(hwnd, message, wParam, lParam);
    }

    return ret;
}

IV-B-2-c. Donner à l'utilisateur la possibilité de redimensionner la fenêtre

Tout d'abord, pour que la fenêtre soit redimensionnable, il faut qu'elle possède le style WS_SIZEBOX. Le problème c'est que ce style inclut le style WS_BORDER donc il faut aussi penser à cacher ces bordures. Pour gérer le redimensionnement de la fenêtre, il suffit de traiter convenablement le message WM_NCHITTEST comme on l'avait déjà fait pour rendre possible son déplacement. Par contre, nous devons afficher une bordure autour de notre fenêtre pour indiquer à l'utilisateur qu'elle peut être redimensionnée.

Voici un exemple de programme qui met en oeuvre toutes ces techniques :

 
Sélectionnez
#include <windows.h>

#define CORNER_R 24 /* Rayon du cercle à utiliser pour arrondir les coins de notre rectangle. */
#define BORDER_W  4 /* Epaisseur de la bodure que nous allons dessiner. */

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
void OnSize(HWND hwnd, int width, int height);
void OnPaint(HWND hwnd);
LRESULT OnNcHitTest(HWND hwnd, INT x, INT y, LRESULT ret);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    WNDCLASS wc;
    HWND hWnd;
    MSG msg;
    
    wc.cbClsExtra     = 0;
    wc.cbWndExtra     = 0;
    wc.hbrBackground  = GetStockObject(BLACK_BRUSH);
    wc.hCursor        = LoadCursor(NULL, IDC_ARROW);
    wc.hIcon          = LoadIcon(NULL, IDI_APPLICATION);
    wc.hInstance      = hInstance;
    wc.lpfnWndProc    = WndProc;
    wc.lpszClassName  = "Classe 1";
    wc.lpszMenuName   = NULL;
    wc.style          = CS_HREDRAW | CS_VREDRAW;

    RegisterClass(&wc);

    hWnd = CreateWindow("Classe 1",
                        "Fenêtres et régions",
                        WS_POPUP | WS_SIZEBOX | WS_SYSMENU,
                        100, 100, 600, 300,
                        NULL,
                        NULL,
                        hInstance,
                        NULL);

    ShowWindow(hWnd, nCmdShow);

    while (GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return (int)msg.wParam;
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    LRESULT ret = 0L;

    switch(message)
    {
    case WM_SIZE:
        OnSize(hwnd, LOWORD(lParam), HIWORD(lParam));
        break;

    case WM_PAINT:
        OnPaint(hwnd);
        break;

    case WM_NCHITTEST:
        ret = DefWindowProc(hwnd, message, wParam, lParam);
        ret = OnNcHitTest(hwnd, LOWORD(lParam), HIWORD(lParam), ret);
        break;

    case WM_DESTROY:
        PostQuitMessage(0);
        break;

    default:
        ret = DefWindowProc(hwnd, message, wParam, lParam);
    }

    return ret;
}

void OnSize(HWND hwnd, int width, int height)
{
    int cxborder, cyborder, x1, y1, x2, y2;
    HRGN hRgn;

    cxborder = GetSystemMetrics(SM_CXSIZEFRAME); /* Largeur d'une bordure de redimensionnement horizontal. */
    cyborder = GetSystemMetrics(SM_CYSIZEFRAME); /* Hauteur d'une bordure de redimensionnement vertical. */

    x1 = cxborder; /* Abscisse du coin supérieur gauche de la zone cliente. */
    y1 = cyborder; /* Ordonnée du coin supérieur gauche de la zone cliente. */
    x2 = x1 + width; /* Abscisse du coin inférieur droit de la zone cliente + 1. */
    y2 = y1 + height; /* Ordonnée du coin inférieur droit de la zone cliente + 1. */

    hRgn = CreateRoundRectRgn(x1, y1, x2, y2, 2 * CORNER_R, 2 * CORNER_R);

    SetWindowRgn(hwnd, hRgn, TRUE);

    DeleteObject(hRgn);
}

void OnPaint(HWND hwnd)
{
    HDC hDC;
    PAINTSTRUCT ps;
    RECT r;

    GetClientRect(hwnd, &r);

    r.left = BORDER_W;
    r.top = BORDER_W;
    r.right -= BORDER_W - 1;
    r.bottom -= BORDER_W - 1;

    hDC = BeginPaint(hwnd, &ps);
    RoundRect(hDC, r.left, r.top, r.right, r.bottom, 2 * (CORNER_R - BORDER_W), 2 * (CORNER_R - BORDER_W));
    EndPaint(hwnd, &ps);
}

LRESULT OnNcHitTest(HWND hwnd, INT x, INT y, LRESULT ret)
{
    POINT P = {x, y};
    RECT r;

    ScreenToClient(hwnd, &P);
    x = P.x;
    y = P.y;

    GetClientRect(hwnd, &r);

    if (ret == HTCLIENT)
    {
        if (x >= r.right - BORDER_W)
            ret = HTRIGHT;
        else if (y < BORDER_W)
            ret = HTTOP;
        else if (x < BORDER_W)
            ret = HTLEFT;
        else if (y >= r.bottom - BORDER_W)
            ret = HTBOTTOM;
        else if (y < CORNER_R / 2 && x >= r.right - CORNER_R / 2)
            ret = HTTOPRIGHT;
        else if (y < CORNER_R / 2 && x < CORNER_R / 2)
            ret = HTTOPLEFT;
        else if (y >= r.bottom - CORNER_R / 2 && x < CORNER_R / 2)
            ret = HTBOTTOMLEFT;
        else if (y >= r.bottom - CORNER_R / 2 && x >= r.right - CORNER_R / 2)
            ret = HTBOTTOMRIGHT;
        else
            ret = HTCAPTION;
    }

    return ret;
}

V. La transparence

V-A. Bitmaps avec un fond transparent

Les images bitmaps en elles-même ne gèrent pas la transparence. De plus, une image bitmap est toujours rectangulaire. Le rectangle qui contient toute l'image est appelé le fond, la couleur de ce rectangle est donc évidemment ce qu'on appelle la couleur de fond de l'image. Si cette couleur est utilisée pour et uniquement pour le fond, alors on peut imaginer une multitude de techniques permettant d'afficher l'image sans le fond, comme si le fond de l'image était réellement transparent. Depuis Windows 98 et 2000, Microsoft a introduit la fonction TransparentBlt qui fait déjà tout le boulot cependant, son implémenation sous Win9x contient, hélas, une fuite de mémoire ce qui fait que, finalement, cette fonction ne devrait être utilisée que sous Windows 2000 et plus récents. Pour l'utiliser, il faut se lier avec msimg32.lib.

 
Sélectionnez
BOOL TransparentBlt( HDC hdcDest, int xDest, int yDest, int nWidthDest, int nHeightDest,
                     HDC hdcSrc, int xSrc, int ySrc, int nWidthSc, int nHeightSrc,
                     UINT crTransparent );

Où crTransparent spécifie bien sûr la couleur de transparence (dans notre cas, la couleur de fond).

V-B. Les Layered Windows

Comme nous l'avons déjà vu plus haut la fonction SetWindowRgn permet de limiter la partie visible d'une fenêtre à une région donnée ce qui permet déjà de créer des fenêtres non rectangulaires. Mais parfois, on veut faire encore plus compliqué, comme par exemple créer une fenêtre ajustée exactement au contour d'une image (en excluant le fond). Cela est possible grâce à la fonction SetLayeredWindowAttributes, disponible qu'à partir de Windows 2000. Pour l'utiliser, il faut définir la macro _WIN32_WINNT à 0x0500 avant d'inclure windows.h. Cette macro permet d'inclure ou d'exclure certaines définitions et/ou déclarations en fonction de la configuration requise par l'application. 0x0500 signifie que l'application requiert la version 5.0 de Window NT (c'est-à-dire Windows 2000). Donc vous l'avez maintenant compris, la macro _WIN32_WINNT permet d'inclure certaines définitions et/ou déclarations de fonctionnalités spécifiques à Windows NT (0x0500 pour Windows 2000, 0x0501 pour XP, 0x0600 pour Vista, 0x0601 pour Windows 7, etc.) et d'exclure les définitions et/ou déclarations incompatibles ou non supportées par la version spécifiée. Mais il y en a également d'autres comme _WIN32_WINDOWS qui permet de choisir la version de Windows 3.x ou 9x et WINVER qui permet de contrôler les définitions et/ou déclarations communes à toutes les versions de Windows. Bien entendu, si une application utilise des fonctionnalités spécifiques à Windows NT, elle ne pourra pas fonctionner sous Windows 3.x ou 9x et vice versa.

 
Sélectionnez
BOOL SetLayeredWindowAttributes(HWND hWnd, COLORREF crKey, BYTE bAlpha, DWORD dwFlags);

Le paramètre crKey permet de spécifier la couleur de transparence. Toute partie de la fenêtre peinte avec cette couleur deviendra transparente. Lorsque ce paramètre est utilisé, il faut l'indiquer dans dwFlags en spécifiant la valeur LWA_COLORKEY. Si le paramètre bAlpha est utilisé (il s'agit d'une valeur n'utilisant que 8 bits donc les valeurs possibles vont de 0 (fenêtre invisible) à 255 (fenêtre opaque)), LWA_ALPHA doit être également spécifié. A noter que les deux paramètres (crKey et bAlpha) sont indépendants (on peut donc utiliser l'un sans l'autre).

Cette fonction ne peut être utilisée que sur des fenêtres possédant le style étendu WS_EX_LAYERED.

VI. Remerciements

Un grand merci à vicenzo pour ses conseils et son soutien.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

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 © 2008 Melem. 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. Droits de diffusion permanents accordés à Developpez LLC.