IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

La programmation de l'interface utilisateur


précédentsommairesuivant

I. Les contrôles

I-A. Vue d'ensemble

I-A-1. Généralités

Nous avons déjà vu que la création d'une fenêtre se fait toujours à partir d'une classe de fenêtre existante. Sous Windows, le concept de fenêtre est assez large. En effet, une fenêtre peut désigner aussi bien une fenêtre (telle que nous l'entendons le plus souvent) qu'un contrôle (bouton, zone de texte, etc.) ou même tout et rien ! Il y a deux grandes catégories de contrôles : les contrôles standard, inhérents à Windows et les contrôles communs, spécifiques de chaque version, implémentés respectivement dans user32.dll et comctl32.dll.

Pour un contrôle, le style WS_CHILD est donc obligatoire. La plupart du temps on spécifie également le style WS_VISIBLE pour que le contrôle soit initialement visible. Et enfin, un contrôle n'a pas de menu, à la place on spécifie un entier (un cast est donc nécessaire …) qui nous permettra de le référencer. Cet entier est appelé l'ID (identifier) du contrôle. Chaque fois qu'un contrôle ou un menu est titillé par l'utilisateur, celui-ci envoie le message WM_COMMAND à sa fenêtre parent et place :

  • son ID dans le mot de poids faible de wParam ;
  • son handle dans lParam ;
  • l'événement qui s'est produit (notification) dans le mot de poids fort de wParam.

En fait pour les contrôles communs, c'est le message WM_NOTIFY qui est envoyé à la place du message WM_COMMAND. La structure de ce message est la suivante :

  • dans wParam : l'ID du contrôle ayant envoyé le message ;
  • dans lParam : l'adresse d'une structure de type NMHDR (pour Notification Message Header) fournissant des informations supplémentaires sur le message.

 
Sélectionnez
typedef struct tagNMHDR {
    HWND hwndFrom; /* Handle du contrôle ayant envoyé le message */
    UINT idFrom; /* ID du contrôle */
    UINT code; /* Raison de ce message */
} NMHDR;


La communication avec les contrôles se fait largement via les messages. La fonction SendMessage permet d'envoyer un message à une fenêtre quelconque (donc y compris les contrôles …) tandis que SendDlgItemMessage n'est utilisable que pour les contrôles.

 
Sélectionnez
LRESULT SendMessage(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
LRESULT SendDlgItemMessage(HWND hwndParent, int nCtrlID, UINT message, WPARAM wParam, LPARAM lParam);

Mais sachez qu'il existe des fonctions et/ou macros qui encapsulent un ou plusieurs SendMessages dans le but d'améliorer la lisibilité du code. En général, les fonctions sont déclarées dans winuser.h (inclus dans windows.h) et les macros dans windowsx.h (qu'il faut inclure après windows.h).

Et pour terminer avec les généralités, sachez que connaissant l'ID d'un contrôle, on peut récupérer son handle et vice versa.

 
Sélectionnez
HWND GetDlgItem(HWND hwndParent, int nCtrlID);
int GetDlgCtrlID(HWND hCtrl);

I-A-2. Les classes de fenêtre prédéfinies

Les classes de fenêtre suivantes sont définies pour toutes les applications fenêtrées et peuvent donc être utilisées pour créer un contrôle (elles sont enregistrées avant même que WinMain ne soit appelée).

Nom

Classe

« BUTTON »

Bouton

« COMBOBOX »

Zone de liste combinée

« EDIT »

Contrôle permettant à l'utilisateur de saisir du texte

« LISTBOX »

Zone de liste

« RichEdit »

Contrôle permettant à l'utilisateur de saisir du texte enrichi

RICHEDIT_CLASS

Amélioration de la classe RichEdit (plus sophistiqué …)

« SCROLLBAR »

Barre de défilement

« STATIC »

Étiquette


Ce sont les contrôles standard (il ne s'agit pas d'une liste exhaustive).

Par exemple :

 
Sélectionnez
CreateWindow( "BUTTON",                   /* Classe : bouton           */
              "OK",                       /* Texte du bouton           */
              WS_CHILD | WS_VISIBLE,      /* Styles                    */
              0,                          /* x                         */
              0,                          /* y                         */
              100,                        /* Largeur                   */
              20,                         /* Hauteur                   */
              hWnd,                       /* Fenêtre parent            */
              (HMENU)1,                   /* ID du bouton : 1          */
              hInstance,                  /* Instance de l'application */
              NULL                        /* Paramètres additionnels   */
            );

Bien que l'on puisse créer le contrôle à n'importe quel moment, le moment idéal pour le faire est pendant la création de sa fenêtre parent c'est-à-dire pendant le traitement du message WM_CREATE. Autre remarque : il est rarement nécessaire de récupérer le handle du contrôle (retourné par CreateWindow), car en général, on travaille avec l'ID du contrôle plutôt qu'avec son handle (parce que c'est plus facile : ça évite de déclarer 50 variables pour 50 contrôles…) et de toute façon, au cas où on aurait besoin de son handle, il y a toujours la fonction GetDlgItem. N'oubliez pas non plus que DestroyWindow détruit non seulement une fenêtre, mais également toutes ses fenêtres enfants. Il n'est donc pas nécessaire de détruire manuellement les contrôles d'une fenêtre ni avant ni après que celle-ci ait été détruite, puisque cela se fait automatiquement.

Voici un exemple qui montre comment profiter du message WM_CREATE pour créer notre contrôle :

 
Sélectionnez
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    static HINSTANCE hInstance;
    
    switch (message)
    {
    case WM_CREATE:        
        hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
        
        CreateWindow("BUTTON", "OK", WS_CHILD | WS_VISIBLE, 0, 0, 100, 24,
        hwnd, (HMENU)1, hInstance, NULL);
        
        break;
    
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    
    default:
        return DefWindowProc(hwnd, message, wParam, lParam);
    }
    
    return 0;
}

Maintenant on va tester si notre bouton fonctionne bien. Pour cela on va émettre un beep en réponse à chaque clic. La fonction Beep reçoit en arguments la fréquence en Hz et la durée en ms du son à émettre. Elle très pratique pour déboguer.

 
Sélectionnez
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    static HINSTANCE hInstance;
    
    switch (message)
    {
    case WM_CREATE:        
        hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
        
        CreateWindow("BUTTON", "OK", WS_CHILD | WS_VISIBLE, 0, 0, 100, 24,
        hwnd, (HMENU)1, hInstance, NULL);
        
        break;
        
    case WM_COMMAND:
        /***************************************************\
        * LOWORD(wParam) = ID du contrôle ou du menu        *
        * HIWORD(wParam) = Raison du message (notification) *
        \***************************************************/
        
        switch(LOWORD(wParam))
        {
        case 1:
            switch(HIWORD(wParam))
            {
            case BN_CLICKED:
                Beep(1000, 100);
                break;
                
            default:
                break;
            }
            
            break;
            
        default:
            break;
        }
        
        break; /* case WM_COMMAND */
        
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    
    default:
        return DefWindowProc(hwnd, message, wParam, lParam);
    }
    
    return 0;
}

I-A-3. La boucle des messages revisitée

Un contrôle peut être atteint par l'utilisation de la touche TAB s'il possède le style WS_TABSTOP. Ce comportement (de même que la notion de bouton par défaut) est en fait typique des boîtes de dialogue (qui sont bien entendu également des fenêtres…). Pour autoriser un tel comportement dans une fenêtre « normale », il suffit de passer tous les messages à IsDialogMessage qui effectue un traitement adéquat si le message est un « Dialog Message ». Si la fonction retourne TRUE, ce qui signifie que le message était bien un Dialog Message (et donc déjà été convenablement traité), il ne faut plus faire un nouveau traitement. La boucle des messages devient donc :

 
Sélectionnez
while (GetMessage(&msg, NULL, 0, 0))
{
    if (!IsDialogMessage(hWnd, &msg))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
}

