Progetto: Una calcolatrice scientifica

Esercitazione 8 – scientific calculator
Progetto: Una calcolatrice scientifica
Utilizzando il Parser reperibile all’indirizzo https://github.com/uklimaschewski/EvalEx creare un’applicazione
Java con interfaccia grafica che permetta di emulare la calcolatrice di Windows in modalità standard e in
modalità scientifica. La calcolatrice avrà anche la possibilità di tracciare il grafico di una funzione f(x) di cui si
conosce l’espressione f e l’intervallo di valori a<=x<=b. La calcolatrice farà l’autoscaling sia sull’asse delle
ascisse che sull’asse delle ordinate. Inoltre passando il mouse sopra il grafico della funzione verranno
visualizzati i valori di x e di f(x).
Nel progetto software l’interfaccia grafica dovrà richiamare i metodi di una classe CalcEngine che gestisce la
macchina a stati e il parser. In altri termini l’applicazione deve essere realizzata in modo da poter
disaccoppiare l’interfaccia utente dal CalcEngine. In seguito la classe CalcEngine sarà utilizzata in
un’applicazione Android.
Ad esempio, supponendo di avere i bottoni:
private javax.swing.JButton C;
private javax.swing.JButton CE;
private javax.swing.JButton MC;
private javax.swing.JButton MR;
private javax.swing.JButton MS;
private javax.swing.JButton Mminus;
private javax.swing.JButton Mplus;
private javax.swing.JButton back;
private javax.swing.JButton one;
private javax.swing.JButton percentage;
private javax.swing.JButton plus;
private javax.swing.JButton plusMinus;
private javax.swing.JButton reciprocal;
private javax.swing.JButton seven;
private javax.swing.JButton six;
private javax.swing.JButton sqrt;
//..e altri..
I gestori degli eventi di ciascun bottone dell’interfaccia grafica richiameranno i metodi di un oggetto “calc” di
tipo CalcEngine, il quale a sua volta utilizzerà un oggetto di classe Expression…
//A titolo esemplificativo….
private void zeroActionPerformed(java.awt.event.ActionEvent evt) {
setNumber(evt);
redisplay();
}
private void oneActionPerformed(java.awt.event.ActionEvent evt) {
setNumber(evt);
redisplay();
}
private void CEActionPerformed(java.awt.event.ActionEvent evt) {
calc.clearCurrentDisplayNumber();
redisplay();
}
private void MCActionPerformed(java.awt.event.ActionEvent evt) {
calc.memoryClearValue();
redisplay();
}
private void MSActionPerformed(java.awt.event.ActionEvent evt) {
calc.memoryStoreValue(calc.getDisplayValue());
redisplay();
}
private void MRActionPerformed(java.awt.event.ActionEvent evt) {
calc.memoryRecallValue();
redisplay();
}
private void MplusActionPerformed(java.awt.event.ActionEvent evt) {
calc.addToMemory(calc.getDisplayValue());
}
Le funzioni di base della calcolatrice di Windows
What the Buttons Do!

Backspace --> Removes the last digit of the displayed number.

CE --> Clears the number displayed at that time.

C --> Clears the entire calculation.

MC --> Clears the numbers in the memory.

MR --> Recalls a number from the memory.

MS --> Stores numbers in the memory.

M+ --> Adds the displayed number to the memory.

M- --> Subtracts the displayed number from the memory.

Sqrt --> This calculates the square root of the number on the screen.
 1/x --> This calculates the reciprocal of the displayed number.
