This assignment will be closed on July 02, 2024 (23:59:59).
You must be authenticated to submit your files

Calculatrice à mémoire avec interface graphique

Xavier Rival, Jean-Marie Madiot, Julien Signoles, Benjamin Werner

Le but de ce TD sur deux séances est double : dans un premier temps, il permettra de programmer le coeur d’une calculatrice capable d’évaluer des opérations de bas niveau tandis que, dans un second temps, il donnera l’occasion de développer deux interfaces, l’une textuelle et l’autre graphique. Il fournit donc l’occasion de s’initier à la programmation graphique et événementielle.

Nous allons ainsi construire progressivement une calculatrice qui ressemblera à ceci :

Le coeur de notre calculatrice : opérations de base

Cette partie a pour but de concevoir le coeur de la calculatrice, qui en stockera l’état interne et pourra effectuer tous les calculs. Cette calculatrice traitera des expressions structurées qui seront écrite suivant une syntaxe classique que l’on étudiera dans la partie suivante. Pour gérer l’évaluation pas à pas de telles expressions, la calculatrice va devoir maintenir un état interne décrivant les valeurs et opérations entrées par l’utilisateur. À ce stade, il n’est pas encore question de lire une expression en entrée mais simplement de traiter une suite de “commandes” (comme la lecture d’un nombre ou d’un opérateur) qui seront envoyées à la calculatrice dans l’ordre de lecture de l’expression, de gauche à droite.

Description de l’état de la calculatrice

Pour décrire l’état de la calculatrice, nous allons utiliser deux piles comme représentation de l’état de la calculatrice. La première, numbers, sert à enregistrer les nombres décimaux à utiliser dans les prochains calculs : lorsque toutes les opérations ont été calculées, elle contient un seul nombre qui est le résultat final du calcul. La seconde pile, operators, contient, sous une forme symbolique, les opérations à effectuer. Par exemple, après la lecture de l’expression 5 + 12.34 * 2, et avant d’effectuer le moindre calcul, numbers est une pile contenant (du bas vers le haut) 5, 12.34 et 2, alors qu’operators contient Operator.PLUS et Operator.MULT, représentations symboliques de + et * respectivement.

Pour représenter les piles, nous allons utiliser celles fournies par la bibliothèque standard de Java, java.util.Stack<E> dont la documentation est disponible en ligne. Il s’agit d’une classe générique paramétrée par le type E des éléments contenus dans la pile. Dans la suite, la première pile sera de type java.util.Stack<Double> pour contenir des doubles, tandis que la seconde sera de type java.util.Stack<Operator> pour contenir des opérateurs.

Exercice 1

Il serait naturel que les champs numbers et operators aient une visibilité private. Cependant, pour permettre aux tests en ligne de vérifier le bon fonctionnement de vos classes, il est exceptionnellement demandé de les rendre public.

Déposer ici vos fichiers :

Upload form is only available when connected

Calcul des opérations de base

Exercice 2

Nous allons maintenant nous intéresser aux opérations arithmétiques et à leur évaluation.

Déposer ici vos fichiers :

Upload form is only available when connected
Exercice 3

Pour prendre en compte correctement la priorité de chaque opérateur, nous allons devoir utiliser intelligemment la pile operators, suivant l’algorithme vu en cours. En particulier, lorsque la calculatrice recevra une “commande” correspondant à un opérateur op, on commencera par évaluer une partie des opérateurs déjà sur la pile operators avant de placer op au sommet de cette même pile (algorithme Shunting-yard). Ainsi, nous allons obtenir une calculatrice capable à ce stade de recevoir deux types de “commandes”, qui correspondent respectivement à l’entrée par l’utilisateur d’un nombre et d’un opérateur, et dans la suite nous allons enrichir ce langage de commandes.

Déposer ici vos fichiers :

Upload form is only available when connected

Commandes pour le contrôle de l’évaluation d’expressions

Exercice 4

Nous ajoutons maintenant quelques commandes qui vont rendre notre calculatrice vraiment utilisable, avec tout d’abord l’opérateur d’évaluation =, puis les parenthèses et enfin la possibilité de réinitialiser l’état de la calculatrice.

Ajouter une méthode void commandEqual() qui évalue l’ensemble des opérations actuellement sur la pile. Tester avec quelques expressions (dont les deux exemples fournis plus haut).

Déposer ici vos fichiers :

Upload form is only available when connected
Exercice 5

Ajouter deux méthodes void commandLPar() et void commandRPar() décrivant l’ouverture et la fermeture de parenthèses. On veillera à évaluer correctement le contenu d’une paire de parenthèses. On recommande pour cela d’ajouter un élément OPEN au type énuméré Operator, et décrivant la position d’une parenthèse ouvrante sur la pile des opérateurs.

Déposer ici vos fichiers :

Upload form is only available when connected
Exercice 6

Ajouter une méthode void commandInit() qui effectue la réinitialisation de l’état de la calculatrice.

