Continuons à explorer les objets que nous propose swing
. Ils sont variés et s'utilisent souvent de la même manière que les boutons. En fait, maintenant que nous avons compris le fonctionnement du pattern observer, nous travaillerons avec des interfaces et devrons donc implémenter des méthodes pour gérer les événements avec nos composants.
Allons-y !
- Les listes : l'objet JComboBox
- Les cases à cocher : l'objet JCheckBox
- Les champs de texte : l'objet JTextField
- Contrôle du clavier : l'interface KeyListener
Les listes : l'objet JComboBox
Première utilisation
Comme à l'accoutumée, nous utiliserons d'abord cet objet dans un contexte exempt de tout code superflu. Créons donc un projet avec une classe contenant la méthode main()
et une classe héritée de JFrame
.
Dans cet exemple, nous aurons bien sûr besoin d'une liste, faites-en une. Cependant, vous ne manquerez pas de constater que notre objet est ridiculement petit. Vous connaissez le remède : il suffit de lui spécifier une taille !
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | import java.awt.BorderLayout; import java.awt.Color; import java.awt.Dimension; import javax.swing.JComboBox; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; public class Fenetre extends JFrame { private JPanel container = new JPanel(); private JComboBox combo = new JComboBox(); private JLabel label = new JLabel("Une ComboBox"); public Fenetre(){ this.setTitle("Animation"); this.setSize(300, 300); this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); this.setLocationRelativeTo(null); container.setBackground(Color.white); container.setLayout(new BorderLayout()); combo.setPreferredSize(new Dimension(100, 20)); JPanel top = new JPanel(); top.add(label); top.add(combo); container.add(top, BorderLayout.NORTH); this.setContentPane(container); this.setVisible(true); } } |
En revanche, cette liste est vide ! Pour résoudre ce problème, il suffit d'utiliser la méthode addItem(Object obj)
.
Sachez que lorsque l'objet affiche les éléments ajoutés, il appelle leur méthode toString()
. Dans cet exemple, nous avons utilisé des objets String
, mais essayez avec un autre objet et vous constaterez le résultat…
Voici le nouveau code :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | //Les imports restent inchangés public class Fenetre extends JFrame { //Les variables d'instance restent inchangées public Fenetre(){ //… combo.setPreferredSize(new Dimension(100, 20)); combo.addItem("Option 1"); combo.addItem("Option 2"); combo.addItem("Option 3"); combo.addItem("Option 4"); //… } } |
Vous pouvez voir ce que ça donne à la figure suivante.
Pour initialiser une JComboBox
, vous pouvez utiliser le constructeur prenant un tableau d'objets en paramètre afin de renseigner tous les éléments d'un coup. Ceci est donc équivalent au code précédent :
1 2 | String[] tab = {"Option 1", "Option 2", "Option 3", "Option 4"}; combo = new JComboBox(tab); |
Vous pouvez assigner un choix par défaut avec la méthode setSelectedIndex(int index)
. Vous avez aussi la possibilité de changer la couleur du texte, la couleur de fond ou la police, exactement comme avec un JLabel
.
Depuis Java 7, l'objet JComboBox
peut être paramétré avec un type générique, comme ceci : JComboBox<String> combo = new JComboBox<String>();
ce qui permet de mieux gérer le contenu de nos listes et ainsi mieux récupérer les valeurs de ces dernières.
Un autre objet dont nous ne parlerons pas accepte aussi un type paramétré, l'objet JList<E>
. Celui-ci étant très proche de l'objet JComboBox<E>
, nous n'en parlerons pas ici mais maintenant vous savez qu'il existe.
Maintenant que nous savons comment fonctionne cet objet, nous allons apprendre à communiquer avec lui.
L'interface ItemListener
Cette interface possède une méthode à redéfinir. Celle-ci est appelée lorsqu'un élément a changé d'état. Puisqu'un exemple est toujours plus éloquent, voici un code implémentant cette interface :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | //Les autres imports import java.awt.event.ItemEvent; import java.awt.event.ItemListener; public class Fenetre extends JFrame { //Les variables d'instance restent inchangées public Fenetre(){ //Le début ne change pas //Ici, nous changeons juste la façon d'initialiser la JComboBox String[] tab = {"Option 1", "Option 2", "Option 3", "Option 4"}; combo = new JComboBox(tab); //Ajout du listener combo.addItemListener(new ItemState()); combo.setPreferredSize(new Dimension(100, 20)); combo.setForeground(Color.blue); //La fin reste inchangée } //Classe interne implémentant l'interface ItemListener class ItemState implements ItemListener{ public void itemStateChanged(ItemEvent e) { System.out.println("événement déclenché sur : " + e.getItem()); } } } |
Dans mon exemple, j'ai cliqué sur Option 2
, puis Option 3
, puis Option 4
, ce qui correspond à la figure suivante.
Vous voyez que lorsque nous cliquons sur une autre option, notre objet commence par modifier l'état de l'option précédente (l'état passe en DESELECTED
) avant de changer celui de l'option choisie (celle-ci passe à l'état SELECTED
). Nous pouvons donc suivre très facilement l'état de nos éléments grâce à cette interface ; cependant, pour plus de simplicité, nous utiliserons l'interface ActionListener
afin de récupérer l'option sélectionnée.
Voici un code implémentant cette interface :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | //Les autres imports import java.awt.event.ActionEvent; import java.awt.event.ActionListener; public class Fenetre extends JFrame { //Les variables d'instance restent inchangées public Fenetre(){ //Le début ne change pas String[] tab = {"Option 1", "Option 2", "Option 3", "Option 4"}; combo = new JComboBox(tab); //Ajout du listener combo.addItemListener(new ItemState()); combo.addActionListener(new ItemAction()); combo.setPreferredSize(new Dimension(100, 20)); combo.setForeground(Color.blue); //La fin reste inchangée } //La classe interne ItemState reste inchangée class ItemAction implements ActionListener{ public void actionPerformed(ActionEvent e) { System.out.println("ActionListener : action sur " + combo.getSelectedItem()); } } } |
Le résultat se trouve à la figure suivante.
Vous constatez qu'en utilisant cette méthode, nous pouvons récupérer l'option sur laquelle l'action a été effectuée. L'appel de la méthode getSelectedItem()
retourne la valeur de l'option sélectionnée ; une fois récupérée, nous pouvons travailler avec notre liste !
Maintenant que nous savons comment récupérer les informations dans une liste, je vous invite à continuer notre animation.
Changer la forme de notre animation
Comme le titre l'indique, nous allons faire en sorte que notre animation ne se contente plus d'afficher un rond : nous pourrons désormais choisir la forme que nous voulons afficher. Bien sûr, je ne vais pas vous faire réaliser toutes les formes possibles et imaginables ; je vous en fournis quelques-unes et, si le cœur vous en dit, vous pouvez ajouter des formes de votre composition.
Très bien : pour réaliser cela, nous devons dynamiser un peu notre classe Panneau
afin qu'elle peigne une forme en fonction de notre choix.
Pour y parvenir, nous allons ajouter une variable d'instance de type String
qui contiendra l'intitulé de la forme que nous souhaitons dessiner - appelons-la forme
- ainsi qu'un mutateur permettant de redéfinir cette variable.
Notre méthode paintComponent()
doit pouvoir dessiner la forme demandée ; ainsi, trois cas de figure se profilent :
- soit nous intégrons les instructions
if
dans cette méthode et l'objetGraphics
dessinera en fonction de la variable ; - soit nous développons une méthode privée appelée dans la méthode
paintComponent()
et qui dessinera la forme demandée ; - soit nous utilisons le pattern strategy afin d'encapsuler la façon dont nous dessinerons nos formes dans notre animation.
Le pattern strategy est de loin la meilleure solution, mais afin de ne pas alourdir nos exemples, nous travaillerons « à l'ancienne ».
Nous allons donc développer une méthode privée - appelons-la draw(Graphics g)
- qui aura pour tâche de dessiner la forme voulue. Nous passerons l'objet Graphics
dans la méthode paintComponent()
de sorte que cette dernière puisse l'utiliser ; c'est donc dans cette méthode que nous placerons nos conditions.
Je vous propose les formes suivantes :
- le rond, forme par défaut ;
- le carré ;
- le triangle ;
- l'étoile (soyons fous).
Cela signifie que notre liste contiendra ces quatre choix et que le rond figurera en premier lieu. Nous créerons aussi une implémentation d'ActionListener
dans une classe interne pour gérer les actions de notre liste. Je l'ai appelée FormeListener
(c'est fou ce que je suis original).
Ce que vous obtiendrez est représenté à la figure suivante.
Essayez de réaliser ces formes vous-mêmes : il n'y a là rien de compliqué, je vous assure ! Bon, l'étoile est peut-être un peu plus complexe que les autres, mais ce n'est pas insurmontable.
Classe Panneau
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | import java.awt.Color; import java.awt.Font; import java.awt.GradientPaint; import java.awt.Graphics; import java.awt.Graphics2D; import javax.swing.JPanel; public class Panneau extends JPanel { private int posX = -50; private int posY = -50; private String forme = "ROND"; public void paintComponent(Graphics g){ //On choisit une couleur de fond pour le rectangle g.setColor(Color.white); //On le dessine de sorte qu'il occupe toute la surface g.fillRect(0, 0, this.getWidth(), this.getHeight()); //On redéfinit une couleur pour le rond g.setColor(Color.red); //On délègue la méthode de dessin à la méthode draw() draw(g); } private void draw(Graphics g){ if(this.forme.equals("ROND")){ g.fillOval(posX, posY, 50, 50); } if(this.forme.equals("CARRE")){ g.fillRect(posX, posY, 50, 50); } if(this.forme.equals("TRIANGLE")){ //Calcul des sommets //Le sommet 1 se situe à la moitié du côté supérieur du carré int s1X = posX + 25; int s1Y = posY; //Le sommet 2 se situe en bas à droite int s2X = posX + 50; int s2Y = posY + 50; //Le sommet 3 se situe en bas à gauche int s3X = posX; int s3Y = posY + 50; //Nous créons deux tableaux de coordonnées int[] ptsX = {s1X, s2X, s3X}; int[] ptsY = {s1Y, s2Y, s3Y}; //Nous utilisons la méthode fillPolygon() g.fillPolygon(ptsX, ptsY, 3); } if(this.forme.equals("ETOILE")){ //Pour l'étoile, on se contente de tracer des lignes dans le carré //correspondant à peu près à une étoile... //Mais ce code peut être amélioré ! int s1X = posX + 25; int s1Y = posY; int s2X = posX + 50; int s2Y = posY + 50; g.drawLine(s1X, s1Y, s2X, s2Y); int s3X = posX; int s3Y = posY + 17; g.drawLine(s2X, s2Y, s3X, s3Y); int s4X = posX + 50; int s4Y = posY + 17; g.drawLine(s3X, s3Y, s4X, s4Y); int s5X = posX; int s5Y = posY + 50; g.drawLine(s4X, s4Y, s5X, s5Y); g.drawLine(s5X, s5Y, s1X, s1Y); } } public void setForme(String form){ this.forme = form; } public int getPosX() { return posX; } public void setPosX(int posX) { this.posX = posX; } public int getPosY() { return posY; } public void setPosY(int posY) { this.posY = posY; } } |
Classe Fenetre
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | import java.awt.BorderLayout; import java.awt.Color; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; public class Fenetre extends JFrame{ private Panneau pan = new Panneau(); private JButton bouton = new JButton("Go"); private JButton bouton2 = new JButton("Stop"); private JPanel container = new JPanel(); private JLabel label = new JLabel("Choix de la forme"); private int compteur = 0; private boolean animated = true; private boolean backX, backY; private int x, y; private Thread t; private JComboBox combo = new JComboBox(); public Fenetre(){ this.setTitle("Animation"); this.setSize(300, 300); this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); this.setLocationRelativeTo(null); container.setBackground(Color.white); container.setLayout(new BorderLayout()); container.add(pan, BorderLayout.CENTER); bouton.addActionListener(new BoutonListener()); bouton2.addActionListener(new Bouton2Listener()); bouton2.setEnabled(false); JPanel south = new JPanel(); south.add(bouton); south.add(bouton2); container.add(south, BorderLayout.SOUTH); combo.addItem("ROND"); combo.addItem("CARRE"); combo.addItem("TRIANGLE"); combo.addItem("ETOILE"); combo.addActionListener(new FormeListener()); JPanel top = new JPanel(); top.add(label); top.add(combo); container.add(top, BorderLayout.NORTH); this.setContentPane(container); this.setVisible(true); } private void go(){ x = pan.getPosX(); y = pan.getPosY(); while(this.animated){ if(x < 1) backX = false; if(x > pan.getWidth() - 50) backX = true; if(y < 1) backY = false; if(y > pan.getHeight() - 50) backY = true; if(!backX) pan.setPosX(++x); else pan.setPosX(--x); if(!backY) pan.setPosY(++y); else pan.setPosY(--y); pan.repaint(); try { Thread.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } } } //Classe écoutant notre bouton public class BoutonListener implements ActionListener{ public void actionPerformed(ActionEvent arg0) { animated = true; t = new Thread(new PlayAnimation()); t.start(); bouton.setEnabled(false); bouton2.setEnabled(true); } } class Bouton2Listener implements ActionListener{ public void actionPerformed(ActionEvent e) { animated = false; bouton.setEnabled(true); bouton2.setEnabled(false); } } class PlayAnimation implements Runnable{ public void run() { go(); } } class FormeListener implements ActionListener{ public void actionPerformed(ActionEvent e) { //La méthode retourne un Object puisque nous passons des Object dans une liste //Il faut donc utiliser la méthode toString() pour retourner un String (ou utiliser un cast) pan.setForme(combo.getSelectedItem().toString()); } } } |
Et voilà le travail ! Vous avez vu : il n'y avait rien de sorcier. En fait, étant donné que vous avez l'habitude d'utiliser des objets graphiques et des implémentations d'interfaces, les choses vont maintenant s'accélérer, car le principe est le même pour tous les objets graphiques de base.
Les cases à cocher : l'objet JCheckBox
Première utilisation
Créez un projet vide avec une classe contenant une méthode main()
et une classe héritant de JFrame
. Cela fait, nous allons utiliser notre nouvel objet. Celui-ci peut être instancié avec un String
en paramètre qui servira de libellé.
Nous pouvons également cocher la case par défaut en appelant la méthode setSelected(Boolean bool)
à laquelle nous passons true
. Cet objet possède, comme tous les autres, une multitude de méthodes nous simplifiant la vie ; je vous invite aussi à fouiner un peu…
Nous créerons directement une implémentation de l'interface ActionListener
, vous connaissez bien la démarche. Contrôlons également que notre objet est coché à l'aide de la méthode isSelected()
qui retourne un booléen. Voici un code mettant tout cela en œuvre :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | import java.awt.BorderLayout; import java.awt.Color; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.JCheckBox; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; public class Fenetre extends JFrame { private JPanel container = new JPanel(); private JCheckBox check1 = new JCheckBox("Case 1"); private JCheckBox check2 = new JCheckBox("Case 2"); public Fenetre(){ this.setTitle("Animation"); this.setSize(300, 300); this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); this.setLocationRelativeTo(null); container.setBackground(Color.white); container.setLayout(new BorderLayout()); JPanel top = new JPanel(); check1.addActionListener(new StateListener()); check2.addActionListener(new StateListener()); top.add(check1); top.add(check2); container.add(top, BorderLayout.NORTH); this.setContentPane(container); this.setVisible(true); } class StateListener implements ActionListener{ public void actionPerformed(ActionEvent e) { System.out.println("source : " + ((JCheckBox)e.getSource()).getText() + " - état : " + ((JCheckBox)e.getSource()).isSelected()); } } } |
Le résultat se trouve à la figure suivante.
Ici, je me suis amusé à cocher et décocher mes cases. Il n'y a rien de bien difficile, ça devient routinier, non ?
Un pseudomorphing pour notre animation
Nous allons utiliser cet objet afin que nos formes changent de taille et proposent un pseudo-effet de morphing.
Premièrement, la taille de notre forme est fixe, il nous faut changer cela. Allez, hop, une variable de type int
dans notre classe Panneau
- disons drawSize
- initialisée à 50. Tout comme avec le déplacement, nous devons savoir lorsqu'il faut augmenter ou réduire la taille de notre forme : nous utiliserons donc la même méthode que celle que nous avions développée à ce moment-là.
Un JCheckBox
sera nécessaire pour savoir si le « mode morphing » est activé.
En ce qui concerne la taille, si on la réduit ou l'augmente d'une unité à chaque rafraîchissement, l'effet de morphing sera ultra rapide. Donc, pour ralentir l'effet, nous utiliserons une méthode retournant 1 ou 0 selon le nombre de rafraîchissements. Cela implique que nous aurons besoin d'une variable pour les dénombrer. Nous effectuerons une augmentation ou une réduction toutes les dix fois.
Pour bien séparer les deux cas de figure, nous insérerons une deuxième méthode de dessin dans la classe Panneau
qui aura pour rôle de dessiner le morphing ; appelons-la drawMorph(Graphics g)
.
Lorsque nous cocherons la case, le morphing s'activera, et il se désactivera une fois décochée. La classe Panneau
devra donc disposer d'un mutateur pour le booléen de morphing.
Souvenez-vous que nous gérons la collision avec les bords dans notre classe Fenetre
. Cependant, en « mode morphing », la taille de notre forme n'est plus constante : il faudra gérer ce nouveau cas de figure dans notre méthode go()
. Notre classe Panneau
devra posséder un accesseur permettant de retourner la taille actuelle de la forme.
Vous avez désormais toutes les clés en main pour réussir cette animation.
La figure suivante donne un aperçu de ce que vous devriez obtenir (je n'ai représenté que le rond et le triangle, mais ça fonctionne avec toutes les formes).
Fichier Panneau.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 | import java.awt.Color; import java.awt.Font; import java.awt.GradientPaint; import java.awt.Graphics; import java.awt.Graphics2D; import javax.swing.JPanel; public class Panneau extends JPanel { private int posX = -50; private int posY = -50; private int drawSize = 50; //Un booléen pour le mode morphing //Un autre pour savoir si la taille doit être réduite private boolean morph = false, reduce = false; private String forme = "ROND"; //Le compteur de rafraîchissements private int increment = 0; public void paintComponent(Graphics g){ g.setColor(Color.white); g.fillRect(0, 0, this.getWidth(), this.getHeight()); g.setColor(Color.red); //Si le mode morphing est activé, on peint le morphing if(this.morph) drawMorph(g); //Sinon, on peint le mode normal else draw(g); } private void draw(Graphics g){ if(this.forme.equals("ROND")){ g.fillOval(posX, posY, 50, 50); } if(this.forme.equals("CARRE")){ g.fillRect(posX, posY, 50, 50); } if(this.forme.equals("TRIANGLE")){ int s1X = posX + 50/2; int s1Y = posY; int s2X = posX + 50; int s2Y = posY + 50; int s3X = posX; int s3Y = posY + 50; int[] ptsX = {s1X, s2X, s3X}; int[] ptsY = {s1Y, s2Y, s3Y}; g.fillPolygon(ptsX, ptsY, 3); } if(this.forme.equals("ETOILE")){ int s1X = posX + 50/2; int s1Y = posY; int s2X = posX + 50; int s2Y = posY + 50; g.drawLine(s1X, s1Y, s2X, s2Y); int s3X = posX; int s3Y = posY + 50/3; g.drawLine(s2X, s2Y, s3X, s3Y); int s4X = posX + 50; int s4Y = posY + 50/3; g.drawLine(s3X, s3Y, s4X, s4Y); int s5X = posX; int s5Y = posY + 50; g.drawLine(s4X, s4Y, s5X, s5Y); g.drawLine(s5X, s5Y, s1X, s1Y); } } //Méthode qui peint le morphing private void drawMorph(Graphics g){ //On incrémente increment++; //On regarde si on doit réduire ou non if(drawSize >= 50) reduce = true; if(drawSize <= 10) reduce = false; if(reduce) drawSize = drawSize - getUsedSize(); else drawSize = drawSize + getUsedSize(); if(this.forme.equals("ROND")){ g.fillOval(posX, posY, drawSize, drawSize); } if(this.forme.equals("CARRE")){ g.fillRect(posX, posY, drawSize, drawSize); } if(this.forme.equals("TRIANGLE")){ int s1X = posX + drawSize/2; int s1Y = posY; int s2X = posX + drawSize; int s2Y = posY + drawSize; int s3X = posX; int s3Y = posY + drawSize; int[] ptsX = {s1X, s2X, s3X}; int[] ptsY = {s1Y, s2Y, s3Y}; g.fillPolygon(ptsX, ptsY, 3); } if(this.forme.equals("ETOILE")){ int s1X = posX + drawSize/2; int s1Y = posY; int s2X = posX + drawSize; int s2Y = posY + drawSize; g.drawLine(s1X, s1Y, s2X, s2Y); int s3X = posX; int s3Y = posY + drawSize/3; g.drawLine(s2X, s2Y, s3X, s3Y); int s4X = posX + drawSize; int s4Y = posY + drawSize/3; g.drawLine(s3X, s3Y, s4X, s4Y); int s5X = posX; int s5Y = posY + drawSize; g.drawLine(s4X, s4Y, s5X, s5Y); g.drawLine(s5X, s5Y, s1X, s1Y); } } //Retourne le nombre à retrancher ou à ajouter pour le morphing private int getUsedSize(){ int res = 0; //Si le nombre de tours est de dix, on réinitialise l'incrément et on retourne 1 if(increment / 10 == 1){ increment = 0; res = 1; } return res; } public int getDrawSize(){ return drawSize; } public boolean isMorph(){ return morph; } public void setMorph(boolean bool){ this.morph = bool; //On réinitialise la taille drawSize = 50; } public void setForme(String form){ this.forme = form; } public int getPosX() { return posX; } public void setPosX(int posX) { this.posX = posX; } public int getPosY() { return posY; } public void setPosY(int posY) { this.posY = posY; } } |
Fichier Fenetre.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | import java.awt.BorderLayout; import java.awt.Color; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; public class Fenetre extends JFrame{ private Panneau pan = new Panneau(); private JButton bouton = new JButton("Go"); private JButton bouton2 = new JButton("Stop"); private JPanel container = new JPanel(); private JLabel label = new JLabel("Choix de la forme"); private int compteur = 0; private boolean animated = true; private boolean backX, backY; private int x, y; private Thread t; private JComboBox combo = new JComboBox(); private JCheckBox morph = new JCheckBox("Morphing"); public Fenetre(){ this.setTitle("Animation"); this.setSize(300, 300); this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); this.setLocationRelativeTo(null); container.setBackground(Color.white); container.setLayout(new BorderLayout()); container.add(pan, BorderLayout.CENTER); bouton.addActionListener(new BoutonListener()); bouton2.addActionListener(new Bouton2Listener()); bouton2.setEnabled(false); JPanel south = new JPanel(); south.add(bouton); south.add(bouton2); container.add(south, BorderLayout.SOUTH); combo.addItem("ROND"); combo.addItem("CARRE"); combo.addItem("TRIANGLE"); combo.addItem("ETOILE"); combo.addActionListener(new FormeListener()); morph.addActionListener(new MorphListener()); JPanel top = new JPanel(); top.add(label); top.add(combo); top.add(morph); container.add(top, BorderLayout.NORTH); this.setContentPane(container); this.setVisible(true); } private void go(){ x = pan.getPosX(); y = pan.getPosY(); while(this.animated){ //Si le mode morphing est activé, on utilise la taille actuelle de la forme if(pan.isMorph()){ if(x < 1)backX = false; if(x > pan.getWidth() - pan.getDrawSize()) backX = true; if(y < 1)backY = false; if(y > pan.getHeight() - pan.getDrawSize()) backY = true; } //Sinon, on fait comme d'habitude else{ if(x < 1)backX = false; if(x > pan.getWidth()-50) backX = true; if(y < 1)backY = false; if(y > pan.getHeight()-50) backY = true; } if(!backX) pan.setPosX(++x); else pan.setPosX(--x); if(!backY) pan.setPosY(++y); else pan.setPosY(--y); pan.repaint(); try { Thread.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } } } public class BoutonListener implements ActionListener{ public void actionPerformed(ActionEvent arg0) { animated = true; t = new Thread(new PlayAnimation()); t.start(); bouton.setEnabled(false); bouton2.setEnabled(true); } } class Bouton2Listener implements ActionListener{ public void actionPerformed(ActionEvent e) { animated = false; bouton.setEnabled(true); bouton2.setEnabled(false); } } class PlayAnimation implements Runnable{ public void run() { go(); } } class FormeListener implements ActionListener{ public void actionPerformed(ActionEvent e) { pan.setForme(combo.getSelectedItem().toString()); } } class MorphListener implements ActionListener{ public void actionPerformed(ActionEvent e) { //Si la case est cochée, on active le mode morphing if(morph.isSelected())pan.setMorph(true); //Sinon, on ne fait rien else pan.setMorph(false); } } } |
Alors, qu'en pensez-vous ? J'aime bien, moi… Vous voyez, l'utilisation des JCheckBox
est très simple. Je vous propose maintenant d'étudier un de ses cousins !
Le petit cousin : l'objet JRadioButton
Le voici, le cousin éloigné… Le principe est de proposer au moins deux choix, mais de ne permettre d'en sélectionner qu'un à la fois. L'instanciation se fait de la même manière que pour un JCheckBox
; d'ailleurs, nous utiliserons l'exemple du début de ce chapitre en remplaçant les cases à cocher par des boutons radio. Voici le code correspondant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | import java.awt.BorderLayout; import java.awt.Color; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.JCheckBox; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JRadioButton; public class Fenetre extends JFrame { private JPanel container = new JPanel(); private JRadioButton jr1 = new JRadioButton("Radio 1"); private JRadioButton jr2 = new JRadioButton("Radio 2"); public Fenetre(){ this.setTitle("Animation"); this.setSize(300, 300); this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); this.setLocationRelativeTo(null); container.setBackground(Color.white); container.setLayout(new BorderLayout()); JPanel top = new JPanel(); jr1.addActionListener(new StateListener()); jr2.addActionListener(new StateListener()); top.add(jr1); top.add(jr2); container.add(top, BorderLayout.NORTH); this.setContentPane(container); this.setVisible(true); } class StateListener implements ActionListener{ public void actionPerformed(ActionEvent e) { System.out.println("source : " + ((JRadioButton)e.getSource()).getText() + " - état : " + ((JRadioButton)e.getSource()).isSelected()); } } } |
Le résultat est représenté à la figure suivante.
Vous pouvez voir que cet objet s'utilise de la même manière que le précédent. Le problème, ici, c'est que nous pouvons sélectionner les deux options (alors que ce n'est normalement pas possible). Pour qu'un seul bouton radio soit sélectionné à la fois, nous devons définir un groupe de boutons à l'aide de ButtonGroup
. Nous y ajouterons nos boutons radio, et seule une option pourra alors être sélectionnée.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | //Les autres imports import javax.swing.ButtonGroup; public class Fenetre extends JFrame { //Les autres variables private ButtonGroup bg = new ButtonGroup(); public Fenetre(){ //Les autres instructions jr1.setSelected(true); jr1.addActionListener(new StateListener()); jr2.addActionListener(new StateListener()); //On ajoute les boutons au groupe bg.add(jr1); bg.add(jr2); top.add(jr1); top.add(jr2); container.add(top, BorderLayout.NORTH); this.setContentPane(container); this.setVisible(true); } class StateListener implements ActionListener{ public void actionPerformed(ActionEvent e) { System.out.println("source : " + jr1.getText() + " - état : " + jr1.isSelected()); System.out.println("source : " + jr2.getText() + " - état : " + jr2.isSelected()); } } } |
Voyez le résultat à la figure suivante.
Les champs de texte : l'objet JTextField
Première utilisation
Je pense que vous savez ce que vous avez à faire. Si ce n'est pas déjà fait, créez un nouveau projet contenant les classes habituelles. Comme l'indique le titre de cette partie, nous allons utiliser l'objet JTextField
. Vous vous en doutez, cet objet propose lui aussi des méthodes de redimensionnement, de changement de couleur… De ce fait, je commence avec un exemple complet. Lisez et testez ce code :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | //Les imports habituels import javax.swing.JTextField; public class Fenetre extends JFrame { private JPanel container = new JPanel(); private JTextField jtf = new JTextField("Valeur par défaut"); private JLabel label = new JLabel("Un JTextField"); public Fenetre(){ this.setTitle("Animation"); this.setSize(300, 300); this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); this.setLocationRelativeTo(null); container.setBackground(Color.white); container.setLayout(new BorderLayout()); JPanel top = new JPanel(); Font police = new Font("Arial", Font.BOLD, 14); jtf.setFont(police); jtf.setPreferredSize(new Dimension(150, 30)); jtf.setForeground(Color.BLUE); top.add(label); top.add(jtf); container.add(top, BorderLayout.NORTH); this.setContentPane(container); this.setVisible(true); } } |
Cela donne la figure suivante.
Nous pouvons initialiser le contenu avec la méthode setText(String str)
ou le récupérer grâce à la méthode getText()
.
Il existe un objet très ressemblant à celui-ci, en un peu plus étoffé. En fait, cet objet permet de créer un JTextField
formaté pour recevoir un certain type de données saisies (date, pourcentage etc.). Voyons cela tout de suite.
Un objet plus restrictif : le JFormattedTextField
Grâce à ce type d'objet, nous pourrons éviter beaucoup de contrôles et de casts sur le contenu de nos zones de texte. Si vous avez essayé de récupérer le contenu du JTextField
utilisé ci-dessus (lors du clic sur un bouton, par exemple), vous avez dû vous rendre compte que le texte qu'il contenait importait peu, mais un jour, vous aurez sans doute besoin d'une zone de texte qui n'accepte qu'un certain type de données. Avec l'objet JFormattedTextField
, nous nous en approchons (mais vous verrez que vous pourrez faire encore mieux). Cet objet retourne une valeur uniquement si celle-ci correspond à ce que vous avez autorisé. Je m'explique : si vous voulez que votre zone de texte contienne par exemple des entiers et rien d'autre, c'est possible ! En revanche, ce contrôle ne s'effectue que lorsque vous quittez le champ en question. Vous pouvez ainsi saisir des lettres dans un objet n'acceptant que des entiers, mais la méthode getText()
ne renverra alors rien, car le contenu sera effacé, les données ne correspondent pas aux attentes de l'objet. Voici un code et deux exemples, ainsi que leur rendu (figure suivante).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | //Les imports habituels public class Fenetre extends JFrame { private JPanel container = new JPanel(); private JFormattedTextField jtf = new JFormattedTextField(NumberFormat.getIntegerInstance()); private JFormattedTextField jtf2 = new JFormattedTextField(NumberFormat.getPercentInstance()); private JLabel label = new JLabel("Un JTextField"); private JButton b = new JButton ("OK"); public Fenetre(){ this.setTitle("Animation"); this.setSize(300, 300); this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); this.setLocationRelativeTo(null); container.setBackground(Color.white); container.setLayout(new BorderLayout()); JPanel top = new JPanel(); Font police = new Font("Arial", Font.BOLD, 14); jtf.setFont(police); jtf.setPreferredSize(new Dimension(150, 30)); jtf.setForeground(Color.BLUE); jtf2.setPreferredSize(new Dimension(150, 30)); b.addActionListener(new BoutonListener()); top.add(label); top.add(jtf); top.add(jtf2); top.add(b); this.setContentPane(top); this.setVisible(true); } class BoutonListener implements ActionListener{ public void actionPerformed(ActionEvent e) { System.out.println("TEXT : jtf " + jtf.getText()); System.out.println("TEXT : jtf2 " + jtf2.getText()); } } } |
Vous voyez qu'en plus, notre objet met automatiquement la saisie en forme lorsqu'elle est valide : il espace les nombres tous les trois chiffres afin d'en faciliter la lecture.
Voici ce que vous pouvez utiliser dans ce genre de champ :
NumberFormat
avec :getIntegerInstance()
getPercentInstance()
getNumberInstance()
DateFormat
avec :getTimeInstance()
getDateInstance()
MessageFormat
Sans entrer dans les détails, vous pouvez aussi utiliser un objet MaskFormatter
qui permet d'attribuer un format de longueur fixe à votre zone de texte. C'est très pratique lorsque vous souhaitez introduire un numéro de téléphone, un numéro de sécurité sociale etc.
Vous devez définir ce format avec un paramètre lors de l'instanciation du masque à l'aide de métacaractères. Ceux-ci indiquent à votre objet MaskFormatter
ce que le contenu de votre zone de texte contiendra. Voici la liste de ces métacaractères :
- # : indique un chiffre ;
- ' : indique un caractère d'échappement ;
- U : indique une lettre (les minuscules sont automatiquement changées en majuscules) ;
- L : indique une lettre (les majuscules sont automatiquement changées en minuscules) ;
- A : indique un chiffre ou une lettre ;
- ? : indique une lettre ;
- * : indique que tous les caractères sont acceptés ;
- H : indique que tous les caractères hexadécimaux sont acceptés (0 à 9, a à f et A à F).
L'instanciation d'un tel objet peut lever une ParseException
. Vous devez donc l'entourer d'un bloc try{…}catch(ParseException e){…}
.
Voici à quoi ressemblerait un format téléphonique :
1 2 3 4 5 6 7 | try{ MaskFormatter tel = new MaskFormatter("## ## ## ## ##"); //Ou encore MaskFormatter tel2 = new MaskFormatter("##-##-##-##-##"); //Vous pouvez ensuite le passer à votre zone de texte JFormattedTextField jtf = new JFormattedTextField(tel2); }catch(ParseException e){e.printStackTrace();} |
Vous voyez qu'il n'y a là rien de compliqué. Je vous invite à essayer cela dans le code précédent, vous constaterez qu'avec le métacaractère utilisé dans notre objet MaskFormatter
, nous sommes obligés de saisir des chiffres. La figure suivante montre le résultat après avoir cliqué sur le bouton.
Je ne sais pas pour le numéro de téléphone américain, mais le numéro français est loin d'être un numéro de téléphone valide. Nous voici confrontés à un problème qui nous hantera tant que nous programmerons : l'intégrité de nos données !
Comme le montre l'exemple précédent, nous pouvons suggérer à l'utilisateur ce qu'il doit renseigner comme données dans les champs, mais nous ne devons pas lui faire aveuglément confiance ! C'est simple : on part du principe de ne jamais faire confiance à l'utilisateur.
Nous sommes donc obligés d'effectuer une multitude de contrôles supplémentaires. Pour ce faire, nous pouvons :
- tester chaque élément du numéro ;
- tester le numéro en entier ;
- dans le cas où nous n'utilisons pas de
MaskFormatter
, vérifier en plus que les saisies sont numériques ; - utiliser une expression régulière ;
- empêcher la saisie d'un type de caractères ;
- etc.
En gros, nous devons vérifier l'intégrité de nos données (dans le cas qui nous intéresse, l'intégrité de nos chaînes de caractères) pendant ou après la saisie. Je ne vous cache pas que cela prendra une grande part de votre temps lorsque vous coderez vos propres logiciels, mais c'est le métier qui veut ça.
Avant de terminer ce chapitre (assez conséquent, je l'avoue), je vous propose de voir comment nous pouvons récupérer les événements du clavier. Nous avons appris à interagir avec la souris, mais pas avec le clavier.
Contrôle du clavier : l'interface KeyListener
Nous connaissons déjà :
- l'interface
MouseListener
qui interagit avec la souris ; - l'interface
ActionListener
qui interagit lors d'un clic sur un composant ; - l'interface
ItemListener
qui écoute les événements sur une liste déroulante.
Voici à présent l'interface KeyListener
. Comme l'indique le titre, elle nous permet d'intercepter les événements clavier lorsque l'on :
- presse une touche ;
- relâche une touche ;
- tape sur une touche.
Vous savez ce qu'il vous reste à faire : créer une implémentation de cette interface dans votre projet. Créez une classe interne qui l'implémente et utilisez l'astuce d'Eclipse pour générer les méthodes nécessaires.
Vous constatez qu'il y en a trois :
keyPressed
(KeyEvent
event), appelée lorsqu'on presse une touche ;keyReleased
(KeyEvent
event), appelée lorsqu'on relâche une touche (c'est à ce moment que le composant se voit affecter la valeur de la touche) ;keyTyped
(KeyEvent
event), appelée entre les deux méthodes citées ci-dessus.
Comme vous vous en doutez, l'objet KeyEvent
nous permettra d'obtenir des informations sur les touches qui ont été utilisées. Parmi celles-ci, nous utiliserons :
getKeyCode()
: retourne le code de la touche ;getKeyChar()
: retourne le caractère correspondant à la touche.
Nous pouvons aussi déterminer lorsque certaines touches de contrôle ont été utilisées (SHIFT, CTRL…), connaître le composant à l'origine de l'événement, etc. Nous n'en parlerons pas ici, mais ce genre d'information est facile à trouver sur Internet.
Pour des raisons de simplicité, nous n'utiliserons pas un JFormattedTextField
mais un JTextField
sans MaskFormatter
. Ainsi, nous n'aurons pas à nous préoccuper des tirets de notre champ.
Pour commencer, nous allons examiner l'ordre dans lequel se déroulent les événements clavier ; il est vrai que ceux-ci se produisent si rapidement que nous n'avons pas le temps de les voir défiler. J'ai donc ajouté une pause à la fin de chaque méthode de l'implémentation afin de mieux observer l'ordre d'exécution.
Voici le code source que nous allons utiliser (il est presque identique aux précédents, rassurez-vous) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | import java.awt.BorderLayout; import java.awt.Color; import java.awt.Dimension; import java.awt.Font; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JTextField; public class Fenetre extends JFrame { private JPanel container = new JPanel(); private JTextField jtf; private JLabel label = new JLabel("Téléphone FR"); private JButton b = new JButton ("OK"); public Fenetre(){ this.setTitle("Animation"); this.setSize(300, 150); this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); this.setLocationRelativeTo(null); container.setBackground(Color.white); container.setLayout(new BorderLayout()); jtf = new JTextField(); JPanel top = new JPanel(); Font police = new Font("Arial", Font.BOLD, 14); jtf.setFont(police); jtf.setPreferredSize(new Dimension(150, 30)); jtf.setForeground(Color.BLUE); //On ajoute l'écouteur à notre composant jtf.addKeyListener(new ClavierListener()); top.add(label); top.add(jtf); top.add(b); this.setContentPane(top); this.setVisible(true); } class ClavierListener implements KeyListener{ public void keyPressed(KeyEvent event) { System.out.println("Code touche pressée : " + event.getKeyCode() + " - caractère touche pressée : " + event.getKeyChar()); pause(); } public void keyReleased(KeyEvent event) { System.out.println("Code touche relâchée : " + event.getKeyCode() + " - caractère touche relâchée : " + event.getKeyChar()); pause(); } public void keyTyped(KeyEvent event) { System.out.println("Code touche tapée : " + event.getKeyCode() + " - caractère touche tapée : " + event.getKeyChar()); pause(); } } private void pause(){ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args){ new Fenetre(); } } |
La figure suivante affiche une petite série d'essais de ce code.
Vous pouvez maintenant vous rendre compte de l'ordre dans lequel les événements du clavier sont gérés : en premier, lorsqu'on presse la touche, en deuxième, lorsqu'elle est tapée, et enfin, lorsqu'elle est relâchée.
Dans le cas qui nous intéresse, nous souhaitons que lorsque l'utilisateur saisit un caractère interdit, celui-ci soit automatiquement retiré de la zone de saisie. Pour cela, nous procéderons à un traitement spécifique dans la méthode keyReleased(KeyEvent event)
.
Si vous avez effectué beaucoup de tests de touches, vous avez dû remarquer que les codes des touches correspondant aux chiffres du pavé numérique sont compris entre 96 et 105.
À partir de là, c'est simple : il nous suffit de supprimer le caractère tapé de la zone de saisie si son code n'est pas compris dans cet intervalle. Toutefois, un problème se pose avec cette méthode : ceux qui possèdent un ordinateur portable sans pavé numérique ne pourront rien saisir alors qu'il est possible d'obtenir des chiffres en appuyant sur MAJ + &, é, ', ( ou -.
Ce souci nous amène à opter pour une autre solution : nous créerons une méthode dont le type de retour sera un booléen nous indiquant si la saisie est numérique ou non. Comment ? Tout simplement en exécutant un Integer.parseInt(value)
, le tout enveloppé dans un try{…}catch(NumberFormatException ex){}
. Si nous essayons de convertir un caractère « a » en entier, l'exception sera levée et nous retournerons alors false
(true
dans le cas contraire).
La méthode parseInt()
prend un String
en paramètre. La méthode getKeyChar()
, elle, renvoie un char
. Il faudra donc penser à faire la conversion.
Voici notre implémentation quelque peu modifiée :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | class ClavierListener implements KeyListener{ public void keyReleased(KeyEvent event) { if(!isNumeric(event.getKeyChar())) jtf.setText(jtf.getText().replace(String.valueOf(event.getKeyChar()), "")); } //Inutile de redéfinir ces méthodes, ous n'en avons plus l'utilité ! public void keyPressed(KeyEvent event) {} public void keyTyped(KeyEvent event) {} //Retourne true si le paramètre est numérique, false dans le cas contraire private boolean isNumeric(char carac){ try { Integer.parseInt(String.valueOf(carac)); } catch (NumberFormatException e) { return false; } return true; } } |
Vous vous apercevez que les lettres simples sont désormais interdites à la saisie : mission accomplie ! Cependant, les caractères spéciaux comme « ô », « ï », etc. ne sont pas pris en charge par cette méthode. Par conséquent, leur saisie reste possible.
- L'objet
JComboBox
se trouve dans le packagejavax.swing
. - Vous pouvez ajouter des éléments dans une liste avec la méthode
addItem(Object obj)
. - Vous pouvez aussi instancier une liste avec un tableau de données.
- L'interface
ItemListener
permet de gérer les états de vos éléments. - Vous pouvez aussi utiliser l'interface
ActionListener
. - La méthode
getSelectedItem()
retourne une variable de typeObject
: pensez donc à effectuer un cast, ou à utiliser la méthodetoString()
si les éléments sont des chaînes de caractères. - Les objets
JCheckBox
,JRadioButton
etButtonGroup
sont présents dans le packagejavax.swing
. - Vous pouvez déterminer si l'un de ces composants est sélectionné grâce à la méthode
isSelected()
. Cette méthode retournetrue
si l'objet est sélectionné,false
dans le cas contraire. - Vous pouvez restreindre le nombre de choix à un parmi plusieurs réponses en utilisant la classe
ButtonGroup
. - Vous pouvez ajouter des boutons à un groupe de boutons grâce à la méthode
add(AbstractButton button)
. - Par défaut, un
JTextField
accepte tous les types de caractères. - Un
JFormattedTextField
correspond, pour simplifier, à unJTextField
plus restrictif. - On peut restreindre la saisie dans ces objets en utilisant l'objet
MaskFormatter
. - Afin de contrôler les événements clavier, l'utilisation d'une implémentation de l'interface
KeyListener
est nécessaire.