1-Principes généraux

La programmation des applications avec interfaces graphiques est généralement basée sur la programmation événementielle. Ce sont les événements (généralement déclenchés par l'utilisateur, mais aussi par le système) qui pilotent l'application. Donc, la programmation événementielle nécessite qu'un processus (en tâche de fond) surveille constamment les actions de l'utilisateur susceptibles de déclencher des événements qui pourront être ensuite traités (ou non) par l'application. En effet, après la création de l'interface graphique, il faut associer du code à chaque événement que l'on souhaite traiter (RegisterCallback). Lorsque l'utilisateur interagit avec l'interface graphique,  des évènements sont générés et le code associé à chacun de ces évènements est automatiquement exécuté par le processus de surveillance qui tourne en tâche de fond.

Tous les objets graphiques de JavaFX sont capables de générer des évènements. Par exemple, lorsque l'utilisateur clique sur un bouton, un évènement de type ActionEvent est généré par le bouton. Les évènements notifient l'application des actions entreprises par l'utilisateur et permettent à l'application de réagir à cet évènement. La plateforme JavaFx permet de capturer un évènement généré afin de le traiter. Un évènement représente la survenance de quelque chose dans l'application comme l'activation d'un bouton. En JavaFX, l'évènement est une instance de la classe javafx.event.Event ou d'une sous-classe de cette dernière. Il est possible de définir un nouvel évènement en créant une sous classe d'Event et il y a des évènements prédéfinis comme DragEventKeyEventMouseEventScrollEvent, etc. Un évènement comprend plusieurs informations comme le type de l'évènement (consultable avec getEventType()), la source (consultable avec getSource()), etc. Un type d'événement peut avoir un sous-type, par exemple : L'évènement KeyEvent contient les sous-types suivants : KEY_PRESSED, KEY_RELEASED et KEY_TYPED.

Le traitement des événements implique les étapes suivantes :

  1. La sélection de la cible (Target) de l'événement :
    • Pour un événement clavier, la cible est le composant qui possède le focus
    • Pour un événement souris, la cible est le composant sur lequel se trouve le curseur. Si plusieurs composant se trouvent à un emplacement donné c'est celui qui est "au-dessus" qui est considéré comme la cible.
    • etc.
  2. La détermination de la chaîne de traitement des événements (Event Dispatch Chain : chemin des événements dans le graphe de scène). Le chemin part de la racine (Stage) et va jusqu'au composant cible en parcourant tous les nœuds intermédiaires
  3. Le traitement des filtres d'événement (Event Filter) : Exécute le code des filtres en suivant le chemin descendant, de la racine (Stage) jusqu'au composant cible.
  4. Le traitement des gestionnaires d'événement (Event Handler) : Exécute le code des gestionnaires d'événement en suivant le chemin montant, du composant cible à la racine (Stage). 

Exemple : 

Si un utilisateur appuie sur le bouton Insert, un événement de type ActionEvent va être déclenché et va se propager d'abord vers le bas, depuis le nœud racine (Stage) jusqu'à la cible (Target), le long du chemin correspondant à la chaîne de traitement (Event Dispatch Chain). Les filtres (Event Filter) pour l'évènement ActionEvent, éventuellement enregistrés sur les noeuds du chemin, sont exécutés (dans l'ordre de passage).

 Capture décran 2022 03 16 à 22.55.29

 

L'événement remonte ensuite depuis la cible jusqu'à la racine (en suivant le même chemin mais dans l'autre sens) et les gestionnaires d'événements (Event Listener) pour l'évènement ActionEvent, éventuellement enregistrés sur les noeuds du chemin, sont exécutés (dans l'ordre de passage).

Capture décran 2022 03 16 à 23.01.01

 

Chaque récepteur d'événement (filtre ou gestionnaire) peut interrompre la chaîne de traitement en consommant l'événement, c’est-à-dire en invoquant la méthode consume(). Par suite,  la propagation de l'événement s'interrompt et les autres récepteurs (qui suivent dans la chaîne de traitement) ne seront plus activés.

