Architecture logicielle & Développement

Du contrôle des fenêtres, première partie | 1 vote(s)

Tags: ,

Je suis en train de faire l'architecture logicielle et le développement d'un tout nouveau framework de classes de gestions du fenêtrage basé sur l'API Win32. Ce qui m'a poussé à faire ça ? Un savant dosage de syndrôme NIH, la volonté de m'essayer à ce problème, et une fonctionnalité manquante dans les MFC de Microsoft.

Bien entendu, j'aurais pu tout aussi bien essayer de trouver un composant sur le web qui m'aurait permit de faire la même chose. Il existe en effet plusieurs framework de ce type:

  • les MFC : si l'architecture logicielle de ce produit laisse véritablement à désirer (mais je ne vais pas polémiquer sur ce sujet), il n'en reste pas moins un produit phare reconnu. Il a de plus l'avantage d'être stable - la plupart de ses bugs sont connus, et il existe de nombreuses possibilités de contournement de ces problèmes. Autre avantage : un grand nombre de bibliothèques se basent sur ce framework pour étendre ses fonctionnalités. Un exemple: la très puissante librairie prof-uis de FOSS Software. je ne vous cache pas que c'est la librairie que je connais le mieux, même si je ne l'ai guère utilisé ces 3 dernières années. Mon problème vis à vis de cette vénérable librairie tient en trois mots: static message map. J'y reviendrais tout à l'heure.
  • Qt de Trolltech : disponible depuis peu en version GPL pour Windows (il existe bien évidemment une version commerciale de ce produit), cette librairie est très simple d'utilisation, et offre l'ensemble des fonctionnalités que je souhaite. Le design est clair, mais peu élégant. A l'époque ou la librairie a été créée et distribuée, les compilateurs C++ pour unix étaient loin de supporter le standard C++ correctement, et un ensemble non négligeable de fonctionnalités de Qt n'a pu être implémenté que grâce à l'adjonction dans la chaine d'outils d'un précompilateur spécifique. Cette phase de précompilation est encore aujourd'hui d'actualité, bien que le standard C++ et les outils récents pourraient permettre de s'en passer. Quoi qu'il en soit, mon problème majeur avec Qt est sa licence : je ne peux pas me permettre de dépenser des milles et des cents pour réaliser mon outil, et je ne souhaite pas mettre le code source sous licence GPL. Je suis donc coincé.
  • GTK+ : développé en C, ce toolkit est à l'origine le toolkit de widgets utilisé par l'équipe qui a réalisé le fabuleux The Gimp. Point de problème de licence ici (GTK+ est distribué sous licence LGPL). Mais un problème de taille : l'origine C du toolkit, et son architecture abracadabrante. Programmer avec GTK+ nécessite de suivre un certain nombre de règles, toutes n'étant pas très heureuses. Il faut en outre un solide sens de l'orientation pour s'y retrouver. Et ne dites pas que je n'aime pas ce toolkit : j'y ai consacré plus d'un an de ma vie, ma contribution majeure au projet étant une réécriture complète de la FAQ officielle lors du passage de la version 1.0 à la version 1.2 (oui, je sais, ça date...).
  • wxWidgets : encore un projet disponible sous licence libre. wxWidgets est basé sur une philosophie proche de cette utilisée par les MFC. Le résultat a bien évidemment un gout de MFC prononcé, mais ce gout est rendu plus subtil par le fait qu'un grand nombre d'erreur d'architecture ont été corrigées. Il est possible de "connecter" un nouveau gestionnaire de message à une fenêtre (via wxEvtHandler::Connect()). Mais hélas, je connais peu ce framework[1].

Il est tout à fait possible que d'autre frameworks traitant du même sujet existe - mais je ne les connais pas, ce qui m'empêche de les incorporer à cette courte liste. Et puis, au final, je développe ce framework sur mon temps libre, donc si j'ai envie de le faire, pourquoi m'en empêcher ? Au niveau architecture logicielle, il s'agit là d'un sujet passionnant. Au niveau code, il n'y a rien d'infaisable - seules certaines techniques de doivent d'être connue.

Structure de base de la classe window

Pour cette courte introduction, je n'ai rien inventé : j'ai repris une structure assez classique pour la gestion d'une fenêtre. Toutes les fonctions ne sont pas présentes (et oui, allez savoir pourquoi, mais je n'ai pas terminé d'implémenter ce framework).

class window
{
  HWND             m_hwnd;
  message_map   m_message_map;
public: window(); virtual ~window();
bool create(DWORD style, DWORD ex_style, const std::wstring& class_name, const std::wstring& window_name, const rect& position, HWND parent_handle, HMENU menu_handle, HINSTANCE instance_handle);
void destroy(); void set_message_handler(UINT message, message_handler* handler);
protected: static LRESULT CALLBACK static_wnd_proc(HWND hwnd, UINT message, WPARAM word_param, LPARAM long_param);
protected: virtual bool pre_create_window(CREATESTRUCT& cs) { return true; } virtual void create_message_map();
private: LRESULT wnd_proc(HWND hwnd, UINT message, WPARAM word_param, LPARAM long_param); bool handle_message(UINT message, WPARAM word_param, LPARAM long_param, LRESULT& return_value); };

La fonction window::static_wnd_proc() est la procédure de réception des messages standard - c'est elle qui effectuera le dispatch des messages vers les fenêtres cibles. Elle récupère le pointeur vers chaque fenêtre dans les données utilisateurs associées au HWND (via GetWindowLong()/SetWindowLong()). Une fois le pointeur récupéré, la fonction appelle window::wnd_proc(), qui sera dévoilée plus tard.

