Java (25) - Kalkulačka

V předchozích dvou dílech jsme si ukázali základy tvorby grafického rozhraní pomocí frameworku Swing. Uvedené příklady však měly k použitelné aplikaci velmi daleko. Dnes si proto ukážeme první skutečnou aplikaci – kalkulačku.

Předem varuji, že je tento díl poněkud rozsáhlejší a náročnější – tak jako všechny skutečné aplikace. Na druhou stranu ale věřím, že o to více čtenáři přinese, jelikož si v něm ukážeme použití mnoha konstrukcí napříč téměř všemi minulými díly.

Požadavky

Abychom si ujasnili, co přesně bude výstupem, tak začneme netradičně – ukážeme si budoucí výsledek našeho snažení.

Kalkulačka
Kalkulačka

Bude se jednat o jednoduchou kalkulačku ve stylu MS Windows Calculator, která bude podporovat operace sčítání, odčítání, násobení a dělení v plovoucí řádové čárce a infixové notaci. Kalkulačka bude mít operace reset a rovná se. Grafické rozraní půjde ovládat jak pomocí myši, tak pomocí klávesnice.

Architektura

Struktura projektu
Struktura projektu

Nyní, když již víme, co budeme programovat, tak si musíme rozmyslet, jakým způsobem budeme aplikaci vnitřně členit – tj. musíme si rozmyslet její architekturu (kterou posléze rozvedeme do balíčků a konkrétních tříd – vizte hierarchie tříd výsledného projektu v rámečku vpravo).

Aplikace se bude skládat ze tří hlavních částí:

Model: Model reprezentuje stav aplikace – data, která kalkulačka potřebuje pro svoji práci. V našem případě se bude jednat o jednu třídu, která bude držet mezivýsledek operací, aktuálně používaný operátor (podporujeme infixovou notaci) a stav displaye kalkulačky (tj. uživatelský vstup).

Action: Druhou částí aplikace je její vlastní chování, respektive reakce na stisknutí jednotlivých tlačítek. V tomto případě si již nevystačíme pouze s jednou třídou, jelikož uživatelských akcí je mnoho typů – stisk tlačítka číslice pouze přidá příslušnou hodnotu na konec vstupu, binární operátory ovšem vykonávají zcela rozdílné akce (sčítání, odčítání...) a každému z nich proto musí odpovídat separátní akce (třída). To samé pak platí pro tlačítko resetu a rovnosti.

View: Poslední částí je view (pohled). View obsahuje veškeré třídy, které se týkají samotného uživatelského rozhraní (vytvářejí okno, layout atp.). Kdybychom chtěli být zcela korektní, tak bychom pod view zahrnuli i akce, jelikož se v našem podání bude jednat pouze o operace nad uživatelským rozhraním (třídy akcí nebudou zcela nezávislé na technologii rozhraní).

Proč aplikaci členit zrovna tímto způsobem?

Při objektovém návrhu je vhodné se držet tří základních principů: single responsibility, high cohesion, low coupling.

Single responsibility

Princip jedné odpovědnosti (single responsibility principle) nám říká, že každá třída by měla vykonávat jednu jasně definovanou odpovědnost. Pokud takto definované třídy zakryjeme rozhraním, tak je v případě potřeby snadno vyměníme v okamžiku, kdy se ukáže, že je daná funkcionalita implementována nevhodně nebo nedostatečně.

High cohesion a low coupling

High cohesion (vysoká soudržnost) a low coupling (nízká provázanost) jsou dva principy, které nás nutí vytvářet software modulárně. Náš kód je vysoce soudržný, pokud se každý z modulů (třída, balíček, fakticky jakákoliv logická část) zabývá jasně vymezenou množinou odpovědností. High cohesion můžeme chápat jako určité zobecnění principu jedné odpovědnosti.

Low coupling je princip, který vysokou soudržnost doplňuje – říká totiž, že moduly musí mít mezi sebou pouze minimum vazeb (abychom je mohli pohodlně zaměňovat). Nemůžeme proto v rámci principu vysoké soudržnosti vytvářet jenom třídy o jedné metodě s jedním řádkem kódu, protože by takto vytvořené třídy mezi sebou měly obrovské množství vazeb (a porušovaly by proto low coupling).

Implementace

Model

Model (paměť kalkulačky) bude reprezentován třídou CalculatorModel s atributy storedResult (mezivýsledek), currentInput (uživatelský vstup), decimalPointPresent (indikace přítomnosti desetinné čárky), operator (aktivní operátor) a defaultOperator (výchozí operátor).