Pour gérer un événement d'un type donné, il faut créer un écouteur d'événement qui capte ce type d'évènement, et l'enregistrer sur les nœuds du graphe de scène où l'on souhaite intercepter l'événement et effectuer un traitement. Un écouteur d'événement peut être enregistré comme filtre ou comme gestionnaire d'événement. La différence principale entre les deux réside dans le moment où le code est exécuté : Les filtres (filters) sont exécutés dans la phase descendante de la chaîne de traitement des événements (avant les gestionnaires) et les gestionnaires (handlers) sont exécutés dans la phase montante de la chaîne de traitement des événements (après les filtres). Les filtres, comme les gestionnaires d'événements, sont des objets qui doivent implémenter l'interface EventHandler qui impose la définition de l'unique méthode handle() qui se charge de traiter l'événement.

public void handle(event-class eventName)

Cette méthode contiendra les instructions à exécuter lorsqu'un évènement de la classe event-class et du type event-type est généré. Event-type est le type de l'évènement capturé; par exemple, KEY_TYPED ou MOUSE_CLICKEDevent-class est la classe à laquelle ce type d'évènement appartient, par exemple, KeyEvent pour les évènements générés par les touches du clavier ou MouseEvent pour les évènements générés par la souris. 

Pour enregistrer un filtre d'événement sur un nœud du graphe de scène, il faut utiliser la méthode addEventFilter(Event-type, EventHandler<? super event-class>) que possèdent tous les nœuds qui sont des sous-classes de Node. De la même façon, pour enregistrer un gestionnaire d'événement, il faut utiliser la méthode addEventHandler(Event-type, EventHandler<? super event-class>). 

Il est aussi possible d'utiliser des méthodes prédéfinies (convenience methods) dont disposent certains composants et qui permettent d'enregistrer un gestionnaire d'événement en tant que propriété du composant. La plupart des composants disposent de méthodes nommées selon le schéma suivant :

node.setOnEvent-type(EventHandler<? super event-class> value)

Par exemple, l'instruction suivante définit une méthode qui enregistre pour un noeud un évènement de type KEY_TYPED et qui est un type d'évènement de la classe KeyEvent :

node.setOnKeyTyped(EventHandler<? super KeyEvent> value)

 

La classe implémentant l'interface EventHandler peut être définie de deux manières : classe anonyme interne ou classe externe. L'interface suivante est développée selon ces deux approches pour les illustrer. Cette interface simple comprend deux champs de texte et deux boutons. Le bouton à gauche sert à copier le texte du champs de texte à gauche dans le champs de texte à droite et le deuxième bouton fait l'inverse.

Capture décran 2022 01 11 à 17.01.16

 

Ci-dessous le code Java pour réaliser l'interface graphique de cette application. Dans ce code, les évènements ne sont pas capturés. Selon la description de l'application, quand l'utilisateur active un des deux boutons avec la souris, il faut capturer l'évènement généré par le bouton activé et copier le texte d'un champs de texte à l'autre. 

 

public class Main extends Application {
Scene mainScene;
TextField tfLeft;
TextField tfRight;
Button bLeft;
Button bRight;

@Override
public void start(Stage primaryStage) throws Exception{
initWidgets();
addWidgetsToScene();
primaryStage.setScene(mainScene);
primaryStage.show();
}

public void initWidgets(){
tfLeft=new TextField("");
tfRight=new TextField("");
bLeft=new Button(">>");
bLeft.setMinWidth(100);
bRight=new Button("<<");
bRight.setMinWidth(100);
}

public void addWidgetsToScene(){
GridPane grid=new GridPane();

grid.setHgap(10);
grid.setVgap(10);
grid.setPadding(new Insets(10, 10, 10, 10));
grid.add(tfLeft, 0, 0);
grid.add(tfRight,1,0);
grid.add(bLeft, 0, 1);
grid.setHalignment(bLeft, HPos.CENTER);
grid.add(bRight,1,1);
grid.setHalignment(bRight, HPos.CENTER);
mainScene=new Scene(grid, 300, 80);
}


public static void main(String[] args) {
launch(args);
}
}

 

