Du contrôle des fenêtres, troisième partie | 0 vote(s)
Par Emmanuel Deloget, jeudi 24 mai 2007 à 18:00 :: C++ :: permalien #76
La seconde partie nous a enfin expliqué le sous-classement des contrôles Windows (qui, je le rappelle, a pour but de modifier le comportement d'un contrôle ou d'une fenêtre créée selon une classe pré-déterminée). Au niveau des fenêtres, il nous reste peu de choses à voir - mis à part peut-être les boites de dialogue, les fenêtres MDI, les barres de contrôles, etc. Ah, quand même. Soit. Laissons nous tenter par les fenêtre MDI.
Fenêtre cadre et client MDI
MDI signifie bien évidemment Multiple Document Interface. Le fonctionnement du système MDI sous Windows est relativement simple, pour peu qu'on passe outre la difficulté principale: la création de la zone client MDI. En effet, les fenêtre document qui apparaissent dans la fenêtre cadre de l'application ne sont pas directement les filles de cette fenêtre cadre, mais sont en fait les filles d'une fenêtre "cachée" appartenant elle même à la fenêtre cadre. Cette fenêtre "cachée" est de la classe MDICLIENT. De plus, la fenêtre principale (frame window) de l'application est gérée différemment - au lieu de passer les messages non traités à DefWindowProc(), elle doit les passer à DefFrameProc().
Plus ennuyeux, nous ne pouvons pas nous servir du paramètre lpParams lors de l'appel à la fonction CreateWindowEx() au moment de la création de la fenêtre MDI client. La raison: ce paramètre est utilisé pour passer une instance de la structure CLIENTCREATESTRUCT. Cette situation se corse encore lorsqu'on considère que l'on ne crée par la classe de la fenêtre (puisqu'il s'agit de la classe système MDICLIENT). Comment passer le pointeur vers notre classe au filtre CBT dans ce cas ? Une technique possible consiste à se rendre compte qu'une fenêtre MDI client n'a pas de titre de fenêtre. On peut donc se servir du champs CREATESTRUCT::lpszName pour stocker le pointeur de la fenêtre - et bien évidemment, le filtre CBT doit être modifié pour permettre la récupération de ce pointeur.
Ce qui pose immédiatement un autre problème. La fenêtre MDI client est généralement créée lors du traitement du message WM_CREATE de la fenêtre cadre. Ceci signifie qu'un autre filtre CBT est déjà enregistré; puisque ces filtres sont chainés, l'ensemble des filtres CBT installés va être appelé lors de la création d'une fenêtre - en fait, c'était déjà le cas avant cette modification, mais cela avait peu d'importance au niveau de l'exécution puisqu'on utilisait les mêmes données à chaque fois. Au pire, on effectuait plusieurs fois la même opération. Mais dans notre nouveau cas, le traitement des filtres est très différent : l'un des filtres va tenter de récupérer le pointeur vers l'instance de window dans CREATESTRUCT::lpCreateParams, tandis que l'autre va le lire dans CREATESTRUCT::lpszName. Si on utilise le premier filtre lors de la création d'une fenêtre MDICLIENT, il va tenter d'interpréter CREATESTRUCT::lpCreateParams (qui, dans ce cas, est un pointeur vers une structure CLIENTCREATESTRUCT) comme étant un pointeur vers une instance de window. On peut parier que cela va créer un problème...
Il nous faut donc trouver une autre technique pour mettre en oeuvre ce filtre CBT[1] - soit pour le rendre général (et éviter ainsi d'avoir à installer plusieurs filtres différents), soit pour éviter de traiter les données avec deux filtres différents. Pour information, les MFC utilise les fonctions TLS (Thread Local Storage) pour mener cette opération à bien, mais pour ce faire elles sont obligée d'utiliser un objet statique (_afxThreadState), ce qui n'est pas particulièrement élégant (le mécanisme de gestion de cet objet étant particulièrement complexe).
Installer deux filtres différent est une mauvaise solution. Rien ne me dit que Windows n'en a pas installé un troisième, donc je n'ai pas le droit de modifier les données que je reçoit du filtre. Mais dans ce cas, je ne peux pas avertir le filtre suivant que le traitement a déjà été effectué. Je ne peux pas plus stopper le traitement - toujours pour la même raison. La solution est donc de n'avoir qu'un seul filtre CBT installé.
En fait, le problème n'est pas si compliqué qu'il en a l'air: je suis déjà capable de déterminer dans le filtre CBT la classe de fenêtre que je dois traiter. Il m'est donc possible de choisir le traitement adapté, en fonction de cette classe de fenêtre. Je me sert pour cela de la fonction GetClassName() qui, à partir d'un handle de fenêtre, me permet de connaitre le nom de la classe associée à cette fenêtre[2]. Une fois le nom de la classe connue, je n'ai plus qu'à le tester et choisir le comportement le plus adapté afin de retrouver le pointeur vers l'instance window.
LRESULT CALLBACK window::static_cbt_hook(int code, WPARAM word_param, LPARAM long_param)
{
if (code == HCBT_CREATEWND)
{
// interpret the CBT hook data
CBT_CREATEWNDW* cbt_create;
HWND hwnd;
cbt_create; = reinterpret_cast<CBT_CREATEWNDW*>(long_param);
hwnd = reinterpret_cast<HWND>(word_param);
// get the window instance
// the method to get it depends on the window class
std::vector<wchar_t> class_name_buf(128);
if (GetClassNameW(hwnd, &class_name_buf.front(), class_name_buf.size()))
{
std::wstring class_name(&class_name_buf.front());
CREATESTRUCT* create_struct = cbt_create->lpcs;
window* wnd = NULL;
std::transform(class_name.begin(),
class_name.end(),
class_name.begin(),
::toupper);
if (class_name == std::wstring(L"IME"))
{
// do not subclass this window
wnd = NULL;
}
else if (class_name == std::wstring(L"MDICLIENT"))
{
LPWSTR name = const_cast<LPWSTR>(create_struct->lpszName);
wnd = reinterpret_cast<window*>(name);
}
else
{
wnd = reinterpret_cast<window*>(create_struct->lpCreateParams);
}
if (wnd && hwnd)
{
save the handle and subclass the window
wnd->cbt_hook(hwnd, cbt_create);
}
}
}
return CallNextHookEx(NULL, code, word_param, long_param);
}
On notera que le procédé n'est pas, en soi, très élégant. En fait, le principe Ouvert/Fermé n'est pas respecté ici, puisque l'ajout d'un nouveau type de classe de fenêtre peut entrainer une modification de ce code. Le code final utilisera de préférence un système de stratégies - où le choix de la stratégie à appliquer dépends du nom de la classe.
Fenêtres MDI filles
Pour créer une fenêtre MDI, plusieurs possibilités s'offrent à nous:
- Utiliser
CreateWindow()ouCreateWindowEx()en précisant le styleWS_EX_MDICHILD. - utiliser
CreateMDIWindow()- les paramètres de cette fonction sont similaires à ceux des fonctions de création de fenêtre normale. - Envoyer un message
WM_MDICREATEà la fenêtre MDICLIENT.
Les messages non traités sont quand à eux passé à la fonction DefMDIChildProc(). Dernier point important: dans les deux derniers cas, la classe de la fenêtre fille doit être créée en spécifiant une taille mémoire légèrement plus importante que lors de la création d'une classe de fenêtre normale (au niveau du code, ceci se traduit par une initialisation non nulle de WNDCLASSEX::cbWndExtra). Il est donc préférable, vu notre système déjà complexe, de passer par une appel à CreateWindowEx().
La création des fenêtres filles MDI soulève un problème: qui les crée ? Si on les crée de manière séparées, alors il faut être sûr de les lier avec une instance de la classe mdi_frame_window. Les fenêtre doivent aussi hériter d'une classe mdi_child_window, qui redéfini le comportement de la classe window. L'autre solution (adoptée par la librairie MFC) est de créer les fenêtre filles MDI via un appel d'une fonction de la classe de fenêtre cadre. Mais dans ce cas se pose un problème encore plus important: comment gérer les classes utilisateur ? Les MFC proposent un mécanisme de création dynamique d'objets - les fenêtres se voient associée un objet du type CRuntimeClass dont l'une des méthode permet la création d'une nouvelle fenêtre du type spécifié par l'utilisateur. C'est une possibilité, mais j'avoue la trouver trop complexe par rapport au service offert. Il est tout aussi simple de passer une instance d'une classe dérivée de window à cette fonction de création de fenêtre fille, à ceci près qu'on peut alors être amené à devoir composer avec les cas interdits (par exemple: une fenêtre fille ne peut pas avoir de menu).
J'ai choisi la première solution : je laisse l'utilisateur créer les fenêtres MDI filles, en spécifiant la fenêtre MDI cadre comme fenêtre parent. Si la fenêtre cadre n'est pas spécifiée ou si elle n'est pas une fenêtre cadre MDI, la fonction mdi_child_window::create() ne s'exécute pas. Le code simplifié (mais quasiment complet) pour une classe mdi_child_window ressemble alors à celui-ci:
#include "mdi_child_window.h" #include "mdi_frame_window.h"
using namespace win;
mdi_child_window::mdi_child_window() : window() { }
mdi_child_window::~mdi_child_window() { }
LRESULT mdi_child_window::default_message_handling(UINT message, WPARAM word_param, LPARAM long_param) { return DefMDIChildProc(get_handle(), message, word_param, long_param); }
bool mdi_child_window::create(const std::wstring& window_name, const rectangle& position, mdi_frame_window* parent) { return window::create(MDIS_ALLCHILDSTYLES, WS_EX_MDICHILD, window_name, position, parent->get_client_handle(), NULL); }
Ce code permet de créer des fenêtre MDI très simples, telle que le montre la capture d'écran ci-dessous.

Conclusion
Globalement, l'ajout du support des fenêtre MDI ne modifie pas profondément l'architecture - en fait, seule quelques classes sont ajoutées, et certaines fonctions statiques de window ont été modifiée (notamment le filtre CBT). Le diagramme de classe suivant montre les liens entre les différentes classes définies.

Il reste bien évidemment bien des sujets à aborder - vous pouvez vous reporter à l'introduction pour avoir une idée des sujets les plus importants. La série va quand même faire une petite pause, de manière à revenir à des sujets plus "traditionnels" pour quelques billets.
Notes
[1] décidément... dès qu'on veut la généraliser, cette création de fenêtre est tout sauf simple...
[2] le MSDN précise que bien qu'il semblerait qu'on puisse lire le nom de classe en utilisant CREATESTRUCT::lpszClass, ce n'est pas toujours le cas - car ce champs peut contenir soit une chaine de caractère, soit un ATOM privé et inaccessible.
Commentaires
Aucun commentaire pour le moment.
:: Fil rss des commentaires de ce billet ::
Ajouter un commentaire