Přítomnost řádové čárky bychom samozřejmě mohli také vždy znovu zjistit průchodem přes řetězec vstupu, ale to by bylo zbytečné mrhání procesorovým časem, zvlášť když tuto informaci dostáváme při stisku příslušného tlačítka kalkulačky.

Důvod, proč potřebujeme dva uložené operátory vychází z toho, jak budeme implementovat vnitřní logiku kalkulačky. V okamžiku, kdy uživatel zadá další operátor tj. 4 \\cdot 3-, tak se provede výpočet předchozích operace a na displayi se mu zobrazí aktuální mezivýsledek (12). Otázkou ovšem zůstává, jak se má kalkulačka zachovat při zadání prvního plus. Odpovědí je výchozí operátor, což v našem případě bude plus, které v kombinaci s uloženým mezivýsledkem (na začátku 0.0) zajistí korektní chování kalkulačky (tj. 0+4\\cdot3-).

 /**
  * Model (data) kalkulacky
  * @author Pavel Micka
  */
 public class CalculatorModel {
 
     private double storedResult;
     private String currentInput;
     private boolean decimalPointPresent;
     private OperatorActionIface operator;
     private OperatorActionIface defaultOperator;
 
     public CalculatorModel() {
         resetModel();
     }
 
     public final void resetModel() {
         currentInput = "";
         storedResult = 0;
         decimalPointPresent = false;
         operator = defaultOperator;
     }
 
     /**
      * @return the storedResult
      */
     public double getStoredResult() {
         return storedResult;
     }
 
     /**
      * @param storedResult the storedResult to set
      */
     public void setStoredResult(double storedResult) {
         this.storedResult = storedResult;
     }
 
     /**
      * @return the currentInput
      */
     public String getCurrentInput() {
         return currentInput;
     }
 
     /**
      * @param currentInput the currentInput to set
      */
     public void setCurrentInput(String currentInput) {
         this.currentInput = currentInput;
     }
 
     /**
      * @return the decimalPointPresent
      */
     public boolean isDecimalPointPresent() {
         return decimalPointPresent;
     }
 
     /**
      * @param decimalPointPresent the decimalPointPresent to set
      */
     public void setDecimalPointPresent(boolean decimalPointPresent) {
         this.decimalPointPresent = decimalPointPresent;
     }
 
     /**
      * @return the operator
      */
     public OperatorActionIface getOperator() {
         return operator;
     }
 
     /**
      * @param operator the operator to set
      */
     public void setOperator(OperatorActionIface operator) {
         this.operator = operator;
     }
 
     /**
      * Nastavi vychozi operator pro tento model. Pokud model dosud nema
      * nastaven zadny aktivni operator, tak bude nastaven tento. Stejne
      * tak v pripade resetu modelu dojde k prednastaveni tohoto operatoru.
      * @param defaultOperator the defaultOperator to set
      */
     public void setDefaultOperator(OperatorActionIface defaultOperator) {
         this.defaultOperator = defaultOperator;
         if (this.operator == null) {
             operator = defaultOperator;
         }
     }
 }
 

Kromě getterů a setterů obsahuje model operaci reset(), která jej vynuluje (vyčistí vstup, mezivýsledek, přítomnost desetinné čárky a nastaví výchozí operátor). Setter výchozího operátoru také nastaví aktivní operátor, není-li již nastaven.

Akce

CalculatorActionIface

Po kliku na libovolné tlačítko kalkulačky musí dojít k nějaké reakci. Specifikujme proto rozhraní, které zaváže implementující třídu tuto reakci poskytovat.

 /**
  * Interface specifikuje akci provedenou po stlaceni tlacitka
  * @author Pavel Micka
  */
 public interface CalculatorActionIface {
     /**
      * Provede akci zpusobenou stlacenim tlacitka
      */
     public void performInputAction();
 }
 

AbstractCalculatorAction

Všechny akce kalkulačky budou nějakým způsobem interagovat s modelem a s displayem kalkukačky (který bude reprezentovám grafickou komponentou JTextField). Abstraktní třída AbstractCalculatorAction nám proto bude sloužit jako společný předek všech akcí.

 /**
  * Spolecny predek pro akce na kalkulacce
  * @author Pavel Micka
  */
 public abstract class AbstractCalculatorAction implements CalculatorActionIface{
 
     protected JTextField resultField;
     protected CalculatorModel model;
 
     
     public AbstractCalculatorAction(JTextField resultField, CalculatorModel model) {
         this.resultField = resultField;
         this.model = model;
     }
 
     @Override
     public abstract void performInputAction();
 
     /**
      * @param resultField the resultField to set
      */
     public void setResultField(JTextField resultField) {
         this.resultField = resultField;
     }
 
     /**
      * @param model the model to set
      */
     public void setModel(CalculatorModel model) {
         this.model = model;
     }
 
     /**
      * @return the resultField
      */
     public JTextField getResultField() {
         return resultField;
     }
 
     /**
      * @return the model
      */
     public CalculatorModel getModel() {
         return model;
     }
 }
 