Déposer ici vos fichiers :

Upload form is only available when connected

Mémoire

Exercice 7

Ici, nous allons sauvegarder les résultats de nos calculs dans une liste afin de pouvoir y accéder ensuite via des variables entières : la variable $i représente le résultat du i-ème dernier calcul effectué (via la commande =). Ainsi, interpréter "1+2*3=$1+$1+1=$1-$2=" doit produire un état où 8.0 est au sommet de la pile. Les résultats ne doivent pas être effacés par un appel à commandInit().

Déposer ici vos fichiers :

Upload form is only available when connected

Une calculatrice textuelle

Dans cette partie, nous allons réaliser une première interface, qui repose sur la lecture de chaînes de caractères passées en argument. C’est un premier pas vers une calculatrice plus facile à utiliser, puisque nous allons bientôt pouvoir lui demander d’évaluer des formules du type 2+3.5*(8-4.2)=.

Pour cela, nous allons écrire une autre classe appelée Tokenizer qui va prendre en charge la lecture de chaînes de caractères dont elle va extraire des commandes de base (souvent appelées “tokens”, d’où son nom). L’extraction de commandes à partir d’une chaîne de caractères n’est pas une opération simple, donc le tokenizer doit maintenir un état interne décrivant le contexte à un instant de la lecture, et mettre à jour cet état à chaque caractère rencontré.

Première version du tokenizer

Pour commencer, on considère que l’état interne du tokenizer est décrit par la donnée :

Exercice 8

Déposer ici vos fichiers :

Upload form is only available when connected
Exercice 9

Ajouter une méthode publique void readString(String s) qui met à jour l’état du tokenizer après avoir lu les caractères de s un à un.

Il pourra également être utile d’ajouter un booléen activant un mode debug pour afficher l’état interne du tokenizer à chaque pas.

Déposer ici vos fichiers :

Upload form is only available when connected

Gestion des opérateurs et parenthèses

Nous allons maintenant étendre readChar afin de gérer les autres opérations de notre calculatrice.

Exercice 10

Modifier readChar afin de supporter les opérateurs binaires +, -, *, et /. Lors de la lecture d’un opérateur, on commencera par passer à la calculatrice la commande qui correspond au nombre en cours de lecture, puis on lui passera la commande correspondant à l’opérateur courant, et on mettra à jour les autres champs du tokenizer pour en maintenir la cohérence.

On conseille d’éviter de dupliquer du code. Pour faciliter la mise au point du tokenizer, on pourra factoriser certaines opérations, comme la finalisation de la lecture d’un nombre (lors de la lecture d’une opération).

Déposer ici vos fichiers :

Upload form is only available when connected
Exercice 11

Modifier readChar pour gérer les parenthèses ouvrantes et fermantes.

Déposer ici vos fichiers :

Upload form is only available when connected

Nombres décimaux et nombres négatifs

Jusqu’ici, notre tokenizer n’accepte que les nombres entiers et positifs ce qui n’est bien sûr pas satisfaisant. Nous avons repoussé un peu le traitement des autres valeurs car celui-ci complique l’état du tokenizer…

Exercice 12

Modifier readChar pour lire les nombres avec partie décimale (délimitée par le caractère .). On pourra ajouter deux champs à l’état interne du tokenizer :

Il faudra prendre soin aussi de modifier les autres fonctions du tokenizer.

Déposer ici vos fichiers :

Upload form is only available when connected
Exercice 13

Modifier readChar pour lire les nombres négatifs, comme -3 ou -.053. Il faudra faire attention au caractère - qui a maintenant deux sens possibles, soit comme opérateur binaire, soit comme comme négation unaire. On note qu’un - observé tout au début de la lecture, ou bien juste après une parenthèse ouvrante, après un caractère = ou après un opérateur binaire décrit nécessairement la négation unaire. On pourra ajouter deux champs à l’état interne du tokenizer :

Déposer ici vos fichiers :

Upload form is only available when connected

Ré-initialisation et mémoire

Exercice 14

Notre interface textuelle est presque prête. Nous allons maintenant prendre en compte les caractères gérant les deux dernières commandes.

Modifier readChar afin de traiter le caractère C comme une commande réinitialisant l’état de la calculatrice.

Déposer ici vos fichiers :

Upload form is only available when connected
Exercice 15

Modifier readChar afin de traiter la chaîne $i comme une lecture dans la mémoire comme défini plus haut (on pourra étendre la définition de l’état interne du tokenizer). Cette question étant plus difficile, on pourra la considérer facultative.

Déposer ici vos fichiers :

Upload form is only available when connected

Une calculatrice graphique

Dans cette dernière partie, nous allons créer une interface graphique (GUI) pour notre calculatrice. Cette seconde interface va transformer des actions de l’utilisateur (actions sur des boutons à l’aide de la souris ou actions sur des touches au clavier) en commandes. Elle repose donc sur un principe similaire à l’interface textuelle, même si la partie visible est bien sûr entièrement différente.

