Table des matières
TP Java: L'horloge
Une introduction à la programmation de processus et à la notion de temps logiciel.
Description du problème
On souhaite réaliser une application qui donne l'heure et qui affiche une image représentant le moment de la journée.
L'horloge sera une horloge à aiguille.
Première analyse du problème
Proposition
L'idée est de représenter le temps qui passe sous la forme d'un compteur d'unité de temps qui devra être incrémenté chaque seconde.
On retient comme concepts principaux pour le modèle: Horloge, Aiguille, Illustration. La gestion du temps sera réalisée par l'application (on aurait pu imaginer que le temps soit représenté par une classe également).
Pour la vue (interface graphique), l'application sera composé d'une fenêtre contenant 2 panneaux, un pour dessiner l'horloge et un pour afficher l'image de l'illustration.
Un contrôleur se chargera de la communication entre la vue et le modèle. Il sera intégré au programme principal (Application). Voici le diagramme de classe complet de l'application:
Implémentation
package modèle
Commençons par réaliser le package modèle. Il n'y a rien de très compliqué, les classes doivent juste être stockées dans un répertoire “modele”.
classe Aiguille
package modele; public class Aiguille { private int angle = 0; public Aiguille(){angle = 0;} public Aiguille(int a){angle = a;} public void changeAngle(int a){angle = a;} public int getAngle(){return angle;} }
On pourrait se passer de cette classe assez simple; cependant, elle nous permet de fabriquer des horloges à deux aiguilles par exemple.
classe Horloge
La classe Horloge est un peu plus complexe; prenez le temps de comprendre les opérations de transtypage (cast) et de conversion.
package modele; public class Horloge { private Aiguille [] aiguilles; private int nbSec = 0; public Horloge() { aiguilles = new Aiguille[3]; //aiguilles des secondes, minutes et heures aiguilles[0] = new Aiguille(50); aiguilles[1] = new Aiguille(45); aiguilles[2] = new Aiguille(30); } public void mettreAHeure(int nbS) { nbSec = nbS % (3600 * 12); //si je ne caste pas nbSec en double, nbSec/3600 donnera un entier double h = (double) nbSec / 3600; //dans les 2 expressions suivantes, je veux uniquement la partie entière de h et m, //je les caste donc en entier. double m = (double) (nbSec - ((int)h*3600))/60; //s est un entier, je dois donc caster l'expression en int. int s = (int) (nbSec - (int)h*3600 - (int)m*60); changerValeur(0, s); changerValeur(1, m); changerValeur(2, h); } public void mettreAHeure(int h, int m, int s) { mettreAHeure(3600 * h + 60 * m + s); } private void changerValeur(int aig, double valeur) { int angle = 0; //angle en degré (midi = 0°, -h = 180°) /* * Pour éviter que les aiguilles des heures et des minutes se déplacent d'un * seul coup, on utilise une valeur double et non pas entière. * Comme cela, lorsqu'il est 15h30 par exemple, l'aiguille des heures est à 105° * (au milieu du 3 et du 4 sur le cadran) */ switch(aig) { //secondes case 0: { angle = (int)(valeur * 6);//=val * 360 / 60 //angle étant un entier, il faut caster en int le résultat de val * 6 //on va perdre la partie décimale du résultat mais ce n'est pas grave dans //notre cas. break; } case 1: //minutes { angle =(int) (valeur * 6); // idem secondes break; } case 2: //heures { angle = (int) (valeur * 30); //=val * 360 / 12 break; } } aiguilles[aig].changeAngle(angle); } public Aiguille getAiguille(int aig){return aiguilles[aig];} }
classe Illustration
package modele; public class Illustration { private int hDeb = 0;//(en secondes) private int hFin = 0; private String image ="defaut.png"; public Illustration(int d, int f, String i) { hDeb = d; hFin = f; image = i; } public boolean estAffichable(int heure)//heure est en secondes { boolean retour = false; if (heure > hDeb && heure < hFin){retour = true;} return retour; } }
package vue
classe PanIllustration
package vue; import javax.swing.JPanel; import java.awt.Dimension; import java.awt.Image; import java.awt.Graphics; public class PanIllustration extends JPanel { private Image image; public PanIllustration() { setPreferredSize(new Dimension(400,200)); setImage("images/defaut.png"); } public void setImage(String adresseImg) { image = getToolkit().getImage(adresseImg); } public void paint(Graphics g) { g.drawImage(image, 0,0,this); } }
Notez que Image est une classe abstraite, on ne fait pas appel au constructeur d'Image mais on passe par la classe ToolKit pour fabriquer un objet.
classe PanHorloge
package vue; import java.awt.Graphics; import java.awt.Color; import java.awt.Dimension; import javax.swing.JPanel; public class PanHorloge extends JPanel { private double angleH=30; private double angleM=60; private int angleS=96; public PanHorloge() { super(); setPreferredSize(new Dimension(200,200)); } public void majHorloge(double aH, double aM, int aS) { angleH = aH; angleM = aM; angleS = aS; } //redéfinition de la méthode paint public void paint(Graphics g) { //fond g.setColor(Color.blue); g.fillRect(0,0,getWidth(), getHeight()); //cadran g.setColor(Color.green); g.fillOval(10,10,getWidth()-20, getHeight()-20); //éléments de décorations du cadran g.drawString("12", 95, 10); g.drawString("3", 190, 100); g.drawString("6", 95, 200); g.drawString("9", 0, 100); g.setColor(Color.black); g.drawLine(100,10,100,20); int x1,y1,x2,y2 = 0; //les heures for(int i=0; i<360; i=i+30) { x1 = (int) (Math.sin(Math.toRadians(i))*90 + 100); y1 = (int) (Math.cos(Math.toRadians(i))*90 + 100); x2 = (int) (Math.sin(Math.toRadians(i))*80 + 100); y2 = (int) (Math.cos(Math.toRadians(i))*80 + 100); g.drawLine(x1,y1,x2,y2); } //les minutes et les secondes for(int i=0; i<360; i=i+6) { x1 = (int) (Math.sin(Math.toRadians(i))*90 + 100); y1 = (int) (Math.cos(Math.toRadians(i))*90 + 100); x2 = (int) (Math.sin(Math.toRadians(i))*87 + 100); y2 = (int) (Math.cos(Math.toRadians(i))*87 + 100); g.drawLine(x1,y1,x2,y2); } //les aiguilles maintenant: g.setColor(Color.red); x1 = (int) (Math.sin(Math.PI - Math.toRadians(angleH))*50 + 100); y1 = (int) (Math.cos(Math.PI - Math.toRadians(angleH))*50 + 100); int [] tabx = {99,101,x1+1,x1-1}; int [] taby = {99,101,y1+1,y1-1}; g.fillPolygon(tabx,taby,4); g.setColor(Color.orange); x1 = (int) (Math.sin(Math.PI - Math.toRadians(angleM))*85 + 100); y1 = (int) (Math.cos(Math.PI - Math.toRadians(angleM))*85 + 100); g.drawLine(100,100,x1,y1); g.setColor(Color.yellow); x1 = (int) (Math.sin(Math.PI - Math.toRadians(angleS))*86 + 100); y1 = (int) (Math.cos(Math.PI - Math.toRadians(angleS))*86 + 100); g.drawLine(100,100,x1,y1); } }
classe Fenetre
package vue; import javax.swing.JFrame; import javax.swing.JPanel; import java.awt.BorderLayout; import java.awt.Dimension; public class Fenetre extends JFrame { private PanHorloge panH = new PanHorloge(); private PanIllustration panI = new PanIllustration(); public Fenetre() { super("Mon Horloge à images"); JPanel p = new JPanel(); p.setLayout(new BorderLayout()); p.add(panI,BorderLayout.NORTH); p.add(panH,BorderLayout.WEST); p.add(new JPanel(), BorderLayout.EAST); setContentPane(p); setSize(new Dimension(400,400)); setResizable(false); pack(); } public PanHorloge getPanH(){return panH;} public PanIllustration getPanI(){return panI;} }
Application
Nous allons utiliser la classe Thread pour générer chaque seconde un top horloge.
La classe Thread permet de lancer un processus et de le faire attendre grâce à la méthode sleep(nbMilliseconde). Le code du programme du processus est ajouté dans le corps de la méthode run(), que l'on redéfinit donc.
Le processus est exécuté en parallèle du programme qui le lance; une technique classique permettant de répéter à volonté un ensemble d'instruction est utilisée dans le code suivant: on réalise une boucle infinie, conditionnée par un booléen dont on change la valeur lorsqu'on souhaite arrêter le processus.
Nous ajouterons également une méthode pour permettre de comparer les différences entre l'heure du système et l'heure du programme: lorsqu'on actionne un bouton de l'interface, on demande au programme d'effectuer la comparaison. Notez que l'endroit où on lance la comparaison n'est pas anodin. Pourquoi à votre avis l'a-t-on fait à cet endroit précis?
import modele.Horloge; import modele.Illustration; import vue.Fenetre; import java.util.ArrayList; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; public class Application extends Thread implements ActionListener { private Horloge horloge; private ArrayList illustrations = new ArrayList(); private Fenetre fenetre; private long hDebut; private int heure=0; private int nbSecEcoules = 0; private boolean running = false; private boolean verifier = false; public Application() { horloge = new Horloge(); fenetre = new Fenetre(this); fabriqueIllustrations(); fenetre.setVisible(true); } private void fabriqueIllustrations() { illustrations.add(new Illustration(0,7*3600,"images/lune.png")); illustrations.add(new Illustration(7*3600,7*3600 + 1800,"images/petitdej.png")); illustrations.add(new Illustration(7*3600 + 1800,8*3600,"images/matin.png")); illustrations.add(new Illustration(8*3600,9*3600,"images/journal.png")); illustrations.add(new Illustration(9*3600,12*3600,"images/clavier.png")); illustrations.add(new Illustration(12*3600,14*3600,"images/midi.png")); illustrations.add(new Illustration(14*3600,16*3600,"images/abaque.png")); illustrations.add(new Illustration(16*3600,17*3600,"images/gouter.png")); illustrations.add(new Illustration(17*3600,19*3600,"images/soir.png")); illustrations.add(new Illustration(19*3600,24*3600,"images/lune.png")); } public void demarrerHorloge() { running = true;//pour répéter le processus heure = 3600 * 7 -10;//heure de début horloge.mettreAHeure(heure); this.start();//démarrage du processus (appel de la méthode run en parallèle) } public void incrementer() { heure++; nbSecEcoules++; horloge.mettreAHeure(heure); } public void verifierHeure() { long heureActu = System.currentTimeMillis() - hDebut; long decalage = heureActu - (nbSecEcoules * 1000); System.out.println("decalage: " + decalage); } //redéfinition de la méthode run de Thread; c'est ici qu'est contenu le code à exécuter dans le processus. public void run() { hDebut = System.currentTimeMillis(); while(running) { try { sleep(1000); } catch(java.lang.InterruptedException ie){} incrementer(); double aH = horloge.getAiguille(2).getAngle(); double aM = horloge.getAiguille(1).getAngle(); int aS = horloge.getAiguille(0).getAngle(); //recherche d'une éventuelle illustration boolean trouve = false; int i =0; while(!trouve && i<illustrations.size()) { //rappel: quand on récupère un objet d'une ArrayList, il faut lui rappeller //sa classe en le castant si on n'a pas utilisé de liste typée //autre solution: déclarer ArrayList<Illustration> illustrations= ... Illustration illust = (Illustration) illustrations.get(i); if(illust.estAffichable(heure)) { trouve = true; fenetre.getPanI().setImage(illust.getImage()); } i++; } fenetre.getPanH().majHorloge(aH,aM,aS); fenetre.repaint(); if (verifier) { verifierHeure(); verifier = false; } } } //redéfinition de actionPerformed de l'interface ActionListener //la classe Application servira d'ActionListener par exemple à un bouton dans une fenetre. public void actionPerformed(ActionEvent ae) { verifier = true; } public void setRunning(boolean r){running = r;}//pour contrôler la boucle du processus //méthode principale public static void main(String [] arg) { Application appli = new Application(); appli.demarrerHorloge(); } }
Voici le code complet première version de notre Horloge.
Critique
Lorsqu'on exécute le programme, tout a l'air de bien se passer; Mais est-ce vraiment le cas?
Regardez le décalage entre l'horloge système et celle du programme: il augmente régulièrement.
Ce décalage est dû au temps de traitement nécessaire des opérations contenues dans la méthode run. Notre horloge retarde!
2ème analyse du problème
Analyse
Nous allons essayer de réparer notre Horloge! L'objectif est de rendre indépendant les tops horloges et les opérations à réaliser chaque seconde.
Un première solution serait de limiter au maximum les opérations à réaliser dans la boucle de la méthode run.
Plutôt que l'Application envoie un message à la vue et au modèle chaque fois qu'ils doivent s'actualiser, nous allons inverser les rôles: ce sont la vue et le modèle qui vont prendre l'initiative et demander à l'application si il y a eu des modifications.
Il faudrait donc installer des Thread dans chaque composant nécessitant une actualisation.
Que pensez-vous de cette solution?
Elle est fonctionnelle, mais elle risque de compliquer énormément les échanges entre les différents composants. Elle va également les rendre dépendants les uns des autres (couplage fort), ce que nous cherchons à éviter dans une architecture MVC.
Proposition
Une autre solution serait de déléguer les tops horloges à une autre classe, qui serait chargée de lancer le processus d'actualisation de la classe Application chaque seconde.
Cette solution semble assez performante, essayons de la mettre en place:
Implémentation
Voici les principales modifications:
Classe Chronometre
Le but de cette classe est uniquement de donner un top horloge.
public class Chronometre extends Thread { private Application appli; private boolean running =false; public Chronometre(Application a) { appli = a; } public void setRunning(boolean r){running = r;} public void run() { while(running) { try { sleep(1000); appli.lancerUpdate(); } catch(java.lang.InterruptedException ie){} } } }
Classe Application
Modification de la méthode demarrerHorloge:
public void demarrerHorloge() { heure = 3600 * 7 -10;//heure de début horloge.mettreAHeure(heure); hDebut = System.currentTimeMillis(); chrono = new Chronometre (this); chrono.setRunning(true); chrono.setDaemon(true); chrono.setPriority(Thread.MAX_PRIORITY);//niveau de priorité du Thread en cas de collision chrono.start();//démarrage du processus (appel de la méthode run en parallèle) running = true; setPriority(Thread.MIN_PRIORITY); this.start(); }
Modification de la méthode run():
public void run() { while(running) { if(update) { update = false; //on remet notre booleen à false incrementer(); [......] etc. [......] } else { try { sleep(10); } catch(java.lang.InterruptedException ie){} } } }
Et pour finir le code complet de la solution 2.
Résultat et critiques
Si vous êtes sous Linux ou Mac, observez-vous une amélioration? Non!
Si vous êtes sous Windows, est-ce le cas? L'horloge retarde moins si le système n'est pas trop sollicité, mais prend plus de retard que sous Linux dans le cas contraire!
Que se passe-t-il?
Si vous avez suivi un cours d'introduction aux systèmes d'exploitation (1ère année à l'INSA ), vous devriez comprendre. Sinon, ou pour vous rafraîchir la mémoire, repensez au fonctionnement d'un processeur: il ne peut faire qu'un seule chose à la fois. Donc lorsqu'il a à réaliser deux tâches en parallèle, le système d'exploitation ne les exécute pas en même temps en réalité.
Les différences de fonctionnement des systèmes unix et windows font que le programme ne se comporte pas tout à fait de la même manière.
Mais notre horloge retarde encore!
Conclusions
Il est donc difficile d'un point de vue logiciel de réaliser des applications autonomes dans la gestion précise du temps. C'est pourquoi il est préférable de demander au système d'exploitation l'heure, car il la gère mieux, plutôt que d'essayer de fabriquer une horloge qui sera quoi qu'il arrive en retard.
Comment réaliseriez-vous cela? Inspirez-vous de la méthode verifierHeure() pour récupérer l'heure système.
Revenons sur la deuxième solution; nous avons en fait réalisé quelquechose d'assez proche de la classe Timer de java (en simplifié), à la différence que la classe Timer génère un évènement.
Voici comment nous aurions pu utiliser un Timer dans la classe Chronomètre:
Implémentation d'un Timer
Classe Chronomètre
Faites attention à l'import: il existe 3 Timer dans l'API java… 2 sont quasi équivalents (awt et swing).
import javax.swing.Timer; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; public class Chronometre implements ActionListener { private Application appli; private Timer timer; public Chronometre(Application a) { appli = a; timer = new Timer(1000,this); //Timer de 1000ms dont l'action listener est l'objet créé } public void demarrer(){timer.start();} public void actionPerformed(ActionEvent ae) { appli.maj(); } }
Classe Application
Voici la nouvelle méthode demmarrerHorloge:
public void demarrerHorloge() { heure = 3600 * 7 -10;//heure de début horloge.mettreAHeure(heure); hDebut = System.currentTimeMillis(); chrono = new Chronometre (this); chrono.demarrer(); }
et
public void maj()
en remplacement de
public void run()
Et pour finir le code complet de la solution avec Timer; remarquez qu'elle se comporte de la même manière du point de vue résultat que la solution 2.