Dans ce TD, nous allons expérimenter l'écriture d'une application Java conçue autour de son interface graphique. Nous verrons successivement comment :
Le fil directeur sera la réalisation d'une application qui permettra
de tracer des courbes définies par des équations, soit de la forme
y = f(x), soit de la forme ( x = f(t), y = g(t) ).
Les fonctions f et g seront définies par le biais de
l'interface, c'est-à-dire qu'il va s'agir de chaînes de caractères dont
l'interprétation sera confiée à CalculiX.
Comme ces équations pourront faire intervenir des paramètres, il faudra
prévoir l'introduction de ces paramètres par le biais de l'interface
graphique.
On aura bien sûr aussi quelques « boutons » pour des actions comme
tracer, arrêter, quitter, …
Il existe principalement deux bibliothèques graphiques pour Java : AWT et Swing. On utilisera plutôt Swing qui est conçue par dessus AWT et qui offre beaucoup plus de possibilités, mais vous aurez à vous référer à deux documentations.
Pour commencer, suivant la procédure habituelle,
Une interface graphique Java prend l'apparence d'une ou plusieurs fenêtres graphiques. Nous utiliserons un JFrame de Swing comme fenêtre et voici donc la documentation de JFrame comme point d'entrée.
Un objet JFrame est la racine d'un arbre de composants
(classe abstraite java.awt.Component), c'est aussi un conteneur
(classe java.awt.Container) particulier.
Il existe de très nombreuses implémentations de composants et de
conteneurs.
Les composants sont faits pour être placés dans des conteneurs.
Les conteneurs sont eux-mêmes des composants et peuvent ainsi constituer
des nœuds internes de l'arbre.
Il existe des exceptions comme JFrame qui est un conteneur, donc
un composant, mais qui ne peut pas être placé dans un autre conteneur.
L'exception n'est pas spécifiée par le typage mais elle se traduit par une
erreur à l'exécution.
Il y aussi des composants qui ne sont pas des conteneurs et qui donc
seront forcément des feuilles de l'arbre.
Voici un exemple défini par ces 3 classes (données dans l'archive td18.zip) :
public class Main { public static void main(String[] args) { new MaFenetre(); // new MaFenetre(); // et deux si on veut System.out.println(Thread.currentThread() + " en fin de main()"); } }
import javax.swing.JFrame; public class MaFenetre { public MaFenetre() { JFrame frame = new JFrame("Un titre"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // comportement modifiable // on place ici la definition de l'arbre de composants frame.add(new MonDessin()); // un raccourci // frame.getContentPane().add(new MonDessin()); // forme exacte // ... // on termine generalement ainsi frame.pack(); frame.setVisible(true); // pour l'exemple System.out.println(Thread.currentThread() + " en fin de MaFenetre()"); } }
import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics; import javax.swing.JPanel; @SuppressWarnings("serial") public class MonDessin extends JPanel { public MonDessin() { this.setPreferredSize(new Dimension(100, 100)); this.setBackground(Color.WHITE); this.setOpaque(true); } public void paintComponent(Graphics g) { super.paintComponent(g); g.fillOval(0, 0, this.getWidth(), this.getHeight()); System.out.println(Thread.currentThread() + " en fin de paintComponent()"); } }
Dans cet exemple, l'arbre est donc réduit à une racine et une feuille.
Pour comprendre le fonctionnement du programme, il faut savoir que le
constructeur de MaFenetre est exécuté par le
thread principal, celui qui exécute la fonction main
de tout programme Java.
Le constructeur de MaFenetre termine rapidement, ainsi donc que
main et le thread principal.
Le relais est pris automatiquement par le thread de l'AWT qui fait
vivre l'interface graphique.
On le voit sur cet exemple en faisant varier la taille de la fenêtre,
ce qui provoque automatiquement des appels à paintComponent.
Pour éviter d'éventuels problèmes de synchronisation, il est préférable de
confier la construction de l'interface au thread de l'AWT.
Ainsi, on préserve un bon principe qui est que ce thread est le seul
à toucher aux objets de l'interface.
Pour cela on utilise la méthode statique
SwingUtilities.invokeLater(Runnable todo).
On peut instancier un objet de type Runnable « à la volée »
comme seule instance d'une classe anonyme :
Runnable r = new Runnable() { public void run() { … } };
Soit, en regroupant en un seul trait :
SwingUtilities.invokeLater(new Runnable() { public void run() { … } });
Expérimentez la construction de deux fenêtres avec un seul Runnable commun, puis avec un Runnable par fenêtre.
Déposez Main.java.
On considère maintenant le cas où le code pour produire le dessin va
prendre beaucoup de temps, par exemple avec la classe
MonDessin2 donnée dans td18.zip.
Le thread de l'AWT reste alors bloqué dans paintComponent
et il ne peut plus faire vivre l'interface graphique. Notamment, un
clic dans le bouton de fermeture de la fenêtre reste sans effet.
La solution est alors de lancer le calcul de dessin dans un autre thread et paintComponent se contente d'actualiser l'écran avec le dessin déjà calculé. Un moyen relativement simple est d'utiliser une image tampon. Le nouveau thread dessine dans cette image et le thread de l'AWT affiche cette image lorsqu'il exécute paintComponent.
Pour cela,
public void paintComponent(Graphics g) { super.paintComponent(g); if (buffer == null) { buffer = this.createImage(WIDTH, HEIGHT); new Thread(painter).start(); } g.drawImage(buffer, 0, 0, null); }
Il vous reste à mettre en place le Runnable painter comme un champ du JPanel, par le biais d'une classe membre anonyme :
private Runnable painter = new Runnable() { … };
Son code reprend l'ancien code de paintComponent.
Pour obtenir un objet Graphics, on utilisera
buffer.getGraphics(), attention plusieurs appels à
getGraphics() peuvent renvoyer des objets différents.
Le dernier problème à régler est de provoquer un appel à
paintComponent, depuis le painter.
On peut pour cela écrire MonDessin2.this.repaint(); où
MonDessin2.this est la syntaxe pour désigner l'objet
MonDessin2 depuis painter et repaint()
est sa méthode pour poster un appel à paintComponent.
On fera cela périodiquement, par exemple tous les 100000 points calculés.
Déposez MonDessin2.java.
L'utilisation d'un SwingWorker constitue une alternative plus propre dans la mesure où le nouveau thread n'aura pas à appeler directement repaint mais communiquera son avancement par l'envoi de messages.
Sur un modèle assez similaire à l'exercice précédent, on déclarera un SwingWorker<Void, Void> en définissant sa méthode doInBackground. Le lancement se fait par sa méthode execute. Au lieu d'appeler MonDessin2.this.repaint, le SwingWorker appellera sa propre méthode setProgress, avec un argument bien choisi.
Pour récupérer les messages postés par setProgress, il faut installer un PropertyChangeListener, ce qui peut se faire depuis le constructeur de MonDessin2, et c'est le code de ce PropertyChangeListener, exécuté par le thread de l'AWT, qui fera les appels à repaint.
Déposez MonDessin2.java.
Pour l'instant, nous nous attachons seulement à définir l'aspect visuel de notre interface. Même si certains composants vont avoir déjà un certain comportement réactif prédéfini, nous ne nous intéressons pas encore à définir des comportements particuliers.
Un conteneur possède un gestionnaire de placement de ses composants
de type LayoutManager.
Pour un JFrame, il s'agit par défaut d'un BorderLayout
qui gère seulement 5 composants placés aux positions désignées par
les constantes NORTH, SOUTH, EAST,
WEST et CENTER.
On peut facilement changer le LayoutManager d'un conteneur,
mais il est généralement encore plus simple d'utiliser le type
conteneur prédéfini pour utiliser un certain LayoutManager.
Dans notre JFrame (classe Traceur fournie), nous allons donc placer :
Ce conteneur de type Box va regrouper les composants qui permettent de paramétrer la courbe :
On doit obtenir, par exemple (le texte message ici est
défini par le programme, les textes dans les autres boîtes ont été entrés
au clavier) :
Déposez les fichiers que vous avez modifiés.
Pour Swing, une action est un objet qui hérite de la classe AbstractAction. On peut généralement instancier un tel objet « à la volée » comme seule instance d'une classe anonyme, par exemple :
AbstractAction a = new AbstractAction() { public void actionPerformed(ActionEvent arg0) { System.exit(0); } };
Ensuite, pour attacher cette action au bouton bQuit, on peut tenter dans un premier temps bQuit.add(a). Le résultat n'est pas tout à fait celui escompté car ceci efface le titre du bouton (une action permet de paramétrer entièrement le bouton). Il faut en fait passer le titre au constructeur de l'action et, en regroupant, on peut définir le bouton et son action en un seul trait :
JButton bQuit = new JButton(new AbstractAction("Quit") { public void actionPerformed(ActionEvent arg0) { System.exit(0); } });
et comme il n'est pas utile de conserver la référence sur ce bouton, on peut encore condenser en … add(new JButton(…));.
On procèdera de manière similaire pour que le bouton "New" ouvre un nouveau JFrame.
Le bouton "Plot" demande plus de travail car il devra lancer le tracé de la courbe. Pour l'instant, on va seulement s'intéresser à récupérer les paramètres du tracé et, pour vérifier, on les affichera par des appels à System.out.println :
Par exemple, on affichera :
mode (f(t),g(t)) X = sin (A*t+D) Y = sin (B*t) 3 parametres : A = 0.0100001 B = 0.02 D = 2.0
Déposez les fichiers que vous avez modifiés.
Le principe général de l'action du bouton "Plot" est le suivant :
Déposez les fichiers que vous avez modifiés.