Prednáška č. 11
- Grafický návrh scény: jednoduchá kalkulačka
- Programovanie riadené udalosťami
- Časovač: pohybujúci sa kruh
- Odkazy
Grafický návrh scény: jednoduchá kalkulačka
Prístup ku grafickému návrhu aplikácií z minulej prednášky, v ktorom sme každému ovládaciemu prvku na scéne manuálne nastavovali jeho polohu a štýl, sa už pri o čo i len málo rozsiahlejších aplikáciách javí byť príliš prácnym a nemotorným. V nasledujúcom sa preto zameriame na alternatívny prístup založený predovšetkým na dvoch základných technikách:
- Namiesto koreňovej oblasti typu
Panebudeme používať jej „inteligentnejšie” podtriedy, ktoré presnú polohu ovládacích prvkov určujú automaticky na základe preferencií daných programátorom. - Formátovanie ovládacích prvkov (napríklad font textu, farba výplne, atď.) obvykle nebudeme nastavovať priamo zo zdrojového kódu, ale pomocou štýlov definovaných v externých JavaFX CSS súboroch. (Tie sa podobajú na klasické CSS používané pri návrhu webových stránok. Na zvládnutie tejto prednášky však nie je potrebná žiadna predošlá znalosť CSS; obmedzíme sa navyše len na naznačenie niektorých základných možností JavaFX CSS štýlov). Výhodou použitia externých CSS štýlov je aj možnosť meniť vzhľad aplikácie bez zásahov do jej zdrojového kódu.
Uvedené techniky demonštrujeme na ukážkovej aplikácii: (azda až
priveľmi) jednoduchej kalkulačke. Výsledný vzhľad tejto aplikácie je na
obrázku vpravo. Jej základná funkcionalita bude pozostávať z možnosti
zadať dve reálne čísla a zvoliť jednu zo štyroch operácií – sčítanie,
odčítanie, násobenie, prípadne delenie. Po stlačení tlačidla Počítaj!
sa zobrazí výsledok vybranej operácie na zadanej dvojici čísel. Okrem
toho aplikácia obsahuje tlačidlo na zmazanie všetkých vstupných údajov a
zobrazeného výsledku a tlačidlo na ukončenie aplikácie.
Základom pre túto aplikáciu bude podobná kostra programu ako na minulej prednáške – jedine rozmery scény už nebudeme nastavovať manuálne, pretože by to neskôr mohlo viesť k rozličným problémom.
import javafx.application.*;
import javafx.stage.*;
import javafx.scene.*;
import javafx.scene.layout.*;
import javafx.scene.control.*;
import javafx.event.*;
public class Calculator extends Application {
@Override
public void start(Stage primaryStage) {
Pane pane = new Pane();
Scene scene = new Scene(pane);
primaryStage.setScene(scene);
primaryStage.setTitle("Kalkulačka");
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Formátovanie pomocou JavaFX CSS štýlov (1. časť)
Predpokladajme, že potrebujeme vytvoriť aplikáciu s dostatočnou veľkosťou písma všetkých jej textových prvkov (napríklad 11 typografických bodov). Mohli by sme túto vlastnosť nastavovať manuálne pre všetky jednotlivé ovládacie prvky, podobne ako na minulej prednáške – takýto prístup však po čase omrzí. Podobne každá zmena požadovanej veľkosti písma (napríklad na 12 bodov) by v budúcnosti vyžadovala vynaloženie rovnakého úsilia. Vhodnejším prístupom je použitie JavaFX CSS štýlov definovaných v externom súbore.
Vytvorme textový súbor styles.css s nasledujúcim obsahom:
.root {
-fx-font-size: 11pt;
}
Následne si v prípade práce s IntelliJ vyberme jednu z nasledujúcich možností:
- Súbor uložme do adresára
<Koreňový adresár projektu>/src. To je najjednoduchšia možnosť, ktorá ale hlavne pri väčších projektoch môže zneprehľadniť súborovú štruktúru projektu. - V koreňovom adresári projektu – t. j. na rovnakej úrovni ako je
adresár
src– vytvorme nový adresár, ktorý môžeme nazvať napríkladresources. Následne v IntelliJ na tento adresár kliknime pravou myšou a označme ho prostredníctvom možnostiMark Directory as --> Resources Root. Súborstyles.cssuložme do tohto novovytvoreného adresára.
JavaFX CSS súbor s uvedeným obsahom hovorí, že východzia veľkosť písma
má byť 11 bodov. Zostáva tak súbor styles.css „aplikovať” na našu
scénu:
@Override
public void start(Stage primaryStage) {
// ...
scene.getStylesheets().add("styles.css");
// ...
}
Ako argument metódy add tu môžeme zadať aj cestu relatívnu od
niektorého z umiestnení opísaných vyššie, prípadne adresu URL.
Scéna a strom uzlov
Obsah JavaFX scény sa reprezentuje v podobe tzv. stromu uzlov alebo grafu uzlov. (Druhý termín je o niečo presnejší, pretože v skutočnosti môže ísť o niekoľko stromov tvoriacich les, pričom viditeľné sú uzly iba jedného z nich.)
- Prvky umiestňované na scénu sa nazývajú uzly – triedy
reprezentujúce tieto prvky majú ako spoločného predka triedu
Node. - Niektoré z uzlov môžu byť rodičmi iných uzlov na scéne – typickým
príkladom sú napríklad oblasti typu
Pane, s ktorými sme sa stretli už minule a ktoré sme využívali ako kontajnery pre ovládacie prvky umiestňované na scénu (tie sa tak stali deťmi danej oblasti). Všetky triedy reprezentujúce uzly, ktoré môžu byť rodičmi iných uzlov, sú potomkami triedyParent; tá je priamou podtriedou triedyNode. Medzi takéto triedy okremPanepatria aj triedy pre ovládacie prvky ako napríkladButtonaleboLabel. Nepatria medzi ne napríklad triedy pre geometrické útvary (triedaShapea jej potomkovia). - Pri vytváraní scény je ako argument konštruktora potrebné zadať
koreňový uzol stromu – tým môže byť inštancia ľubovoľnej triedy,
ktorá je potomkom triedy
Parent. Ďalšie „poschodia” stromu uzlov sa typicky pridávajú podobne ako na minulej prednáške, napríklad s využitím metódygetChildren().addpre jednotlivé rodičovské uzly.
Rozloženie uzlov na scéne
Vráťme sa teraz k nášmu projektu jednoduchej kalkulačky. Naším
najbližším cieľom bude umiestnenie jednotlivých ovládacích prvkov na
scénu. Chceli by sme sa pritom vyhnúť manuálnemu nastavovaniu ich polôh;
namiesto oblasti typu Pane preto ako koreňový uzol použijeme oblasť,
ktorá sa bude o rozloženie ovládacích prvkov starať do veľkej miery
samostatne.
GridPane
Odmyslime si na chvíľu tlačidlá „Zmaž” a „Skonči” a umiestnime na
scénu zvyšné ovládacie prvky. Ako koreňový uzol použijeme namiesto
oblasti typu Pane oblasť typu
GridPane.
Trieda GridPane je jednou z podtried triedy Pane umožňujúcich
„inteligentné” spravovanie rozloženia uzlov.
@Override
public void start(Stage primaryStage) {
// ...
// Pane pane = new Pane();
GridPane grid = new GridPane();
// Scene scene = new Scene(pane);
Scene scene = new Scene(grid);
// ...
}
Oblasť typu GridPane umožňuje pridávanie ovládacích prvkov do
obdĺžnikovej mriežky. Pridajme teda prvý ovládací prvok – popisok
obsahujúci text „Zadajte vstupné hodnoty:”. Riadky aj stĺpce mriežky
sa pri GridPane indexujú počínajúc nulou; maximálny index je (takmer)
neobmedzený. Vytvorený textový popisok teda vložíme do políčka v nultom
stĺpci a v nultom riadku. Okrem toho povieme, že obsah vytvoreného
textového popisku môže prípadne zabrať až dva stĺpce mriežky, ale iba
jeden riadok.
Label lblHeader = new Label("Zadajte vstupné hodnoty:"); // Vytvorenie textoveho popisku
grid.getChildren().add(lblHeader); // Pridanie do stromu uzlov za syna oblasti grid
GridPane.setColumnIndex(lblHeader, 0); // Vytvoreny popisok bude v 0-tom stlpci
GridPane.setRowIndex(lblHeader, 0); // Vytvoreny popisok bude v 0-tom riadku
GridPane.setColumnSpan(lblHeader, 2); // Moze zabrat az 2 stlpce...
GridPane.setRowSpan(lblHeader, 1); // ... ale iba 1 riadok
Všimnime si, že pozíciu lblHeader v mriežke nastavujeme pomocou
statických metód triedy GridPane. Ak sa riadok resp. stĺpec
nenastavia ručne, použije sa východzia hodnota 0. Podobne sa pri
nenastavení zvyšných dvoch hodnôt použije východzia hodnota 1 (na čo
sa budeme často spoliehať).
Uvedený spôsob pridania ovládacieho prvku do mriežky je však pomerne
prácny – vyžaduje si až päť príkazov. Existuje preto skratka: všetkých
päť príkazov možno vykonať v rámci jediného volania metódy grid.add:
Label lblHeader = new Label("Zadajte vstupné hodnoty:");
grid.add(lblHeader, 0, 0, 2, 1);
// grid.getChildren().add(lblHeader);
// GridPane.setColumnIndex(lblHeader, 0);
// GridPane.setRowIndex(lblHeader, 0);
// GridPane.setColumnSpan(lblHeader, 2);
// GridPane.setRowSpan(lblHeader, 1);
(Bez explicitného uvedenia posledných dvoch parametrov metódy add by
sa použili ich východzie hodnoty 1, 1.)
Podobne môžeme do mriežky umiestniť aj ďalšie ovládacie prvky:
Label lblNumber1 = new Label("Prvý argument:");
grid.add(lblNumber1, 0, 1);
Label lblNumber2 = new Label("Druhý argument:");
grid.add(lblNumber2, 0, 2);
Label lblOperation = new Label("Operácia:");
grid.add(lblOperation, 0, 3);
Label lblResultText = new Label("Výsledok:");
grid.add(lblResultText, 0, 5);
Label lblResult = new Label("0");
grid.add(lblResult, 1, 5);
TextField tfNumber1 = new TextField();
grid.add(tfNumber1, 1, 1);
TextField tfNumber2 = new TextField();
grid.add(tfNumber2, 1, 2);
ComboBox<String> cbOperation = new ComboBox<>();
grid.add(cbOperation, 1, 3);
cbOperation.getItems().addAll("+", "-", "*", "/");
cbOperation.setValue("+");
Button btnOK = new Button("Počítaj!");
grid.add(btnOK, 1, 4);
Novým prvkom je tu „vyskakovací zoznam”
ComboBox<T>
prvkov typu T. Jeho metóda getItems vráti zoznam všetkých možností
na výber (ten je na začiatku prázdny), do ktorého následne vkladáme
možnosti zodpovedajúce jednotlivým operáciám. Metóda setValue nastaví
aktuálne zvolenú možnosť. (V takomto východzom stave nemožno do
ComboBox-u zadávať text manuálne; v prípade potreby je ale možné túto
možnosť aktivovať metódou setEditable s parametrom true.)
Pre účely ladenia ešte môžeme zviditeľniť deliace čiary mriežky nasledujúcim spôsobom:
grid.setGridLinesVisible(true);
Môžeme ďalej napríklad nastaviť preferované rozmery niektorých ovládacích prvkov (neskôr ale uvidíme lepší spôsob, ako to robiť):
tfNumber1.setPrefWidth(300);
tfNumber2.setPrefWidth(300);
cbOperation.setPrefWidth(300);
Tiež si môžeme všimnúť, že medzi jednotlivými políčkami mriežky nie sú žiadne medzery. To vyriešime napríklad nasledovne:
grid.setHgap(10); // Horizontalna medzera medzi dvoma polickami mriezky bude 10 pixelov
grid.setVgap(10); // To iste pre vertikalnu medzeru
Podobne nie je žiadna medzera medzi mriežkou a okrajmi okna. To možno vyriešiť pomocou nastavenia „okrajov”:
import javafx.geometry.*;
// ...
grid.setPadding(new Insets(10, 20, 10, 20)); // horny okraj 10 pixelov, pravy 20, dolny 10, lavy 20
Trieda
Insets
(skratka od angl. Inside Offsets) reprezentuje iba súbor štyroch
hodnôt pre „veľkosti okrajov” a je definovaná v balíku
javafx.geometry. Vzhľad aplikácie v tomto momente je na obrázku
vpravo.
Ďalej si môžeme všimnúť, že obsah mriežky zostáva aj pri zväčšovaní veľkosti okna v jeho ľavom hornom rohu. Zarovnanie obsahu mriežky na stred dostaneme volaním
grid.setAlignment(Pos.CENTER);
Pre oblasť grid je vyhradené prakticky celé okno; uvedeným volaním
hovoríme, že jej reálny obsah sa má zarovnať na stred tejto vyhradenej
oblasti.
Pomerne žiadúcim správaním aplikácie pri zväčšovaní šírky okna je súčasné rozširovanie textových polí a „vyskakovacieho zoznamu”. Zrušme najprv manuálne nastavenú preferovanú šírku uvedených ovládacích prvkov:
// tfNumber1.setPrefWidth(300);
// tfNumber2.setPrefWidth(300);
// cbOperation.setPrefWidth(300);
Cielený efekt dosiahneme naplnením zoznamu „obmedezní pre jednotlivé
stĺpce”, ktorý si každá oblasť typu GridPane udržiava. „Obmedzenia”
na nultý stĺpec nebudú žiadne; v „obmedzeniach” nasledujúceho stĺpca
nastavíme jeho preferovanú šírku na 300 pixelov a povieme tiež, aby sa
pri rozširovaní oblasti rozširoval aj daný stĺpec. „Obmedzenia” pre
jednotlivé stĺpce sú reprezentované triedou
ColumnConstraints.
(Analogicky je možné nastavovať „obmedzenia” aj pre jednotlivé riadky.)
ColumnConstraints cc = new ColumnConstraints();
cc.setPrefWidth(300);
cc.setHgrow(Priority.ALWAYS);
grid.getColumnConstraints().addAll(new ColumnConstraints(), cc);
Uvedený kód funguje až na jeden detail: pri rozširovaní okna sa nemení veľkosť „vyskakovacieho zoznamu”. To je dané tým, že jeho východzia maximálna veľkosť je totožná s jeho preferovanou veľkosťou; na dosiahnutie kýženého efektu je teda potrebné prestaviť túto maximálnu veľkosť tak, aby viac „neprekážala”:
cbOperation.setMaxWidth(Double.MAX_VALUE);
Nastavme ešte zarovnanie niektorých ovládacích prvkov na pravý okraj ich
políčka mriežky. Využijeme pritom statickú metódu setHalignment
triedy GridPane:
GridPane.setHalignment(lblNumber1, HPos.RIGHT);
GridPane.setHalignment(lblNumber2, HPos.RIGHT);
GridPane.setHalignment(lblOperation, HPos.RIGHT);
GridPane.setHalignment(lblResultText, HPos.RIGHT);
GridPane.setHalignment(lblResult, HPos.RIGHT);
GridPane.setHalignment(btnOK, HPos.RIGHT);
S návrhom rozloženia ovládacích prvkov v mriežke sme teraz hotoví a môžeme teda aj zrušiť zobrazovanie deliacich čiar:
// grid.setGridLinesVisible(true);
Momentálny vzhľad aplikácie je na obrázku vpravo.
BorderPane a VBox
Pridáme teraz tlačidlá „Zmaž” a „Skonči”. Mohli by sme ich
samozrejme umiestniť napríklad do ďalšieho stĺpca mriežky grid. Tu si
však ukážeme odlišný prístup – namiesto oblasti typu GridPane
použijeme ako koreňový uzol scény oblasť typu
BorderPane.
Tá sa ako koreňový uzol scény používa asi najčastejšie, pretože umožňuje
nastaviť päť základných častí scény: hornú, pravú, dolnú, ľavú a
stredovú časť.
Typicky každá z týchto častí (ak je definovaná) pozostáva z ďalšej
oblasti nejakého iného typu – v našom prípade za centrálnu časť zvolíme
už vytvorenú mriežku grid:
BorderPane border = new BorderPane();
border.setCenter(grid);
// Scene scene = new Scene(grid);
Scene scene = new Scene(border);
Zostáva si teraz vytvoriť „kontajnerovú” oblasť pre pravú časť a
umiestniť do nej spomínané dve tlačidlá. Keďže majú byť tieto tlačidlá
umiestnené nad sebou, pravdepodobne najlepšou voľbou ich „kontajnerovej”
oblasti je oblasť typu
VBox,
do ktorej sa jednotlivé uzly vkladajú vertikálne jeden pod druhý:
VBox right = new VBox(); // Vytvorenie oblasti typu VBox
right.setPadding(new Insets(10, 20, 10, 60)); // Nastavenie okrajov (v poradi horny, pravy, dolny, lavy)
right.setSpacing(10); // Vertikalne medzery medzi vkladanymi uzlami
right.setAlignment(Pos.BOTTOM_LEFT); // Zarovnanie obsahu oblasti vertikalne nadol a horizontalne dolava
border.setRight(right); // Nastavenie oblasti right ako pravej casti oblasti border
Vložíme teraz do oblasti right obidve tlačidlá:
Button btnClear = new Button("Zmaž");
right.getChildren().add(btnClear);
Button btnExit = new Button("Skonči");
right.getChildren().add(btnExit);
Vidíme
ale, že tlačidlá majú rôznu šírku, čo nevyzerá veľmi dobre. Rovnakú
šírku by sme samozrejme vedeli dosiahnuť manuálnym nastavením veľkosti
tlačidiel na nejakú fixnú hodnotu; to však nie je najideálnejší prístup.
Na dosiahnutie rovnakého efektu využijeme skutočnosť, že šírka oblasti
right sa automaticky nastaví na preferovanú šírku širšieho z oboch
tlačidiel. Užšie z tlačidiel ostáva menšie preto, lebo jeho východzia
maximálna šírka je rovná jeho preferovanej šírke. Po prestavení
maximálnej šírky na dostatočne veľkú hodnotu sa toto tlačidlo taktiež
roztiahne na celú šírku oblasti right:
btnClear.setMaxWidth(Double.MAX_VALUE);
btnExit.setMaxWidth(Double.MAX_VALUE);
Momentálny vzhľad aplikácie je na obrázku vpravo.
Ďalšie rozloženia
Okrem GridPane, BorderPane a VBox existuje v JavaFX aj niekoľko
ďalších oblastí umožňujúcich (polo)automaticky spravovať rozloženie
jednotlivých uzlov:
HBox: ide o horizontálnu obdobuVBox-u.StackPane: umiestňuje prvky na seba (dá sa použiť napríklad pri tvorbe grafických komponentov; môžeme dajme tomu jednoducho vytvoriť obdĺžnik obsahujúci nejaký text, atď.).FlowPane: umiestňuje prvky za seba po riadkoch, prípadne po stĺpcoch. Pri zmene rozmerov okna môže dôjsť k zmene pozície jednotlivých prvkov.TilePane: udržiava „dlaždice” rovnakej veľkosti.AnchorPane: umožňuje ukotvenie prvkov na danú pozíciu.
Kalkulačka: oživenie aplikácie
Pridajme teraz jednotlivým ovládacím prvkom aplikácie ich funkcionalitu
(vystačíme si pritom s metódami z minulej prednášky). Kľúčovou je pritom
funkcionalita tlačidla btnOK.
public class Calculator extends Application {
// ...
/**
* Metoda, ktora aplikuje na argumenty arg1, arg2 operaciu reprezentovanu retazcom op.
* @param operation Jeden z retazcov "+", "-", "*", "/" reprezentujucich vykonavanu operaciu.
* @param arg1 Prvy operand.
* @param arg2 Druhy operand.
* @return Vysledok operacie operation aplikovanej na operandy arg1, arg2.
*/
private double calculate(String operation, double arg1, double arg2) {
switch (operation) {
case "+":
return arg1 + arg2;
case "-":
return arg1 - arg2;
case "*":
return arg1 * arg2;
case "/":
return arg1 / arg2;
default:
throw new IllegalArgumentException();
}
}
// ...
@Override
public void start(Stage primaryStage) {
// ...
btnOK.setOnAction(event -> {
try {
lblResult.setText(Double.toString(calculate(
cbOperation.getValue(),
Double.parseDouble(tfNumber1.getText()),
Double.parseDouble(tfNumber2.getText()))));
} catch (NumberFormatException exception) {
lblResult.setText("Výnimka!");
}
});
// ...
}
}
Podobne môžeme pridať aj funkcionalitu zostávajúcich dvoch tlačidiel.
@Override
public void start(Stage primaryStage) {
// ...
btnClear.setOnAction(event -> {
tfNumber1.clear();
tfNumber2.clear();
cbOperation.setValue("+");
lblResult.setText("0");
});
btnExit.setOnAction(event -> {
Platform.exit();
});
// ...
}
Formátovanie pomocou JavaFX CSS štýlov (2. časť)
Finálny vzhľad aplikácie získame doplnením súboru styles.css. Môžeme
začať tým, že okrem východzej veľkosti fontu nastavíme aj východziu
skupinu fontov a textúru na pozadí aplikácie:
.root {
-fx-font-size: 11pt;
-fx-font-family: 'Tahoma';
-fx-background-image: url("texture.jpg");
-fx-background-size: cover;
}
Na internete je množstvo textúr dostupných pod licenciou Public Domain
(CC0) – to je aj prípad textúry z ukážky finálneho vzhľadu aplikácie z
úvodu tejto prednášky. Súbor s textúrou je potrebné uložiť do rovnakého
adresára ako súbor styles.css.
Možno tiež nastavovať formát jednotlivých skupín ovládacích prvkov. Nasledovne napríklad docielime, aby sa pri všetkých tlačidlách a textových popiskoch použilo tučné písmo; textové popisky navyše ofarbíme bielou farbou:
.label {
-fx-font-weight: bold;
-fx-text-fill: white;
}
.button {
-fx-font-weight: bold;
}
Formát ovládacích prvkov je možné nastavovať aj individuálne – v takom prípade ale musíme dotknutým prvkom v zdrojovom kóde aplikácie nastaviť ich identifikátor:
@Override
public void start(Stage primaryStage) {
// ...
lblHeader.setId("header");
lblResult.setId("result");
// ...
}
V JavaFX CSS súbore následne vieme prispôsobiť formát pomenovaných ovládacích prvkov:
#header {
-fx-font-size: 18pt;
}
#result {
-fx-font-size: 16pt;
-fx-text-fill: black;
}
Kalkulačka: kompletný kód aplikácie
Zdrojový kód aplikácie:
import javafx.application.*;
import javafx.stage.*;
import javafx.scene.*;
import javafx.scene.layout.*;
import javafx.scene.control.*;
import javafx.event.*;
import javafx.geometry.*;
public class Calculator extends Application {
/**
* Metoda, ktora aplikuje na argumenty arg1, arg2 operaciu reprezentovanu retazcom op.
* @param operation Jeden z retazcov "+", "-", "*", "/" reprezentujucich vykonavanu operaciu.
* @param arg1 Prvy operand.
* @param arg2 Druhy operand.
* @return Vysledok operacie operation aplikovanej na operandy arg1, arg2.
*/
public double calculate(String operation, double arg1, double arg2) {
switch (operation) {
case "+":
return arg1 + arg2;
case "-":
return arg1 - arg2;
case "*":
return arg1 * arg2;
case "/":
return arg1 / arg2;
default:
throw new IllegalArgumentException();
}
}
@Override
public void start(Stage primaryStage) {
GridPane grid = new GridPane();
Label lblHeader = new Label("Zadajte vstupné hodnoty:");
grid.add(lblHeader, 0, 0, 2, 1);
lblHeader.setId("header");
Label lblNumber1 = new Label("Prvý argument:");
grid.add(lblNumber1, 0, 1);
GridPane.setHalignment(lblNumber1, HPos.RIGHT);
Label lblNumber2 = new Label("Druhý argument:");
grid.add(lblNumber2, 0, 2);
GridPane.setHalignment(lblNumber2, HPos.RIGHT);
Label lblOperation = new Label("Operácia:");
grid.add(lblOperation, 0, 3);
GridPane.setHalignment(lblOperation, HPos.RIGHT);
Label lblResultText = new Label("Výsledok:");
grid.add(lblResultText, 0, 5);
GridPane.setHalignment(lblResultText, HPos.RIGHT);
Label lblResult = new Label("0");
grid.add(lblResult, 1, 5);
GridPane.setHalignment(lblResult, HPos.RIGHT);
lblResult.setId("result");
TextField tfNumber1 = new TextField();
grid.add(tfNumber1, 1, 1);
TextField tfNumber2 = new TextField();
grid.add(tfNumber2, 1, 2);
ComboBox<String> cbOperation = new ComboBox<>();
grid.add(cbOperation, 1, 3);
cbOperation.getItems().addAll("+", "-", "*", "/");
cbOperation.setValue("+");
cbOperation.setMaxWidth(Double.MAX_VALUE);
Button btnOK = new Button("Počítaj!");
grid.add(btnOK, 1, 4);
GridPane.setHalignment(btnOK, HPos.RIGHT);
ColumnConstraints cc = new ColumnConstraints();
cc.setPrefWidth(300);
cc.setHgrow(Priority.ALWAYS);
grid.getColumnConstraints().addAll(new ColumnConstraints(), cc);
grid.setHgap(10);
grid.setVgap(10);
grid.setPadding(new Insets(10, 20, 10, 20));
grid.setAlignment(Pos.CENTER);
VBox right = new VBox();
right.setPadding(new Insets(10, 20, 10, 60));
right.setSpacing(10);
right.setAlignment(Pos.BOTTOM_LEFT);
Button btnClear = new Button("Zmaž");
right.getChildren().add(btnClear);
btnClear.setMaxWidth(Double.MAX_VALUE);
Button btnExit = new Button("Skonči");
right.getChildren().add(btnExit);
btnExit.setMaxWidth(Double.MAX_VALUE);
BorderPane border = new BorderPane();
border.setCenter(grid);
border.setRight(right);
btnOK.setOnAction(event -> {
try {
lblResult.setText(Double.toString(calculate(
cbOperation.getValue(),
Double.parseDouble(tfNumber1.getText()),
Double.parseDouble(tfNumber2.getText()))));
} catch (NumberFormatException exception) {
lblResult.setText("Výnimka!");
}
});
btnClear.setOnAction(event -> {
tfNumber1.setText("");
tfNumber2.setText("");
cbOperation.setValue("+");
lblResult.setText("0");
});
btnExit.setOnAction(event -> {
Platform.exit();
});
Scene scene = new Scene(border);
scene.getStylesheets().add("styles.css");
primaryStage.setScene(scene);
primaryStage.setTitle("Kalkulačka");
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Súbor JavaFX CSS:
.root {
-fx-font-size: 11pt;
-fx-font-family: 'Tahoma';
-fx-background-image: url("texture.jpg");
-fx-background-size: cover;
}
.label {
-fx-font-weight: bold;
-fx-text-fill: white;
}
.button {
-fx-font-weight: bold;
}
#header {
-fx-font-size: 18pt;
}
#result {
-fx-font-size: 16pt;
-fx-text-fill: black;
}
Programovanie riadené udalosťami
Základné princípy programovania riadeného udalosťami
V súvislosti s JavaFX sme začali používať novú paradigmu: programovanie
riadené udalosťami. Namiesto sekvenčného vykonávania jednotlivých
príkazov sa tu s vykonávaním kódu čaká na udalosť zvonka, ktorou môže
byť napríklad stlačenie tlačidla používateľom. Tento spôsob
programovania má svoje špecifiká – s niektorými z nich sme sa už koniec
koncov stretli. Na lepšie ozrejmenie princípov programovania riadeného
udalosťami teraz na chvíľu odbočíme od programovania aplikácií s
grafickým používateľským rozhraním a demonštrujeme podstatu tejto
paradigmy na jednoduchej konzolovej aplikácii. Stále však budeme
využívať triedy pre udalosti definované v balíku javafx.event.
Pre zmysluplnú prácu s udalosťami potrebujeme minimálne tri triedy: aspoň jednu triedu pre samotnú udalosť, aspoň jednu triedu pre spracovávateľa udalostí a aspoň jednu triedu schopnú udalosti spúšťať (o spúšťanie udalostí v JavaFX sa zvyčajne stará prostredie).
Definujme teda najprv triedu MyEvent reprezentujúcu jednoduchú udalosť
obsahujúcu nejakú správu o sebe.
package events;
import javafx.event.*;
public class MyEvent extends Event { // Nasa trieda dedi od Event, ktora je najvyssou triedou pre udalosti v JavaFX
private static EventType myEventType = new EventType("MyEvent"); // Typ udalosti zodpovedajuci udalostiam MyEvent (pre nas nepodstatna technikalita)
private String message;
public MyEvent(Object source, String message) { // Konstruktor, ktory ma vytvorit udalost s danym odosielatelom source a spravou message
super(source, NULL_SOURCE_TARGET, myEventType); // Volanie konstruktora nadtriedy. Druhy a treti parameter su pre nase ucely nepodstatne
this.message = message; // Nastavime spravu zodpovedajucu nasej udalosti
}
public String getMessage() { // Metoda, ktora vrati spravu zodpovedajucu udalosti
return message;
}
}
Spracovávateľ JavaFX udalostí typu T sa vyznačuje tým, že implementuje
rozhranie
EventHandler<T>.
S týmto rozhraním sme sa stretli už minule a vieme, že vyžaduje
implementáciu jedinej metódy handle (čo okrem iného umožňuje nahradiť
inštancie takýchto tried lambda výrazmi). Vytvorme teda jednoduchú
triedu MyEventHandler pre spracovávateľa udalostí MyEvent.
package events;
import javafx.event.*;
public class MyEventHandler implements EventHandler<MyEvent> {
@Override
public void handle(MyEvent event) {
System.out.println("Spracuvam udalost: " + event.getMessage());
}
}
Potrebujeme ešte triedu MyEventSender, ktorá bude schopná udalosti
typu MyEvent vytvárať. Tá bude zo všetkých najkomplikovanejšia. Musí
totiž:
- Uchovávať zoznam
eventHandlersvšetkých spracúvateľov udalostí, ktoré čakajú na ňou generované udalosti (v JavaFX sme zatiaľ pracovali len so situáciou, keď na jednu udalosť čaká najviac jeden spracúvateľ; hoci je to najčastejší prípad, nebýva to vždy tak). - Poskytovať metódu
addEventHandlerpridávajúcu spracúvateľa udalosti. (Tá sa podobá napríklad na metódusetOnActioninštancie triedyButtons tým rozdielom, že metódasetOnActionnepridáva ďalšieho spracúvateľa, ale nastavuje nového jediného spracúvateľa. AjButtonvšak poskytuje metóduaddEventHandler, ktorá je dokonca o niečo všeobecnejšia, než bude tá naša.) - Poskytovať metódu
fireAction, ktorá udalosť spustí. To si vyžaduje zavolať metóduhandlevšetkých spracúvateľov zo zoznamueventHandlers.
package events;
import java.util.*;
import javafx.event.*;
public class MyEventSender {
private String name;
private List<EventHandler<MyEvent>> eventHandlers; // Zoznam spracovavatelov udalosti
public MyEventSender(String name) {
this.name = name;
eventHandlers = new ArrayList<>();
}
public String getName() {
return name;
}
/* Metoda pridavajuca spracovavatela udalosti: */
public void addEventHandler(EventHandler<MyEvent> handler) {
eventHandlers.add(handler);
}
/* Metoda spustajuca udalost: */
public void fireAction(int type) {
MyEvent event = new MyEvent(this, "UDALOST " + type);
for (EventHandler<MyEvent> handler : eventHandlers) {
handler.handle(event);
}
}
}
Môžeme teraz ešte upraviť triedu MyEventHandler tak, aby využívala
metódu getName inštancie triedy MyEventSender.
public class MyEventHandler implements EventHandler<MyEvent> {
@Override
public void handle(MyEvent event) {
System.out.println("Spracuvam udalost: " + event.getMessage());
Object sender = event.getSource();
if (sender instanceof MyEventSender) {
System.out.println("Odosielatel udalosti: " + ((MyEventSender) sender).getName());
}
}
}
Trieda s metódou main potom môže vyzerať napríklad nasledovne.
package events;
public class SimpleEvents {
public static void main(String[] args) {
MyEventSender sender1 = new MyEventSender("prvy");
MyEventSender sender2 = new MyEventSender("druhy");
MyEventHandler handler = new MyEventHandler();
sender1.addEventHandler(handler);
sender2.addEventHandler(event ->
System.out.println("Spracuvavam udalost " + event.getMessage() + " inym sposobom."));
sender2.addEventHandler(handler);
sender2.addEventHandler(event ->
System.out.println("Spracuvavam udalost " + event.getMessage() + " este inym sposobom."));
sender1.fireAction(1000);
sender2.fireAction(2000);
}
}
Konzumácia udalostí
V triede
Event
sú okrem iného definované dve špeciálne metódy: consume() a
isConsumed(). Ak je udalosť skonzumovaná, znamená to zhruba toľko, že
už je spracovaná a nemusí sa predávať prípadným ďalším spracúvateľom. V
našom jednoduchom programe vyššie napríklad môžeme upraviť triedu
MyEventHandler tak, aby pri spracovaní udalosti túto udalosť aj rovno
skonzumovala; triedu MyEventSender naopak upravíme tak, aby metódy
handle jednotlivých spracúvateľov volala len kým ešte udalosť nie je
skonzumovaná.
public class MyEventHandler implements EventHandler<MyEvent> {
@Override
public void handle(MyEvent event) {
System.out.println("Spracuvam udalost: " + event.getMessage());
Object sender = event.getSource();
if (sender instanceof MyEventSender) {
System.out.println("Odosielatel udalosti: " + ((MyEventSender) sender).getName());
}
event.consume();
}
}
public class MyEventSender {
// ...
public void fireAction(int type) {
MyEvent event = new MyEvent(this, "UDALOST " + type);
for (EventHandler<MyEvent> handler : eventHandlers) {
handler.handle(event);
if (event.isConsumed()) {
break;
}
}
}
// ...
}
V JavaFX je mechanizmus konzumovania udalostí o niečo zložitejší.
JavaFX: udalosti myši
- Udalosti nejakým spôsobom súvisiace s myšou (napríklad stlačenie
alebo uvoľnenie tlačidla) v JavaFX reprezentuje trieda
MouseEvent. - Obsahuje napríklad metódy
getButton(),getSceneX(),getSceneY()umožňujúce získať informácie o danej udalosti.
Vytváranie a spracovanie udalostí myši v JavaFX funguje nasledovne:
- Ako prvá sa udalosť vytvorí na tom uzle, ktorý je v mieste udalosti na scéne viditeľný (zaujímavé najmä v prípade prekrývajúcich sa uzlov).
- Spracúvatelia danej udalosti na danom uzle môžu udalosť spracovať.
- Ak po vykonaní predchádzajúceho kroku ešte nie je udalosť skonzumovaná, môže sa dostať aj k iným uzlom.
- Celkovo je predávanie udalostí k ďalším uzlom relatívne komplikovaný proces (viac detailov tu).
JavaFX: udalosti klávesnice
- Udalosti súvisiace s klávesnicou v JavaFX reprezentuje trieda
KeyEvent. - Kľúčovou metódou tejto triedy je
getCode, ktorá vracia kód stlačeného tlačidla klávesnice. - Udalosť sa vytvorí na uzle, ktorý má tzv. fokus – každý uzol oň môže
požiadať metódou
requestFocus().
Časovač: pohybujúci sa kruh
V balíku javafx.animation je definovaná abstraktná trieda
AnimationTimer,
ktorá umožňuje „periodické” vykonávanie určitej udalosti (zakaždým, keď
sa nanovo prekreslí obsah scény). Obsahuje implementované metódy
start() a stop() a abstraktnú metódu s hlavičkou
abstract void handle(long now)
Prekrytím tejto metódy v podtriede dediacej od AnimationTimer možno
špecifikovať udalosť, ktorá sa bude „periodicky” vykonávať. Jej
vstupným argumentom je „časová pečiatka” now reprezentujúca čas v
nanosekundách; pomocou nej sa dá ako-tak prispôsobiť interval
vykonávania jednotlivých udalostí.
Použitie takéhoto časovača demonštrujeme na jednoduchej aplikácii: v okne sa bude buď vodorovne alebo zvisle pohybovať kruh určitej veľkosti. Pri každom „náraze” na okraj scény sa otočí o 180 stupňov. Pri stlačení niektorej zo šípok klávesnice sa kruh začne pohybovať daným smerom. Navyše sa raz za cca. pol sekundy náhodne zmení farba kruhu.
import javafx.application.*;
import javafx.stage.*;
import javafx.scene.*;
import javafx.scene.layout.*;
import javafx.scene.shape.*;
import javafx.scene.paint.*;
import javafx.animation.*;
import javafx.scene.input.*;
import java.util.*;
public class MovingCircle extends Application {
private enum MoveDirection { // Vymenovany typ reprezentujuci mozne smery pohybu kruhu
UP,
RIGHT,
DOWN,
LEFT
}
private Scene scene; // Scena hlavneho okna aplikacie
private Circle circle; // Pohybujuci sa kruh
private MoveDirection moveDirection; // Aktualny smer pohybu kruhu
// Metoda, ktora posunie kruh circle smerom moveDirection o pocet pixelov delta:
private void moveCircle(double delta) {
double newX, newY;
switch (moveDirection) {
case UP:
newY = circle.getCenterY() - delta;
if (newY >= circle.getRadius()) { // Ak kruh nevyjde von zo sceny, posun ho
circle.setCenterY(newY);
} else { // V opacnom pripade zmen smer o 180 stupnov
moveDirection = MoveDirection.DOWN;
}
break;
case DOWN:
newY = circle.getCenterY() + delta;
if (newY <= scene.getHeight() - circle.getRadius()) { // Ak kruh nevyjde von zo sceny, posun ho
circle.setCenterY(newY);
} else { // V opacnom pripade zmen smer o 180 stupnov
moveDirection = MoveDirection.UP;
}
break;
case LEFT:
newX = circle.getCenterX() - delta;
if (newX >= circle.getRadius()) { // Ak kruh nevyjde von zo sceny, posun ho
circle.setCenterX(newX);
} else { // V opacnom pripade zmen smer o 180 stupnov
moveDirection = MoveDirection.RIGHT;
}
break;
case RIGHT:
newX = circle.getCenterX() + delta;
if (newX <= scene.getWidth() - circle.getRadius()) { // Ak kruh nevyjde von zo sceny, posun ho
circle.setCenterX(newX);
} else { // V opacnom pripade zmen smer o 180 stupnov
moveDirection = MoveDirection.LEFT;
}
break;
}
}
private Color randomColour(Random random) {
return Color.color(random.nextDouble(), random.nextDouble(), random.nextDouble());
}
@Override
public void start(Stage primaryStage) {
Pane pane = new Pane();
scene = new Scene(pane, 400, 400);
Random random = new Random();
double radius = 20; // Fixny polomer kruhu
double x = radius + (random.nextDouble() * (scene.getWidth() - 2 * radius)); // Nahodna pociatocna x-ova suradnica kruhu
double y = radius + (random.nextDouble() * (scene.getHeight() - 2 * radius)); // Nahodna pociatocna y-ova suradnica kruhu
circle = new Circle(x , y, radius); // Vytvorenie kruhu s danymi parametrami
pane.getChildren().add(circle);
circle.setFill(randomColour(random));
moveDirection = MoveDirection.values()[random.nextInt(4)]; // Nahodne zvoleny pociatocny smer pohybu
circle.requestFocus(); // Kruh dostane fokus, aby mohol reagovat na klavesnicu
circle.setOnKeyPressed(event -> { // Nastavime reakciu kruhu na stlacenie klavesy
switch (event.getCode()) {
case UP: // Ak bola stlacena niektora zo sipok, zmenime podla nej smer
moveDirection = MoveDirection.UP;
break;
case RIGHT:
moveDirection = MoveDirection.RIGHT;
break;
case DOWN:
moveDirection = MoveDirection.DOWN;
break;
case LEFT:
moveDirection = MoveDirection.LEFT;
break;
}
});
AnimationTimer animationTimer = new AnimationTimer() { // Vytvorenie casovaca
private long lastMoveTime = 0; // Casova peciatka posledneho pohybu kruhu
private long lastColourChangeTime = 0; // Casova peciatka poslednej zmeny farby kruhu
@Override
public void handle(long now) {
// Ak bol kruh naposledy posunuty pred viac ako 20 milisekundami, posun ho o 5 pixelov
if (now - lastMoveTime >= 20000000) {
moveCircle(5);
lastMoveTime = now;
}
// Ak sa farba kruhu naposledy zmenila pred viac ako 500 milisekundami, zmen ju nahodne
if (now - lastColourChangeTime >= 500000000) {
circle.setFill(randomColour(random));
lastColourChangeTime = now;
}
}
};
animationTimer.start(); // Spusti casovac
primaryStage.setScene(scene);
primaryStage.setTitle("Pohyblivý kruh");
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}