Všimněme si, že k oběma polím budeme zvenčí přistupovat skrze gettery a settery, avšak podtřídy (akce samotné) k nim mohou přistupovat přímo díky modifikátoru přístupu protected. Jelikož je třída abstraktní, tak můžeme dále metodu performInputAction() ponechat nadále neimplementovanou (budou ji implementovat specifičtí potomci).

DecimalPointAction

 /**
  * Akce provedena po stisku tlacitka desetinne carky
  * @author Pavel Micka
  */
 public class DecimalPointAction extends AbstractCalculatorAction {
    
     public DecimalPointAction(JTextField resultField, CalculatorModel model) {
         super(resultField, model);
     }
 
     @Override
     public void performInputAction() {
         if (!model.isDecimalPointPresent()) {
             getModel().setDecimalPointPresent(true);
             if (getModel().getCurrentInput().length() > 0) {
                 getModel().setCurrentInput(getModel().getCurrentInput() + ".");
             } else {
                 getModel().setCurrentInput("0.");
             }
             getResultField().setText(getModel().getCurrentInput());
         }
     }
 }
 

Po stisku tlačítka desetinné čárky nejprve zkontrolujeme, že čárka již není přítomna. Pokud není, tak v modelu nastavíme příznak její přítomnosti (pokud je čárka přítomna, tak metoda terminuje). Pokud je uživatelský vstup neprázdný, tak tečku přidáme na jeho konec, v opačném případě před ni předřadíme nulu. Na závěr zaktualizujeme stav displaye kalkukačky.

NumberAction

Akce po stisku čísla je ještě jednodušší – pouze toto číslo přidáme na konec vstupu a zaktualizujeme display.

 /**
  * Akce provedena po stisku tlacitka hodnoty (cisla)
  * @author Pavel Micka
  */
 public class NumberAction extends AbstractCalculatorAction {
 
     private String value;
 
     public NumberAction(JTextField resultField, CalculatorModel model, String value) {
         super(resultField, model);
         this.value = value;
     }
 
     @Override
     public void performInputAction() {
         getModel().setCurrentInput(getModel().getCurrentInput() + value);
         getResultField().setText(getModel().getCurrentInput());
     }
 }
 

ResetAction

Akce resetu kalkulačky vynuluje model a nastaví display na výchozí hodnotu.

 /**
  * Akce resetovaciho tlacitka
  * @author Pavel Micka
  */
 public class ResetAction extends AbstractCalculatorAction {
 
     public ResetAction(JTextField resultField, CalculatorModel model) {
         super(resultField, model);
     }
 
     @Override
     public void performInputAction() {
         getModel().resetModel();
         getResultField().setText(String.valueOf(getModel().getStoredResult()));
     }
     
 }
 

ResultAction

Po stisku tlačítka rovnosti se nejprve vykoná poslední uložený operátor a poté se na displayi zobrazí výsledek.

 /**
  * Akce tlacitka rovnost
  * @author Pavel Micka
  */
 public class ResultAction extends AbstractCalculatorAction{
 
     public ResultAction(JTextField resultField, CalculatorModel model) {
         super(resultField, model);
     }
 
     @Override
     public void performInputAction() {
         getModel().getOperator().performOperatorAction();
         getResultField().setText(String.valueOf(getModel().getStoredResult()));
     }
     
 }
 

OperatorActionIface

Operátory (sčítání, odčítání, násobení, dělení) jako takové reagují jak na stisk odpovídajícího tlačítka (uloží se do paměti jakožto aktivní operátory), tak dále musí poskytovat metodu, která vykoná samotnou operaci v okamžiku, kdy jsou již známy oba operandy. Tuto metodu definuje rozhraní OperatorActionIface.

 /**
  * Interface specifikujici akci operatoru
  * @author Pavel Micka
  */
 public interface OperatorActionIface {
     /**
      * Provede akci operatoru
      */
     public void performOperatorAction();
 }
 

AbstractOperatorAction

