Gestion des interactions graphiques

images/orangebelt.png

Les objets "événement"

Si on le configure de manière appropriée un composant peut créer et propager des objets chargés de véhiculer des informations sur les interactions entre l’utilisateur et la présentation graphique.

Ces objets de diagnostic sont capables de donner des renseignements sur des choses comme:

  • Quel est l’objet Component qui a généré le diagnostic?
  • Quand ce diagnostic a-t’il été émis?
  • Quelle était la combinaison de touches clavier enfoncées à ce moment?
  • etc. etc. (données spécifiques à la catégorie de diagnostic)

De tels objets sont des "événements" (sous-classes de Event) et sont, pour la plupart, définis dans le package java.awt.event.

Par exemple:

  • événements "physiques" comme KeyEvent (interactions avec le clavier), MouseEvent (interactions avec la souris),…
  • événements sur les composants comme WindowEvent (modification du statut d’une fenêtre), FocusEvent (cible de saisie déplacée),…
  • événements "sémantique" comme ActionEvent (on vient de valider qqch), ItemEvent (un item vient d'être sélectionné/désélectionné),…

Ces événements sont créés par le système de fenêtrage et passés en argument à des méthodes spécifiques aux circonstances ("handler methods").

Par exemple pour un MouseEvent le système graphique peut choisir une méthode comme:

  • mousePressed(MouseEvent evt) (bas niveau)
  • mouseEntered(MouseEvent evt) (encore du bas niveau: la souris a passé les frontières du composant).
  • mouseClicked(MouseEvent evt) (plus haut niveau: dans les détails de l'événement on trouvera s’il y a eu double clic ou non …)

Sur quels objets seront appelées ces méthodes?

L’objet réflexe (call back pattern)

call-back

Le principe général est le suivant:

  • Un objet "veilleur" (Listener) est enregistré auprès du composant pour traiter un type d'événement.
  • Quand un événement est généré par ce composant (et propagé par le Thread awt) , la méthode appropriée du veilleur est choisie et invoquée par le système graphique.

Le veilleur peut être n’importe quel code mais le composant doit être sûr au moment de l’enregistrement que le veilleur a les méthodes appropriées. Donc le composant doit être sûr des capacités du veilleur et ce contrôle de type passe par l’utilisation d’une interface java.

Par exemple un bouton Button a une méthode d’enregistrement comme addActionListener(ActionListener act) et ActionListener est une interface.

Un code de veilleur. 

public class ShowIt extends Panel implements ActionListener {
   private TextField show = new TextField(20) ;
   ...... // autres codes y compris constructeurs

   public void actionPerformed(ActionEvent evt) {
      show.setText("action -avec étiquette"
         + evt.getActionCommand() + "-" ) ;
      // autres actions ...

   }
}

Enregistrement d’un veilleur. 

   // .....
   Button bouton = new Button("OK") ;
   // ....
   ShowIt showIt = new ShowIt() ;
   // ...
   bouton.addActionListener(showIt) ;
   // ...

Interfaces de veille et "adaptateurs"

La réalisation de classes conformes aux contrats de veille (interfaces Listener) impliquent parfois la définition de nombreuses méthodes.

public class MouseIt implements MouseListener {
   public void mouseClicked(MouseEvent e) {
      // faire qqch.
   }
    public void    mouseEntered(MouseEvent e) {}
    public void    mouseExited(MouseEvent e) {}
    public void    mousePressed(MouseEvent e) {}
    public void    mouseReleased(MouseEvent e)  {}

}

On écrit ainsi plusieurs méthodes qui ne font strictement rien! Dans ce cas une facilité d'écriture consiste à utiliser une classe "adaptateur" (Adapter) - à la condition qu’on ne soit pas déjà en train d'étendre une autre classe -.

public class MouseClick extends MouseAdapter {
   public void mouseClicked(MouseEvent e) {
      // faire qqch.
   }
}

--exercices--

Considérations architecturales liées à la gestion des événements: un exemple

La mise en place de différents codes pour la disposition et les événements peut s’avérer délicate. Il y a des choix stratégiques à faire et pas vraiment de solution universelle.

Pour les besoins de la discussion partons d’un exemple simple:

exemple ihm simple

Ici nous voulons un code de veille qui incrémente une variable "nombre de clics" chaque fois que l’on appuie sur le bouton.

Quand l’action est effectuée le code graphique appelle un code applicatif (incrémentation de "nombre de clics") puis ce code rappelle un code graphique! Chaque solution a des avantages et des inconvénients qui sont discutés ci-après…

Considérations architecturales : veilleur dans une classe distincte

public class ControleurDeClic implements ActionListener {
   private int clics ;
   private TextField message ; // reference au composant IHM

   public ControleurDeClic(TextField txt) {
      this.message = txt ;
   }

   public void actionPerformed(ActionEvent evt){
      this.clics++ ;
      this.message.setText(" nombre de clics = " + this.clics ) ;
   }

}
public class IHMClics extends Panel {
   private TextField textfield = new TextField(40) ;
   private Button bouton = new Button("Cliquez moi!") ;
   // code
   public IHMClics() {
      // code
      this.bouton.addActionListener(new ControleurDeClic(this.textfield)) ;
   }

Nous utilisons ici un "truc" classique: une classe a besoin d’une référence vers un objet … on la lui passe en paramètre au constructeur, et elle est conservée comme variable membre.

Dans ce cas particulier il y a quelques inconvénients:

  • Les classes ControleurDeClic et IHMClics ne sont pas indépendantes. Bien que ça ne soit pas évident de prime abord elles sont couplées: par exemple si on décide de modifier IHMClics et on remplace le TextField par un Label alors on doit changer le code de ControleurDeClic . C’est possible mais il vaut mieux éviter (rappel: un couplage fort c’est quand ClassA utilise ClassB et ClassB utilise ClassA -directement ou indirectement-).

Considérations architecturales: veille depuis la classe courante

public class IHMClics extends Panel implements ActionListener {
   private TextField message = new TextField(40) ;
   private Button bouton = new Button("Cliquez moi!") ;

   private int clics; // logique applicative au sein de l'IHM

   public IHMClics() {
      // on ajoute des composants
      this.bouton.addActionListener(this) ;
   }

   public void actionPerformed(ActionEvent evt){
      this.clics++ ;
      this.message.setText(" nombre de clics = " + this.clics ) ;
   }
}

Ici nous avons poussé la logique de la non-indépendance des deux classes précédentes jusqu’au bout: les codes ne forment qu’un! Ce n’est pas nécessairement l’option la plus favorable:

  • S’il y a plusieurs boutons à surveiller le code peut devenir piégé (voir le mauvais exemple ci-après)
  • Si le code lié à l’action est profondément du domaine de la logique applicative il vaut mieux ne pas mélanger trop intimement les codes graphiques et les codes qui relèvent de la logique "métier".
  • La conformité au contrat ActionListener est exposée publiquement dans l’API de la classe: ce peut être une source de confusion.
[Avertissement]anti-pattern
   // ANTI-PATTERN !   à réserver à des cas très particuliers

   public void actionPerformed(ActionEvent evt){
      Object source = evt.getSource() ;
      if( source == boutonA ){ /* faire qqch. */ }
      else if( source == boutonB ){ /* faire autre chose */ }
      // ... ainsi de suite ...

Considérations architecturales : veille depuis une classe interne

public class IHMClics extends Panel {
   private TextField message = new TextField(40) ;
   private Button bouton = new Button("Cliquez moi!") ;

   class ControleurClic implements ActionListener {
      int clics;
      public void actionPerformed(ActionEvent evt){
         this.clics++ ;
         message.setText(" nombre de clics = " + this.clics ) ;
         // "message" est EN PORTEE SYNTAXIQUE !
         // mais ce n'est pas "this.message"
      }
   }

   public IHMClics() {
      // disposition des composants
      this.bouton.addActionListener(new ControleurClic()) ;
   }
}

Bien que cette architecture mélange encore la présentation et la logique "métier" on peut maintenant gérer facilement des boutons différents et les capacités d'ActionListener ne sont pas exposées publiquement.

On notera que la classe membre d’instance est en portée syntaxique et peut utiliser un membre private de la classe englobante.

On notera aussi que la classe interne n’est en fait utilisée qu’une fois.

Note: le nom de la classe interne est IHMClics.ControleurClic; le fichier "binaire" correspondant sera IHMClics$ControleurClic.class

Considérations architecturales : veille depuis une classe anonyme

public class IHMClics extends Panel {
   private TextField message = new TextField(40) ;
   private Button bouton = new Button("Cliquez moi!") ;

   public IHMClics() {
      // disposition de composants
      this.bouton.addActionListener( new ActionListener() {
         int clics;
         public void actionPerformed(ActionEvent evt){
            message.setText(" nombre de clics = " + ++clics ) ;
         }
      } );
   }
}

Le code new ActionListener(){…} définit une classe à l’intérieur même du code. En principe on ne peut pas faire new sur une interface: ici le code définit une classe sans nom qui fait implicitement extends Object implements ActionListener (Les codes binaires des classes anonymes seront générées dans votre système de fichier avec des noms compremant un numéro précédé d’un signe $)

Une classe anonyme peut-être utilisée quand il y a une seule instance d’une classe veilleur. On notera que la variable membre "message" est en portée syntaxique (si c'était une variable locale elle devrait alors être déclarée final).

Un exemple typique d’un petit code de veilleur :

   frame.addWindowListener(new WindowAdapter() {
      public void windowClosing(WindowEvent evt) {
         frame.dispose() ;
      }
   }) ;

--exercices--

Considérations architecturales : transfert vers du code applicatif

Le code graphique d’IHM et la logique "métier" applicative devraient être séparés le plus possible.

Il y a plusieurs modèles possibles : dans la plupart des cas on a, à des points stratégiques, des classes qui ont à la fois des connaissances graphiques et des références vers des services "métiers" (des classes controleur) et les différents niveaux communiquent au travers d’interfaces de service.

--exercice avancé--