Per il calcolo percentuale: osservare che la percentuale è rispetto all’espressione già inserita. Ad esempio:
50 + 5%  50 + 2,5
50 *5%  50 * 2,5
Per il calcolo della radice si può usare l’elevamento a potenza usando come esponente (0.5).
Il grafico di funzione
Ci sono diversi metodi per effettuare il grafico di una funzione di una variabile.
Metodo diretto
Si utilizzano le classi Polygon, Graphics, etc. di Java come nel seguente esempio:
http://stackoverflow.com/questions/19914476/plot-the-sine-and-cosine-functions
import javax.swing.*;
import java.awt.*;
public class Exercise extends JFrame {
public Exercise() {
setLayout(new BorderLayout());
add(new DrawSine(), BorderLayout.CENTER);
}
public static void main(String[] args) {
Exercise frame = new Exercise();
frame.setSize(400, 300);
frame.setTitle("Exercise");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
class DrawSine extends JPanel {
double f(double x) {
return Math.sin(x);
}
double gCos(double y) {
return Math.cos(y);
}
protected void paintComponent(Graphics g)
{
super.paintComponent(g);
g.drawLine(10, 100, 380, 100);
g.drawLine(200, 30, 200, 190);
g.drawLine(380,
g.drawLine(380,
g.drawLine(200,
g.drawLine(200,
100, 370, 90);
100, 370, 110);
30, 190, 40);
30, 210, 40);
g.drawString("X", 360, 80);
g.drawString("Y", 220, 40);
Polygon p = new Polygon();
Polygon p2 = new Polygon();
for (int x = -170; x <= 170; x++) {
p.addPoint(x + 200, 100 - (int) (50 * f((x / 100.0) * 2
* Math.PI)));
}
for (int x = -170; x <= 170; x++) {
p2.addPoint(x + 200, 100 - (int) (50 * gCos((x / 100.0) * 2
* Math.PI)));
}
g.setColor(Color.red);
g.drawPolyline(p.xpoints, p.ypoints, p.npoints);
g.drawString("-2\u03c0", 95, 115);
g.drawString("-\u03c0", 147, 115);
g.drawString("\u03c0", 253, 115);
g.drawString("2\u03c0", 305, 115);
g.drawString("0", 200, 115);
g.setColor(Color.blue);
g.drawPolyline(p2.xpoints, p2.ypoints, p2.npoints);
}
}
}
Utilizzo della classe Draw di Sedgewick e Wayne
Usando le classi e interfacce:
http://introcs.cs.princeton.edu/java/stdlib/Draw.java
http://introcs.cs.princeton.edu/java/stdlib/DrawListener.java
Si tratta di classi presenti nella libreria standard del corso universitario di introduzione alla programmazione
dell’università di Princeton:
http://introcs.cs.princeton.edu/java/stdlib/
http://introcs.cs.princeton.edu/java/15inout/
La libreria in formato jar può essere scaricata direttamente da qui:
http://introcs.cs.princeton.edu/java/stdlib/stdlib.jar
Tuttavia per quello che bisogna fare in questo progetto basta scaricare il codice della classe Draw.java e
dell’interfaccia DrawListener.java e fare qualche lieve adattamento/modifica:
Interfaccia DrawListener.java
Aggiungere il metodo:
/**
*
* @param x the x-coordinate of the mouse
* @param y the y-coordinate of the mouse
*/
void mouseMoved(double x, double y);
Classe Draw.java
Aggiungere il metodo
public void plot(double[] x, double[] y) {
int N = x.length;
GeneralPath path = new GeneralPath();
path.moveTo((float) scaleX(x[0]), (float) scaleY(y[0]));
for (int i = 0; i < N - 1; i++) {
path.lineTo((float) scaleX(x[i]), (float) scaleY(y[i]));
}
offscreen.draw(path);
draw();
}
Ridefinire il metodo mouseMoved come di seguito riportato a titolo esemplificativo:
/**
* This method cannot be called directly.
*/
@Override
public void mouseMoved(MouseEvent e) {
synchronized (mouseLock) {
mouseX = userX(e.getX());
mouseY = userY(e.getY());
for (DrawListener listener : listeners) {
listener.mouseMoved(userX(e.getX()), userY(e.getY()));
}
}
}
Aggiungere la nested static class (dentro la classe Draw):
private static class DrawListenerImpl implements DrawListener {
private final Draw draw;
private final double[] x;
private final double[] y;
public DrawListenerImpl(Draw draw, double[] x, double[] y) {
this.draw = draw;
this.x = x;
this.y = y;
}
@Override
public void mousePressed(double x, double y) {
System.out.println("mouse pressed x= " + x + " y= " + y);
draw.clear();
double deltaX = draw.xmax - draw.xmin;
double deltaY = draw.ymax - draw.ymin;
draw.setXscale(x - deltaX / 4, x + deltaX / 4);
draw.setYscale(y - deltaY / 4, y + deltaY / 4);
draw.plot(this.x, this.y);
}
@Override
public void mouseDragged(double x, double y) {
System.out.println("mouse dragged x= " + x + " y= " + y);
}
@Override
public void mouseReleased(double x, double y) {
}
@Override
public void keyTyped(char c) {
}
@Override
public void keyPressed(int keycode) {
}
@Override
public void keyReleased(int keycode) {
}
@Override
public void mouseMoved(double x, double y) {
System.out.println("mouse moved x= " + x + " y= " + y);
}
}
Riscrivere il main come nel seguente esempio per testare le funzionalità della classe Draw:
public static void main(String[] args) {
// number of line segments to plot
int N = Integer.parseInt("20000");//args[0]
// the function y = sin(4x) + sin(20x), sampled at N points
// between x = 0 and x = pi
double[] x2 = new double[N + 1];
double[] y2 = new double[N + 1];
for (int i = 0; i <= N; i++) {
x2[i] = Math.PI * i / N;
y2[i] = Math.sin(4 * x2[i]) + Math.sin(2 * x2[i]);
}
// rescale the coordinate system
Draw draw3 = new Draw("Test client 3");
draw3.setXscale(0, Math.PI);
draw3.setYscale(-2.0, +2.0);
draw3.plot(x2, y2);
/*
// plot the approximation to the function
for (int i = 0; i < N; i++) {
draw.line(x[i], y[i], x[i+1], y[i+1]);
}
*/
draw3.addListener(new DrawListenerImpl(draw3, x2, y2));
}
Gestione dei tasti della tastiera
Per associare i tasti della tastiera (inclusi i tasti del numpad e le combinazioni di tasti come SHIFT+altro
tasto…)
Links di riferimento
http://stackoverflow.com/questions/14318191/shift-enter-enter-in-the-same-component-in-java
Dont use KeyListener/KeyAdapter with Swing. Always use KeyBindings
https://tips4java.wordpress.com/2008/10/10/key-bindings/
http://docs.oracle.com/javase/tutorial/uiswing/misc/keybinding.html
https://docs.oracle.com/javase/7/docs/api/javax/swing/KeyStroke.html
http://stackoverflow.com/questions/16069502/how-to-define-the-keystroke-for-the-numpad-enter-key
http://docs.oracle.com/javase/7/docs/api/java/awt/event/KeyEvent.html
http://stackoverflow.com/questions/2419608/java-swing-keystrokes-how-to-make-ctrl-modifier-work
https://docs.oracle.com/javase/7/docs/api/java/awt/KeyEventPostProcessor.html
http://docs.oracle.com/javase/7/docs/api/java/awt/KeyboardFocusManager.html
http://docs.oracle.com/javase/7/docs/api/java/awt/KeyEventDispatcher.html
http://www.simplesoft.it/java/tips/implementare-un-keylistener-in-java-swing.html
http://stackoverflow.com/questions/100123/application-wide-keyboard-shortcut-java-swing#100754
http://stackoverflow.com/questions/286727/java-keylistener-for-jframe-is-being-unresponsive
Esempio applicativo
import
import
import
import
import
import
java.awt.event.ActionEvent;
java.awt.event.InputEvent;
java.awt.event.KeyEvent;
javax.swing.AbstractAction;
javax.swing.JComponent;
javax.swing.KeyStroke;
/**
*
* @author Prof.G
*/
public class UserInterface extends javax.swing.JFrame {
private static final long serialVersionUID = 1L;
private final CalcEngine calc;
public UserInterface(CalcEngine calc) {
this.calc = calc;
initComponents();
notificationText.setVisible(false);
//GESTISCE I KEYSTROKE
//moltiplicazione
//KeyStroke.getKeyStroke(KeyEvent.VK_PLUS, InputEvent.SHIFT_DOWN_MASK) -> SHIFT + (+)
this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_PLUS,
InputEvent.SHIFT_DOWN_MASK, false), "moltiplica");
//KeyStroke.getKeyStroke(KeyEvent.VK_MULTIPLY, 0) --> * on numpad
this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_MULTIPLY
, 0, false), "moltiplica");
this.getRootPane().getActionMap().put("moltiplica", new AbstractAction()
{ //multiplication by SHIFT and + on the keyboard
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
calc.multiply();
redisplay();
}
});
//somma
//KeyStroke.getKeyStroke(KeyEvent.VK_PLUS,0, true) --> + on keyboard
//KeyStroke.getKeyStroke(KeyEvent.VK_ADD,0, true) --> + on numpad of
keyboard
this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_PLUS,0,
false), "somma");
this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ADD,0,
false), "somma");
this.getRootPane().getActionMap().put("somma", new AbstractAction() {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
calc.plus();
redisplay();
}
});
//differenza
//KeyStroke.getKeyStroke(KeyEvent.VK_MINUS,0) --> - on keyboard
//KeyStroke.getKeyStroke(KeyEvent.VK_SUBTRACT,0) --> - on numpad of
keyboard
this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_MINUS,0,
false), "differenza");
this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_SUBTRACT
,0, false), "differenza");
this.getRootPane().getActionMap().put("differenza", new AbstractAction()
{
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
calc.minus();
redisplay();
}
});
//divisione
//KeyStroke.getKeyStroke(KeyEvent.VK_7,InputEvent.SHIFT_DOWN_MASK) --> /
on keyboard
//KeyStroke.getKeyStroke(KeyEvent.VK_DIVIDE,0) --> / on numpad of
keyboard
this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_7,InputE
vent.SHIFT_DOWN_MASK, false), "divisione");
this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_DIVIDE,0
, false), "divisione");
this.getRootPane().getActionMap().put("divisione", new AbstractAction()
{
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
calc.divide();
redisplay();
}
});
//uguale - invio
this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER,0,
false), "uguale");
this.getRootPane().getActionMap().put("uguale", new AbstractAction() {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
calc.equals();
redisplay();
}
});
//inserimento numeri
//
this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_0,0,
false), "zero");
this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD0,
0, false), "zero");
this.getRootPane().getActionMap().put("zero", new AbstractAction() {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
setCifraAndRedisplay(0);
}
});
this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_1,0,
false), "uno");
this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD1,
0, false), "uno");
this.getRootPane().getActionMap().put("uno", new AbstractAction() {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
setCifraAndRedisplay(1);
}
});
this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_2,0,
false), "due");
this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD2,
0, false), "due");
this.getRootPane().getActionMap().put("due", new AbstractAction() {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
setCifraAndRedisplay(2);
}
});
this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_3,0,
false), "tre");
this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD3,
0, false), "tre");
this.getRootPane().getActionMap().put("tre", new AbstractAction() {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
setCifraAndRedisplay(3);
}
});
this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_4,0,
false), "quattro");
this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD4,
0, false), "quattro");
this.getRootPane().getActionMap().put("quattro", new AbstractAction() {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
setCifraAndRedisplay(4);
}
});
this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_5,0,
false), "cinque");
this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD5,
0, false), "cinque");
this.getRootPane().getActionMap().put("cinque", new AbstractAction() {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
setCifraAndRedisplay(5);
}
});
this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_6,0,
false), "sei");
this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD6,
0, false), "sei");
this.getRootPane().getActionMap().put("sei", new AbstractAction() {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
setCifraAndRedisplay(6);
}
});
this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_7,0,
false), "sette");
this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD7,
0, false), "sette");
this.getRootPane().getActionMap().put("sette", new AbstractAction() {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
setCifraAndRedisplay(7);
}
});
this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_8,0,
false), "otto");
this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD8,
0, false), "otto");
this.getRootPane().getActionMap().put("otto", new AbstractAction() {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
setCifraAndRedisplay(8);
}
});
this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_9,0,
false), "nove");
this.getRootPane().getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_NUMPAD9,
0, false), "nove");
this.getRootPane().getActionMap().put("nove", new AbstractAction() {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
setCifraAndRedisplay(9);
}
});
}
/**
* Update the interface display to show the current value of the calculator.
*/
private void redisplay() {
//check if the "M" on the dispaly has to be shown
if (calc.getMemoryValue() != 0) {//if a value diffeerent than 0 is in
memory
notificationText.setVisible(true);
} else {
notificationText.setVisible(false);
}
display.setText(calc.getDisplayValueString());
}
/**
* intercetta l'evento corrispondente alla pressione di un tasto
* corrispondente a un numero; il numero è passato all'oggetto CalcEngine.
* prerequisito: il tasto ha un nome corrispondente al numero, ad esempio
* "2" per il tasto corrispondente a 2.
*
* @param evt evento corrispondente alla pressione del numero
*/
private void setNumber(java.awt.event.ActionEvent evt) {
String command = evt.getActionCommand();
int number = Integer.parseInt(command);
calc.numberPressed(number);
}
private void setCifraAndRedisplay(int numero) {
calc.numberPressed(numero);
redisplay();
}
@SuppressWarnings("unchecked")
// <editor-fold defaultstate="collapsed" desc="Generated Code">
private void initComponents() { // da fare}
private void zeroActionPerformed(java.awt.event.ActionEvent evt) {
/*
setNumber(evt);
redisplay();
*/
setCifraAndRedisplay(0);
}
private void oneActionPerformed(java.awt.event.ActionEvent evt) {
/*
setNumber(evt);
redisplay();
*/
setCifraAndRedisplay(1);
}
private void twoActionPerformed(java.awt.event.ActionEvent evt) {
/*
setNumber(evt);
redisplay();
*/
setCifraAndRedisplay(2);
}
//e altri metodi da implementare
}
Valutazione
Livello sufficiente-discreto (voti da 6 a 7): calcolatrice standard di Windows con interfaccia grafica, usando il
parser della classe ReversePolishNotation e l’algoritmo di valutazione di RPNCalc, senza l’uso della classe
BigDecimal.
Livello buono (voti da 7 a 8): uso del parser https://github.com/uklimaschewski/EvalEx, uso della classe
BigDecimal, e doppia modalità della calcolatrice (base e scientifica). Come nella calcolatrice di Windows
bisogna avere un pulsante per mostrare lo storico dei calcoli effettuati. Tale storico potrà essere scartato
oppure salvato su file.
Livello eccellente-ottimo (voti da 8 al 10):
1. Tutto quanto previsto dal livello eccellente con in aggiunta la possibilità di eseguire il grafico di una
funzione di una variabile di cui si inserisce in input l’espressione.
2. Il grafico di funzione dovrà essere interattivo (click sinistro si fa uno zoom in avanti, SHIFT+ click
sinistro si fa uno zoom indietro).
3. Il pannello che mostra il grafico di funzione dovrà permettere di definire anche il numero di campioni
della funzione (con valore di default se non impostato dall’utente).
4. La calcolatrice dovrà avere la possibilità di associare almeno 5 funzioni, programmabili da utente, ad
altrettanti pulsati f0, f1,…,f4.
5. La calcolatrice dovrà avere la possibilità di mostrare, con colori diversi, almeno 5 grafici di funzione
differenti sullo stesso pannello, con legenda dei colori (ad es. f0 rosso, f1 blu, etc.). Inoltre l’utente
dovrà avere la possibilità di deselezionare un grafico, ad esempio, mediante una checkbox posta
sopra il pannello del grafico.
6. Sugli assi cartesiani dovrà essere riportata una scala graduata con i valori numerici.
7. La scala utilizzata userà impostazioni di default (calcolo automatico della scala), ma l’utente dovrà
avere, sul frame che contiene il pannello che rappresenta il grafico, la possibilità di impostare
manualmente i valori massimi e minimi degli assi cartesiani.
8. L’utente dovrà avere la possibilità di salvare il grafico di funzione.
9. L’utente dovrà avere la possibilità di esportare i valori della funzione (x e f(x)) in un file csv.
10. Quando l’utente porta il mouse sul grafico di una funzione viene visualizzato un tooltip che riporta il
valore della funzione e della coordinata x.
11. Il grafico di funzione deve escludere le singolarità: i punti per i quali il calcolo con i double fornirebbe
NaN, POSITIVE_INFINITY, NEGATIVE_INFINITY oppure con i BigDecimal fornirebbe
NumberFormatException
http://stackoverflow.com/questions/10080084/how-to-convert-double-positive-infinity-tobigdecimal
Suggerimento implementare una versione di plot che esclude dal path i punti di singolarità.