I-B. Les contrôles standard

I-B-1. Le contrôle Bouton

I-B-1-a. Les styles

Dans la colonne de gauche, la liste des constantes permettant de spécifier le type du bouton et dans celle de droite, la liste des constantes permettant de choisir la disposition texte du bouton.

Type du bouton

Disposition

BS_PUSHBUTTON

Position du texte par rapport au bouton

BS_DEFPUSHBUTTON

BS_LEFTTEXT (BS_RIGHTBUTTON)

BS_CHECKBOX

 

BS_AUTOCHECKBOX

Alignement du texte

BS_RADIOBUTTON

BS_LEFT

BS_AUTORADIOBUTTON

BS_RIGHT

BS_3STATE

BS_TOP

BS_AUTO3STATE

BS_BOTTOM

BS_PUSHLIKE

BS_CENTER

BS_GROUPBOX

BS_VCENTER

BS_OWNERDRAW

 


BS_PUSHBUTTON (bouton poussoir ou de commande) est le type par défaut. Le type BS_DEFPUSHBUTTON est en principe réservé au bouton par défaut (celui qui est activé lorsqu'on appuie sur ENTREE) d'une boîte de dialogue. Par défaut, ce bouton s'il est présent doit avoir l'ID IDOK. Mais on peut changer cela en envoyant à la fenêtre le message DM_SETDEFID qui permet d'utiliser une autre valeur (que l'on placera dans wParam) à la place de IDOK.

Un bouton automatique (BS_AUTOXXX) est en gros un bouton dont l'état change automatiquement chaque fois qu'on clique dessus.

Un BS_CHECKBOX et un BS_3STATE ont la même apparence (case à cocher) sauf qu'un BS_3STATE, en plus de pouvoir être coché (BST_CHECKED) ou décoché (BST_UNCHECKED), peut aussi être dans un état non assigné (BST_INDETERMINATE). Les cases à cocher sont généralement utilisées pour donner à l'utilisateur la possibilité de sélectionner une ou plusieurs options parmi un groupe d'options. À l'inverse les boutons radio sont généralement utilisés pour demander à l'utilisateur de faire un choix parmi un groupe d'options. Si cette convention est conforme à vos attentes, vous n'avez aucune raison d'utiliser les boutons de type BS_CHECKBOX, BS_3STATE ou BS_RADIOBUTTON à la place de BS_AUTOCHECKBOX, BS_AUTO3STATE ou BS_AUTORADIOBUTTON.

Pour être encore plus rationnel (et cela est même requis pour les radio buttons), il ne faut pas se contenter de placer les contrôles les uns à côté des autres à l'intérieur d'un rectangle assez net pour former un « vrai » groupe de contrôles. En plus de cela, le premier contrôle du groupe doit avoir le style WS_GROUP. Autrement dit, un contrôle possédant ce style commence un nouveau groupe. Généralement, on donne aussi le style WS_TABSTOP au premier contrôle de chaque groupe pour permettre à l'utilisateur de naviguer facilement entre les différents groupes à l'aide de la touche TAB et de passer d'un contrôle à un autre du même groupe en utilisant les touches fléchées du clavier. Du point de vue esthétique, il est coutume de placer un groupe d'options à l'intérieur d'un group box (BS_GROUPBOX), mais bien entendu, cela n'est point une règle.

Et enfin, BS_PUSHLIKE est un style qu'on peut donner aux « state-buttons » (BS_AUTOCHECKBOX, BS_AUTORADIOBUTTON, etc.) pour leur donner une apparence de push button.

I-B-1-b. Les fonctions

CheckDlgButton :

 
Sélectionnez
BOOL CheckDlgButton(HWND hwndParent, int nIDButton, UINT uCheck);

Cette fonction permet de modifier l'état d'une case à cocher ou d'un bouton radio. Les valeurs utilisables sont : BST_CHECKED (coché), BST_UNCHECKED (décoché) et BST_INDETERMINATE (non assigné).

CheckRadioButton :

 
Sélectionnez
BOOL CheckRadioButton(HWND hwndParent, int nIDFirstButton, int nIDLastButton, int nIDCheckButton);

Permet de sélectionner (cocher) un bouton radio tout en s'assurant qu'un et seul seulement parmi un groupe ne soit sélectionné (les autres seront tous décochés).

IsDlgButtonChecked :

 
Sélectionnez
UINT IsDlgButtonChecked(HWND hwndParent, int nIDButton);

Cette fonction ne retourne pas une valeur booléenne (!), mais l'état du bouton (qui doit être une case à cocher ou un bouton radio) c'est-à-dire coché, décoché ou non assigné.

I-B-1-c. Les messages

BM_CLICK : ce message peut être envoyé à un bouton pour simuler un clic de la part de l'utilisateur. Il ne nécessite aucun paramètre (c'est-à-dire que vous laissez tout simplement wParam et lParam à zéro).

BM_SETCHECK : modifie l'état d'une case à cocher ou d'un bouton radio. Utilise wParam comme unique paramètre, qui peut être BST_CHECKED, BST_UNCHECKED ou BST_INDETERMINATE (pour les boutons à 3 états uniquement). Macro équivalente :

 
Sélectionnez
void Button_SetCheck(HWND hwndCtl, int check);


BM_GETCHECK : retourne l'état d'une case à cocher ou d'un bouton radio. Ne nécessite aucun paramètre. Macro équivalente :

 
Sélectionnez
int Button_GetCheck(HWND hwndCtl);


BM_SETSTATE : modifie l'état d'un bouton-poussoir. Utilise wParam comme unique paramètre. Mettez TRUE pour rendre le bouton enfoncé et FALSE pour le relâcher. Macro équivalente :

 
Sélectionnez
UINT Button_SetState(HWND hwndCtl, int state);


BM_GETSTATE : retourne l'état d'un bouton. Attention ! Ce n'est pas « l'inverse » du message BM_SETSTATE. BM_GETSTATE peut être utilisé avec n'importe quel bouton. Il ne nécessite aucun paramètre. Et enfin, il retourne une combinaison des valeurs suivantes :

  • BST_CHECKED : le bouton est coché ;
  • BST_UNCHECKED : le bouton est décoché ;
  • BST_INDETERMINATE : le bouton est non assigné ;
  • BST_FOCUS : le bouton a le focus clavier ;
  • BST_PUSHED : le bouton est maintenu enfoncé.

Ces valeurs sont toutes indépendantes donc on peut utiliser l'opérateur & pour extraire les informations utiles. Par exemple :

 
Sélectionnez
int state = (int)SendDlgItemMessage(hWnd, MYBUTTON, BM_GETSTATE, 0, 0);
if (state & BST_CHECKED) {
    ...
}

La Macro équivalente est :

 
Sélectionnez
int Button_GetState(HWND hwndCtl);