Analogicky s tlačítky vstupu vytvoříme i tlačítkům operátorů abstraktního předka, který bude implementovat obecnou funkcionalitu. Její první část můžeme přímo oddědit z třídy AbstractCalculatorAction (abstraktní předek všech tlačítek). Dále pak implementujeme rozhraní OperatorActionIface.

 /**
  * Abstraktni predek akci operatoru
  * @author Pavel Micka
  */
 public abstract class AbstractOperatorAction extends AbstractCalculatorAction implements OperatorActionIface {
 
     public AbstractOperatorAction(JTextField resultField, CalculatorModel model) {
         super(resultField, model);
     }
 
     @Override
     public void performInputAction() {
         getModel().getOperator().performOperatorAction();
         getModel().setOperator(this);
     }
 
     @Override
     public void performOperatorAction() {
         if (getModel().getCurrentInput().length() > 0) {
             performActualOperatorAction();
             getModel().setCurrentInput("");
             getResultField().setText(String.valueOf(getModel().getStoredResult()));
         }
     }
 
     /**
      * Vykona samotnou akci operatoru
      */
     abstract protected void performActualOperatorAction();
 }
 

Kód metody performInputAction() není příliš zajímavý – pouze vykonáme operaci předchozího operátoru a uložíme tento operátor do modelu.

Metoda performOperatorAction() ale obsahuje jeden zajímavý trik. Všechny akce operátorů musí nejprve zkontrolovat, jestli mají k dispozici druhý operand (pokud ne, tak není co počítat). Pokud jej mají, tak mohou vykonat daný výpočet/akci. Po provedení akce vyčistí uživatelský vstup a zobrazí na displayi výsledek. Abychom nemuseli tento kód stále opakovat ve všech specifických potomcích, tak vytvoříme novou abstraktní metodu performActualOperatorAction(), jejímž prostřednictvím budou potomci implementovat specifické chování daného operátoru. Tuto metodu opatříme modifikátorem přístupu protected. Tímto pro vnější svět vytvoříme iluzi, že veškerou práci dělá metoda specifikovaná v rozhraní performOperatorAction(), která však slouží pouze jako šablona (tzv. template method).

PlusAction, MinusAction, MultiplicationAction, DivisionAction

Samotné operace sčítání, odčítání, násobení a dělení jsou již nyní naprosto triviální.

 /**
  * Operace scitani
  * @author Pavel Micka
  */
 public class PlusAction extends AbstractOperatorAction {
 
     public PlusAction(JTextField resultField, CalculatorModel model) {
         super(resultField, model);
     }
 
     @Override
     public void performActualOperatorAction() {
         getModel().setStoredResult(getModel().getStoredResult() + Double.valueOf(getModel().getCurrentInput()));
     }
     
 }
 
 /**
  * Operace odecitani
  * @author Pavel Micka
  */
 public class MinusAction extends AbstractOperatorAction {
 
     public MinusAction(JTextField resultField, CalculatorModel model) {
         super(resultField, model);
     }
 
     @Override
     public void performActualOperatorAction() {
         getModel().setStoredResult(getModel().getStoredResult() - Double.valueOf(getModel().getCurrentInput()));
     }
 }
 
 /**
  * Operace nasobeni
  * @author Pavel Micka
  */
 public class MultiplicationAction extends AbstractOperatorAction{
 
     public MultiplicationAction(JTextField resultField, CalculatorModel model) {
         super(resultField, model);
     }
 
 
     @Override
     public void performActualOperatorAction() {
         getModel().setStoredResult(getModel().getStoredResult() * Double.valueOf(getModel().getCurrentInput()));
     }
     
 }
 
 /**
  * Operace deleni
  * @author Pavel Micka
  */
 public class DivisionAction extends AbstractOperatorAction{
     
     public DivisionAction(JTextField resultField, CalculatorModel model) {
         super(resultField, model);
     }
 
     @Override
     protected void performActualOperatorAction() {
         getModel().setStoredResult(getModel().getStoredResult() / Double.valueOf(getModel().getCurrentInput()));
     }
     
 }
 
UML diagram tříd akcí – přerušovaná šipka: realizace rozhraní, plná šipka: generalizace (dědičnost)
UML diagram tříd akcí – přerušovaná šipka: realizace rozhraní, plná šipka: generalizace (dědičnost)

View

Poslední částí naší aplikace je náhled (view), který vytvoříme pomocí frameworku Swing. Ještě než začneme se samotnou tvorbou rozhraní, tak si napíšeme dvě pomocné třídy – první z nich bude třída tlačítka, které může být ovládáno také pomocí klávesnice, druhou pak potomek třídy GridBagConstraints, který nám nabídne dodatečné konstruktory, jež nám v další fázi ušetří hodně psaní.

