Du contrôle des fenêtres, seconde partie | 0 vote(s)
Par Emmanuel Deloget, lundi 14 mai 2007 à 18:00 :: C++ :: permalien #75
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 !
Commentaires
1. Le mardi 15 mai 2007 à 14:26, par BFranck
2. Le mardi 15 mai 2007 à 20:52, par Emmanuel Deloget
:: Fil rss des commentaires de ce billet ::
Ajouter un commentaire