Architecture logicielle & Développement

Du contrôle des fenêtres, seconde partie | 0 vote(s)

Tags: ,

Dans la première partie, nous avons vaguement étudié un système nous permettant de recevoir des messages Windows dans une classe de fenêtre, et de lier à ces messages une fonction de gestion (un handler). On pourrait croire que notre tâche est presque terminée - mais il y a quelques problèmes...

Les sous-messages

Et bien évidemment, le premier d'entre eux concerne la gestion des sous-messages. Prenons le cas de WM_COMMAND, le message envoyé à la fenêtre parent d'un bouton ou d'un menu lorsque celui-ci est activé. Si on doit créer le handler de ce message, on peut aisément écrire :

template <class RECEIVER>
public wm_command_handler : public win::message_handler
{
public:
  typedef RECEIVER receiver_type;
  typedef void (receiver_type::*func_type)(WPARAM,LPARAM);
private: receiver_type* m_receiver; func_type m_func;
public: wm_command_handler(receiver_type* receiver, func_type func) : m_receiver(receiver), m_func(func) { }
virtual LRESULT call(WPARAM word_param, LPARAM long_param) { (m_receiver->*m_func)(word_param, long_param); return 0; } };

Oui mais voilà, ce handler va maintenant recevoir l'ensemble des messages WM_COMMAND ! Chaque bouton cliqué, chaque menu sélectionné, en fait presque toutes les opérations de l'utilisateur sur l'interface graphique vont générer ce message. Il nous faut alors différencier les différentes commandes - représentée par un identifiant stocké dans les bits de poids faible de WPARAM. Comment fait-on dans ce cas ? Doit-on implémenter un mécanisme à base de switch ? Mauvaise idée. Doit-on alors implémenter un mécanisme similaire à celui-ci pour gérer les commandes ? C'est possible, mais il faudra alors faire de même pour tous les autres types de sous-messages. Clairement, ce n'est pas une très bonne solution.

En fait, la solution optimale consiste à améliorer notre système de sélection du handler à exécuter. A l'heure actuelle, celui-ci ne prends en compte que le type du message. Nous n'avons qu'à améliorer le système pour gérer les sous-messages dans le même temps. Cette solution a le mérite d'être simple à implémenter, l'ensemble du code étant contenu dans la classe message_map. Cette classe est principalement composée d'une instance de la classe std::map<UINT,message_handler*>. la clef, de type UINT est en fait le message auquel le handler est attaché. Il suffit de modifier le type de cette clef pour prendre en compte les messages de commande.

struct message_key
{
   message_key() { }
   message_key(const message_info& msg_info)
     : message(msg_info.get_message())
     , command_id(msg_info.get_command_id())
   { }
   // we let the compiler create the copy ctor, operator=
   // and the destructor
   UINT message;
   UINT command_id;
};
bool operator<(const message_key& lhs, const message_key& rhs) { if (lhs.message < rhs.message) return true; if (lhs.message == rhs.message) { return lhs.command_id < rhs.command_id; } return false; }
bool operator==(const message_key& lhs, const message_key& rhs) { if (lhs.message == rhs.message) { if (lhs.command_id) { return lhs.command_id == rhs.command_id; } } return false; }

La classe message_info (non représentée) est une classe d'aide, qui permet de créer une clef de message à partir des données d'un message. Le principe de base est le suivant: si le message est WM_COMMAND ou un autre message pouvant être accompagné de sous-messages, message_info extrait l'identifiant de la commande de WPARAM et LPARAM. Sinon, message_info::get_command_id() retourne 0.

Est-ce que cette technique est performante ? C'est une bonne question - nous allons devoir dans certains cas gérer des dizaines de messages par seconde. J'ai fait le test suivant : j'ai implémenter un handler pour l'évènement WM_MOUSEMOVE, perdu au milieu d'une petite dizaine de handlers pour d'autres messages. J'ai ensuite vérifier le nombre de messages WM_MOUSEMOVE que je pouvais traiter chaque seconde, et j'ai comparé ce chiffre au nombre de message que j'aurais du pouvoir traiter avec une architecture plus simple - mais fixe[1]. La version C++ me permet de traiter une petite soixantaine de messages par seconde (entre 55 et 59). La version C ne fait pas mieux - une petite dizaines de tests me donnent les même résultats. Conclusion : ce n'est pas l'implémentation qui ralentira le traitement des messages. La question de la performance de la technique ne se pose donc pas.

Les classes prédéfinies de fenêtres

Windows implémente un certain nombre de classes prédéfinies de fenêtres - principalement pour gérer les contrôles des boites de dialogue : boutons, boites d'éditions, etc. On peut aisément créer des fenêtres de ce type avec notre système, en utilisant la fonction de l'API Win32 CreateWindow() ou CreateWindowEx() de manière appropriée. On se heurte pourtant à une limitation sévère de notre modèle : il est impossible de modifier le comportement de ces objets.

La raison de cette limitation est simple : nous n'avons pas la possibilité de redéfinir la procédure de gestion de messages qui sera appelée par le système pour répondre aux messages envoyés, car cette procédure est définie par le système. Il nous faut donc sous-classer la fenêtre créée pour pouvoir brancher notre procédure à nous. Mais nous nous heurtons alors à un problème : l'implémentation actuelle effectue le lien entre l'objet win::window et le handle de la fenêtre lors du traitement du message WM_CREATE. La structure CREATESTRUCT accompagnant ce message est utilisée pour stocker le pointeur de la fenêtre associée. Mais puisque nous ne gérons pas le traitement du message WM_CREATE dans le cas d'une fenêtre ayant été créée à partir d'une classe système, nous ne recevrons pas WM_CREATE, et nous ne pourrons pas faire le lien entre les deux entités. La solution courante ne fonctionne donc pas. Comment faire ?