JCalculatorButton

 /**
  * Trida reprezentujici tlacitko kalkulacky
  * @author Pavel Micka
  */
 public class JCalculatorButton extends JButton {
 
     public JCalculatorButton(String text, int key, final CalculatorActionIface action) {
         super(text);
         ActionListener al = new ActionListener() {
 
             @Override
             public void actionPerformed(ActionEvent e) {
                 action.performInputAction();
             }
         };
         this.addActionListener(al);
         this.registerKeyboardAction(al, KeyStroke.getKeyStroke(key, 0), JComponent.WHEN_IN_FOCUSED_WINDOW);
     }
 }
 

Tlačítko kalkulačky má pouze vlastní konstruktor, který přijme popis tlačítka, kód asociovaného tlačítka klávesnice a samozřejmě akci samotnou. Text nejprve předáme konstruktoru předka, poté vytvoříme vnitřní třídu listeneru tlačítka, která na při jeho stisku zavolá námi definovanou akci a nakonec pomocí volání registerKeyboardAction() akci přiřadíme taktéž ke stisku příslušné klávesy v aktivním okně kalkulačky (bez jakékoliv masky (např. alt nebo shift)).

ButtonGridBagConstraints

Druhá pomocná třída ButtonGridBagConstraints jak již bylo předesláno pouze nabízí dodatečné konstruktory, jež budeme v aplikaci využívat – třída GridbagConstraints obsahuje pouze jeden všemocný konstruktor, který bychom museli stále dokola vyplňovat téměř totožnými hodnotami.

 /**
  * Trida pro zjednoduseni konstrukce GridBagConstraints
  * @author Pavel Micka
  */
 public class ButtonGridBagConstraints extends GridBagConstraints {
 
     /**
      * Vychozi sirka vnitrnich okraju
      */
     public static final int DEFAULT_INSETS = 5;
 
     /**
      * Zkonstruuje GridBagConstraints pro umisteni komponenty na danych souradnicich
      * s vychozimi okraji (na vsechny strany DEFAULT_INSETS) a sirkou a vyskou
      * pres jednu bunku. Anchor na stredu, roztazeni do obou stran, vaha 1.
      * @param x souradnice x
      * @param y souradnice y
      */
     public ButtonGridBagConstraints(int x, int y) {
         this(x, y, 1, 1);
     }
 
     /**
      * Zkonstruuje GridBagConstraints pro umisteni komponenty na danych souradnicich,
      * sirkou a vyskou pres jednu bunku. Anchor na stredu, roztazeni do obou stran, vaha 1.
      * @param x souradnice x
      * @param y souradnice y
      * @param insets vnitrni okraje
      */
     public ButtonGridBagConstraints(int x, int y, Insets insets) {
         this(x, y, 1, 1, insets);
     }
 
     /**
      * Zkonstruuje GridBagConstraints pro umisteni komponenty na danych souradnicich
      * s vychozimi okraji (na vsechny strany DEFAULT_INSETS). Anchor na 
      * stredu, roztazeni do obou stran, vaha 1.
      * @param x souradnice x
      * @param y souradnice y
      * @param gridWidth pocet bunek, ktere na sirku zabere dana komponenta
      * @param gridHeight pocet bunek, ktere na vysku zabere dana komponenta
      */
     public ButtonGridBagConstraints(int x, int y, int gridWidth, int gridHeight) {
         this(x, y, gridWidth, gridHeight, new Insets(DEFAULT_INSETS, DEFAULT_INSETS, DEFAULT_INSETS, DEFAULT_INSETS));
     }
 
     /**
      * Zkonstruuje GridBagConstraints pro umisteni komponenty na danych souradnicich.
      * Anchor na stredu, roztazeni do obou stran, vaha 1.
      * @param x souradnice x
      * @param y souradnice y
      * @param gridWidth pocet bunek, ktere na sirku zabere dana komponenta
      * @param gridHeight pocet bunek, ktere na vysku zabere dana komponenta
      * @param insets vnitrni okraje
      */
     public ButtonGridBagConstraints(int x, int y, int gridWidth, int gridHeight, Insets insets) {
         super(x, y, gridWidth, gridHeight, 1d, 1d, GridBagConstraints.CENTER, GridBagConstraints.BOTH, insets, 0, 0);
     }
 }
 

Konstruktor předka voláme pouze v jednom nejobecnějším konstruktoru, čímž opět docílíme toho, že se případné změny dotknou pouze minima příkazů.

JCalculatorFrame

