III. Les menus▲
III-A. Création dynamique d'un menu▲
La fonction qui permet de créer un menu est CreateMenu. Après qu'on ait créé un menu, on peut lui ajouter des éléments à l'aide de la fonction AppendMenu. Évidemment un menu peut comporter d'autres menus. Par exemple les menus Fichier, Edition, Affichage etc. sont des sous-menus du menu principal. Chacun de ces menus peut ensuite encore comporter d'autres menus et ainsi de suite. Le problème c'est que les éléments « terminaux » (tels que Enregistrer, Fermer, Quitter, etc.) sont également appelés menus ce qui est généralement source de confusion. On se réfère donc aux termes anglais « popup » pour désigner un menu qui peut en comporter d'autres (mais qui n'est pas le menu principal) et « menu item » pour désigner un élément terminal d'un menu. En résumé, « menu » est un terme générique (et confus) qui peut être à la fois utilisé pour désigner un menu principal, un popup menu ou un menu item. Notez également qu'un menu principal a toutes les fonctionnalités d'un popup menu mais ce n'est pas un popup menu (c'est un menu principal !).
Le fonctionnement des menus est très similaire à celui des boutons au sens où le fait de cliquer sur un menu (terminal) provoque l'émission du message WM_COMMAND avec comme paramètres l'identificateur du menu (LOWORD(wParam)) et NULL (lParam).
Dans l'exemple suivant, on va créer un menu (le menu principal) qui contiendra juste un sous-menu : Fichier, qui contiendra à son tour 5 éléments terminaux à savoir Nouveau, Ouvrir, Enregistrer, Enregistrer sous et Quitter. Un séparateur (qui se présente sous forme d'une ligne horizontale) sera placé entre les 4 premiers et la dernière. On peut créer le menu avant ou après la fenêtre, tout dépend de la manière dont on veut s'y prendre. La première solution permet de spécifier le handle du menu directement dans l'argument hMenu de CreateWindow. Dans cet exemple, c'est la deuxième méthode que nous allons utiliser.
#include <windows.h>
LRESULT CALLBACK WndProc
(
HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
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 =
(
HBRUSH)(
COLOR_WINDOW +
1
);
wc.hCursor =
LoadCursor
(
NULL
, IDC_ARROW);
wc.hIcon =
LoadIcon
(
NULL
, IDI_APPLICATION);
wc.hInstance =
hInstance;
wc.lpfnWndProc =
WndProc;
wc.lpszClassName =
"
Menu Demo
"
;
wc.lpszMenuName =
NULL
;
wc.style =
CS_HREDRAW |
CS_VREDRAW;
RegisterClass
(&
wc);
hWnd =
CreateWindow
(
"
Menu Demo
"
, "
M-D
"
, WS_OVERLAPPEDWINDOW, 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)
{
static
HMENU hMenu, hFichier;
switch
(
message)
{
case
WM_CREATE:
hMenu =
CreateMenu
(
);
hFichier =
CreateMenu
(
);
AppendMenu
(
hFichier, MF_STRING, (
UINT_PTR)1
, "
&Nouveau
"
);
AppendMenu
(
hFichier, MF_STRING, (
UINT_PTR)2
, "
&Ouvrir
"
);
AppendMenu
(
hFichier, MF_STRING, (
UINT_PTR)3
, "
&Enregistrer
"
);
AppendMenu
(
hFichier, MF_STRING, (
UINT_PTR)4
, "
En®istrer sous ...
"
);
AppendMenu
(
hFichier, MF_SEPARATOR, (
UINT_PTR)-
1
, ""
);
AppendMenu
(
hFichier, MF_STRING, (
UINT_PTR)5
, "
&Quitter
"
);
AppendMenu
(
hMenu, MF_POPUP, (
UINT_PTR)hFichier, "
&Fichier
"
);
SetMenu
(
hwnd, hMenu);
break
;
case
WM_COMMAND:
switch
(
LOWORD
(
wParam))
{
case
1
:
MessageBox
(
hwnd, "
Crée un nouveau document
"
, "
M-D
"
, MB_OK);
break
;
case
2
:
MessageBox
(
hwnd, "
Ouvre un document
"
, "
M-D
"
, MB_OK);
break
;
case
3
:
MessageBox
(
hwnd, "
Enregistre le document
"
, "
M-D
"
, MB_OK);
break
;
case
4
:
MessageBox
(
hwnd, "
Enregistre sous un autre nom
"
, "
M-D
"
, MB_OK);
break
;
case
5
:
DestroyWindow
(
hwnd);
break
;
}
break
;
case
WM_DESTROY:
PostQuitMessage
(
0
);
break
;
default
:
return
DefWindowProc
(
hwnd, message, wParam, lParam);
}
return
0
;
}
Vous vous demandez certainement pourquoi il n'est pas nécessaire de détruire le menu avant de quitter le programme. La réponse est : si, il faut le détruire (avec DestroyMenu). Cependant, lorsqu'un menu est attaché à une fenêtre, il sera automatiquement détruit en même temps que cette fenêtre. D'accord, mais qu'en est-il des sous-menus ? Ils ont également été détruits en même temps que le menu, car DestroyMenu est récursive, c'est-à-dire que la destruction d'un menu entraîne également la destruction de ses sous-menus.
III-B. Création à l'aide d'un fichier de ressources▲
On peut également créer un menu en ressource. Dans le programme, il ne restera plus qu'à charger cette ressource avec la fonction LoadMenu, qui retournera le handle du menu en question.
HMENU LoadMenu
(
HINSTANCE hInstance, LPCTSTR lpMenuName);
Les éditeurs de ressources simplifient beaucoup la création de menus, mais à défaut, on peut toujours taper le code nous-mêmes. Par exemple :
MyMenu MENU
{
POPUP "
&Fichier
"
{
MENUITEM "
&Nouveau
"
, 1
MENUITEM "
&Ouvrir ...
"
, 2
MENUITEM "
&Enregistrer
"
, 3
MENUITEM "
En®istrer sous ...
"
, 4
MENUITEM SEPARATOR
MENUITEM "
&Quitter
"
, 5
}
}
On peut également directement spécifier le nom du menu (ici « MyMenu ») dans le membre lpszMenuName de la structure WNDCLASS et dans ce cas on n'a plus besoin ni de LoadMenu, ni de SetMenu. En outre, l'argument hMenu de CreateWindow doit être tout simplement NULL.
III-C. Les menus contextuels▲
Dans une application standard, on affiche normalement un menu contextuel lorsqu'on reçoit le message WM_CONTEXTMENU qui, rappelons-le, est envoyé lorsque l'utilisateur a cliqué avec le bouton droit à l'intérieur de la fenêtre.
Un menu contextuel est un menu comme tous les autres. N'importe quel popup peut être affiché en tant que menu contextuel. Notez bien cependant que le menu doit être obligatoirement un popup menu, pas n'importe quel menu. Il faut donc utiliser CreatePopupMenu à la place de CreateMenu. À noter également que dans l'exemple précédent (création dynamique), on aurait aussi pu créer le menu Fichier à l'aide de CreatePopupMenu (puisqu'il s'agit bien d'un popup menu) par contre on ne peut pas utiliser cette fonction pour créer le menu principal (qui n'est pas un popup menu …). Et enfin, sachez que, puisqu'ils sont reconnus par le système comme étant de vrais popup menus, les sous-menus, peu importe la manière dont ils ont été créés (en ressource, avec CreateMenu, avec CreatePopupMenu, etc.), peuvent également être utilisés. Si on n'a pas le handle du sous-menu (ce qui est fréquemment le cas lorsque le menu a été créé en ressource), on peut toujours le récupérer avec GetSubMenu. La fonction GetMenu, qui retourne le handle du menu d'une fenêtre, peut également s'avérer utile.
HMENU GetMenu
(
HWND hWnd);
HMENU GetSubMenu
(
HMENU hMenu, int
nPos);
La fonction TrackPopupMenu permet d'afficher le menu.
BOOL TrackPopupMenu
(
HMENU hPopup, UINT uFlags, int
x, int
y, int
nReserved, HWND hWnd, const
RECT *
prcIgnored);
Le dernier paramètre est de la décoration. Il est ignoré par Windows ! L'extrait de code suivant permet d'afficher un menu contextuel :
case
WM_CONTEXTMENU:
TrackPopupMenu
(
hFichier, 0
, LOWORD
(
lParam), HIWORD
(
lParam), 0
, hwnd, NULL
);
break
;
III-D. Autres fonctions▲
EnableMenuItem▲
BOOL EnableMenuItem
(
HMENU hPopup, UINT uItem, UINT uEnable);
Le paramètre uEnable peut prendre l'une des valeurs suivantes :
- MF_ENABLED : active le menu de sorte que l'utilisateur peut le sélectionner ;
- MF_DISABLED : désactive le menu de sorte que l'utilisateur ne peut pas le sélectionner, mais ne change pas son apparence ;
- MF_GRAYED : désactive le menu de sorte que l'utilisateur ne peut pas le sélectionner et change son apparence.
En fait, on peut combiner ces valeurs avec MF_BYCOMMAND (par défaut) ou MF_BYPOSITION. MF_BYCOMMAND entraîne que uItem fait référence à l'ID du menu que l'on veut activer ou désactiver et MF_BYPOSITION que uItem fait référence à l'indice du menu à activer / désactiver (le premier élément possède l'indice 0).
CheckMenuItem▲
BOOL CheckMenuItem
(
HMENU hPopup, UINT uItem, UINT uCheck);
Les valeurs utilisables dans l'argument uCheck MF_CHECKED, MF_UNCHECKED, MF_BYCOMMAND et MF_BYPOSITION.
III-E. Les accélérateurs clavier▲
Les accélérateurs (ou raccourcis) clavier sont des combinaisons de touches qui permettent à l'utilisateur d'émettre une commande (c'est-à-dire le message WM_COMMAND) à l'aide du clavier. Ils sont généralement utilisés comme raccourcis vers une commande proposée par un menu ou un bouton par exemple. On va par exemple créer les raccourcis suivants pour notre menu Fichier : Ctrl + N (Nouveau), Ctrl + O (Ouvrir), Ctrl + S (Enregistrer) et F12 Enregistrer sous.
Traiter le message WM_KEYDOWN puis tester l'état de la touche Ctrl est inefficace, car ce message n'est émis que si la fenêtre a le focus du clavier. Dès que le focus passe à un contrôle, ce message peut ne pas être envoyé. C'est pourquoi il faut utiliser les accélérateurs. Tout ce qu'on a à faire c'est d'associer les combinaisons de touches Ctrl + N, Ctrl + O, Ctrl + S et F12 aux commandes 1 (Nouveau), 2 (Ouvrir), 3 (Enregistrer) et 4 (Enregistrer sous). On peut faire cette association dynamiquement à l'aide de la fonction CreateAcceleratorTable, mais c'est plus simple et plus vite de la créer en ressource.
#include <windows.h> /* pour VK_F12 (défini dans winuser.h) */
MyAccels ACCELERATORS
{
"
N
"
, 1
, CONTROL, VIRTKEY
"
O
"
, 2
, CONTROL, VIRTKEY
"
S
"
, 3
, CONTROL, VIRTKEY
VK_F12, 4
, VIRTKEY
}
Le flag VIRTKEY spécifie que le code de la touche de l'accélérateur est un code de touche virtuelle et non le code d'un caractère ASCII. Si on omet ce flag, Windows suppose qu'il s'agit d'un code de caractère ASCII (donc « N » par exemple correspondra au caractère « N » majuscule, c'est-à-dire Shift + N, et non la touche N). Pour ce qui est des touches d'accompagnement, on peut utiliser n'importe quelle combinaison des flags CONTROL, ALT et SHIFT ou aucune.
Il faut ensuite charger la ressource.
HACCEL hAccel =
LoadAccelerators
(
hInstance, "
MyAccels
"
);
Et enfin, adapter la boucle des messages.
while
(
GetMessage
(&
msg, NULL
, 0
, 0
))
{
if
(!
TranslateAccelerator
(
hWnd, hAccel, &
msg))
{
TranslateMessage
(&
msg);
DispatchMessage
(&
msg);
}
}
La fonction TranslateAccelerator examine un message et si ce dernier provient du clavier et correspond à une entrée dans la table des accélérateurs, il sera traité comme il se doit et la fonction retourne TRUE, sinon FALSE est retournée. Si la fonction réussit, le message a donc déjà été traité et il ne faut plus appeler ni Translate ni Dispatch Message.