Bien évidemment, si nous trouvons une solution à ce problème, l'idéal serait qu'elle permette aussi de gérer le cas des fenêtres normales.

Bien évidemment, cette solution existe, et reste relativement simple. L'idée consiste à installer un filtre de messages (hook) qui sera activé chaque fois qu'une fenêtre sera créée. Bien évidemment, il ne sert à rien de laisser ce filtre actif pendant tout le temps ou l'application fonctionne, aussi prendra-t-on soin de l'enlever lorsque la fenêtre aura été créée.

Parmi tous les types de hook possible, le plus intéressant est le hook CBT[2], qui est activé entre autres à chaque création de fenêtre. Il transmet alors à la fonction de filtre le handle de la fenêtre nouvellement créée ainsi qu'un pointeur sur une structure CBT_CREATEWND. Chance indue : cette structure contient à son tour un pointeur sur une copie de la structure CREATESTRUCT, dont le membre lpParams peut être initialiser aisément avec un pointeur sur notre instance de classe dérivée de win::window. La boucle est bouclée : non seulement nous pouvons initialiser la nouvelle fenêtre avec son handle, mais nous pouvons aussi sous-classer cette nouvelle fenêtre pour brancher notre propre procédure de gestion des messages.

LRESULT CALLBACK
cb_hook_proc(int code, WPARAM word_param, LPARAM long_param)
{
  if (code == HCBT_CREATEWND)
  {
    // get the parameters
    CBT_CREATEWND* cbt_struct;
    HWND           hwnd;
cbt_struct = reinterpret_cast<CBT_CREATEWND*>(long_param); hwnd = reinterpret_cast<HWND>(word_param);
// get the win::window instance pointer win::window* wnd; wnd = static_cast<win::window*>(cbt_struct->lpcs->lpParams);
// attach the handle to the window, and subclass the window if (wnd && hwnd) { wnd->attach(hwnd); wnd->subclass(); } }
// process the next hook return CallNextHookEx(NULL[3], code, word_param, long_param); }

Il ne nous reste plus qu'à intercepter la fonction de gestion de message et la remplacer par la nôtre, via un appel à SetWindowLongPtr() - en se rappelant qu'il faut stocker le retour de cette fonction puisqu'il s'agit de la procédure de fenêtre originelle - et que dans bien des cas, on voudra pouvoir appeler celle ci.

Conclusion

Nous pouvons maintenant créer tous les types de fenêtre imaginable, et modifier à loisir le comportement de celles-ci. Il nous reste toutefois de nombreux points à aborder, dont les boites de dialogues et leurs contrôles. A bientôt !

Notes

[1] un programme C utilisant l'API Win32 de manière classique

[2] "Computer-Based Training"

[3] D'après la documentation du SDK Win32 (Windows Server 2003 R2), ce paramètre est ignoré

Trackbacks

Aucun trackback.

Les trackbacks pour ce billet sont fermés.

Commentaires

1. Le mardi 15 mai 2007 à 14:26, par BFranck

Gravatar

Je découvre avec beaucoup d'intérêt ce blog où nombre de sujets techniques très intéressants (pour moi en tout cas) sont traités.

J'ai juste une question de pseudo-néophyte à propos de cet article en particulier. Pourquoi "détourner" des API Windows quand on pourrait faire tout ça en Java ?

PS : ce commentaire (qui n'en est même pas un) n'est qu'un prétexte à une question ; êtes-vous le Emmanuel Deloget que j'ai cotoyé durant l'année 1994-1995 au lycée Gustave Eiffel de Dijon ? Une réponse privée par mail est possible. Si je fais fausse route, vous pouvez bien entendu me tancer vertement pour cette intrusion non conforme sur ce blog.

2. Le mardi 15 mai 2007 à 20:52, par Emmanuel Deloget

Gravatar

Je suis cet Emmanuel là. Enfin, j'ai un peu vieilli depuis. Pétard, 95, c'est vieux... Meme mes meilleures bouteilles de vin sont plus jeunes que ca :)

Quand à la question, elle n'est pas tant que ca une question de néophyte (ou de pseudo-néophyte). Bon, je n'aime pas Java, mais on peut se poser la meme question avec .NET.

Et bien figure toi que je n'ai pas de réponse. Enfin, j'ai bien une réponse, mais elle ne tient pas tant que ca la route: J'avais envie de le faire :) Ce qui m'en a donné envie, c'est le nombre toujours grandissant de questions liées a l'API Win32 posées sur les forums de GameDev.Net (là, la réponse est simple: il s'agit au final de s'interfacer avec DirectX dans son incarnation la plus pure, c'est à dire sous la forme d'une API C ou C++; ce qui impose l'utilisation de ce langage pour la gestion des appels Win32). Et puis mon autre motivation c'est que j'ai toujours été attiré par l'architecture des API C++ de gestion des interfaces graphiques (entre autre). J'ai noté à ce propos que c'était l'une des voies de recherches pour le futur TR2 du C++ (qui sortira, si tout va bien, en 2012/2013; a une époque ou le langage lui meme aura éte oublié :)), donc j'expérimente.

Et puis je me sers aussi de cette base logicielle pour travailler sur un projet personnel, mais ca c'est un secret... :)

Ajouter un commentaire

Si votre navigateur est compatible, vous pouvez vous aider de la barre d'outils placée au-dessus de la zone de saisie pour enrichir vos commentaires.