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í.
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
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
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. , tak se provede výpočet předchozích operace a na displayi se mu zobrazí aktuální mezivýsledek (
). 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
) zajistí korektní chování kalkulačky (tj.
).
01.
/**
02.
* Model (data) kalkulacky
03.
* @author Pavel Micka
04.
*/
05.
public
class
CalculatorModel {
06.
07.
private
double
storedResult;
08.
private
String currentInput;
09.
private
boolean
decimalPointPresent;
10.
private
OperatorActionIface operator;
11.
private
OperatorActionIface defaultOperator;
12.
13.
public
CalculatorModel() {
14.
resetModel();
15.
}
16.
17.
public
final
void
resetModel() {
18.
currentInput =
""
;
19.
storedResult =
0
;
20.
decimalPointPresent =
false
;
21.
operator = defaultOperator;
22.
}
23.
24.
/**
25.
* @return the storedResult
26.
*/
27.
public
double
getStoredResult() {
28.
return
storedResult;
29.
}
30.
31.
/**
32.
* @param storedResult the storedResult to set
33.
*/
34.
public
void
setStoredResult(
double
storedResult) {
35.
this
.storedResult = storedResult;
36.
}
37.
38.
/**
39.
* @return the currentInput
40.
*/
41.
public
String getCurrentInput() {
42.
return
currentInput;
43.
}
44.
45.
/**
46.
* @param currentInput the currentInput to set
47.
*/
48.
public
void
setCurrentInput(String currentInput) {
49.
this
.currentInput = currentInput;
50.
}
51.
52.
/**
53.
* @return the decimalPointPresent
54.
*/
55.
public
boolean
isDecimalPointPresent() {
56.
return
decimalPointPresent;
57.
}
58.
59.
/**
60.
* @param decimalPointPresent the decimalPointPresent to set
61.
*/
62.
public
void
setDecimalPointPresent(
boolean
decimalPointPresent) {
63.
this
.decimalPointPresent = decimalPointPresent;
64.
}
65.
66.
/**
67.
* @return the operator
68.
*/
69.
public
OperatorActionIface getOperator() {
70.
return
operator;
71.
}
72.
73.
/**
74.
* @param operator the operator to set
75.
*/
76.
public
void
setOperator(OperatorActionIface operator) {
77.
this
.operator = operator;
78.
}
79.
80.
/**
81.
* Nastavi vychozi operator pro tento model. Pokud model dosud nema
82.
* nastaven zadny aktivni operator, tak bude nastaven tento. Stejne
83.
* tak v pripade resetu modelu dojde k prednastaveni tohoto operatoru.
84.
* @param defaultOperator the defaultOperator to set
85.
*/
86.
public
void
setDefaultOperator(OperatorActionIface defaultOperator) {
87.
this
.defaultOperator = defaultOperator;
88.
if
(
this
.operator ==
null
) {
89.
operator = defaultOperator;
90.
}
91.
}
92.
}
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.
01.
/**
02.
* Interface specifikuje akci provedenou po stlaceni tlacitka
03.
* @author Pavel Micka
04.
*/
05.
public
interface
CalculatorActionIface {
06.
/**
07.
* Provede akci zpusobenou stlacenim tlacitka
08.
*/
09.
public
void
performInputAction();
10.
}
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í.
01.
/**
02.
* Spolecny predek pro akce na kalkulacce
03.
* @author Pavel Micka
04.
*/
05.
public
abstract
class
AbstractCalculatorAction
implements
CalculatorActionIface{
06.
07.
protected
JTextField resultField;
08.
protected
CalculatorModel model;
09.
10.
11.
public
AbstractCalculatorAction(JTextField resultField, CalculatorModel model) {
12.
this
.resultField = resultField;
13.
this
.model = model;
14.
}
15.
16.
@Override
17.
public
abstract
void
performInputAction();
18.
19.
/**
20.
* @param resultField the resultField to set
21.
*/
22.
public
void
setResultField(JTextField resultField) {
23.
this
.resultField = resultField;
24.
}
25.
26.
/**
27.
* @param model the model to set
28.
*/
29.
public
void
setModel(CalculatorModel model) {
30.
this
.model = model;
31.
}
32.
33.
/**
34.
* @return the resultField
35.
*/
36.
public
JTextField getResultField() {
37.
return
resultField;
38.
}
39.
40.
/**
41.
* @return the model
42.
*/
43.
public
CalculatorModel getModel() {
44.
return
model;
45.
}
46.
}
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
01.
/**
02.
* Akce provedena po stisku tlacitka desetinne carky
03.
* @author Pavel Micka
04.
*/
05.
public
class
DecimalPointAction
extends
AbstractCalculatorAction {
06.
07.
public
DecimalPointAction(JTextField resultField, CalculatorModel model) {
08.
super
(resultField, model);
09.
}
10.
11.
@Override
12.
public
void
performInputAction() {
13.
if
(!model.isDecimalPointPresent()) {
14.
getModel().setDecimalPointPresent(
true
);
15.
if
(getModel().getCurrentInput().length() >
0
) {
16.
getModel().setCurrentInput(getModel().getCurrentInput() +
"."
);
17.
}
else
{
18.
getModel().setCurrentInput(
"0."
);
19.
}
20.
getResultField().setText(getModel().getCurrentInput());
21.
}
22.
}
23.
}
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.
01.
/**
02.
* Akce provedena po stisku tlacitka hodnoty (cisla)
03.
* @author Pavel Micka
04.
*/
05.
public
class
NumberAction
extends
AbstractCalculatorAction {
06.
07.
private
String value;
08.
09.
public
NumberAction(JTextField resultField, CalculatorModel model, String value) {
10.
super
(resultField, model);
11.
this
.value = value;
12.
}
13.
14.
@Override
15.
public
void
performInputAction() {
16.
getModel().setCurrentInput(getModel().getCurrentInput() + value);
17.
getResultField().setText(getModel().getCurrentInput());
18.
}
19.
}
ResetAction
Akce resetu kalkulačky vynuluje model a nastaví display na výchozí hodnotu.
01.
/**
02.
* Akce resetovaciho tlacitka
03.
* @author Pavel Micka
04.
*/
05.
public
class
ResetAction
extends
AbstractCalculatorAction {
06.
07.
public
ResetAction(JTextField resultField, CalculatorModel model) {
08.
super
(resultField, model);
09.
}
10.
11.
@Override
12.
public
void
performInputAction() {
13.
getModel().resetModel();
14.
getResultField().setText(String.valueOf(getModel().getStoredResult()));
15.
}
16.
17.
}
ResultAction
Po stisku tlačítka rovnosti se nejprve vykoná poslední uložený operátor a poté se na displayi zobrazí výsledek.
01.
/**
02.
* Akce tlacitka rovnost
03.
* @author Pavel Micka
04.
*/
05.
public
class
ResultAction
extends
AbstractCalculatorAction{
06.
07.
public
ResultAction(JTextField resultField, CalculatorModel model) {
08.
super
(resultField, model);
09.
}
10.
11.
@Override
12.
public
void
performInputAction() {
13.
getModel().getOperator().performOperatorAction();
14.
getResultField().setText(String.valueOf(getModel().getStoredResult()));
15.
}
16.
17.
}
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.
01.
/**
02.
* Interface specifikujici akci operatoru
03.
* @author Pavel Micka
04.
*/
05.
public
interface
OperatorActionIface {
06.
/**
07.
* Provede akci operatoru
08.
*/
09.
public
void
performOperatorAction();
10.
}
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.
01.
/**
02.
* Abstraktni predek akci operatoru
03.
* @author Pavel Micka
04.
*/
05.
public
abstract
class
AbstractOperatorAction
extends
AbstractCalculatorAction
implements
OperatorActionIface {
06.
07.
public
AbstractOperatorAction(JTextField resultField, CalculatorModel model) {
08.
super
(resultField, model);
09.
}
10.
11.
@Override
12.
public
void
performInputAction() {
13.
getModel().getOperator().performOperatorAction();
14.
getModel().setOperator(
this
);
15.
}
16.
17.
@Override
18.
public
void
performOperatorAction() {
19.
if
(getModel().getCurrentInput().length() >
0
) {
20.
performActualOperatorAction();
21.
getModel().setCurrentInput(
""
);
22.
getResultField().setText(String.valueOf(getModel().getStoredResult()));
23.
}
24.
}
25.
26.
/**
27.
* Vykona samotnou akci operatoru
28.
*/
29.
abstract
protected
void
performActualOperatorAction();
30.
}
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í.
01.
/**
02.
* Operace scitani
03.
* @author Pavel Micka
04.
*/
05.
public
class
PlusAction
extends
AbstractOperatorAction {
06.
07.
public
PlusAction(JTextField resultField, CalculatorModel model) {
08.
super
(resultField, model);
09.
}
10.
11.
@Override
12.
public
void
performActualOperatorAction() {
13.
getModel().setStoredResult(getModel().getStoredResult() + Double.valueOf(getModel().getCurrentInput()));
14.
}
15.
16.
}
01.
/**
02.
* Operace odecitani
03.
* @author Pavel Micka
04.
*/
05.
public
class
MinusAction
extends
AbstractOperatorAction {
06.
07.
public
MinusAction(JTextField resultField, CalculatorModel model) {
08.
super
(resultField, model);
09.
}
10.
11.
@Override
12.
public
void
performActualOperatorAction() {
13.
getModel().setStoredResult(getModel().getStoredResult() - Double.valueOf(getModel().getCurrentInput()));
14.
}
15.
}
01.
/**
02.
* Operace nasobeni
03.
* @author Pavel Micka
04.
*/
05.
public
class
MultiplicationAction
extends
AbstractOperatorAction{
06.
07.
public
MultiplicationAction(JTextField resultField, CalculatorModel model) {
08.
super
(resultField, model);
09.
}
10.
11.
12.
@Override
13.
public
void
performActualOperatorAction() {
14.
getModel().setStoredResult(getModel().getStoredResult() * Double.valueOf(getModel().getCurrentInput()));
15.
}
16.
17.
}
01.
/**
02.
* Operace deleni
03.
* @author Pavel Micka
04.
*/
05.
public
class
DivisionAction
extends
AbstractOperatorAction{
06.
07.
public
DivisionAction(JTextField resultField, CalculatorModel model) {
08.
super
(resultField, model);
09.
}
10.
11.
@Override
12.
protected
void
performActualOperatorAction() {
13.
getModel().setStoredResult(getModel().getStoredResult() / Double.valueOf(getModel().getCurrentInput()));
14.
}
15.
16.
}
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
01.
/**
02.
* Trida reprezentujici tlacitko kalkulacky
03.
* @author Pavel Micka
04.
*/
05.
public
class
JCalculatorButton
extends
JButton {
06.
07.
public
JCalculatorButton(String text,
int
key,
final
CalculatorActionIface action) {
08.
super
(text);
09.
ActionListener al =
new
ActionListener() {
10.
11.
@Override
12.
public
void
actionPerformed(ActionEvent e) {
13.
action.performInputAction();
14.
}
15.
};
16.
this
.addActionListener(al);
17.
this
.registerKeyboardAction(al, KeyStroke.getKeyStroke(key,
0
), JComponent.WHEN_IN_FOCUSED_WINDOW);
18.
}
19.
}
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.
01.
/**
02.
* Trida pro zjednoduseni konstrukce GridBagConstraints
03.
* @author Pavel Micka
04.
*/
05.
public
class
ButtonGridBagConstraints
extends
GridBagConstraints {
06.
07.
/**
08.
* Vychozi sirka vnitrnich okraju
09.
*/
10.
public
static
final
int
DEFAULT_INSETS =
5
;
11.
12.
/**
13.
* Zkonstruuje GridBagConstraints pro umisteni komponenty na danych souradnicich
14.
* s vychozimi okraji (na vsechny strany <span class="bold">DEFAULT_INSETS</span>) a sirkou a vyskou
15.
* pres jednu bunku. Anchor na stredu, roztazeni do obou stran, vaha 1.
16.
* @param x souradnice x
17.
* @param y souradnice y
18.
*/
19.
public
ButtonGridBagConstraints(
int
x,
int
y) {
20.
this
(x, y,
1
,
1
);
21.
}
22.
23.
/**
24.
* Zkonstruuje GridBagConstraints pro umisteni komponenty na danych souradnicich,
25.
* sirkou a vyskou pres jednu bunku. Anchor na stredu, roztazeni do obou stran, vaha 1.
26.
* @param x souradnice x
27.
* @param y souradnice y
28.
* @param insets vnitrni okraje
29.
*/
30.
public
ButtonGridBagConstraints(
int
x,
int
y, Insets insets) {
31.
this
(x, y,
1
,
1
, insets);
32.
}
33.
34.
/**
35.
* Zkonstruuje GridBagConstraints pro umisteni komponenty na danych souradnicich
36.
* s vychozimi okraji (na vsechny strany <span class="bold">DEFAULT_INSETS</span>). Anchor na
37.
* stredu, roztazeni do obou stran, vaha 1.
38.
* @param x souradnice x
39.
* @param y souradnice y
40.
* @param gridWidth pocet bunek, ktere na sirku zabere dana komponenta
41.
* @param gridHeight pocet bunek, ktere na vysku zabere dana komponenta
42.
*/
43.
public
ButtonGridBagConstraints(
int
x,
int
y,
int
gridWidth,
int
gridHeight) {
44.
this
(x, y, gridWidth, gridHeight,
new
Insets(DEFAULT_INSETS, DEFAULT_INSETS, DEFAULT_INSETS, DEFAULT_INSETS));
45.
}
46.
47.
/**
48.
* Zkonstruuje GridBagConstraints pro umisteni komponenty na danych souradnicich.
49.
* Anchor na stredu, roztazeni do obou stran, vaha 1.
50.
* @param x souradnice x
51.
* @param y souradnice y
52.
* @param gridWidth pocet bunek, ktere na sirku zabere dana komponenta
53.
* @param gridHeight pocet bunek, ktere na vysku zabere dana komponenta
54.
* @param insets vnitrni okraje
55.
*/
56.
public
ButtonGridBagConstraints(
int
x,
int
y,
int
gridWidth,
int
gridHeight, Insets insets) {
57.
super
(x, y, gridWidth, gridHeight, 1d, 1d, GridBagConstraints.CENTER, GridBagConstraints.BOTH, insets,
0
,
0
);
58.
}
59.
}
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í.
001.
/**
002.
* Graficke rozhrani kalkukacky
003.
* @author Pavel Micka
004.
*/
005.
public
class
JCalculatorFrame
extends
JFrame {
006.
007.
private
CalculatorModel model;
008.
009.
public
JCalculatorFrame() {
010.
this
.model =
new
CalculatorModel();
011.
createAndShowGUI();
012.
}
013.
014.
private
void
createAndShowGUI() {
015.
this
.setTitle(
"Calculator"
);
016.
this
.setLayout(
new
BorderLayout());
017.
this
.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
018.
try
{
019.
//ikonka (freeware) byla ziskana zde: http://www.iconarchive.com/show/mobile-icons-by-webiconset/calculator-icon.html
020.
this
.setIconImage(ImageIO.read(
this
.getClass().getResource(
"calculator-icon.png"
)));
021.
}
catch
(IOException ex) {
022.
//nepovedlo se, nebudeme z toho delat vedu :-), uzivatel proste neuvidi ikonku, jenom vypiseme stacktrace do konzole
023.
ex.printStackTrace();
024.
}
025.
026.
createMenu();
027.
createCalculator();
028.
029.
adjustAndDisplayWindow();
030.
}
031.
032.
/**
033.
* Zarovna okno a zobrazi jej uzivateli
034.
* @param frame hlavni ramec aplikace
035.
*/
036.
private
void
adjustAndDisplayWindow() {
037.
this
.setResizable(
false
);
038.
this
.pack();
//Nechame spocitat (a zmenit) velikost okna dle obsazenych komponent
039.
040.
Toolkit t = Toolkit.getDefaultToolkit();
//z tridy toolkit muzeme ziskat mnoho uzitecnych informaci o displayi
041.
//okno zarovname na prostredek displaye
042.
this
.setLocation(t.getScreenSize().width /
2
-
this
.getWidth() /
2
, t.getScreenSize().height /
2
-
this
.getHeight() /
2
);
043.
044.
this
.setVisible(
true
);
//a nakonec okno ukazeme uzivateli
045.
}
046.
047.
/**
048.
* Vytvori menu aplikace
049.
* @param frame hlavni ramec aplikace
050.
*/
051.
private
void
createMenu() {
052.
JMenuBar menuBar =
new
JMenuBar();
//prouzek menu
053.
JMenu fileMenu =
new
JMenu(
"File"
);
//jednotliva nabidka
054.
JMenuItem exit =
new
JMenuItem(
"Exit"
);
//polozka nabidky
055.
056.
//Hot-key alt+q zpusobi zavreni aplikace (stisk menuitem exit)
057.
exit.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Q, ActionEvent.ALT_MASK));
058.
059.
exit.addActionListener(
new
ActionListener() {
060.
061.
@Override
062.
public
void
actionPerformed(ActionEvent e) {
063.
JCalculatorFrame.
this
.dispose();
//zrus okno, rozpustenim vsech oken muze dojit k ukonceni aplikace
064.
}
065.
});
066.
067.
fileMenu.add(exit);
//polozku vlozime do nabidky
068.
menuBar.add(fileMenu);
//nabidku do prouzku
069.
this
.add(menuBar, BorderLayout.NORTH);
//prouzek na sever hlavniho okna (ramce)
070.
}
071.
072.
/**
073.
* Vytvori vlastni kalkulacku
074.
* @param frame hlavni ramec aplikace
075.
*/
076.
private
void
createCalculator() {
077.
JPanel panel =
new
JPanel();
078.
GridBagLayout layout =
new
GridBagLayout();
079.
panel.setLayout(layout);
080.
081.
JTextField resultField =
new
JTextField(String.valueOf(model.getStoredResult()));
//pole pro vysledek
082.
resultField.setHorizontalAlignment(JTextField.TRAILING);
//vysledek bude zarovnan na pravou stranu
083.
resultField.setEditable(
false
);
//nenechame do nej uzivatele sahat
084.
resultField.setBackground(Color.WHITE);
//editable false zabarvi pozadi do sede barvy
085.
panel.add(resultField,
new
GridBagConstraints(
0
,
0
,
5
,
1
,
1
,
1
, GridBagConstraints.CENTER, GridBagConstraints.BOTH,
new
Insets(
5
,
5
,
10
,
5
),
0
,
0
));
086.
model.setDefaultOperator(
new
PlusAction(resultField, model));
087.
088.
final
int
inset = ButtonGridBagConstraints.DEFAULT_INSETS;
089.
final
int
bigInset = inset *
3
;
090.
091.
panel.add(
new
JCalculatorButton(
"7"
, KeyEvent.VK_NUMPAD7,
new
NumberAction(resultField, model,
"7"
)),
new
ButtonGridBagConstraints(
0
,
1
));
092.
panel.add(
new
JCalculatorButton(
"8"
, KeyEvent.VK_NUMPAD8,
new
NumberAction(resultField, model,
"8"
)),
new
ButtonGridBagConstraints(
1
,
1
));
093.
panel.add(
new
JCalculatorButton(
"9"
, KeyEvent.VK_NUMPAD9,
new
NumberAction(resultField, model,
"9"
)),
new
ButtonGridBagConstraints(
2
,
1
));
094.
panel.add(
new
JCalculatorButton(
"4"
, KeyEvent.VK_NUMPAD4,
new
NumberAction(resultField, model,
"4"
)),
new
ButtonGridBagConstraints(
0
,
2
));
095.
panel.add(
new
JCalculatorButton(
"5"
, KeyEvent.VK_NUMPAD5,
new
NumberAction(resultField, model,
"5"
)),
new
ButtonGridBagConstraints(
1
,
2
));
096.
panel.add(
new
JCalculatorButton(
"6"
, KeyEvent.VK_NUMPAD6,
new
NumberAction(resultField, model,
"6"
)),
new
ButtonGridBagConstraints(
2
,
2
));
097.
panel.add(
new
JCalculatorButton(
"1"
, KeyEvent.VK_NUMPAD1,
new
NumberAction(resultField, model,
"1"
)),
new
ButtonGridBagConstraints(
0
,
3
));
098.
panel.add(
new
JCalculatorButton(
"2"
, KeyEvent.VK_NUMPAD2,
new
NumberAction(resultField, model,
"2"
)),
new
ButtonGridBagConstraints(
1
,
3
));
099.
panel.add(
new
JCalculatorButton(
"3"
, KeyEvent.VK_NUMPAD3,
new
NumberAction(resultField, model,
"3"
)),
new
ButtonGridBagConstraints(
2
,
3
));
100.
panel.add(
new
JCalculatorButton(
"0"
, KeyEvent.VK_NUMPAD0,
new
NumberAction(resultField, model,
"0"
)),
new
ButtonGridBagConstraints(
0
,
4
,
2
,
1
));
101.
panel.add(
new
JCalculatorButton(
"."
, KeyEvent.VK_DECIMAL,
new
DecimalPointAction(resultField, model)),
new
ButtonGridBagConstraints(
2
,
4
));
102.
103.
104.
panel.add(
new
JCalculatorButton(
"+"
, KeyEvent.VK_ADD,
new
PlusAction(resultField, model)),
new
ButtonGridBagConstraints(
3
,
1
,
new
Insets(inset, bigInset, inset, inset)));
105.
panel.add(
new
JCalculatorButton(
"-"
, KeyEvent.VK_SUBTRACT,
new
MinusAction(resultField, model)),
new
ButtonGridBagConstraints(
4
,
1
));
106.
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
107.
panel.add(
new
JCalculatorButton(
"/"
, KeyEvent.VK_DIVIDE,
new
DivisionAction(resultField, model)),
new
ButtonGridBagConstraints(
4
,
2
));
108.
109.
panel.add(
new
JCalculatorButton(
"AC"
, KeyEvent.VK_DELETE,
new
ResetAction(resultField, model)),
new
ButtonGridBagConstraints(
3
,
3
,
1
,
2
,
new
Insets(inset, bigInset, inset, inset)));
110.
panel.add(
new
JCalculatorButton(
"="
, KeyEvent.VK_ENTER,
new
ResultAction(resultField, model)),
new
ButtonGridBagConstraints(
4
,
3
,
1
,
2
));
111.
this
.add(panel, BorderLayout.CENTER);
112.
113.
}
114.
}
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.
01.
/**
02.
* Java pro zacatecniky 25 - kalkulacka
03.
* @author Pavel Micka
04.
*/
05.
public
class
Calculator {
06.
07.
/**
08.
* @param args the command line arguments
09.
*/
10.
public
static
void
main(String[] args) {
11.
javax.swing.SwingUtilities.invokeLater(
new
Runnable() {
12.
13.
@Override
14.
public
void
run() {
15.
new
JCalculatorFrame();
//referenci drzi interne JVM
16.
}
17.
});
18.
}
19.
}
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 *.jar – Java 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.