BM_SETIMAGE : associe une nouvelle image (une icône ou un bitmap) à un bouton. Le bouton doit avoir le style BS_ICON pour pouvoir afficher une icône et BS_BITMAP pour pouvoir afficher une image bitmap (sachez également que le style BS_TEXT permet à un bouton d'afficher du texte, mais ce style est appliqué par défaut). Avec les boutons « normaux », on ne peut qu'afficher soit du texte soit une image. Pour pouvoir afficher à la fois du texte et une image (et faire d'autres trucs jusqu'ici impossibles), il faudra utiliser des boutons personnalisés (Owner Draw Buttons) que nous verrons encore plus loin. Ce message requiert en paramètres le type de l'image qu'on veut associer au bouton (wParam) qui peut prendre les valeurs IMAGE_ICON ou IMAGE_BITMAP et le handle de l'image en question (lParam). En retour, on a le handle de l'ancienne image associée au bouton (ou NULL).

BM_GETIMAGE : retourne le handle de l'image associée au bouton ou NULL si aucune image ne lui a été associée. Nécessite en paramètre (wParam) le type de l'image (IMAGE_ICON ou IMAGE_BITMAP).

I-B-1-d. Les notifications

BN_CLICKED : envoyé lorsque le bouton a été cliqué

BN_DBLCLK : envoyé lorsque le bouton a été double-cliqué. Seuls les boutons radio et les boutons personnalisés savent envoyer automatiquement cette notification. Pour les autres types de boutons, il faut qu'ils aient le style BS_NOTIFY.

I-B-2. Le contrôle Edit

I-B-2-a. Les styles
Format

ES_LOWERCASE / ES_UPPERCASE : convertit automatiquement en minuscules / majuscules tous les caractères entrés.

ES_NUMBER : le texte tapé ne doit comporter que des chiffres.

ES_ PASSWORD : affiche des * à la place des caractères entrés. Utilisé pour saisir un mot de passe.

ES_LEFT / ES_RIGHT / ES_TEXT : permet de choisir l'alignement du texte.

Comportement

ES_MULTILINE : spécifie un contrôle multi lignes. Par défaut un contrôle Edit n'est constitué que d'une seule ligne.

ES_WANTRETURN : par défaut, l'appui sur la touche ENTREE provoque l'activation du bouton par défaut si celui-ci est bien sûr présent. Pour spécifier que l'appui sur la touche ENTREE doit provoquer le saut à la ligne suivante et non l'activation du bouton par défaut, il faut spécifier le style ES_WANTRETURN.

WS_HSCROLL / WS_VSCROLL : en fait, ces styles ne sont pas spécifiques des contrôles Edit mais communs à toutes les fenêtres. Ils permettent d'indiquer si la fenêtre comporte oui ou non une barre de défilement horizontale/verticale. Ces barres lorsqu'elles sont présentes sont automatiquement prises en charge par le contrôle Edit alors qu'il faut en général soi-même les gérer pour les autres classes de fenêtre.

ES_AUTOHSCROLL : active la saisie « kilométrique » c'est-à-dire permet à l'utilisateur de continuer la saisie sur la même ligne même quand la largeur du texte dépasse la largeur du contrôle.

ES_AUTOVSCROLL : fait défiler automatiquement le texte vers le haut quand l'utilisateur tape ENTREE à la dernière ligne.

ES_READONLY : spécifie un contrôle en lecture seule, c'est-à-dire que le contrôle peut afficher du texte, mais l'utilisateur ne pourra pas le modifier.

I-B-2-b. Les messages

WM_COPY : copie la sélection courante vers le presse-papiers. Ne nécessite aucun paramètre.

WM_CUT : coupe la sélection courante et place le texte ainsi coupé dans le presse-papiers. Ne nécessite aucun paramètre

WM_PASTE : copie si possible le contenu du presse-papiers à partir de la position courante dans le contrôle. Ne nécessite aucun paramètre.

WM_CLEAR : efface la sélection courante. Ne nécessite aucun paramètre.

EM_CANUNDO : retourne l'état du flag « Can Undo » indiquant s'il est possible d'annuler la dernière action (TRUE) ou non (FALSE). Ce message ne nécessite aucun paramètre. Macro équivalente :

 
Sélectionnez
BOOL Edit_CanUndo(HWND hwndCtl);


EM_UNDO : annule, si possible, la dernière action effectuée. Ne nécessite aucun paramètre. Macro équivalente :

 
Sélectionnez
BOOL Edit_Undo(HWND hwndCtl);


EM_LIMITTEXT : ou EM_SETLIMITTEXT. Limite le nombre maximum de caractères que l'utilisateur peut entrer à un nombre passé en paramètre (wParam). On peut mettre 0 pour limiter ce nombre au maximum supporté par le système.

EM_GETLIMITTEXT : retourne le nombre maximum de caractères que l'utilisateur peut entrer. Ne nécessite aucun paramètre.

EM_SETPASSWORDCHAR : spécifie le caractère à utiliser pour masquer les mots de passe (par défaut : '*'). Le seul paramètre requis est le caractère de remplacement (wParam). Macro équivalente :

 
Sélectionnez
void Edit_SetPasswordChar(HWND hwndCtl, UINT ch);


EM_GETPASSWORDCHAR : retourne le caractère actuellement utilisé pour masquer les mots de passe. Macro équivalente :

 
Sélectionnez
TCHAR Edit_GetPasswordChar(HWND hwndCtl);


EM_GETLINECOUNT : retourne le nombre actuel de lignes d'un contrôle multi lignes. Ce nombre est toujours supérieur ou égal à 1, même si le contrôle est vide (auquel cas la valeur retournée est 1, comme s'il y avait une ligne). Macro équivalente :

 
Sélectionnez
int Edit_GetLineCount(HWND hwndCtl);


EM_LINELENGTH : retourne la longueur c'est-à-dire le nombre de caractères d'une ligne. Ce message ne nécessite qu'un seul paramètre (wParam) à savoir de numéro de la ligne que l'on veut interroger. Le numéro de la première ligne est 0. Macro équivalente :

 
Sélectionnez
int Edit_LineLength(HWND hwndCtl, int line);


EM_GETLINE : copie le contenu d'une ligne dans un buffer. Il faut placer le numéro de la ligne à copier dans le paramètre wParam et l'adresse du buffer destiné à recevoir les caractères copiés dans lParam. Il faut également placer la taille du buffer dans le premier mot (WORD) de ce buffer. Le caractère de fin de chaîne n'est pas ajouté. En retour, on a le nombre de caractères copiés.

En général, on envoie donc dans un premier temps le message EM_LINELENGTH avant d'envoyer le message EM_GETLINE, comme montré dans l'extrait de code suivant :

 
Sélectionnez
size_t len = (size_t)SendMessage(hEdit, EM_LINELENGTH, (WPARAM)iLine, 0);

TCHAR * lpBuffer = malloc( max(len + 1, sizeof(WORD)) * sizeof(TCHAR) );

if (lpBuffer != NULL)
{
    memcpy(lpBuffer, &len, sizeof(WORD));
    SendMessage(hEdit, EM_GETLINE, (WPARAM)iLine, (LPARAM)lpBuffer);
    lpBuffer[len] = '\0';
    
    MessageBox(hWnd, lpBuffer, "", MB_OK);
    
    free(lpBuffer);
}

La Macro équivalente est :

 
Sélectionnez
int Edit_GetLine(HWND hwndCtl, int line, LPTSTR lpBuffer, WORD wBufLength);


Bien entendu on peut toujours utiliser GetWindowText pour récupérer tout le texte d'un contrôle Edit comme avec n'importe quelle fenêtre. GetWindowTextLength permet de connaître le nombre de caractères constituant le texte d'une fenêtre.

Mais il y a tout de même un problème. En fait, GetWindowText n'est pas trop recommandé pour récupérer le contenu d'un contrôle multi lignes puisque cette opération peut consommer beaucoup de mémoire. Pour avoir accès à tout le contenu, il vaut mieux accéder directement à la mémoire utilisée par le contrôle lui-même. C'est là qu'entre en jeu le message EM_GETHANDLE.

