Badr Benmammar bbm@badr-benmammar.com Formation Développeur Java Thread et Swing Badr Benmammar bbm@badr-benmammar.com
Plan Tâche longue dans un une interface utilisateur graphique (GUI ) Deadlock d’initialisation de GUI Pattern de création d’un GUI Tâche périodique dans un GUI Règle d’unicité du thread GUI Class SwingWorker : tâche longue proprement Tâche longue avec interruption possible Interaction avec l’event-dispatcher
Tâche longue dans un GUI Problème : exécuter une tâche relativement longue dans le cadre d’une (GUI) interface graphique pour l’utilisateur. La tâche dure quelques secondes voire des minutes. L’interface doit pouvoir encore réagir aux actions de l’utilisateur. La longue tâche à exécuter est simulée par des "sleep" dans sa méthode execute(). Elle doit indiquer son avancement dans une barre de progression, mais aussi dans la console.
Tâche longue dans un GUI import java.awt.*; import java.awt.event.*; import javax.swing.*; public class GUIetTacheLongue1 extends JFrame { JProgressBar barreProgression; public GUIetTacheLongue1() { super("GUIetTacheLongue1"); Container contentPane = getContentPane(); barreProgression = new JProgressBar(); JButton bouton= new JButton("Demarrer la tache longue"); contentPane.add(bouton,BorderLayout.NORTH); contentPane.add(barreProgression,BorderLayout.SOUTH); bouton.addActionListener(new ActionListener() { public void actionPerformed (ActionEvent e) { LongueTache1 tache = new LongueTache1(); tache.execute(barreProgression); } ); pack(); setVisible(true); } public static void main(String[] args) { new GUIetTacheLongue1(); } La tâche longue class LongueTache1 { public void execute (JProgressBar barreProgression) { barreProgression.setMinimum(0); barreProgression.setMaximum(99); barreProgression.setValue(0); for (int i = 0; i < 100; i++ ) { barreProgression.setValue(i); System.out.print("."); try {Thread.sleep(100);} catch (InterruptedException e) {} } System.out.println("") ; Classe interne anonyme pour installer un écouteur correspond au composant bouton
Tâche longue dans un GUI Exécution: ................................. ............................................... $ Remarque : Arrêtez avec <ctrl-C> car la fermeture de la fenêtre et l’arrêt de l’appli ne sont pas prévus pour alléger le code. La "longue tâche", visiblement, fonctionne car elle se trace dans la console. Mais la barre de progression Swing n’est mise à jour qu’à la fin !
Multitâche avec un thread import java.awt.*; import java.awt.event.*; import javax.swing.*; public class GUIetTacheLongue2 extends JFrame { JProgressBar barreProgression; public GUIetTacheLongue2() { super("GUIetTacheLongue2"); Container contentPane = getContentPane(); barreProgression = new JProgressBar(); JButton bouton= new JButton("Demarrer la tache longue"); contentPane.add(bouton,BorderLayout.NORTH); contentPane.add(barreProgression,BorderLayout.SOUTH); bouton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { Thread t = new Thread() { public void run() { LongueTache2 tache = new LongueTache2(); tache.execute(barreProgression); } }; t.start(); } } ); pack(); setVisible(true);} //fin constructeur public static void main(String[] args) { new GUIetTacheLongue2(); } } // fin de la classe GUIetTacheLongue2 class LongueTache2 { public void execute (JProgressBar barreProgression) { barreProgression.setMinimum(0); barreProgression.setMaximum(99); barreProgression.setValue(0); for(int i = 0; i < 100; i++ ) { barreProgression.setValue(i); System.out.print("."); try {Thread.sleep(100); } catch (InterruptedException e) {} } System.out.println("") ; Une nouvelle classe interne anonyme qui est imbriquée dans la précédente classe interne anonyme afin d’implémenter le thread.
Multitâche avec un thread Pour que la longue tâche et le Gui soient exécutés "en même temps", on lance un thread de plus pour la longue tâche. Exécution : ................................. ............................................... $ Le thread "longue tâche", visiblement, fonctionne car il se trace dans la console et la barre de progression Swing est mise à jour progressivement.
Utilitaire pour tracer les threads import java.awt.*; import java.awt.event.*; import javax.swing.*; class ThreadSwing { public static void main(String args[]) { System.out.println("main -----> thread name = " + Thread.currentThread().getName()); JFrame frame = new JFrame("ThreadSwing"); JButton bouton = new JButton("Afficher"); frame.getContentPane().add(bouton); bouton.addActionListener(new ActionListener() { public void actionPerformed (ActionEvent e) { System.out.println ("actionPerformed -----> thread name = " +Thread.currentThread().getName()); } }); frame.pack(); frame.setVisible(true); Exécution : main -----> thread name = main actionPerformed -----> thread name = AWT-EventQueue-0
Utilitaire pour tracer les threads Ce code s’exécute sur 2 threads : main pour pack, setVisible (réaliser les composants). AWT-EventQueue-0 pour l’action (les événements). Swing (et AWT) dispose d’un thread spécifique pour traiter les événements : l’event dispatcher : AWT-EventQueue-0 qui exécute les "handlers"/méthodes associées aux événements. Une file permet de mettre en attente ordonnée les événements qui arrivent.
Deadlock d’initialisation de GUI import …. public class SwingThreadDeadlock extends JPanel implement ComponentListener{ private static JFrame frame; private JTextArea trace; public SwingThreadDeadlock() { …. } void componentHidden(ComponentEvent e) {} // invoquer quand le composant devient invisible Void componentMoved(ComponentEvent e) {} // invoquer quand le composant change de position Void componentResized(ComponentEvent e) {} // invoquer quand le composant change de taille void componentShown(ComponentEvent e) {} // invoquer quand le composant devient visible private static void createAndShowGUI() { … frame.pack(); frame.setVisible(true); public static void main(String[] args) { createAndShowGUI(); C’est un écouteur de composant
Deadlock d’initialisation de GUI private static void createAndShowGUI() { … frame.pack(); frame.setVisible(true); } public static void main(String[] args) { createAndShowGUI(); public void componentResized (ComponentEvent e) { Afficher un texte dans un composant qui appartient a la frame. (trace de type JTextAera). } AWT-EventQueue-0 pack provoque des events componentResized. La méthode componentResized agit sur le composant JTextAera qui n’est pas encore visible. D’où l’interblocage (deadlock). Solution : il faut s’assurer que tous les composants sont visibles avant de lancer les évènements.
Pattern de création d’un GUI public static void main(String[] args) { SwingUtilities.invokeLater (new Runnable() { public void run() { createAndShowGUI(); } }); invokeLater (Runnable tache) est une méthode static de la classe SwingUtilities : Fera exécuter la méthode createAndshowGUI dans le thread event-dispatcher (AWT-EventQueue-0). Cette demande est ajoutée dans la file des événements en attente de traitement, comme il n’y a pas encore de composant, ce sera le premier traitement et ceci évite le deadlock. Cette méthode retourne immédiatement dans le thread appelant.
Tâche périodique dans un GUI Tâche à effectuer : déclencher un compte à rebours de 6 secondes. Toutes les secondes, il faut changer le label affiché et, à la fin, afficher fini. La tâche périodique est exécutée par un Timer Swing. Le Timer Swing se trouve dans javax.swing. Différent de Timer qui est dans java.util. Le constructeur Timer (period, listener) instancie un Timer de la période désirée avec un listener désigné. Aux instants ad-hoc, le timer enverra un événement ActionEvent au listener. La méthode setInitialDelay (délai) définit un délai initial avant l’établissement des périodes. La méthode setCoalesce (booléen) définit la politique du timer en cas d’encombrement : si true, et s’il y a plusieurs events à générer (du fait d’un retard), alors le timer n’envoie qu’un unique event.
Tâche périodique dans un GUI public void actionPerformed(ActionEvent e) { if (e.getActionCommand() != null) { boutonGo.setEnabled(false); compteur = 6; etatTachePeriodique.setText("reste "+ compteur +" secondes"); timer = new Timer(1000, this); /* chaque seconde, le timer envoie un événement à son écouteur */ timer.setInitialDelay(1000); // délai initial timer.setCoalesce(true); /* true, s’il y a plusieurs events à générer (encombrement), alors le timer n’envoie qu’un unique event*/ timer.start(); } else { // l’event de timer n'a pas de nom de commande ! if (--compteur <= -1) { timer.stop(); boutonGo.setEnabled(true); etatTachePeriodique.setText("fini !"); } else etatTachePeriodique.setText("reste "+compteur+" secondes"); } } public static void main(String[] args) { SwingUtilities.invokeLater(new Runnable() { public void run() {new GUIetTachePeriodique8();} });} } import java.awt.*; import java.awt.event.*; import javax.swing.*; public class GUIetTachePeriodique8 extends JFrame implements ActionListener { private JLabel etatTachePeriodique; private JButton boutonGo; private int compteur; private Timer timer; public GUIetTachePeriodique8() { super("GUIetTachePeriodique8"); Container contentPane = getContentPane(); boutonGo = new JButton("Demarrer"); contentPane.add(boutonGo,BorderLayout.NORTH); etatTachePeriodique = new JLabel ("pas de tache periodique"); contentPane.add(etatTachePeriodique,BorderLayout.CENTER); boutonGo.addActionListener(this); pack(); setVisible(true); }
Règle d’unicité du thread GUI Le modèle d’unicité du thread GUI : Tout le code qui affecte des composants Swing ou qui dépend de leurs états doit être exécuter par le thread event-dispatcher. A l’exception de quelques méthodes "thread-safe" de JComponent: repaint() : pour forcer le rafraîchissement de l’affichage d’un composant. revalidate() et invalidate() : pour forcer un composant à réorganiser ses enfants en fonction du layout choisi. invokeLater est une méthode "thread-safe".
Règle d’unicité du thread GUI import java.awt.*; import java.awt.event.*; import javax.swing.*; public class GUIetTacheLongue3 extends JFrame implements ActionListener { JProgressBar barreProgression; JButton bouton; public GUIetTacheLongue3() { super("GUIetTacheLongue3"); Container contentPane = getContentPane(); barreProgression = new JProgressBar(); barreProgression.setMinimum(0); barreProgression.setMaximum(99); barreProgression.setValue(0); bouton = new JButton("Demarrer la tache longue"); contentPane.add(bouton,BorderLayout.NORTH); contentPane.add(barreProgression,BorderLayout.SOUTH); bouton.addActionListener(this); pack(); setVisible(true); } public void actionPerformed(ActionEvent e) { bouton.setEnabled(false); Thread t = new Thread() { public void run() { LongueTache3 tache = new LongueTache3(); tache.execute(); } }; t.start(); } Le constructeur AWT-EventQueue-0 Thread-3
Règle d’unicité du thread GUI class LongueTache3 { public void execute() { for (int i = 0; i < 100; i++ ) { setProgression(i); System.out.print("."); try {Thread.sleep (100);} catch (InterruptedException e) {} } System.out.println("") ; reset(); void setProgression (final int niveau ) { Runnable mettreAJourProgression = new Runnable() { public void run() { barreProgression.setValue (niveau); } }; SwingUtilities.invokeLater(mettreAJourProgression); } void reset() { Runnable remettreAZero = new Runnable() { barreProgression.setValue(0); bouton.setEnabled(true); SwingUtilities.invokeLater(remettreAZero); } public static void main(String[] args) { SwingUtilities.invokeLater(new Runnable() { public void run() { new GUIetTacheLongue3(); } }); } AWT-EventQueue-0 Exécution:
Règle d’unicité du thread GUI Dans cet exemple, il y a 3 threads : main : réaliser les composants. AWT-EventQueue-0 : traiter les évènements. Thread-3 : réaliser la tâche longue. Les actions sur composants Swings sont tous dans le thread event dispatcher grâce à l’appel à la méthode invokeLater().
SwingWorker : tâche longue proprement import java.awt.*; import java.awt.event.*; import javax.swing.*; public class GUIetTacheLongue4 extends JFrame implements ActionListener { JLabel etatLongueTache; JButton bouton; SwingWorker worker4; public GUIetTacheLongue4() { super ("GUIetTacheLongue4"); Container contentPane = getContentPane(); bouton = new JButton("Demarrer la tache longue"); contentPane.add(bouton,BorderLayout.NORTH); etatLongueTache = new JLabel("pas de Longue tache"); contentPane.add(etatLongueTache,BorderLayout.SOUTH); bouton.addActionListener(this); pack(); setVisible(true); } public void actionPerformed(ActionEvent e) { bouton.setEnabled(false); etatLongueTache.setText("tache en cours"); worker4 = new LongueTache4(); worker4.start(); class LongueTache4 extends SwingWorker { private int fin; public LongueTache4() { fin = (int)(Math.random()*100)+100; } public Object construct() { for(int i = 0; i < fin; i++ ) { System.out.print("."); try { Thread.sleep(100);} catch (InterruptedException e) {} } System.out.println("") ; return new Integer(fin); } public void finished() { bouton.setEnabled(true); etatLongueTache.setText("tache finie = " + fin); public static void main(String[] args) { SwingUtilities.invokeLater( new Runnable() { public void run() {new GUIetTacheLongue4();} }); } // fin de la classe GUIetTacheLongue4
SwingWorker : tâche longue proprement Dans cet exemple, la tâche n’affiche plus sa progression. La tâche est désormais une sous-classe de SwingWorker au lieu de Thread. SwingWorker permet d’implémenter facilement une tâche de fond dans un GUI. Son fonctionnement anciennement dans la méthode run() de Thread se retrouve dans la méthode construct() de SwingWorker. Elle est démarrée par start(), en plus dans finished(), se trouve le code à exécuter quand construct est terminé. Affichage du résultat de construct et réactivation du bouton stop.
SwingWorker : tâche longue proprement Il faut tout d’abord hériter de cette classe : Redéfinir la méthode construct() qui est l’équivalente de la méthode run(), mais retourne un Object. Sert typiquement à implémenter : De long calculs. Une attente bloquante sur entrée/sortie disque ou sur le net. Une attente de ressource. Eventuellement redéfinir la méthode finished() qui est appelée quand "construct" est terminée. Elle s’exécute sur le thread event-dispatcher et peut servir à implémenter les effets sur le GUI. La méthode start() permet de lancer l’exécution de contruct() dans un nouveau thread. Exécution:
SwingWorker : tâche longue avec interruption possible Désormais, un bouton permet d’interrompre (définitivement) la tâche longue. Exécution normale : Exécution avec interruption :
SwingWorker : tâche longue avec interruption possible public Object construct() { try { for(int i = 0; i < fin; i++ ) { System.out.print("."); Thread.sleep(100); } } catch (InterruptedException e) { System.out.println("interrupt !"); return new String("interrupt !"); } System.out.println("") ; return String.valueOf(fin); } public void finished() { boutonGo.setEnabled(true); boutonStop.setEnabled(false); String valeurFin = (String)get(); etatLongueTache.setText ("tache finie = " + valeurFin); } } // fin LongueTache5 public static void main(String[] args) { SwingUtilities.invokeLater( new Runnable() { public void run() { new GUIetTacheLongue5(); }); } import java.awt.*; import java.awt.event.*; import javax.swing.*; public class GUIetTacheLongue5 extends JFrame implements ActionListener { JLabel etatLongueTache; JButton boutonGo; SwingWorker worker5; JButton boutonStop; public GUIetTacheLongue5() { super("GUIetTacheLongue5"); Container contentPane = getContentPane(); boutonGo = new JButton ("Demarrer la tache longue"); contentPane.add( boutonGo,BorderLayout.NORTH); boutonStop = new JButton("Stopper"); boutonStop.setEnabled(false); boutonStop.setBackground(Color.RED); boutonStop,BorderLayout.SOUTH); etatLongueTache = new JLabel("pas de Longue tache"); etatLongueTache,BorderLayout.CENTER); boutonGo.addActionListener(this); boutonStop.addActionListener(this); pack(); setVisible(true); } public void actionPerformed(ActionEvent e) { if (e.getActionCommand().equals ("Demarrer la tache longue")) { boutonGo.setEnabled(false); boutonStop.setEnabled(true); etatLongueTache.setText ("tache en cours"); worker5 = new LongueTache5(); worker5.start(); } else { boutonStop.setEnabled(false); worker5.interrupt(); boutonGo.setEnabled(true); class LongueTache5 extends SwingWorker { private int fin; public LongueTache5() { fin = (int)(Math.random()*100)+100;
SwingWorker en interaction avec l’event-dispatcher Nous réintroduisons la barre de progression donc le thread SwingWorker a besoin de la mettre à jour. Celle-ci doit être manipulée dans le cadre du thread event-dispatcher d’où l’emploi d’invokeLater. Exécution :
SwingWorker en interaction avec l’event-dispatcher import java.awt.*; import java.awt.event.*; import javax.swing.*; public class GUIetTacheLongue6 extends JFrame implements ActionListener { JProgressBar barreProgression; JButton boutonGo; SwingWorker worker6; JButton boutonStop; public GUIetTacheLongue6() { super("GUIetTacheLongue6"); Container contentPane = getContentPane(); boutonGo = new JButton("Demarrer la tache longue"); contentPane.add(boutonGo,BorderLayout.NORTH); boutonStop = new JButton("Stopper"); boutonStop.setEnabled(false); boutonStop. setBackground(Color.RED); contentPane.add(boutonStop,BorderLayout.SOUTH); barreProgression = new JProgressBar(); barreProgression.setMinimum(0); barreProgression.setMaximum(99); barreProgression.setValue(0); contentPane.add(barreProgression,BorderLayout.CENTER); boutonGo.addActionListener(this); boutonStop.addActionListener(this); pack(); setVisible(true); } class LongueTache6 extends SwingWorker { public Object construct() { try { for(int i = 0; i < 100; i++ ) { System.out.print("."); setProgression(i); Thread.sleep(100); } } catch (InterruptedException e) { System.out.println("interrupt !"); return new String("interrupt !"); } System.out.println("") ; return new String("tache accomplie"); } public void finished() { boutonGo.setEnabled(true); boutonStop.setEnabled(false); } // fin LongueTache6 public static void main(String[] args) { SwingUtilities.invokeLater( new Runnable() { public void run() { new GUIetTacheLongue6(); }); } // fin GUIetTacheLongue6 public void actionPerformed (ActionEvent e) { if (e.getActionCommand().equals("Demarrer la tache longue")) { boutonGo.setEnabled(false); boutonStop.setEnabled(true); barreProgression.setValue(0); worker6 = new LongueTache6(); worker6.start(); } else { boutonStop.setEnabled(false); worker6.interrupt(); boutonGo.setEnabled(true); void setProgression (final int niveau) { Runnable mettreAJour = new Runnable() { public void run() { barreProgression.setValue(niveau); }; SwingUtilities.invokeLater(mettreAJour);