Mise en oeuvre d'une Interface Graphique

Introduction

 Qu'ils s'appellent Motif (Unix), Interface Kit (BeOS), AppKit (Cocoa / MacOS X), Swing (Java) ou Appareance Manager (MacOS) tous les moteurs d'interface graphique suivent à peu pres les mêmes règles de conception. Et ceci pour une simple et bonne raison, les codes de ces moteurs d'interface sont basés sur des structures de données équivalentes.

 Réaliser un moteur d'interface grahique peut donc revenir à retrouver cette structure de données et à l' implémenter.
 C'est ce que nous nous proposons de faire dans le présent document.

 L'implantation se fera en C++ sur plateforme MacOS (mais cela peut être facilement porté sous BeOS, Java, Windows, etc...).

 Afin de ne pas déroger à la règle, exposons les pré-requis nécessaires à la bonne compréhension de ce document:

 Si surprenante que puisse être cette révélation, celle-ci s'avère néanmoins exacte. Afin de confirmer ceci, nous allons procéder en 2 étapes:
  1. Décortiquer ce qui se cache derrière une simple copie d'écran d'un dialogue MacOS.
  2. Reconstruire à partir de cette analyse un moteur d'interface graphique orienté objet.
Enquète sur un dialogue

Soit la capture d'écran suivante:

Capture d'écran

 L'analyse de cette image nous donne les informations suivantes:

Une fenêtre contient 3 objets:

  • 1 bouton texte "Ok"
  • 1 bouton texte "Cancel"
  • 1 boîte englobante "Options"
 La boîte englobante contient elle-même 2 objets:
  • 1 boîte à cocher "Option 1"
  • 1 boîte à cocher "Option 2"

 Si l'on passe cette image au rayon X (en cachant les bords de la fenêtre), on obtient le cliché suivant:

Rayon X

 L'analyse devient alors:

Un rectangle: A contient 3 rectangles: B, C et D.

Le rectangle B contient 2 rectangles: E et F

 Si l'on essaye de représenter sous forme d'arbre les relations entre les différents rectangles, on obtient ceci:

Hierarchie Horizontale

 Ce qui peut se traduire aussi sous la forme suivante:

Hierarchie Definitive

 Si l'on souhaite créer un objet permettant de représenter informatiquement cet arbre ,il ressemblerait à ceci:

class myGraphicObject {
    
  public:
    myGraphicObject(void) {
      in_=next_=NULL;
    }

myGraphicObject * in_; myGraphicObject * next_; };
Listing 1

 Le code servant à créer la hiérarchie ressemblerait quant à lui à ceci:

myGraphicObject A,B,C,D,E,F;

A.in_=B;
B.next_=C;
C.next_=D;

B.in_=E;
E.next_=F;
Listing 2

 La partie hiérarchie représente la partie invisible de notre interface. Si l'on souhaite pouvoir visualiser notre mini moteur d'interface, il suffit de rajouter une donnée membre et des méthodes de dessin.  Notre classe devient alors:

class myGraphicObject {

  protected:

    Rect  rectangle_;   // BRect ou NSRect ou Rectangle
    
  public:
 
    myGraphicObject(Rect);
 
    myGraphicObject * in_;
    myGraphicObject * next_;

    void DrawEveryObject(void);
    virtual void DrawObject(void);
};
Listing 3

myGraphicObject::myGraphicObject(Rect inRectangle_) {
  rectangle_=inRectangle_;
  in_=next_=NULL;
}

void myGraphicObject::DrawEveryObject(void) {
  DrawObject();
  if (in_!=NULL) in_->DrawEveryObject();
  if (next_!=NULL) next_->DrawEveryObject();
}

void myGraphicObject::DrawObject(void) {
  ::ForeColor(30);
  ::PaintRect(&rectangle_);
  ::ForeColor(33);
  ::FrameRect(&rectangle_);
}
Listing 4

 Il suffit maintenant de modifier le code principal:

myGraphicObject * A,* B,* C,* D,* E, *F;
Rect r;

::SetRect(&r,0,0,215,138);
A = new myGraphicObject(r);