Nyní již máme vše připraveno ke konstrukci samotného grafického rozhraní.

 /**
  * Graficke rozhrani kalkukacky
  * @author Pavel Micka
  */
 public class JCalculatorFrame extends JFrame {
 
     private CalculatorModel model;
 
     public JCalculatorFrame() {
         this.model = new CalculatorModel();
         createAndShowGUI();
     }
 
     private void createAndShowGUI() {
         this.setTitle("Calculator");
         this.setLayout(new BorderLayout());
         this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
         try {
             //ikonka (freeware) byla ziskana zde: http://www.iconarchive.com/show/mobile-icons-by-webiconset/calculator-icon.html
             this.setIconImage(ImageIO.read(this.getClass().getResource("calculator-icon.png")));
         } catch (IOException ex) {
             //nepovedlo se, nebudeme z toho delat vedu :-), uzivatel proste neuvidi ikonku, jenom vypiseme stacktrace do konzole
             ex.printStackTrace();
         }
 
         createMenu();
         createCalculator();
 
         adjustAndDisplayWindow();
     }
 
     /**
      * Zarovna okno a zobrazi jej uzivateli
      * @param frame hlavni ramec aplikace 
      */
     private void adjustAndDisplayWindow() {
         this.setResizable(false);
         this.pack(); //Nechame spocitat (a zmenit) velikost okna dle obsazenych komponent
 
         Toolkit t = Toolkit.getDefaultToolkit(); //z tridy toolkit muzeme ziskat mnoho uzitecnych informaci o displayi
         //okno zarovname na prostredek displaye
         this.setLocation(t.getScreenSize().width / 2 - this.getWidth() / 2, t.getScreenSize().height / 2 - this.getHeight() / 2);
 
         this.setVisible(true); //a nakonec okno ukazeme uzivateli
     }
 
     /**
      * Vytvori menu aplikace
      * @param frame hlavni ramec aplikace
      */
     private void createMenu() {
         JMenuBar menuBar = new JMenuBar(); //prouzek menu
         JMenu fileMenu = new JMenu("File"); //jednotliva nabidka
         JMenuItem exit = new JMenuItem("Exit"); //polozka nabidky
 
         //Hot-key alt+q zpusobi zavreni aplikace (stisk menuitem exit)
         exit.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Q, ActionEvent.ALT_MASK));
 
         exit.addActionListener(new ActionListener() {
 
             @Override
             public void actionPerformed(ActionEvent e) {
                 JCalculatorFrame.this.dispose(); //zrus okno, rozpustenim vsech oken muze dojit k ukonceni aplikace
             }
         });
 
         fileMenu.add(exit); //polozku vlozime do nabidky
         menuBar.add(fileMenu); //nabidku do prouzku        
         this.add(menuBar, BorderLayout.NORTH); //prouzek na sever hlavniho okna (ramce)
     }
 
     /**
      * Vytvori vlastni kalkulacku
      * @param frame hlavni ramec aplikace
      */
     private void createCalculator() {
         JPanel panel = new JPanel();
         GridBagLayout layout = new GridBagLayout();
         panel.setLayout(layout);
 
         JTextField resultField = new JTextField(String.valueOf(model.getStoredResult())); //pole pro vysledek
         resultField.setHorizontalAlignment(JTextField.TRAILING); //vysledek bude zarovnan na pravou stranu
         resultField.setEditable(false); //nenechame do nej uzivatele sahat
         resultField.setBackground(Color.WHITE); //editable false zabarvi pozadi do sede barvy
         panel.add(resultField, new GridBagConstraints(0, 0, 5, 1, 1, 1, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(5, 5, 10, 5), 0, 0));
         model.setDefaultOperator(new PlusAction(resultField, model));
 
         final int inset = ButtonGridBagConstraints.DEFAULT_INSETS;
         final int bigInset = inset * 3;
 
         panel.add(new JCalculatorButton("7", KeyEvent.VK_NUMPAD7, new NumberAction(resultField, model, "7")), new ButtonGridBagConstraints(0, 1));
         panel.add(new JCalculatorButton("8", KeyEvent.VK_NUMPAD8, new NumberAction(resultField, model, "8")), new ButtonGridBagConstraints(1, 1));
         panel.add(new JCalculatorButton("9", KeyEvent.VK_NUMPAD9, new NumberAction(resultField, model, "9")), new ButtonGridBagConstraints(2, 1));
         panel.add(new JCalculatorButton("4", KeyEvent.VK_NUMPAD4, new NumberAction(resultField, model, "4")), new ButtonGridBagConstraints(0, 2));
         panel.add(new JCalculatorButton("5", KeyEvent.VK_NUMPAD5, new NumberAction(resultField, model, "5")), new ButtonGridBagConstraints(1, 2));
         panel.add(new JCalculatorButton("6", KeyEvent.VK_NUMPAD6, new NumberAction(resultField, model, "6")), new ButtonGridBagConstraints(2, 2));
         panel.add(new JCalculatorButton("1", KeyEvent.VK_NUMPAD1, new NumberAction(resultField, model, "1")), new ButtonGridBagConstraints(0, 3));
         panel.add(new JCalculatorButton("2", KeyEvent.VK_NUMPAD2, new NumberAction(resultField, model, "2")), new ButtonGridBagConstraints(1, 3));
         panel.add(new JCalculatorButton("3", KeyEvent.VK_NUMPAD3, new NumberAction(resultField, model, "3")), new ButtonGridBagConstraints(2, 3));
         panel.add(new JCalculatorButton("0", KeyEvent.VK_NUMPAD0, new NumberAction(resultField, model, "0")), new ButtonGridBagConstraints(0, 4, 2, 1));
         panel.add(new JCalculatorButton(".", KeyEvent.VK_DECIMAL, new DecimalPointAction(resultField, model)), new ButtonGridBagConstraints(2, 4));
 
 
         panel.add(new JCalculatorButton("+", KeyEvent.VK_ADD, new PlusAction(resultField, model)), new ButtonGridBagConstraints(3, 1, new Insets(inset, bigInset, inset, inset)));
         panel.add(new JCalculatorButton("-", KeyEvent.VK_SUBTRACT, new MinusAction(resultField, model)), new ButtonGridBagConstraints(4, 1));
         panel.add(new JCalculatorButton("\\u00D7", KeyEvent.VK_MULTIPLY, new MultiplicationAction(resultField, model)), new ButtonGridBagConstraints(3, 2, new Insets(inset, bigInset, inset, inset))); //\\u00D7 je UTF8 kod pro znamenko multiplikace
         panel.add(new JCalculatorButton("/", KeyEvent.VK_DIVIDE, new DivisionAction(resultField, model)), new ButtonGridBagConstraints(4, 2));
 
         panel.add(new JCalculatorButton("AC", KeyEvent.VK_DELETE, new ResetAction(resultField, model)), new ButtonGridBagConstraints(3, 3, 1, 2, new Insets(inset, bigInset, inset, inset)));
         panel.add(new JCalculatorButton("=", KeyEvent.VK_ENTER, new ResultAction(resultField, model)), new ButtonGridBagConstraints(4, 3, 1, 2));
         this.add(panel, BorderLayout.CENTER);
 
     }
 }
 