EM_GETHANDLE : retourne le handle de la mémoire utilisée par un contrôle multi lignes (en effet son utilisation avec les contrôles à une seule ligne présente peu d'intérêt). Ce handle, passé à LocalLock, permet d'obtenir ensuite un pointeur vers cette mémoire, qui est un tableau de caractères (ANSI ou UNICODE selon le jeu de caractères que vous utilisez) terminé par zéro. Le programme peut lire le contenu de cette mémoire, mais ne peut pas la modifier. Pour déverrouiller le contrôle, on appellera la fonction LocalUnlock. Par exemple :

 
Sélectionnez
HLOCAL hMemory = (HLOCAL)SendMessage(hEdit, EM_GETHANDLE, 0, 0);

TCHAR * pMemory = LocalLock(hMemory);

MessageBox(hWnd, pMemory, "", MB_OK);

LocalUnlock(hMemory);

La Macro équivalente est :

 
Sélectionnez
HLOCAL Edit_GetHandle(HWND hwndCtl);


EM_GETSEL : permet de récupérer le début et la fin de la sélection courante. Nécessite en paramètres les adresses respectives de la variable destinée à recevoir l'indice du début de la sélection (wParam) et de celle destinée à recevoir l'indice de la fin de sélection (lParam). La fin de la sélection n'est pas le dernier caractère sélectionné, mais le caractère suivant.

EM_SETSEL : modifie la sélection courante. Ce message nécessite en paramètres les indices respectifs du début (wParam) et de la fin (lParam) de la nouvelle sélection. Utilisez respectivement 0 et -1 pour tout sélectionner.

EM_REPLACESEL : remplace le texte de la sélection courante par un autre (lParam). Il faut également spécifier (wParam) si l'action peut être annulée (TRUE) ou non (FALSE).

EM_GETMODIFY : retourne l'état du flag « Modified » indiquant si le contenu a été modifié depuis la dernière fois qu'on l'a marqué comme sain et sauf. TRUE indique que le contenu a été modifié. Macro équivalente :

 
Sélectionnez
BOOL Edit_GetModify(HWND hwndCtl);


EM_SETMODIFY : permet de modifier l'état du flag « Modified » du contrôle. Mettre FALSE (wParam) pour indiquer que le contenu est sain et sauf. Macro équivalente :

 
Sélectionnez
void Edit_SetModify(HWND hwndCtl, UINT fModified);
I-B-2-c. Les notifications

EN_UPDATE : envoyé chaque fois que l'utilisateur modifie le contenu du contrôle. Cette notification est envoyée avant que la modification ne prenne effet.

EN_CHANGE : envoyé chaque fois que l'utilisateur modifie le contenu du contrôle. Cette notification est envoyée après que la modification ait pris effet.

EN_MAXTEXT : envoyé lorsque l'utilisateur a tenté d'entrer plus de texte que le contrôle ne peut supporter.

EN_SETFOCUS : envoyé lorsque le contrôle reçoit le focus du clavier.

EN_KILLFOCUS : envoyé lorsque le contrôle vient de perdre le focus du clavier.

I-B-3. Le contrôle Static

Certainement un des plus simples. Par contre les styles sont très nombreux, mais nous ne les détaillerons pas tous, MSDN est là pour ça. Le contrôle étiquette est la plupart du temps utilisé pour afficher du texte (les styles SS_LEFT, SS_RIGHT et SS_CENTER permettent de spécifier l'alignement), mais sachez qu'il peut également afficher une image (une icône ou un bitmap). Pour cela il suffit de spécifier le style SS_ICON ou SS_BITMAP selon le type d'image qu'on veut afficher puis envoyer le message STM_SETIMAGE pour spécifier l'image à afficher. À part ça, ce contrôle n'envoie des notifications (SN_CLICKED, SN_DBLCLK, etc.) que si le style SS_NOTIFY est spécifié. En général on n'a pas besoin des notifications venant de ce contrôle.

I-B-4. Le contrôle Zone de liste (ListBox)

I-B-4-a. Les styles

LBS_DISABLENOSCROLL : spécifie que la barre de défilement du contrôle doit toujours être visible. Par défaut, cette barre n'apparaît que lorsque le contrôle contient trop d'éléments pour être tous affichés en même temps.

LBS_EXTENDEDSEL : spécifie que l'utilisateur peut sélectionner plusieurs éléments en même temps à l'aide de la souris et les touches SHIFT ou CTRL ou à l'aide d'une combinaison spéciale de touches.

LBS_MULTIPLESEL : spécifie que l'utilisateur peut sélectionner plusieurs éléments en même temps en cliquant successivement sur les éléments à sélectionner. La sélection / désélection d'un élément se fait à l'aide d'un simple clic.

LBS_NOSEL : spécifie que les éléments de la liste peuvent être uniquement visualisés et non sélectionnés.

LBS_SORT : spécifie que les éléments de la liste doivent être triés alphabétiquement.

LBS_NOTIFY : autorise le contrôle à envoyer des notifications (LBN_CLICKED, LBN_DBLCLK, etc.).

LBS_STANDARD : zone de liste standard (inclut les styles les plus courants, à savoir WS_BORDER, LBS_SORT et LBS_NOTIFY).

I-B-4-b. Les messages

LB_ADDSTRING : ajoute une chaîne (lParam) à la fin de la liste. Macro équivalente :

 
Sélectionnez
int ListBox_AddString(HWND hwndCtl, LPCTSTR lpszString);


LB_INSERTSTRING : insère une chaîne (lParam) à la position numéro n (wParam) sachant que l'indice du premier élément est 0. Si wParam = -1, la chaîne sera ajoutée à la fin. Macro équivalente :

 
Sélectionnez
int ListBox_InsertString(HWND hwndCtl, int index, LPCTSTR lpszString);


LB_DELETESTRING : supprime une chaîne de la liste. Le seul paramètre requis est l'indice de la chaîne à supprimer (wParam). Macro équivalente :

 
Sélectionnez
int ListBox_AddString(HWND hwndCtl, int index);


LB_GETCOUNT : retourne le nombre d'éléments dans la liste. Macro équivalente :

 
Sélectionnez
int ListBox_GetCount(HWND hwndCtl);


LB_GETTEXTLEN : retourne la longueur (strlen) du texte de l'élément spécifié (wParam). Macro équivalente :

 
Sélectionnez
int ListBox_GetTextLen(hwndCtl, int index);


LB_GETTEXT : copie le texte de l'élément spécifié (wParam) pour le placer dans un buffer (lParam). Ce buffer doit être suffisamment grand pour contenir le texte copié ainsi que le caractère de fin de chaîne (qui est automatiquement ajouté). Macro équivalente :

 
Sélectionnez
int ListBox_GetTextLen(hwndCtl, int index, LPTSTR lpszBuffer);


LB_FINDSTRING : recherche une chaîne (lParam) à partir de la position spécifiée (wParam), plus précisément à partir de la position suivante. Donc pour chercher depuis le début de la liste (position 0), il faut passer -1 en paramètre. En fait, LB_FINDSTRING cherche une chaîne contenant le préfixe placé dans lParam et non la chaîne exacte. Pour chercher la chaîne exacte, il faut utiliser LB_FINDSTRINGEXACT à la place de LB_FINDSTRING. Dans tous les cas, la recherche ne tient pas compte de la casse et la valeur retournée est l'indice du premier élément trouvé ou LB_ERR si aucun élément n'a été trouvé. Macro équivalente :

 
Sélectionnez
int ListBox_FindString(HWND hwndCtl, int indexStart, LPCTSTR lpszString);


LB_SETITEMDATA : associe une valeur (lParam) à un élément de la liste (wParam). Macro équivalente :

 
Sélectionnez
int ListBox_SetItemData(HWND hwndCtl, int index, LPARAM data);


LB_GETITEMDATA : retourne la valeur associée à un élément de la liste (wParam). Macro équivalente :

 
Sélectionnez
LRESULT ListBox_SetItemData(HWND hwndCtl, int index);


LB_GETSEL : retourne l'état d'un élément de la liste (wParam). Une valeur positive indique que l'élément est sélectionné et 0 indique qu'il est désélectionné. LB_ERR est retourné en cas d'erreur. Macro équivalente :

 
Sélectionnez
int ListBox_GetSel(HWND hwndCtl, int index);


LB_SETSEL : modifie l'état (wParam) d'un élément de la liste (lParam). Utilisez TRUE pour sélectionner l'élément et FALSE pour le désélectionner. Si lParam = -1, la modification sera appliquée à tous les éléments de la liste. Macro équivalente :

 
Sélectionnez
int ListBox_GetSel(HWND hwndCtl, BOOL fSelect, int index);


LB_GETCURSEL : dans un contrôle à sélection simple, retourne l'indice de la sélection courante ou LB_ERR si aucun élément n'est sélectionné. Macro équivalente :

 
Sélectionnez
int ListBox_GetCurSel(HWND hwndCtl);


LB_SETCURSEL : sélectionne un élément (wParam). Macro équivalente :

 
Sélectionnez
int ListBox_GetCurSel(HWND hwndCtl, int index);


LB_GETSELCOUNT : dans un contrôle à sélection multiple, retourne le nombre d'éléments sélectionnés, 0 si aucun élément n'est sélectionné. Macro équivalente :

 
Sélectionnez
int ListBox_GetSelCount(HWND hwndCtl);


LB_GETSELITEMS : permet de récupérer dans un tableau (lParam) d'entiers les indices des éléments sélectionnés. On devra également placer dans wParam le nombre maximum d'éléments que peut accepter le tableau. Macro équivalente :

 
Sélectionnez
int ListBox_GetSelItems(HWND hwndCtl, int cItems, int * lpItems)


LB_RESETCONTENT : réinitialise la liste (supprime tous les éléments). Macro équivalente :

 
Sélectionnez
BOOL ListBox_ResetContent(HWND hwndCtl);

I-B-5. Le contrôle Zone de liste combinée (ComboBox)

Le contrôle ComboBox combine les contrôles Edit et ListBox bien qu'il n'implémente pas forcément toutes leurs fonctionnalités. Le style CBS_SIMPLE affiche en permanence le contrôle ListBox alors qu'avec le style CBS_DROPDOWN ce dernier n'apparaît que lorsque l'utilisateur déroule la liste. Avec le style CBS_DROPDOWNLIST, l'utilisateur ne peut plus saisir directement du texte dans le contrôle Edit, mais doit choisir un élément dans la liste. Les messages LB_GETTEXT et LB_GETTEXTLEN ont été traduits en CB_GETLBTEXT et CB_GETLBTEXTLEN (et non en CB_GETTEXT et CB_GETTEXTLEN auxquels on aurait pu s'attendre) et les noms de macros commencent tous par ComboBox (ComboBox_AddString, ComboBox_GetCursel, ComboBox_GetLBText, ComboBox_GetLBTextLen, etc.). De même, beaucoup des styles des contrôles Edit et ListBox n'ont plus été retenus, en particulier le style CBS_NOTIFY qui n'existe plus, car n'étant plus nécessaire étant donné que ce contrôle émet abondamment des notifications dont voici une liste non exhaustive :

CBN_DROPDOWN : la zone de liste est sur le point d'être déroulée (envoyé par ceux qui ont le style CBS_DROPDOWN ou CBS_DROPDOWNLIST uniquement).

CBN_DBLCLK : un élément de la zone de liste a été double-cliqué.

CBN_CLOSEUP : la zone de liste est sur le point d'être repliée (envoyé par ceux qui ont le style CBS_DROPDOWN ou CBS_DROPDOWNLIST uniquement).

CBN_SELCHANGE : la sélection dans zone de la liste vient de changer.

CBN_SELENDCANCEL : L'utilisateur a quitté le contrôle sans valider la sélection.

CBN_SELENDOK : l'utilisateur a bien fermé la zone de liste, indiquant qu'il a validé son choix.

CBN_EDITCHANGE : l'utilisateur a modifié le texte du contrôle Edit du ComboBox.

CBN_EDITUPDATE : l'utilisateur tente de modifier le texte du contrôle Edit du ComboBox.

CBN_KILLFOCUS : le contrôle a perdu le focus du clavier.

I-C. Les contrôles communs

I-C-1. Introduction

Les contrôles communs sont les contrôles spécifiques d'une version donnée de Windows (barre d'outils, barre d'état, barre de progression, glissière, etc.). Ils sont implémentés dans le fichier comctl32.dll. Pour les utiliser, il faut se lier avec comctl32.lib et inclure commctrl.h.

I-C-2. Initialisation

Il ne nous est plus à rappeler que la création d'une fenêtre se fait toujours à partir d'une classe de fenêtre existante. Lorsqu'un programme se lie avec user32.dll, le système enregistre les classes de fenêtre standard (les contrôles standard) avant même que la fonction WinMain ne soit appelée. C'est pour cette raison que nous n'avons jamais eu à enregistrer ces classes nous-mêmes. Ce n'est pas le cas avec les contrôles communs et l'application doit donc initialiser ces contrôles elle-même avant de pouvoir les utiliser. Cela se fait tout simplement en appelant la fonction InitCommonControlsEx (qui est venue remplacer la fonction InitCommonControls, devenue obsolète).

 
Sélectionnez
void InitCommonControls(VOID);
BOOL InitCommonControlsEx(LPINITCOMMONCONTROLSEX lpInitCommCtrls);

Le type INITCOMMONCONTROLSEX est tout simplement une structure comportant deux champs : dwSize et dwICC. Le premier doit indiquer la taille en octets de la structure et le dernier la ou les classes de fenêtre qu'on veut enregistrer (nous y reviendrons là-dessus un peu plus bas). La fonction InitCommonControls ne permet pas de spécifier individuellement les contrôles à initialiser.

I-C-3. Les versions

Le fichier comctl32.dll n'est pas forcément le même sur tous les ordinateurs, car sa version dépend du système d'exploitation et/ou logiciels installés (en particulier Internet Explorer). Il n'est pas exclu non plus que plusieurs versions soient simultanément présentes sur un même ordinateur (évidemment séparées dans des répertoires différents). Par exemple, Windows XP et Vista sont accompagnés des versions 5 (en l'occurrence 5.82) et 6 alors que Windows 2000 n'était accompagné que de la version 5 (5.81). Si vous avez Windows 95, vous avez la version 4.0, mais si vous installez ensuite Internet Explorer 3.x, vous bénéficierez de la version 4.70. Chaque nouvelle version est un sur ensemble des versions antérieures.

La version 6 intègre non seulement les contrôles supplémentaires, mais aussi les contrôles standard, cependant ces derniers ne sont pas les mêmes que ceux de user32 (mais ils continuent à envoyer leurs notifications via le message WM_COMMAND, et non WM_NOTIFY comme le reste des contrôles communs). En effet, Windows XP est venu avec une nouvelle interface utilisateur personnalisable, permettant à l'utilisateur de choisir lui-même le style visuel à appliquer. Les contrôles de la version 6 utilisent ce style alors que ceux de user32 et des versions antérieures à 6 sont basés sur le style classique. Ainsi, si vous voulez utiliser les styles visuels dans vos applications, vous devez utiliser les contrôles de la version 6 ou plus récente. Nous en reparlerons plus tard.

En outre, il faut également savoir que la fonction InitCommonControlsEx n'a été introduite que depuis la version 4.70 (livrée avec IE 3.x). Selon la version de votre fichier d'en-tête, il se peut donc que cette fonction ne soit déclarée que si vous définissez explicitement la macro _WIN32_IE à 0x0300 ou supérieur avant d'inclure commctrl.h (sous Visual Studio .NET, elle vaut par défaut 0x0500).

I-C-4. Choix des bibliothèques

Comme nous venons tout juste de le dire, la version 6.0 de la Common Controls Library est la première à avoir supporté les styles visuels. Or les applications compilées avec Visual Studio .NET et 2005 utilisent par défaut les contrôles de user32.dll et de comctl32.dll version 5 pour être compatibles avec les anciennes versions de Windows. Pour utiliser la version 6 (ou une autre …), c'est à l'application de le spécifier. En effet, quand on se lie avec comctl32.lib, il n'est spécifié nulle part quelle version de comctl32.dll veut-on utiliser. C'est là qu'interviennent les fichiers MANIFEST.

Un fichier manifest est un fichier texte, utilisant une grammaire XML, permettant dans Windows XP et plus récents (en effet il est ignoré par les versions antérieures) de spécifier entre autres les composants (les « dépendances ») requis par une application pour fonctionner. Le nom d'un fichier manifest doit être le nom de l'application suivi de l'extension .manifest (par exemple hello.exe.manifest). Par exemple, pour utiliser comctl32.dll version 6.0, il faut créer le manifest suivant :

 
Sélectionnez
<?xml version='1.0' encoding='UTF-8' standalone='yes'?>

<assembly xmlns='urn:schemas-microsoft-com:asm.v1' manifestVersion='1.0'>
    <dependency>
        <dependentAssembly>
            <assemblyIdentity type='Win32'
                              name='Microsoft.Windows.Common-Controls'
                              version='6.0.0.0'
                              processorArchitecture='x86'
                              publicKeyToken='6595b64144ccf1df'
            />
        </dependentAssembly>
    </dependency>
</assembly>

Les paramètres sont plutôt parlants.

De plus, si vous utilisez par exemple Visual Studio 2005, vos applications utiliseront la version 8.0 du C Run-Time Library (msvcr80.dll), sauf bien sûr si vous utilisez la version statique. Dans ce cas, il faut également le spécifier dans le manifest. Donc si vous utilisez msvcr80.dll et comctl32.dll version 6.0, vous devez créez le manifest suivant :

 
Sélectionnez
<?xml version='1.0' encoding='UTF-8' standalone='yes'?>

<assembly xmlns='urn:schemas-microsoft-com:asm.v1' manifestVersion='1.0'>

    <!-- Microsoft C Run-Time Library version 8.0 -->
    <dependency>
        <dependentAssembly>
            <assemblyIdentity type='win32'
                              name='Microsoft.VC80.CRT'
                              version='8.0.50608.0'
                              processorArchitecture='x86'
                              publicKeyToken='1fc8b3b9a1e18e3b'
            />
        </dependentAssembly>
    </dependency>

    <!-- Microsoft Common Controls Library version 6.0 -->
    <dependency>
        <dependentAssembly>
            <assemblyIdentity type='Win32'
                              name='Microsoft.Windows.Common-Controls'
                              version='6.0.0.0'
                              processorArchitecture='x86'
                              publicKeyToken='6595b64144ccf1df'
            />
        </dependentAssembly>
    </dependency>

</assembly>

En Debug vous mettrez plutôt VC80.DebugCRT à la place de VC80.CRT.

Il existe deux manières différentes d'utiliser un manifest (dans tous les cas, n'oubliez pas que le manifest n'est lu qu'à l'exécution) :

  • en le plaçant dans le même répertoire que l'exécutable ;
  • en l'embarquant à l'intérieur même de l'exécutable, autrement dit le mettre en ressource.

Lorsqu'on place un manifest dans le répertoire de l'exécutable, celui qui se trouve en ressource sera tout simplement ignoré par Windows. Sachez également que lorsqu'un manifest est utilisé, il n'est plus nécessaire d'appeler la fonction InitCommonControlsEx().

En fait, Visual Studio .NET et plus récents génèrent automatiquement un manifest, ne serait-ce que pour spécifier la version du CRT utilisée. Le fichier final, créé à partir d'un fichier « intermédiaire » nommé votreapp.exe.intermediate.manifest et des éventuelles options de génération que vous avez spécifiées, nommé votreapp.exe.embed.manifest, est ensuite embarqué dans l'exécutable de sorte que ce dernier soit plus ou moins autonome. Si vous voulez néanmoins l'utiliser en tant que fichier à part, accompagnant votre exécutable, vous n'avez qu'à renommer le fichier votreapp.exe.embed.manifest en votreapp.exe.manifest ou, tout simplement (et plus proprement !), de dire à Visual Studio de ne pas embarquer le manifest.

Visual Studio dispose d'une interface simple et intuitive permettant de contrôler très simplement et efficacement la génération de manifest. Vous pouvez par exemple spécifier des fichiers à fusionner avec le fichier par défaut utilisé pour générer le manifest grâce à l'option Additional manifest Files du gestionnaire de fichiers manifest (donc dans vos fichiers, vous ne devez plus lister la CRT parmi les dépendances requises, car elle est déjà listée dans le fichier par défaut (votreapp.exe.intermediate.manifest)).

Ceci étant, voyons maintenant comment embarquer un manifest à l'intérieur de l'exécutable sans utiliser l'interface de Visual Studio. Et bien, pour mettre un manifest dans la section ressources de votre fichier exécutable, il suffit de créer un script de ressource contenant la ligne suivante (inclure windows.h) :

 
Sélectionnez
CREATEPROCESS_manifest_RESOURCE_ID RT_manifest "hello.exe.manifest"

I-C-5. Exemple : Le contrôle ListView

Le contrôle ListView (WC_LISTVIEW) est un contrôle permettant, comme son nom l'indique, d'afficher une liste. La zone d'affichage des dossiers et des fichiers dans l'explorateur Windows, la zone d'affichage des processus en cours d'exécution dans le gestionnaire des tâches, etc. sont par exemple des contrôles ListView.

Le contrôle ListView peut afficher les éléments de la liste de 4 façons différentes dont voici les plus utilisées :

  • affichage en icônes (style LVS_ICON) : Les éléments sont affichés avec des grandes icônes avec leur en dessous de l'icône ;
  • affichage en liste (style LVS_LIST) : Les éléments sont affichés en liste, organisée en colonnes, avec des petites icônes avec leur nom à droite de l'icône ;
  • affichage en liste détaillée (style LVS_REPORT) : Chaque élément occupe une ligne et est chaque ligne comporte une ou plusieurs colonnes. La colonne la plus à gauche affiche l'icône et nom de l'élément (placé à droite de l'icône). Les autres colonnes servent à afficher d'autres informations. Chaque colonne est pourvue d'un en-tête qui sert à afficher son nom sauf si le style LVS_NOCOLUMNHEADER a été spécifié.


Depuis Windows XP (plus précisément comctl32.dll version 6.0), il est également possible d'utiliser d'autres styles d'affichage comme l'affichage en mosaïques (messages en jeu : LVM_SETVIEW, LVM_SETTILEVIEWINFO et LVM_SETTILEINFO) ou par groupe (messages en jeu : LVM_ENABLEGROUP, LVM_SETGROUPINFO et LVM_SETGROUPMETRICS) par exemple.

Comme la plupart des contrôles communs, le contrôle ListView envoie ses notifications à la fenêtre parent via le message WM_NOTIFY avec dans wParam son identifiant (ID), comme le fait n'importe quel contrôle commun, et dans lParam un pointeur vers une structure dérivée de la structure de base NMHDR. En fait, cette dérivation (spécialisation) de la structure NMHDR est utilisée par la quasi-totalité des contrôles communs. Il faut lire la documentation du contrôle pour savoir quelle structure est utilisée dans quelles circonstances.

L'exemple suivant a pour but de vous aider à comprendre le fonctionnement des contrôles communs à travers la création et l'utilisation d'un contrôle ListView.

 
Sélectionnez
#include <windows.h>
#include <commctrl.h>

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
void OnCreate(HWND hwnd, LPCREATESTRUCT lpCreateStruct);
void lvInitColumns(HWND hwndLV);
void lvInsertItems(HWND hwndLV);
void OnNotify(HWND hwnd, LPNMHDR lpnmhdr);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    INITCOMMONCONTROLSEX icc;
    WNDCLASS wc;
    HWND hWnd;
    MSG msg;
    
    icc.dwSize = sizeof(icc);
    icc.dwICC = ICC_LISTVIEW_CLASSES;
    
    InitCommonControlsEx(&icc);
    
    wc.cbClsExtra     = 0;
    wc.cbWndExtra     = 0;
    wc.hbrBackground  = (HBRUSH)(COLOR_WINDOW + 1);
    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", "Contrôle ListView",
        WS_POPUP | WS_BORDER | WS_CAPTION | WS_MINIMIZEBOX | WS_SYSMENU,
        300, 150, 400, 200,
        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)
{
    switch(message)
    {
    case WM_CREATE:
        OnCreate(hwnd, (LPCREATESTRUCT)lParam);
        break;

    case WM_NOTIFY:
        OnNotify(hwnd, (LPNMHDR)lParam);
        break;

    case WM_DESTROY:
        PostQuitMessage(0);
        break;

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

    return 0L;
}

void OnCreate(HWND hwnd, LPCREATESTRUCT lpCreateStruct)
{
    HWND hwndLV;
    RECT r;

    GetClientRect(hwnd, &r); 

    hwndLV = CreateWindow( WC_LISTVIEW, "", WS_CHILD | WS_VISIBLE | LVS_REPORT,
        r.left, r.top, r.right, r.bottom,
        hwnd, (HMENU)1, lpCreateStruct->hInstance, NULL
    );

    lvInitColumns(hwndLV);
    lvInsertItems(hwndLV);
}

void lvInitColumns(HWND hwndLV)
{
    LVCOLUMN lvc;

    /* Paramètres communs à toutes les colonnes. */

    lvc.mask = LVCF_TEXT | LVCF_FMT;
    lvc.fmt = LVCFMT_LEFT;

    /* Colonne 0 (première colonne). */

    lvc.pszText = "Langage";
    ListView_InsertColumn(hwndLV, 0, &lvc);
    ListView_SetColumnWidth(hwndLV, 0, 100);

    /* Colonne 1 (deuxième colonne). */

    lvc.pszText = "Créateur";
    ListView_InsertColumn(hwndLV, 1, &lvc);
    ListView_SetColumnWidth(hwndLV, 1, 200);
}

void lvInsertItems(HWND hwndLV)
{
    LVITEM lvi;

    /* Paramètres communs à tous les éléments que nous allons insérer. */

    lvi.mask = LVIF_TEXT;

    /* Ligne 0 (première ligne). */

    lvi.iItem = 0;

    /* Ligne 0 - Colonne 0. */
    lvi.iSubItem = 0;
    lvi.pszText = "C";
    ListView_InsertItem(hwndLV, &lvi);
    
    /* Ligne 0 - Colonne 1. */
    lvi.iSubItem = 1;
    lvi.pszText = "Brian Kernighan & Denis Ritchie";
    ListView_SetItem(hwndLV, &lvi);
}

void OnNotify(HWND hwnd, LPNMHDR lpnmhdr)
{
    if (lpnmhdr->idFrom == 1) /* 1 est l'ID de notre ListView. */
    {
        HWND hwndLV = lpnmhdr->hwndFrom;

        if (lpnmhdr->code == NM_DBLCLK)
        {
            /* ComCtl32 version 4.71 et plus récents : Quand lpnmhdr->code vaut NM_DBLCLK, */
            /* la structure complète est une structure de type NMITEMACTIVATE.             */
            /* Cette structure contient entre autres les coordonnées du point du clic.     */

            LPNMITEMACTIVATE lpnmia = (LPNMITEMACTIVATE)lpnmhdr;
            LVHITTESTINFO lvhti; /* Contiendra le numéro de ligne et de colonne de la "cellule" double-cliquée. */
            int ret;

            /* Récupérons le numéro de ligne et de colonne de la cellule double-cliquée. */

            lvhti.pt = lpnmia->ptAction; /* Coordonnées du point du clic. */
            ret = ListView_SubItemHitTest(hwndLV, &lvhti);

            /* Il faut tester ret car le clic a pu avoir lieu hors d'une cellule ... */

            if (ret != -1)
            {
                /* Récupérons puis affichons le texte de la cellule. */

                LVITEM lvi;
                char lpBuffer[256];

                lvi.mask = LVIF_TEXT;
                lvi.pszText = lpBuffer;
                lvi.cchTextMax = sizeof(lpBuffer);
                lvi.iItem = lvhti.iItem;
                lvi.iSubItem = lvhti.iSubItem;
                ListView_GetItem(hwndLV, &lvi);

                /* Il n'est pas garanti que le texte ait réellement été placé dans lpBuffer.  */
                /* Il est possible que le système ait utilisé un autre buffer.                */
                /* Il faut donc toujours utiliser lvi.pszText pour récupérer le texte.        */
                /* Il y a aussi la macro ListView_GetItemText qui est plus simple à utiliser. */

                MessageBox(hwnd, lvi.pszText, "", MB_OK);
            }
        }
    }
}

I-D. Personnalisation des contrôles

I-D-1. Choisir la brosse, la couleur de fond et la couleur du texte

Au temps des versions 16 bits de Windows, le message WM_CTLCOLOR fut envoyé chaque fois qu'un contrôle est sur le point d'être dessiné. En traitant ce message, la fenêtre parent peut spécifier une brosse, une couleur de fond et une couler du texte à utiliser avec le contexte de périphérique du contrôle. Les paramètres de ce message sont :

  • dans wParam : handle du contexte de périphérique du contrôle ;
  • dans lParam : handle du contrôle ayant envoyé le message.

Si l'application traite ce message, elle doit retourner le handle de la brosse à utiliser avec le contexte de périphérique du contrôle. Par contre on utilise tout simplement SetBkColor, SetBkMode et SetTextColor pour spécifier respectivement la couleur de fond, la transparence du fond et la couleur du texte.

Depuis les versions 32 bits, ce message a été remplacé par des messages plus spécifiques comme :

  • WM_CTLCOLORBTN (envoyé par un bouton) ;
  • WM_CTLCOLOREDIT (envoyé par un contrôle Edit) ;
  • WM_CTLCOLORLISTBOX (envoyé par une ListBox) ;
  • WM_CTLCOLORSTATIC (envoyé par un contrôle Static) ;


Mais le principe est le même. À noter toutefois qu'un contrôle Edit, lorsqu'il est désactivé ou en lecture seule, envoie le message WM_CTLCOLORSTATIC à la place de WM_CTLCOLOREDIT (sachez également que le rectangle des cases à cocher et des boutons radio sont en fait de vrais contrôles Static). En outre, les boutons de commande n'utilisent pas la brosse retournée puisque leur apparence dépend de leur état (enfoncé / relâché). Pour personnaliser l'apparence des boutons de commande, il faudra utiliser les boutons personnalisés.

I-D-2. Choisir la police des caractères

Pour choisir la police à utiliser avec un contrôle, il suffit de lui envoyer le message WM_SETFONT avec le handle de la police dans WPARAM et MAKELPARAM(fRedraw, 0) dans lParam où fRedraw doit être égal à TRUE si on veut que le contrôle soit immédiatement redessiné dès qu'il aura reçu le message.

I-D-3. Dessiner soi-même ses contrôles

I-D-3-a. La théorie

La technique la plus puissante pour obtenir des contrôles très personnalisés c'est évidemment de les dessiner soi-même. Fondamentalement, cela se fait tout simplement en traitant le message WM_PAINT à l'intérieur de la procédure de fenêtre du contrôle. On peut spécifier une nouvelle procédure de fenêtre pour n'importe quelle fenêtre déjà créée en modifiant l'adresse indiquée dans sa « variable interne » GWL_WNDPROC à l'aide de la fonction SetWindowLong. En fait, SetWindowLong est une fonction obsolète, car elle ne peut être utilisée que sur les plateformes 32 bits. Pour avoir du code compatible avec tous les processeurs (32 ou 64 bits), il faudra utiliser SetWindowLongPtr (et GWLP_WNDPROC).

En pratique, on n'aura pas qu'à traiter le message WM_PAINT pour dessiner le contrôle cela demande généralement trop de détails à prendre en charge. De plus, un contrôle doit généralement changer d'aspect en fonction de son état. Par exemple, pour un bouton, les états possibles sont normal, survolé par la souris, enfoncé, etc. Or les paramètres qui accompagnent le message WM_PAINT ne permettent pas de connaître dans quel état est actuellement le contrôle. C'est pourquoi, lorsqu'on veut dessiner soi-même ses contrôles, il est généralement préférable d'utiliser les « owner-draw controls ».

Chaque fois qu'un owner-draw control doit être dessiné ou redessiné, Windows envoie à sa fenêtre parent le message WM_DRAWITEM. Si le message provient d'un contrôle, alors le paramètre wParam contient l'ID de ce contrôle sinon zéro. lParam quant à lui contient toujours l'adresse d'une structure de type DRAWITEMSTRUCT contenant les informations nécessaires permettant d'effectuer un traitement convenable.

 
Sélectionnez
typedef struct tagDRAWITEMSTRUCT {
    UINT CtlType;
    UINT CtlID;
    UINT itemID;
    UINT itemAction;
    UINT itemState;
    HWND hwndItem;
    HDC hDC;
    RECT rcItem;
    ULONG_PTR itemData;
} DRAWITEMSTRUCT;

La description complète de cette structure peut être évidemment trouvée dans l'aide de MSDN (elle n'est d'ailleurs pas compliquée), mais ce qu'il faut au moins retenir ce sont les champs CtlID (ID du contrôle), hDC (handle du DC à utiliser pour dessiner le contrôle) et rcItem (le rectangle qui décrit le contour du contrôle). Le champ itemState a également son importance, car il spécifie l'état actuel du contrôle. Sa valeur peut être une ou une combinaison de constantes (indépendantes) définies dans winuser.h parmi lesquels ODS_SELECTED (l'élément est sélectionné c'est-à-dire maintenu enfoncé), ODS_DISABLED (l'élément a été désactivé) et ODS_FOCUS (l'élément a le focus clavier).

I-D-3-b. Exemple : Un bouton personnalisé

Un owner-draw button (bouton personnalisé) est un bouton possédant le style BS_OWNERDRAW. Ce style ne peut être mélangé avec les autres styles de boutons (BS_XXX). Pour dessiner le bouton, il n'est généralement pas nécessaire de traiter le message WM_PAINT. Par contre, il est nécessaire d'ajouter un traitement sur réception des messages WM_MOUSEMOVE et WM_MOUSELEAVE pour le faire changer d'aspect respectivement quand la souris vient le survoler et quand la souris le quitte. Le dessin ne se fera cependant que dans le traitement du message WM_DRAWITEM, c'est-à-dire dans la procédure de fenêtre de la fenêtre parent.

À noter que WM_MOUSELEAVE n'est pas envoyé automatiquement. Pour recevoir ce message, il faut avoir préalablement appelé la fonction TrackMouseEvent qui n'existe que depuis Windows 2000.

Voici un exemple de création et d'utilisation d'un owner-draw button.

 
Sélectionnez
#define _WIN32_WINNT 0x0500 /* TrackMouseEvent n'existe que depuis Windows 2000 (Windows NT 5.0). */

#include <windows.h>

/* La structure MYAPP regroupe les variables globales utilisées par notre application. */

typedef struct _MYAPP {
    HWND hCtl; /* Handle du contrôle actuellement survolé par la souris. */
} MYAPP;

MYAPP MyApp;
WNDPROC DefButtonProc; /* "Pointeur" vers l'ancienne procédure de fenêtre de notre bouton. */

#define BUTTON_WIDTH 100
#define BUTTON_HEIGHT 24
#define BUTTON_TEXT "QUIT"

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
void OnCreate(HWND hwnd);
LRESULT CALLBACK ButtonProc(HWND hButton, UINT message, WPARAM wParam, LPARAM lParam);
void OnDrawItem(HWND hwnd, LPDRAWITEMSTRUCT lpdis);
void OnCommand(HWND hwnd, int CtrlID, int Event);

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  = CreateSolidBrush(RGB(0x99, 0xCC, 0xFF));
    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", "Owner-Draw Button",
        WS_POPUP |WS_BORDER | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX,
        100, 100, 200, 100, 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)
{
    switch(message)
    {
    case WM_CREATE:
        OnCreate(hwnd);
        break;

    case WM_DRAWITEM:
        OnDrawItem(hwnd, (LPDRAWITEMSTRUCT)lParam);
        break;

    case WM_COMMAND:
        OnCommand(hwnd, LOWORD(wParam), HIWORD(wParam));
        break;

    case WM_DESTROY:
        PostQuitMessage(0);
        break;

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

    return 0L;
}

void OnCreate(HWND hwnd)
{
    RECT r;
    int x, y;
    HWND hButton;

    /* Centrer le bouton. */
    
    GetClientRect(hwnd, &r);
    x = (r.right - BUTTON_WIDTH) / 2;
    y = (r.bottom - BUTTON_HEIGHT) / 2;

    hButton = CreateWindow( "BUTTON", "", WS_CHILD | WS_VISIBLE | BS_OWNERDRAW,
        x, y, BUTTON_WIDTH, BUTTON_HEIGHT, hwnd, (HMENU)1, NULL, NULL
    );

    /* On modifie la procédure de fenêtre du bouton. */

    DefButtonProc = (WNDPROC)SetWindowLongPtr(hButton, GWLP_WNDPROC, (LONG_PTR)ButtonProc);
}

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

    /* A la fin du traitement d'un message, il reste parfois nécessaire d'appeler l'ancienne  */
    /* procédure de fenêtre. Il n'est cependant pas garanti que DefButtonProc soit réellement */
    /* un pointeur de fonction. Il faut appeler CallWindowProc pour utiliser DefButtonProc.   */

    switch(message)
    {
    case WM_MOUSEMOVE: /* Quand la souris survole le bouton ... */

        if (MyApp.hCtl != hButton)
        {
            TRACKMOUSEEVENT tme;

            MyApp.hCtl = hButton; /* Le contrôle actuellement survolé par la souris est hButton. */

            /* Générer WM_MOUSELEAVE quand la souris quitte le bouton. */

            tme.cbSize = sizeof(tme);
            tme.dwFlags = TME_LEAVE;
            tme.hwndTrack = hButton;
            TrackMouseEvent(&tme);

            InvalidateRect(hButton, NULL, TRUE); /* Redessiner le bouton. */
        }

        ret = CallWindowProc(DefButtonProc, hButton, message, wParam, lParam);

        break;

    case WM_MOUSELEAVE: /* Quand la souris quitte le bouton ... */

        MyApp.hCtl = NULL; /* Aucun contrôle n’est actuellement survolé par la souris. */

        InvalidateRect(hButton, NULL, TRUE); /* Redessiner le bouton. */

        ret = CallWindowProc(DefButtonProc, hButton, message, wParam, lParam);

        break;

    default:

        ret = CallWindowProc(DefButtonProc, hButton, message, wParam, lParam);
    }

    return ret;
}

void OnDrawItem(HWND hwnd, LPDRAWITEMSTRUCT lpdis)
{
    if (lpdis->CtlID == 1) /* 1 est l'ID de notre bouton. */
    {
        HDC hDC = lpdis->hDC;
        LPRECT lprcItem = &lpdis->rcItem;
        COLORREF Color;
        HBRUSH hBrush;

        if (lpdis->itemState & ODS_SELECTED) /* Le bouton est pressé. */
            Color = RGB(0x33, 0x33, 0xCC);
        else if (MyApp.hCtl == lpdis->hwndItem) /* Le bouton est survolé par la souris. */
            Color = RGB(0x66, 0x99, 0xFF);
        else /* Le bouton est dans son état normal. */
            Color = RGB(0x66, 0xCC, 0xFF);

        hBrush = CreateSolidBrush(Color);
    
        SelectObject(hDC, hBrush);
        SetBkMode(hDC, TRANSPARENT);
        Rectangle(hDC, lprcItem->left, lprcItem->top, lprcItem->right, lprcItem->bottom);
        DrawText(hDC, BUTTON_TEXT, (int)strlen(BUTTON_TEXT), lprcItem, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
        
        DeleteObject(hBrush);
    }
}

void OnCommand(HWND hwnd, int CtrlID, int Event)
{
    if (CtrlID == 1 && Event == BN_CLICKED)
        SendMessage(hwnd, WM_CLOSE, 0, 0);
}

précédentsommairesuivant

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 ni 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.