::SetRect(&r,15,18,196,88);
B = new myGraphicObject(r);

::SetRect(&r,63,102,122,121);
C = new myGraphicObject(r);

::SetRect(&r,137,102,196,121);
D = new myGraphicObject(r);

::SetRect(&r,25,40,101,57);
E = new myGraphicObject(r);

::SetRect(&r,25,63,101,80);
F = new myGraphicObject(r);

A->in_=B;
B->next_=C;
C->next_=D;

B->in_=E;
E->next_=F;
Listing 5

 Pour obtenir le dessin exact de la première capture, il suffirait donc de créer des classes dérivées de myGraphicObject: myButtonObject, myGroupObject, myCheckObject et de surcharger la méthode DrawObject.


 Ce que l'on a obtenu comme proprieté sans s'en rendre compte:
Gestion du clic souris

 Maintenant que notre splendide interface se dessine correctement, on aimerait bien pouvoir interagir avec elle. Pour l'instant tout clic dans les cadres ne produit rien.

 L'interaction sur clic-souris se produit en 2 étapes:

  1. Déterminer quel élément est concerné par le clic souris.
  2. Faire réagir l'élément de manière appropriée.
Déterminer quel élément est concerné par le clic souris

 L'algorithme décrivant cette action pourrait être conçu ainsi:

Le clic se produit-il dans l'objet ?

  • Oui
    • Regarder si le clic ne se produit pas dans un objet contenu par l'objet
    • Sinon c'est bien dans cet objet que le clic s'est produit
  • Non
    • Regarder si le clic se produit sur les objets suivants
Algorithme 1

 Ce qui nous donnerait la fonction suivante:

myGraphicObject * myGraphicObject::Who(Point inPoint) {
  if (::PtInRect(pt,&rectangle_)==true) {
    if (in_!=NULL) {
      myGraphicObject temp=in_->Who(inPoint);

      if (temp!=NULL) return temp;
    }
    return this;
  }
  else {
    if (next_!=NULL) return next_->Who(inPoint);
  }
  return NULL;  
}
Listing 6

 Cette fonction pourrait sembler parfaite si ne surgissait un problème majeur.

 Soit l'interface suivante:

Contre-exemple

 Cette interface est représentée par la hiérarchie suivante:

Hiérarchie du contre-exemple

 Si l'on utilise la fonction précédente quand un clic se produit dans une zone commune au rectangle B et C, c'est le rectangle B qui est renvoyé comme concerné alors que sur le dessin il est clair que c'est C qui est devant B et qui doit donc être l'élément concerné par le clic.
 Si l'on rejette un coup d'oeil à notre fonction actuelle en tenant compte de la gestion des plans vue plus haut, on s'apercoit que l'on fait notre recherche de l'arrière plan vers l'avant plan alors qu'en fait il faudrait faire la recherche de l'avant plan vers l'arrière plan.
 Et là gros probleme - si on a bien suivi - le champ next_ traduit la relation arrière-pan vers avant plan. Ce qui necessite donc de modifier notre objet graphique afin de prendre en compte la relation avant_plan vers arrière-plan.

 Notre hiérarchie devient la suivante:

Nouvelle hiérarchie

 Notre objet devient:

class myGraphicObject {

  protected:
 
    Rect rectangle_;
   
  public:

    myGraphicObject(Rect);
   
    myGraphicObject * in_;
    myGraphicObject * next_;
    myGraphicObject * previous_;
    
    void DrawEveryObject(void);
    virtual void DrawObject(void);
    
    myGraphicObject * Who(Point);
};
Listing 7

 L'implantation du code devenant celle-ci:

myGraphicObject::myGraphicObject(Rect inRectangle_) {
  rectangle_=inRectangle_;
  previous_=in_=next_=NULL;
}

