Interfaces graphiques
1 Introduction
La bibliothèque Java contient trois ensembles de classes permettant la création d'interfaces graphiques. Dans l'ordre chronologique de leur apparition, ce sont :
- AWT (Abstract Window Toolkit), tombé en désuétude mais servant de base à Swing,
- Swing, utilisé pour la majorité des applications existantes mais en cours de remplacement par Java FX,
- Java FX.
Cette leçon décrit Swing, mais la plupart des concepts se retrouvent dans Java FX et dans d'autres bibliothèques similaires pour d'autres langages.
Swing est composé d'un très grand nombre de classes appartenant toutes (ou presque) au paquetage javax.swing
et à ses sous-paquetages. De plus, étant donné que Swing se base sur AWT, les classes du paquetage java.awt
et ses sous-paquetages sont souvent nécessaires.
2 Composants
Une interface graphique Swing se construit en combinant un certain nombre de composants (components), imbriqués les uns dans les autres. Par exemple, les fenêtres, les menus, les boutons, les zones de texte, etc. sont des composants. Les classes représentant ces composants ont toutes un nom commençant par J
, et héritent de la classe abstraite JComponent
— à quelques exceptions près.
Il existe deux types de composants Swing :
- les composants de base, qui ne contiennent pas d'autres composants (boutons, etc.),
- les conteneurs, dont le but principal est de contenir et d'organiser d'autres composants (fenêtres, etc.).
Les conteneurs sont séparés en deux catégories :
- les conteneurs de niveau intermédiaire, qui peuvent eux-même être contenus par d'autres conteneurs,
- les conteneurs de niveau supérieur (top-level containers), qui ne peuvent pas l'être.
Les composants d'une application Swing sont organisés en (au moins) une hiérarchie arborescente dont :
- la racine est un conteneur de niveau supérieur,
- les nœuds — autres que la racine — sont des composants intermédiaires,
- les feuilles sont des composants de base.
Le principal type de conteneur de niveau supérieur est la fenêtre (classe JFrame
), donc à chaque fenêtre affichée à l'écran par une application Swing correspond une hiérarchie. A noter qu'un composant ne peut pas appartenir à plus d'une hiérarchie, ou apparaître plus d'une fois dans la même hiérarchie.
La figure ci-dessous présente un exemple (simplifié) de hiérarchie correspondant à une application Swing dotée d'une fenêtre contenant plusieurs composants. Le composant portant l'étiquette 1 est un conteneur de niveau supérieur (une fenêtre dans ce cas), celui portant l'étiquette 2 est un conteneur intermédiaire, et celui contenant l'étiquette 3 est un composant de base.
Figure 1 : Hiérarchie de composants Swing
A l'écran, tout composant occupe une zone rectangulaire. Les fils d'un conteneur se partagent généralement sa zone à lui, et il n'y a pas de chevauchement entre composants frères. La hiérarchie de composants est donc aplatie en un ensemble de zones rectangulaires imbriquées.
Figure 2 : Hiérarchie de composants et apparence à l'écran
2.1 Composants Swing
La classe abstraite JComponent
sert de classe-mère à tous les composants Swing, sauf ceux de niveau supérieur. JComponent
et ses sous-classes possèdent un grand nombre de propriétés (attributs) modifiables. A chacune d'entre elles est associée une méthode de lecture nommée get…
et une méthode d'écriture nommée set…
. Par exemple, la propriété font
, qui contient la police utilisée pour le texte d'un composant, se lit avec la méthode getFont
et s'écrit avec la méthode setFont
.
Les composants de base offerts par Swing incluent :
- une étiquette, textuelle ou graphique (
JLabel
), - différents boutons : à un état (
JButton
), à deux états (JToggleButton
), « radio » (JRadioButton
), à cocher (JCheckBox
), - un champ textuel (
JTextField
), également en version formatante (JFormattedTextField
), ou masquée pour les mots de passe (JPasswordField
), - une liste de valeurs dont certaines peuvent être sélectionnées (
JList
), - etc.
Les conteneurs intermédiaires offerts par Swing incluent :
- un panneau séparé en deux parties redimensionnables, affichant chacune un composant (
JSplitPane
), - un panneau à onglets (
JTabbedPane
), - un panneau affichant une partie d'un composant trop grand pour être affiché en entier (
JScrollPane
), - un panneau permettant de superposer plusieurs composants (
JLayeredPane
), - un panneau sans représentation graphique (
JPanel
), - etc.
Finalement, les conteneurs de niveau supérieur offerts par Swing sont au nombre de trois :
- la fenêtre (
JFrame
), - la boîte de dialogue (
JDialog
), - l'« applet » (
JApplet
) destinée principalement à être utilisée dans un navigateur Web, rarement utilisée de nos jours.
Par exemple, l'interface graphique d'une calculatrice à quatre opérations pourrait être obtenue au moyen de la hiérarchie suivante :
Figure 3 : Hiérarchie d'une calculatrice
2.2 Composants de niveau supérieur
Les composants de niveau supérieur (top-level components) sont à la racine de toute hiérarchie visible à l'écran. Contrairement aux autres types de composants, ceux de niveau supérieur ne peuvent pas être inclus dans d'autres composants. Swing offre trois types de conteneurs de niveau supérieur : la fenêtre, la boîte de dialogue et l'applet. Seuls les deux premiers sont examinés ici.
2.2.1 JFrame
JFrame
représente une fenêtre. Ses principales propriétés sont : son titre (title
), sa taille et sa position à l'écran (bounds
), sa visibilité (visible
, valant false
par défaut), son comportement en cas de fermeture (defaultCloseOperation
) et son panneau de contenu (contentPane
).
Figure 4 : Fenêtre Swing (JFrame
)
Par défaut, lorsque l'utilisateur ferme une fenêtre, celle-ci est simplement rendue invisible. Ce comportement est souvent inadéquat pour la fenêtre principale d'une application, car il est préférable que la fermeture de cette fenêtre provoque l'arrêt complet de l'application, p.ex. via un appel à System.exit
. Pour obtenir ce comportement, on peut changer la propriété defaultCloseOperation
afin de lui donner la valeur EXIT_ON_CLOSE
:
JFrame f = …; f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Comme cela sera expliqué plus loin, l'agencement des composants à l'écran est généralement fait automatiquement dans une application Swing. Cet agencement se fait récursivement, en commençant par les feuilles puis en remontant jusqu'à la racine. Les composants de niveau supérieur offrent une méthode pack
permettant d'agencer tous les composants de la hiérarchie dont ils constituent la racine.
2.2.2 JDialog
JDialog
représente une boîte de dialogue, visuellement similaire à une fenêtre mais se comportant différemment. Une boîte de dialogue est toujours liée à une fenêtre existante, dans le sens où lorsque cette dernière est fermée ou iconifiée, il en va de même de la première.
Beaucoup de boîtes de dialogue sont modales (modal), dans le sens où toute interaction avec le reste de l'application est impossible tant et aussi longtemps que la boîte n'est pas fermée.
2.2.3 Panneaux racine/contenu
Les conteneurs de niveau supérieur ne contiennent pas directement un certain nombre de composants fils. Au lieu de cela, ils possèdent un unique panneau racine (root pane), un type spécial de conteneur intermédiaire.
Ce panneau racine contient, entre autres, un panneau de contenu (content pane), dans lequel les composants spécifiques à l'application sont ajoutés. La plupart des applications ignorent donc le panneau racine et interagissent uniquement avec le panneau de contenu.
Le panneau de contenu est une propriété modifiable des conteneurs de niveau supérieur, nommée contentPane
.
Figure 5 : Panneau racine et de contenu
2.3 Conteneurs intermédiaires
Les conteneurs intermédiaires sont les nœuds — autres que la racine — de la hiérarchie. Le but principal d'un conteneur intermédiaire est de grouper et d'organiser un certain nombre d'autres composants, nommé ses fils. Dès lors, sa représentation graphique propre est souvent minimale, voire inexistante.
2.3.1 JSplitPane
JSplitPane
permet de diviser le composant en deux parties, chacune affichant un composant fils. La division peut être verticale ou horizontale, et est redimensionnable, éventuellement par l'utilisateur.
Figure 6 : JSplitPane
2.3.2 JTabbedPane
JTabbedPane
est un panneau composé d'un certain nombre d'onglets affichant chacun un composant fils différent. Un seul onglet est visible à un instant donné.
Figure 7 : JTabbedPane
2.3.3 JScrollPane
JScrollPane
donne accès à une sous-partie d'un composant trop grand pour tenir à l'écran et permet de déplacer la zone visualisée de différentes manières, p.ex. au moyen de barres de défilement.
Figure 8 : JScrollPane
2.3.4 JLayeredPane
JLayeredPane
permet de superposer plusieurs composants, ce qui peut être utile pour dessiner au-dessus de composants existants ou pour intercepter les clics de souris qui leur sont destinés.
Figure 9 : JLayeredPane
2.3.5 JPanel
JPanel
représente un panneau, un conteneur intermédiaire sans représentation graphique propre — exception faite de son éventuelle bordure, voir plus loin — et pouvant avoir un nombre quelconque de fils. Malgré son invisibilité, JPanel
joue un rôle fondamental dans l'organisation des interfaces, de par sa capacité à grouper et placer — via son gestionnaire d'agencement — d'autres composants.
Les méthodes add
et remove
permettent de gérer les fils d'un conteneur JPanel
:
void add(JComponent c, Object l, int i)
: insère le composantc
à la positioni
dans les fils (-1 signifiant la fin) et lui associe l'information d'agencementl
(voir plus loin),void add(JComponent c, Object l)
équivaut àadd(c, l, -1)
void add(JComponent c)
équivaut àadd(c, null)
void remove(JComponent c)
: supprime le composantc
des fils,void remove(int i)
: supprime le fils d'indexi
.
Tout composant fils d'un panneau occupe un rectangle imbriqué dans celui de son parent. On nomme bornes (bounds) la position (x
, y
) et la taille (width
, height
) de ce rectangle, exprimés dans le repère du panneau.
Figure 10 : Bornes d'un composant fils
L'agencement des fils — leur positionnement à l'intérieur du rectangle de leur parent — peut se faire de deux manières :
- « manuellement », en changeant leurs bornes au moyen de la méthode
setBounds
, - via un gestionnaire d'agencement (layout manager) attaché au parent et responsable de l'agencement de ses fils et de son dimensionnement.
Dans la quasi-totalité des cas, la seconde solution est choisie.
2.3.6 Gestionnaires d'agencement
Les gestionnaires d'agencement peuvent utiliser plusieurs caractéristiques des composants fils pour les placer :
- leur index dans la séquence des fils,
- leur taille minimale (propriété
minimumSize
), préférée (preferredSize
) et maximale (maximumSize
), - l'éventuelle information d'agencement qui leur a été associée au moment de leur ajout via la méthode
add
.
Attention : les gestionnaires ont le droit, mais pas l'obligation, d'utiliser ces caractéristiques. En particulier, plusieurs gestionnaires ignorent totalement les informations de taille.
L'interface LayoutManager
représente un gestionnaire d'agencement.
Un certain nombre de gestionnaires prédéfinis l'implémentant sont fournis avec Swing : FlowLayout
, BorderLayout
, GridLayout
, GridBagLayout
, etc. Ces gestionnaires agencent chacun les fils en fonction d'une technique qui leur est propre.
2.3.7 Gestionnaire BorderLayout
BorderLayout
découpe le composant en cinq régions nommées, chacune d'entre elles pouvant contenir au plus un composant.
Figure 11 : BorderLayout
Toutes les zones sauf la centrale sont dimensionnées en fonction de leur contenu — qui peut être inexistant. La totalité de l'espace restant est attribué à la zone centrale.
2.3.8 Gestionnaire FlowLayout
FlowLayout
place les fils de la même manière que les mots d'un texte sont arrangés en lignes successives.
Cet agencement peut se faire de gauche à droite ou de droite à gauche, et les lignes peuvent être alignées à gauche, à droite, ou centrées. Les fils sont séparés horizontalement et verticalement par un espacement configurable. Par exemple, il peut agencer six fils d'un panneau en les plaçant de gauche à droite et en centrant chaque ligne, comme sur la figure 12.
Figure 12 : FlowLayout
2.3.9 Gestionnaire BoxLayout
2.3.10 Gestionnaire GridLayout
GridLayout
place les fils dans une grille de n×m cellules de taille identique, en partant du coin haut-gauche et en suivant le sens de lecture.
Par exemple, il peut agencer neuf fils d'un panneau dans une grille de 3×3 cellules, comme sur la figure 14.
Figure 14 : GridLayout
2.3.11 Autres gestionnaires
Swing offre encore d'autres gestionnaires d'agencement, plus puissants que ceux présentés jusqu'ici, parmi lesquels :
GridBagLayout
, version améliorée deGridLayout
permettant de dimensionner les lignes et colonnes individuellement, et de combiner des cellules,GroupLayout
, associant chaque composant à un groupe horizontal et un groupe vertical et alignant de manière configurable tous les composants d'un groupe.
Malheureusement, comme tous les gestionnaires d'agencement offerts par Swing, ils montrent assez vite leurs limites lors de la construction d'interfaces complexes.
2.3.12 Exemple de mise en page
L'interface de la calculatrice peut être mise en page au moyen de deux gestionnaires :
- le premier de type
BorderLayout
, attaché au panneau de contenu, permettant de placer l'affichage dans la zonePAGE_START
et le panneau du clavier dans la zoneCENTER
, - le second de type
GridLayout
et de taille 4×4, attaché au panneau clavier.
Figure 15 : Mise en page d'une calculatrice
Le code correspondant est le suivant :
JPanel p = new JPanel(new BorderLayout()); JFormattedTextField display = …; // … configuration de display p.add(display, BorderLayout.PAGE_START); JPanel keyboard = new JPanel(new GridLayout(4, 4)); // … configuration de keyboard (boutons, …) p.add(keyboard, BorderLayout.CENTER); JFrame frame = new JFrame("RPN Calc"); frame.setContentPane(p);
2.4 Composants de base
Les composants de base sont ceux apparaissant aux feuilles de la hiérarchie. Ils ne peuvent pas contenir d'autres composants.
2.4.1 Etiquette
JLabel
représente l'un des composants les plus simples qui soit, une étiquette textuelle et/ou graphique. Ses principales propriétés sont le texte (text
) et l'image (icon
) qu'elle affiche, pouvant les deux être vides (null
).
La figure 16 illustre l'utilisation de trois étiquettes (Nom, Prénom et Age) dans un formulaire.
Figure 16 : JLabel
2.4.2 Bouton à un état
JButton
représente un bouton à un état, destiné à être cliqué pour effectuer une action. Sa principale propriété est le texte qu'il affiche (text
). Des auditeurs peuvent lui être attachés pour gérer les clics de l'utilisateur — voir plus loin.
Figure 17 : JButton
2.4.3 Case à cocher
JCheckBox
représente une case à cocher, qui peut être sélectionnée ou non. Ses principales propriétés sont le texte qu'elle affiche (text
) et son état (selected
).
Figure 18 : JCheckBox
2.4.4 Bouton radio
JRadioButton
représente un bouton « radio », similaire à une case à cocher mais faisant partie d'un groupe (de type ButtonGroup
) dont un seul peut être sélectionné. Ses principales propriétés sont les mêmes que celles de JCheckBox
.
Figure 19 : JRadioButton
2.4.5 Combo box
JComboBox
représente une « combo box » permettant de choisir une valeur parmi un ensemble prédéfini et éventuellement d'en entrer une autre.
Figure 20 : JComboBox
2.4.6 Composants textuels
JTextField
et ses sous-classes JFormattedTextField
et JPasswordField
représentent des champs textuels à une ligne et à présentation uniforme (police unique, etc.). Leur principale propriété est leur texte (text
), une simple chaîne de caractères de type String
.
Figure 21 : JTextField
et composants similaires
JFormattedTextField
stocke une valeur non textuelle dans sa propriété value
et la transforme en texte (et inversement) au moyen d'un formateur qui lui est attaché.
JTextArea
est similaire à JTextField
mais permet les lignes multiples, tandis que JTextPane
et JEditorPane
permettent l'affichage — et éventuellement l'édition — de texte mis en page. Leur principale propriété est le texte qu'ils contiennent (text
), également disponible en version mise en page (document
).
Figure 22 : JTextArea
2.4.7 Barre de progression
JProgressBar
représente une barre de progression, permettant de visualiser l'état d'avancement d'une opération de longue durée. Sa principale propriété est sa valeur (value
), un entier compris entre 0 et une valeur maximale choisie, indiquant l'état d'avancement actuel.
Figure 23 : JProgressBar
2.4.8 Macro-composants
En plus des composants de base « simples » décrits précédemment, Swing offre des macro-composants souvent utiles, construits à partir des composants de base :
JColorChooser
, qui permet de choisir une couleur de différentes manières (via une palette, en choisissant ses composantes dans différents modèles, etc.),JFileChooser
, qui permet de choisir un ou plusieurs fichiers ou répertoires, existants ou non, en naviguant dans le système de fichiers.
Ces deux macro-composants peuvent s'utiliser comme des composants normaux ou dans des boîtes de dialogue.
Finalement, Swing offre des composants permettant d'afficher des données structurées : listes, tableaux, hiérarchie arborescente. Ces composants sont plus complexes à utiliser que les autres étant donné la nature des données qu'ils affichent. Pour les obtenir, ces composants utilisent un modèle qu'on leur fournit.
2.4.9 Modèles
Plusieurs composants de base permettent d'afficher et de manipuler des données complexes, par exemple :
JList
affiche une liste d'éléments,JTree
affiche une hiérarchie arborescente,JTable
affiche une table bidimensionnelle de cellules,- etc.
Où stocker les données affichées par ces composants et quel type leur attribuer ?
Une solution serait de stocker les données directement dans les composants. Par exemple, le composant JList
pourrait stocker la liste des éléments qu'il affiche. Cette solution est mauvaise car elle va à l'encontre de la séparation conseillée par le patron MVC : les données à afficher font partie du modèle et ne doivent donc pas être stockée dans la vue.
Une meilleure solution est de faire en sorte que les composants puissent directement utiliser les données du modèle.
Pour ce faire, Swing offre la possibilité d'attacher à chacun des composants complexes précités un objet appelé modèle (model) et représentant les données sous-jacentes. Ce modèle doit être observable au sens du patron Observer pour permettre au composant de se mettre à jour automatiquement. Le type de ces modèles est spécifié par des interfaces fournies par Swing, et il est donc généralement nécessaire d'écrire un adaptateur pour adapter les données du programme aux types attendus par Swing.
Par exemple, le composant JList
affiche une liste d'éléments et permet leur sélection. Il doit donc pouvoir effectuer les opérations suivantes sur la liste à afficher :
- obtenir sa taille,
- obtenir l'élément d'indice donné,
- observer la liste pour être informé des changements.
Ces opérations sont regroupées dans l'interface ListModel
, représentant un modèle de liste.
Figure 24 : Modèle de liste
L'interface ListModel
offre d'une part des méthodes permettant de gérer l'ensemble des observateurs — appelés ici auditeurs (listeners) — et d'autre part des méthodes de consultation de la liste de valeurs.
public interface ListModel<E> { void addListDataListener(ListDataListener l); void removeListDataListener(ListDataListener l); int getSize(); E getElementAt(int i); }
Pour faciliter l'écriture de modèles de listes, Swing offre la classe AbstractListModel
qui met en œuvre les méthodes gérant les auditeurs, laissant uniquement les méthodes getSize
et getElementAtIndex
abstraites.
Cette classe fournit de plus des méthodes dont le nom commence par fire…
et permettant d'informer les auditeurs de changements dans le modèle — similaires à la méthode notifyObservers
des sujets du patron Observer.
L'interface ListDataListener
représente un observateur de modèle de liste, appelé ici auditeur.
public interface ListDataListener { void intervalAdded(ListDataEvent e); void intervalRemoved(ListDataEvent e); void contentsChanged(ListDataEvent e); }
Plutôt qu'une unique méthode de mise à jour (update), il en possède trois. La dernière est générale, tandis que les deux premières représentent des cas particuliers — ajout et suppression d'éléments — qui ont été jugés assez fréquents pour valoir la peine d'être traités séparément.
La classe ListDataEvent
contient les informations liées au changement du contenu de la liste observée, à savoir :
- le type de changement — ajout d'un intervalle, suppression d'un intervalle, changement général,
- l'index du premier élément affecté par le changement,
- l'index du dernier élément affecté par le changement.
Le diagramme de classes de la figure 25 illustre — de manière légèrement simplifiée — les classes qui seraient utilisées par une application incluant un composant JList
.
Figure 25 : Classes liste et modèle de liste
2.5 Composants personnalisés
Même si les composants prédéfinis couvrent un grand nombre de besoins, ils ne sauraient les couvrir tous. Lorsqu'une application nécessite un composant ne figurant pas parmi les prédéfinis, il est possible de le définir sous la forme d'une nouvelle sous-classe de JComponent
. Les principales méthodes à redéfinir dans ce cas sont :
getPreferredSize
,getMinimumSize
etgetMaximumSize
, qui permettent aux gestionnaires d'agencement de le dimensionner correctement,paintComponent
, qui dessine le composant.
Les méthodes getPreferredSize
, getMinimumSize
et getMaximumSize
sont utilisées par certains gestionnaires d'agencement — mais pas tous — pour déterminer la taille du composant. Il peut donc être utile de les redéfinir lorsque la taille du composant ne peut pas être arbitraire.
La méthode paintComponent
est automatiquement appelée par Swing chaque fois que le composant doit être (re)dessiné, par exemple lorsqu'il apparaît pour la première fois à l'écran. Cette méthode reçoit en argument le contexte graphique à utiliser pour effectuer le dessin. Le repère qui lui est associé est celui du composant.
Par exemple, le composant ci-dessous affiche un rectangle rouge centré sur fond noir :
public final class RonB extends JComponent { @Override protected void paintComponent(Graphics g) { int w = (int)getBounds().getWidth(); int h = (int)getBounds().getHeight(); g.setColor(Color.BLACK); g.fillRect(0, 0, w, h); g.setColor(Color.RED); g.fillRect(w/4, h/4, w/2, h/2); } }
Il peut arriver qu'un composant doive être redessiné, p.ex. suite à un changement des données du modèle qu'il représente.
Cela ne doit en aucun cas être fait en appelant directement paintComponent
, cette méthode étant uniquement destinée à être appelée par Swing. Au lieu de cela, il convient d'appeler la méthode repaint
du composant, afin de demander à Swing de le redessiner (via sa méthode paintComponent
) aussi vite que possible. A noter que Swing redessine automatiquement les composants dont la taille a changé, sans qu'un appel à repaint
ne soit nécessaire.
2.6 Bordures
Il est possible d'ajouter une bordure (border) à n'importe quel composant au moyen de la méthode setBorder
de JComponent
. Il est assez fréquent d'ajouter une bordure à un panneau (JPanel
) afin de grouper visuellement un certain nombre de composants liés logiquement. Par exemple, une bordure peut être utilisée pour entourer (et nommer) les données personnelles dans un formulaire, comme dans la figure 26.
Figure 26 : Bordure
Contrairement à d'autres objets Swing, les bordures ne doivent pas être créées directement au moyen de l'énoncé new
. Au lieu de cela, il convient d'appeler l'une des méthodes de la classe BorderFactory
, par exemple :
createLineBorder
prend une couleur et une épaisseur de trait et retourne une bordure composée d'une simple ligne,createTitledBorder
prend une bordure existante et une chaîne, qu'elle lui ajoute en titre,- etc.
3 Gestion des événements
Un programme doté d'une interface graphique ne fait généralement qu'attendre que l'utilisateur interagisse avec celle-ci, réagit en conséquence, puis se remet à attendre. Chaque fois que le programme est forcé de réagir, on dit qu'un événement (event) s'est produit.
Cette caractéristique des programmes graphiques induit un style de programmation particulier nommé programmation événementielle (event-driven programming).
Ce style se retrouve dans toutes les applications dont le but principal est de réagir à des événements externes, p.ex. les serveurs (Web et autres).
Au cœur de tout programme événementiel se trouve une boucle traitant successivement les événements dans leur ordre d'arrivée. Cette boucle, nommée boucle événementielle (event loop), pourrait s'exprimer ainsi en pseudo-code :
tant que le programme n'est pas terminé attendre le prochain événement traiter cet événement
Cette boucle est souvent fournie par une bibliothèque et ne doit dans ce cas pas être écrite explicitement. Il en va ainsi de la plupart des bibliothèques de gestion d'interfaces graphiques, dont Swing.
Si la boucle événementielle peut être identique pour tous les programmes, la manière dont les différents événements sont gérés est bien entendu spécifique à chaque programme.
Dès lors, il doit être possible de définir la manière de traiter chaque événement. Cela se fait généralement en écrivant, pour chaque événement important, un morceau de code le gérant, appelé le gestionnaire d'événement (event handler). Ce gestionnaire est associé, d'une manière ou d'une autre, à l'événement.
La boucle événementielle est séquentielle, dans le sens où un événement ne peut être traité que lorsque le traitement de l'événement précédent est terminé. Dans le cas d'une interface graphique, cela signifie que celle-ci est totalement bloquée tant et aussi longtemps qu'un événement est en cours de traitement. Pour cette raison, les gestionnaires d'événements doivent autant que possible s'exécuter rapidement. Si cela n'est pas possible, il faut utiliser la concurrence pour effectuer les longs traitements en tâche de fond — ce qui sort du cadre de ce cours.
3.1 Gestion des événements dans Swing
Dans une application graphique Swing, la boucle événementielle s'exécute dans un fil d'exécution (thread) séparé, nommé le fil de gestion des événements (event dispatching thread ou EDT). Ce fil d'exécution est démarré automatiquement lors de l'utilisation de Swing, rendant la boucle événementielle particulièrement invisible au programmeur. Elle n'en existe pas moins !
De manière générale, tous les appels à des méthodes de Swing doit se faire depuis le fil de gestion des événements. En particulier, toute la création de l'interface doit s'y faire.
Dans Swing, les gestionnaires d'événements sont des objets, appelés auditeurs (listeners), implémentant une interface qui définit une ou plusieurs méthodes de gestion d'événement. Les auditeurs sont attachés à une source d'événements — généralement un composant — et leurs méthodes sont appelées chaque fois qu'un événement se produit. Les auditeurs sont assez similaires aux observateurs du patron Observer, la méthode update
de ces derniers gérant l'événement « l'état du sujet a changé ».
La classe ci-dessous illustre la structure typique d'un programme Swing. L'interface graphique est créée par la méthode createUI
, appelée indirectement via invokeLater
afin de garantir qu'elle s'exécute sur le fil de gestion des événements. La méthode createUI
créée une fenêtre et y ajoute un bouton dont l'auditeur affiche un message à l'écran lorsqu'on lui clique dessus.
public final class Main { private static void createUI() { JFrame w = new JFrame(); w.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); JButton b = new JButton("click me!"); b.addActionListener(e ->System.out.println("click!")); w.getContentPane().add(b); w.pack(); w.setVisible(true); } public static void main(String[] args) { SwingUtilities.invokeLater(() -> createUI()); } }
La figure 27 illustre l'exécution des différentes parties du programme sur les deux fils existants.
Figure 27 : Fils d'exécution d'un programme Swing simple
Lorsqu'un événement se produit, Swing collecte les informations à son sujet dans un objet passé à l'auditeur. Par un léger abus de langage, cet objet lui-même est nommé événement (event). Généralement, à chaque type d'événement correspond un type d'auditeur et un type d'objet-événement.
3.2 Auditeurs Swing
Swing définit un grand nombre de types d'auditeurs, en fonction du type d'événement qu'ils écoutent. Les principaux types d'auditeurs sont :
- les auditeurs d'action (action listeners),
- les auditeurs de souris (mouse listeners),
- les auditeurs de menu (menu listeners),
- les auditeurs de clavier (key listeners),
- les auditeurs de cible de saisie (focus listeners).
Seuls les deux premiers sont examinés ici, les autres ayant un fonctionnement similaire.
3.2.1 ActionListener
L'interface (fonctionnelle) ActionListener
représente un auditeur à l'écoute des événements de type « action ». Ces événements se produisent p.ex. lorsque l'utilisateur clique sur un bouton, sélectionne un menu ou presse Entrée dans un champ textuel.
public interface ActionListener { void actionPerformed(ActionEvent e); }
La classe ActionEvent
contient différentes informations au sujet de l'événement, p.ex. les touches de modifications (Control, Shift, …) pressées au moment du déclenchement de l'action.
Un auditeur de type ActionListener
peut être attaché à plusieurs types de composants, principalement les boutons, les champs textuels et les entrées de menus. Chacun de ces types de composants offre les méthodes addActionListener
et removeActionListener
pour ajouter/supprimer un auditeur d'action.
3.2.2 Auditeurs de souris
L'interface MouseListener
représente un auditeur à l'écoute de certains événements liés à la souris : entrée et sortie des bornes du composant, pression et relâchement des boutons :
public interface MouseListener { void mouseEntered(MouseEvent e); void mouseExited(MouseEvent e); void mouseClicked(MouseEvent e); void mousePressed(MouseEvent e); void mouseReleased(MouseEvent e); }
La classe MouseEvent
contient différentes informations concernant l'événement, p.ex. la position de la souris.
L'interface MouseMotionListener
représente un auditeur à l'écoute des événements liés au déplacement de la souris, avec un bouton pressé (mouseDragged
) ou non (mouseMoved
) :
public interface MouseMotionListener { void mouseDragged(MouseEvent e); void mouseMoved(MouseEvent e); }
Chaque mouvement de la souris constituant un nouvel événement, ceux-ci peuvent être très nombreux. Les auditeurs de type MouseMotionListener
se doivent donc de traiter rapidement les événements.
L'interface (fonctionnelle) MouseWheelListener
représente un auditeur à l'écoute des événements liés à la molette de la souris :
public interface MouseWheelListener { void mouseWheelMoved(MouseWheelEvent e); }
La classe MouseWheelEvent
étend MouseEvent
en lui ajoutant des informations liées à la molette : distance et sens de rotation, etc.
Les trois types d'auditeurs liés à la souris peuvent être attachés à (et détachés de) n'importe quel composant au moyen des méthodes addMouse…Listener
et removeMouse…Listener
.
La classe héritable MouseAdapter
fournit une mise en œuvre par défaut des interfaces MouseListener
, MouseMotionListener
et MouseWheelListener
. Elle implémente la totalité des méthodes de ces interfaces, en ne faisant rien dans tous les cas.
Attention : malgré son nom, cette classe n'est pas un adaptateur au sens du patron Adapter. Un nom plus conforme aux conventions généralement utilisées dans la bibliothèque Java serait AbstractMouseListener
.
4 Références
- The Swing Tutorial de Oracle.
- la documentation de l'API Java, en particulier les classes et interfaces suivantes :
- la classe
javax.swing.JComponent
et ses sous-classes, - la classe
javax.swing.JFrame
, - l'interface
java.awt.LayoutManager
et les classes qui l'implémentent, - la classe
javax.swing.SwingUtilities
.
- la classe