Application et fenêtre graphiques

Nous allons utiliser la bibliothèque javafx. Il vous faudra utiliser plusieurs constructions fournies par cette librairie, et parfois rechercher les classes et méthodes adaptées. Pour cela, vous devrez vous référer à la Documentation en ligne de javafx

Notre classe étendra la classe Application. Une instance de notre classe sera donc une fenêtre graphique possédant des décorations (bordure, titre, ...) et des boutons permettant de la fermer, la miniaturiser ou l’agrandir. Pour lancer une GUI, une méthode main est nécessaire (comme pour les applications sans GUI). Cette méthode doit appeler avec ses propres arguments la méthode launch, qui est fournie et qu’on n’aura pas besoin de définir. Au travers launch, javafx préparera alors la fenêtre et appellera la méthode start(Stage stage) qu’il nous faut définir. Le code de votre GUI ressemblera donc au code suivant (les parties TODO seront complétées par la suite) :

import javafx.application.Application;
import javafx.stage.Stage;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.scene.input.KeyEvent;

public class GraphicsCalculator extends Application {
    Tokenizer tok;
    
    @Override
    public void start(Stage stage) {
        stage.show();
        // TODO
        Scene scene = new Scene(new VBox(/* TODO */));
        stage.setScene(scene);
    }
    
    public static void main(String[] args) {
        launch(args);
    }
}

La première étape est de vous assurer que vous arrivez à utiliser JavaFX, le programme ci-dessus devrait ouvrir une fenêtre blanche.

Terminal sur les ordinateurs de l’école

export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-21.0.3.0.9-1.el9.alma.1.x86_64

export PATH_TO_FX=/usr/local/javafx-sdk-21.0.3/lib

javac --module-path $PATH_TO_FX --add-modules javafx.controls HelloFX.java

(ou bien *.java à la fin pour compiler tous les fichiers présents)

java --module-path $PATH_TO_FX --add-modules javafx.controls HelloFX (ou bien GraphicsCalculator pour votre calculatrice)

VSCode
Eclipse

Si vous avez une erreur de compilation de la forme

The type 'Application' is not API (restriction on required library rt.jar)

exécutez les actions suivantes :

Début

On vous demande de :

  1. Modifier GraphicsCalculator de manière à changer le titre de votre GUI en utilisant une méthode de stage.

  2. Définissez la largeur et la longueur de la fenêtre (par exemple 200x200) en utilisant des méthodes de stage.

La structure de la fenêtre va comme suit : stage contient une Scene scene, qui contient une VBox, boîte d’alignement verticale, au constructeur de laquelle on pourra fournir des HBoxs, respectivement horizontales, auxquelles on pourra fournir un Label pour le résultat et des Buttons pour les entrées.

  1. Ajouter à votre classe GraphicsCalculator un champ de type Label correspondant à la zone de résultat votre calculatrice et ajouter ce dernier dans une HBox, qui sera la première ligne donnée au constructeur de VBox.

Boutons

  1. Ajouter une méthode Button b(char c) (non statique), qui renvoie un nouveau bouton de taille constante, par exemple 30x30, en utilisant setMinSize et setMaxSize. Utilisez-la une fois par caractère nécessaire à votre calculatrice, en répartissant les boutons sur plusieurs lignes HBox en ajoutant ces dernières à VBox.

  2. Placer les boutons et la zone de texte de votre calculatrice de manière similaire à l’image du début de l’énoncé, l’opérateur '$' étant optionnel.

  3. Définir une méthode update(char c) qui interprète le caractère c et affiche le résultat dans la zone textuelle.

  4. Si b est un bouton, alors b.setOnAction(value -> /*TODO*/); exécutera /*TODO*/ à chaque clic du bouton (on appelle cette notation “lambda-notation” et cela correspond à la définition d’une fonction prenant en argument value et qui évalue l’expression qui remplacera /*TODO*/). Faites en sorte qu’appuyer sur un bouton appelle la méthode update avec le caractère correspondant, et testez votre calculatrice.

Clavier

Les événements sont les interactions avec l’utilisateur. Ils rendent par exemple un programme réactif aux clics de souris ou au fait qu’une touche du clavier soit pressée ou relâchée.

  1. Implémenter un gestionnaire d’événements de façon à mettre correctement à jour votre calculatrice lorsqu’une touche du clavier est pressée. Pour cela, on ajoutera l’écouteur d’événements clavier à la scène en cours : scene.setOnKeyTyped(e -> handlekey(e));. Implémentez la méthode void handlekey(KeyEvent e) et testez votre calculatrice. Certaines touches spéciales (Entrée, Échap) sont plus compliquées à gérer et on pourra les ignorer, au moins dans un premier temps.

Déposer ici vos fichiers (le test va échouer, ignorez-le, le but est seulement de fournir les fichiers) :

Upload form is only available when connected