A noter que seuls deux messages doivent être traités différemment : les messages WM_CREATE et WM_NCCREATE. Avant le traitement de ce message, le HWND associé à la fenêtre n'existe pas, il faut donc trouver un autre moyen de transférer l'objet window qui sera par la suite associé au HWND. Cette opération est réalisée grâce au paramètre lpParam de la fonction de l'API Win32 CreateWindowEx(). Ce paramètre supplémentaire (de la taille d'un pointeur) est ensuite stocké dans une structure interne CREATESTRUCT, qui est passée en paramètre (LPARAM) à window::static_wnd_proc. A partir de là, il devient aisé de le récupérer, ce qui nous permet de traiter les message WM_CREATE et WM_NCCREATE dans des fonctions de gestion de message de la classe elle-même.

Table des messages dynamique

Dans les MFC, la table des messages est on ne peut plus statique. Elle est fixée au moment de la compilation, et ne peut pas changer par la suite. On peut bien évidemment étendre les MFC pour gérer des tables de message dynamique, mais la complexité d'une telle entreprise est assez élevée.

Dans mon nouveau projet, j'ai besoin de gérer la table des messages de manière dynamique. Un plugin, une fois chargé, peut modifier de manière subtile l'interface utilisateur. Il doit donc pouvoir aussi brancher les gestionnaires de messages (des méthodes dans une classe) sur une fenêtre existante.

J'ai bien évidemment d'autres besoin, qui s'exprime notamment en termes d'élégances de code, de respect du design du langage, etc. Le plus important d'entre eux est probablement le respect du système de typage du C++. Je souhaite éviter au maximum les pertes d'informations relatives au type des données que je manipule. En deux mots : je veux que mon code soit type safe. Mais je souhaite aussi ne pas être obligé de me limiter à l'entrée usuelle d'une fonction de gestion de message typique de l'APi Windows (LRESULT func(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)). Je souhaite pouvoir décoder les informations reçue du système, et appeler mes méthodes de manière naturelle (par exemple: BOOL on_create(CREATESTRUCT* cs)).

Le nombre de messages pouvant être gérés dans une application est gigantesque. Je ne peux pas me permettre de créer un bloc switch pour traduire les paramètres d'un type vers une autre - pour deux raisons : premièrement, le code serait particulièrement fastidieux à écrire, et deuxièmement, il ne respecterait pas le principe OCP, puisque l'ajout d'un nouveau type de message entrainerait une modification du code déjà écrit.

La solution, je l'emprunte à GTK+ (même si elle est utilisée dans bien d'autres cas). L'appel d'une fonction de gestion de message est indirecte - un objet intermédiaire se chargeant d'interpréter les données d'entrées et d'appeler la fonction nécessaire. Le code de la fonction window::wnd_proc est relativement simple

LRESULT window::wnd_proc(HWND hwnd, 
                         UINT message, 
                         WPARAM word_param, 
                         LPARAM long_param)
{
  if (message == WM_CREATE || message == WM_NCCREATE)
  {
    CREATESTRUCT*   create_struct;
    window*         wnd;
create_struct = reinterpret_cast<CREATESTRUCT *>(long_param); wnd = static_cast<window*>(create_struct->lpCreateParams);
SetWindowLongPtrW(hwnd, GWL_USERDATA, reinterpret_cast<LONG_PTR>(wnd)); m_hwnd = hwnd; }
LRESULT return_value = 0;
if (handle_message(message, word_param, long_param, return_value)) { return return_value; }
return DefWindowProcW(hwnd, message, word_param, long_param); }

Tout se passe alors dans la fonction window::handle_message()

bool window::handle_message(UINT message, 
                            WPARAM word_param, 
                            LPARAM long_param, 
                            LRESULT& return_value)
{
  message_handler* handler = m_message_map.find_handler(message);
if (handler) { return_value = handler->call(word_param, long_param); return true; }
return false; }

La classe message_handler est une classe abstraite, définissant juste la méthode virtuelle pure message_handler::call(). Le but de cette fonction est d'analyser les paramètres (WPARAM, LPARAM) et d'appeler la fonction de gestion du message correspondant.

Oui, mais comment implémenter ce mécanisme ? C'est là que la métaprogrammation va nous aider un peu. En effet, rien ne m'empêche de créer un nombre infini de classe template dérivant de message_handler. Voici par exemple la classe qui gère le message WM_CREATE :

#include <functional> // for std::mem_fun1
#include "win/message_handler.h"
template <class RECEIVER> class wm_create_handler : public win::message_handler { public: typedef RECEIVER receiver_type; typedef BOOL (receiver_type::*func_type)(CREATESTRUCT*);
private: receiver_type* m_receiver; func_type m_func;

public:

  wm_create_handler(receiver_type* receiver, func_type func)
    : m_receiver(receiver), m_func(func)
  { }
virtual LRESULT call(WPARAM word_param, LPARAM long_param) { CREATESTRUCT* create_struct;
create_struct = reinterpret_cast<CREATESTRUCT *>(long_param); return std::mem_fun1(m_func)(m_receiver, create_struct); } };

L'utilisation est alors extrêmement simplifiée :

// stripped down
class my_main_window : public win::window
{
  BOOL wm_create(CREATESTRUCT* create_struct);
protected: virtual void create_message_map() { message_handler *handler;
handler = new wm_create_handler<my_main_window>( this, &my_main_window::wm_create);
set_message_handler(WM_CREATE, handler); } };

Conclusion

C'est tout pour aujourd'hui. Il nous reste à traiter un cas important (le cas des sous-messages, dont l'exemple type est WM_COMMAND), puis nous terminerons sur une note d'architecture logicielle.

Notes

[1] oui, je sais, c'est une très mauvaise excuse... Mais il faut bien que j'en trouve une, non ?

Trackbacks

Aucun trackback.

Les trackbacks pour ce billet sont fermés.

Commentaires

Aucun commentaire pour le moment.

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.