Třídu rámce (okna) oddědíme od třídy JFrame. V jejím konstruktoru pak nejprve vytvoříme model kalkulačky a poté zavoláme metodu createAndShowGUI(), v níž vyskládáme celé rozhraní.

Metoda createAndShowGUI()

V metodě createAndShowGUI() musíme kromě vytváření samotného rozhraní (createMenu() a createCalculator()) také udělat několik povinných operací. Nejprve okno pojmenujeme, aby uživatel viděl, kterou aplikaci používá a onu samotnému nastavíme správce rozmístění – BorderLayout. Dále nastavíme, že po stisknutí zavíracího tlačítka (obvykle křížek v pravém horním rohu) dojde k vypnutí příslušné JVM a tím pádem i k terminaci programu.

Poté programu přiřadíme i ikonku – což zajistíme voláním setIconImage() jež přijímá vstupní proud (více v 18. dílu tohoto seriálu). Pro vytvoření proudu zavoláme metodu read() pomocné třídy ImageIO, která obsahuje mnoho užitečných metod pro manipulaci s obrázky. Zde použijeme menší trik a načteme obrázek, který je umístěn na classpath ve stejném balíčku jako je námi vytvářená třída JCalculatorFrame. V obecném případě nelze doporučit umísťování obrázků a obecně jakýchkoliv zdrojů na classpath, ale pokud se jedná pouze o jeden (málo) zdrojů, tak to může zjednodušit distribuci programu (můžeme jej distribuovat v binární podobě jako jeden soubor).

Poslední operací v rámci metody createAndShowGUI() je volání adjustAndDisplayWindow(), které zajistí správné umístění okna a jeho zobrazení uživateli.

Metoda adjustAndDisplayWindow()