myGraphicObject * myGraphicObject::Who(Point inPoint) {
  if (::PtInRect(inPoint,&rectangle_)==false) {
    if (previous_!=NULL) return previous_->Who(inPoint);
    else return NULL;
  }
  if (in_!=NULL) {
    myGraphicObject * tObject;

    tObject=in_;
	
    while (tObject->next_!=NULL) {
      tObject=tObject->next_;
    }
    
    tObject=tObject->Who(inPoint);
	
    if (tObject==NULL) return this;
    else return tObject;
  }
  return this;
}


// Les autres méthodes sont inchangées

// [...]

myGraphicObject * A,* B,* C,* D,* E, *F;
Rect r;

::SetRect(&r,0,0,215,138);
A = new myGraphicObject(r);

::SetRect(&r,15,18,196,88);
B = new myGraphicObject(r);

::SetRect(&r,63,102,122,121);
C = new myGraphicObject(r);

::SetRect(&r,137,102,196,121);
D = new myGraphicObject(r);

::SetRect(&r,25,40,101,57);
E = new myGraphicObject(r);

::SetRect(&r,25,63,101,80);
F = new myGraphicObject(r);

A->in_=B;

B->next_=C;
C->previous_=B;

C->next_=D;
D->previous_=C;

B->in_=E;

E->next_=F;
F->previous_=E;
Listing 8

Réaction de l'objet au clic souris

 Pour vérifier que cela fonctionne bien nous allons sur un clic souris alterner la couleur de fond de l'objet cliqué:

void myGraphicObject::ClicInContent(EventRecord * inEvent) {

  ::ForeColor(205);
  ::PaintRect(&rectangle_);

  while (::StillDown());

  DrawEveryObject(); // Pour rétablir un dessin harmonieux
}
Listing 9

 Notre moteur fonctionne désormais correctement au niveau dessin et réactivité au clic souris. Cependant il estpour l'instant très contraignant de réaliser la partie création de la hiérarchie. Cela peut etre amélioré.

 En simplifiant la seule fonction dont nous ayons besoin est celle qui permette d'imbriquer un élément dans un autre. Cela nous donne la fonction suivante:

void myGraphicObject::AddChild(myGraphicObject * inObject) {
  if (in_==NULL) {
    in_=inObject;
  }
  else {
    myGraphicObject * temp=in_;

    while (temp->next_!=NULL) {
      temp=temp->next_;
    }
 
    temp->next_=inObject;
    inObject->previous_=temp;
  }
}
Listing 10

 La création de la hiérarchie devient plus simple:

myGraphicObject * A,* B,* C,* D,* E, *F;
Rect r;

::SetRect(&r,0,0,215,138);
A = new myGraphicObject(r);

::SetRect(&r,15,18,196,88);
B = new myGraphicObject(r);

::SetRect(&r,63,102,122,121);
C = new myGraphicObject(r);

::SetRect(&r,137,102,196,121);
D = new myGraphicObject(r);

::SetRect(&r,25,40,101,57);
E = new myGraphicObject(r);

::SetRect(&r,25,63,101,80);
F = new myGraphicObject(r);

A->AddChild(B);
A->AddChild(C);
A->AddChild(D);

B->AddChild(E);
B->AddChild(F);
Listing 11

Epilogue

 Pour l'instant notre moteur d'interface aussi simple soit-il permet de réaliser des interfaces assez complexes. Cependant afin d'avoir quelque chose de tout à fait complet, il faudrait ajouter les éléments suivants:

Une fois tout ceci fait, le code principal pourrait ressembler à ceci:

NSButton * ok,* cancel;
NSBox * box;
NSCheckBox * check1,* check2;

ok=new NSButton(NSRect(137,102,196,121),"Ok");
ok->SetIdentifier("ok");
AddChild(ok);

cancel=new 
NSButton(NSRect(63,102,122,121),"Cancel");
ok->SetIdentifier("cancel");
AddChild(cancel);

box=new NSBox(NSRect(15,18,196,88),"Options");
AddChild(box);

check1=new 
NSCheckBox(NSRect(25,40,101,57),"Option 1");
box->AddChild(check1);

check2=new 
NSCheckBox(NSRect(25,63,101,80),"Option 2");
box->AddChild(check2);

Listing 12

Télécharger le code d'exemple