2- Classe anonyme interne

La capture d'un type d'évènements, générés par un composant graphique, peut se faire dans une classe interne anonyme qui implémente l'interface EventHandler. Cette classe doit définir la méthode handle() de l'interface EventHandler qui sera exécutée quand le composant graphique génère ce type d'évènement. Dans le code suivant, pour chaque bouton de l'interface graphique décrite ci-dessus, une classe interne et anonyme est définie pour capturer les évènements de type ActionEvent générés par chacun de ces deux boutons. La méthode handle est définie dans chacune de ces classes et sera exécutée lorsqu'un évènement de type ActionEvent est capturé par l'EventHandler. Dans le code suivant, la méthode handle() copie le texte d'un champs de texte à l'autre comme expliqué dans la description de l'application.

 

private void addListeners() {
bLeft.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
tfRight.setText(tfLeft.getText());
}
});

bRight.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
tfLeft.setText(tfRight.getText());
}
});
}
 

La méthode addListeners() sera appelée dans la méthode start() après l'appel de la méthode addWidgetsToScene().

 

3- Classe externe

 

Une classe externe peut implémenter l'interface EventHandler et définir la méthode handle() pour l'évènement ActionEvent. une Instance de cette classe sera associée à chacun de ces deux boutons pour capturer les évènements ActionEvent qu'ils génèrent. La même instance peut-être associée aux deux boutons mais dans la méthode handle() il faut détecter la source de l'évènement et adapter le traitement en fonction de la source de l'évènement. Qui plus est, pour que le contrôleur externe puisse avoir accès aux composants de l'interface graphique, il faut qu'il possède un attribut qui contient une référence à l'interface graphique. Dans le code suivant, la classe ControlButton est définie et implémente l'interface EventHandler pour gérer l'évènement ActionEvent. Le constructeur de cette classe prend en paramètre une référence à l'interface graphique et la sauvegarde dans un attribut local "app" pour qu'il puisse accéder aux composants de cette dernière. Enfin, la méthode getSource() de l'évènement permet de savoir quel bouton a généré l'évènement.

 

import javafx.event.ActionEvent;
import javafx.event.Event;
import javafx.event.EventHandler;
import sample.Main;

public class ControlButton implements EventHandler<ActionEvent> {
Main app;
public ControlButton(Main application){
this.app=application;
}
@Override
public void handle(ActionEvent event) {
if(event.getSource()==app.bLeft){
app.tfRight.setText(app.tfLeft.getText());
}else{
app.tfLeft.setText(app.tfRight.getText());
}
}
}

 

 Pour enregistrer une instance de cette classe comme écouteur des deux boutons, il faut remplacer, dans la classe Main, la méthode addListerners() par la suivante :

 

private void addListeners() {
ControlButton cb = new ControlButton(this);
bLeft.setOnAction(cb);
bRight.setOnAction(cb);
}

 

Le this passé en paramètre au constructeur de ControlButton référence l'interface graphique. Vous pouvez constater que le même contrôleur est associé aux deux boutons. 

 

3- Comparatif avantages/inconvénients des deux méthodes

  Classes internes anonymes Classes externes
avantages
  • Dans le code, les actions sont "proches" de l'objet graphique qui émet l'évènement.
  • Moins de classes à créer.
  • Plus facile/rapide à coder.
  • Possibilité de capturer le même type d'évènement produit par plusieurs objets graphique.
  • Les différents traitements ne sont pas éparpillés dans le code.
  • Code lisible.
inconvénients
  • Impossible de capturer le même type d'évènement produit par plusieurs objets graphiques.
  • Code peu lisible.
  • Dans le code, les actions sont "éloignées" de l'objet graphique qui émet l'évènement.
  • Codage un peu plus difficile/lent


Conclusion :

  • Petites interfaces =  classe interne,
  • Grosses interfaces = classe externe.