Metoda adjustAndDisplayWindow() nejprve nastaví oknu neměnnou velikost (zvětšování kalkulačky nedává příliš velkou logiku a bylo by i poměrně ošklivé). Poté nechá systém přepočítat a nastavit velikost obsažených komponent voláním pack(). Při ponechání výchozího nastavení by se okno zobrazilo v levém horním rohu, což je z uživatelského hlediska nešikovné. Proto za pomoci třídy Toolkit, jež poskytuje informace o displayi, okno přemístíme na prostředek obrazovky (střed na střed). Nyní již nám zbývá pouze okno zobrazit.

Metoda createMenu()

Vraťme se nyní zhruba doprostřed metody createAndShowGUI(), zde voláme dvě metody, které vytvářejí hlavní část grafického rozhraní. První z nich – metoda createMenu() – jak již název napovídá, konstruuje menu aplikace. Ve Swingu menu vytváříme pomocí tří tříd – třídy proužku menu (JMenuBar), třídy nabídky (JMenu) a konečně třídy položky (JMenuItem). V našem případě máme poměrně snadnou pozici, protože menu bude mít jedinou položku Exit, která bude sloužit k opuštění aplikace.

Položce Exit proto přiřadíme ActionListener, který při svém vykonání zničí okno kalkulačky, čímž programu umožní terminovat (program terminuje v okamžiku, kdy již neobsahuje žádné běžící vlákno (jež není daemon) ani okno). Jelikož chceme, aby aplikace šla zavřít i notoricky známou zkratkou alt+Q, tak přiřadíme položce i klávesovou zkratku (accelerator).

Nakonec do sebe celé menu řádně zanoříme a umístíme jej do severní části okna (vzpomeňme si, že jsme oknu nastavili BorderLayout v metodě createAndShowGUI()).

Metoda createCalculator()

Nyní se konečně dostáváme k metodě createCalculator(), která kreslí jádro naší aplikace – kalkukačku samotnou.

Nejprve vytvoříme JPanel, do nějž budeme komponenty umísťovat. Tomuto panelu přiřadíme GridBagLayout jako správce rozmístění, jenž je pro daný účel nejvhodnější – tlačítka tvoří mřížku a některá z nich zabírají více řádků, display naopak zabírá pouze jeden řádek, ale jde přes všechny sloupce.

Display kalkulačky bude reprezentován textovým polem (JTextField), jež nebude editovatelné, bude mít bílé pozadí a text v něm bude zarovnán na jeho pravou stranu. Po přidání komponenty na správné místo mřížky můžeme konečně vytvořit výchozí operátor, který umístíme do modelu (doposud nám k jeho vytvoření chyběla právě komponenta reprezentující display).

Rozmístění samotných tlačítek je již pouze otrocká práce, kdy pro každé z nich musíme vytvořit novou instanci JCalculatorButton, jíž při konstrukci předáme popisek, kód asociovaného tlačítka klávesnice a akci, jež se po stisku daného tlačítka vykoná. Toto tlačítko pak umístíme do mřížky za využití námi vytvořené třídy ButtonGridBagConstraints. Po vytvoření a přidání všech tlačítek umístíme panel kalkulačky na střed BoderLayoutu okna aplikace.

Calculator

Poslední částí dnešního příkladu je vstupní bod aplikace – třída Calculator. Tento kód jsme již v rámci tohoto seriálu několikrát v různých obměnách viděli a vždy dělal to samé – asynchronně inicializoval grafické rozhraní aplikace.

 /**
  * Java pro zacatecniky 25 - kalkulacka
  * @author Pavel Micka
  */
 public class Calculator {
 
     /**
      * @param args the command line arguments
      */
     public static void main(String[] args) {
         javax.swing.SwingUtilities.invokeLater(new Runnable() {
 
             @Override
             public void run() {
                 new JCalculatorFrame(); //referenci drzi interne JVM
             }
         });
     }
 }
 

V kódu si pouze všimněme toho, že instanci aplikace (JCalculatorFrame) nemusíme ukládat do jakékoliv proměnné, jelikož si reference na okna drží implicitně JVM.

Kód ke stažení a spouštění aplikace

NetBeans projekt obsahující veškeré kódy dnešního dílu si můžete stáhnout zde. Pouze zde upozorním na adresář dist, ve kterém NetBeans při kompilaci projektu vytvářejí distribuční balíček (vždy s příponou *.jarJava Archive). Pokud projektu nastavíme ve vlastnostech projektu, záložce Run vstupní bod (Main class, zde: net.algoritmy.jpz25.Calculator) a máme *.jar soubory asociovány s Java Virtual Machine, tak danou aplikaci můžeme spustit v grafickém prostředí operačního systému pomocí dvojkliku.








Doporučujeme

Internet pro vaši firmu na míru