Programovanie (2) v Jave
1-INF-166, letný semester 2023/24
Letný semester, prednáška č. 11: Rozdiel medzi revíziami
(63 medziľahlých úprav od 2 ďalších používateľov nie je zobrazených) | |||
Riadok 1: | Riadok 1: | ||
== Oznamy == | == Oznamy == | ||
− | * | + | * Riešenia druhej domácej úlohy je potrebné odovzdať najneskôr do začiatku zajtrajších cvičení. |
− | * Každý termín skúšky bude pozostávať z | + | * Krátko po skončení tejto prednášky bude na testovači zverejnené zadanie tretej domácej úlohy s odovzdávaním ''do utorka 14. mája 2024, 9:50'' – čiže do začiatku trinástych cvičení. Úloha bude za 6 bodov + 2 bonusové body za prípadné vylepšenia. |
− | ** | + | * Počas zajtrajších cvičení (od 9:50 do 11:20) bude prebiehať štvrtý test zameraný na látku z prvých desiatich týždňov. Body z testu bude možné získať iba v prípade prítomnosti na cvičeniach v miestnosti I-H6. |
− | ** | + | * ''V AISe sú vypísané nasledujúce termíny skúšok'' (všetky v miestnosti F1-248): |
− | ** | + | ** Piatok, 17. mája o 10:00 (predtermín). |
− | * | + | ** Piatok, 24. mája o 10:00. |
+ | ** Piatok, 31. mája o 10:00. | ||
+ | ** Piatok, 14. júna o 10:00 (predposledný skúškový termín, predovšetkým pre opravné termíny). | ||
+ | ** Piatok, 21. júna o 10:00 (posledný skúškový termín, predovšetkým pre druhé opravné termíny). | ||
+ | * Prihlasovanie na skúšky je otvorené od dnešného poludnia. | ||
+ | * Na každý z termínov je potrebné prihlásiť sa najneskôr 24 hodín vopred. | ||
+ | * Každý z termínov je pre najviac 18 študentov. | ||
+ | * Pri prihlasovaní na skúškový termín nerozhoduje, či má ísť o riadny, prvý opravný, alebo druhý opravný termín. Myslite ale na to, aby vám vždy zostal dostatočný počet termínov aj na prípadné opravy (nie je teda napríklad rozumné využiť niektorý z posledných dvoch termínov skúšky ako riadny). | ||
+ | * Skúšky sa môžu zúčastniť všetci, čo zo semestra získajú aspoň 20 bodov (a budú teda mať aspoň teoretickú šancu na úspešné absolvovanie predmetu). | ||
+ | * Skúška bude pozostávať z: | ||
+ | ** Písomnej časti (cca. 30 minút) za maximálne 10 bodov. (Bude potrebné poznať teóriu z prednášok a rozumieť jej.) | ||
+ | ** Praktickej časti (cca. 90 minút) za maximálne 20 bodov spočívajúcej v riešení jednej programátorskej úlohy s odovzdávaním na testovač. | ||
+ | ** Ústnej časti, ktorej čas a miesto konania budú upresnené v deň skúšky (treba počítať s tým, že všetky tri časti skúšky môžu dohromady trvať celý deň). | ||
+ | * Na úspešné absolvovanie ''skúšky'' je potrebné: | ||
+ | ** Získať z písomnej a praktickej časti dohromady aspoň 15 bodov. | ||
+ | ** Uspieť na ústnej časti skúšky. | ||
+ | * Spôsob, ktorým bude určené výsledné hodnotenie, je uvedený v [[Letný semester, pravidlá|pravidlách]]. | ||
== Grafický návrh scény: jednoduchá kalkulačka == | == Grafický návrh scény: jednoduchá kalkulačka == | ||
Riadok 18: | Riadok 34: | ||
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 <tt>Počítaj!</tt> 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. | 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 <tt>Počítaj!</tt> 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 | + | 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. |
<syntaxhighlight lang="java"> | <syntaxhighlight lang="java"> | ||
Riadok 49: | Riadok 65: | ||
=== Formátovanie pomocou JavaFX CSS štýlov (1. časť) === | === 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 [https://openjfx.io/javadoc/ | + | 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 [https://openjfx.io/javadoc/21/javafx.graphics/javafx/scene/doc-files/cssref.html JavaFX CSS štýlov] definovaných v externom súbore. |
Vytvorme textový súbor <tt>styles.css</tt> s nasledujúcim obsahom: | Vytvorme textový súbor <tt>styles.css</tt> s nasledujúcim obsahom: | ||
Riadok 78: | Riadok 94: | ||
=== Scéna a strom uzlov === | === Scéna a strom uzlov === | ||
− | Obsah JavaFX scény sa reprezentuje v podobe tzv. ''stromu 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 <tt>[https://openjfx.io/javadoc/ | + | * Prvky umiestňované na scénu sa nazývajú ''uzly'' – triedy reprezentujúce tieto prvky majú ako spoločného predka triedu <tt>[https://openjfx.io/javadoc/21/javafx.graphics/javafx/scene/Node.html Node]</tt>. |
− | * Niektoré z uzlov môžu byť rodičmi iných uzlov na scéne – typickým príkladom sú napríklad oblasti typu [https://openjfx.io/javadoc/ | + | * Niektoré z uzlov môžu byť rodičmi iných uzlov na scéne – typickým príkladom sú napríklad oblasti typu [https://openjfx.io/javadoc/21/javafx.graphics/javafx/scene/layout/Pane.html <tt>Pane</tt>], 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 triedy [https://openjfx.io/javadoc/21/javafx.graphics/javafx/scene/Parent.html <tt>Parent</tt>]; tá je priamou podtriedou triedy <tt>Node</tt>. Medzi takéto triedy okrem <tt>Pane</tt> patria aj triedy pre ovládacie prvky ako napríklad [https://openjfx.io/javadoc/21/javafx.controls/javafx/scene/control/Button.html <tt>Button</tt>] alebo [https://openjfx.io/javadoc/21/javafx.controls/javafx/scene/control/Label.html <tt>Label</tt>]. ''Nepatria'' medzi ne napríklad triedy pre geometrické útvary (trieda [https://openjfx.io/javadoc/21/javafx.graphics/javafx/scene/shape/Shape.html <tt>Shape</tt>] a 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 <tt>Parent</tt>. Ďalšie „poschodia” stromu uzlov sa typicky pridávajú podobne ako na minulej prednáške, napríklad s využitím | + | * 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 <tt>Parent</tt>. Ďalšie „poschodia” stromu uzlov sa typicky pridávajú podobne ako na minulej prednáške, napríklad s využitím metódy <tt>getChildren().add</tt> pre jednotlivé rodičovské uzly. |
=== Rozloženie uzlov na scéne === | === Rozloženie uzlov na scéne === | ||
Riadok 90: | Riadok 106: | ||
==== <tt>GridPane</tt> ==== | ==== <tt>GridPane</tt> ==== | ||
− | Odmyslime si na chvíľu tlačidlá „<tt>Zmaž</tt>” a „<tt>Skonči</tt>” a umiestnime na scénu zvyšné ovládacie prvky. Ako koreňový uzol použijeme namiesto oblasti typu <tt>Pane</tt> oblasť typu [https://openjfx.io/javadoc/ | + | Odmyslime si na chvíľu tlačidlá „<tt>Zmaž</tt>” a „<tt>Skonči</tt>” a umiestnime na scénu zvyšné ovládacie prvky. Ako koreňový uzol použijeme namiesto oblasti typu <tt>Pane</tt> oblasť typu [https://openjfx.io/javadoc/21/javafx.graphics/javafx/scene/layout/GridPane.html <tt>GridPane</tt>]. Trieda <tt>GridPane</tt> je jednou z podtried triedy <tt>Pane</tt> umožňujúcich „inteligentné” spravovanie rozloženia uzlov. |
<syntaxhighlight lang="java"> | <syntaxhighlight lang="java"> | ||
Riadok 96: | Riadok 112: | ||
public void start(Stage primaryStage) { | public void start(Stage primaryStage) { | ||
− | ... | + | // ... |
// Pane pane = new Pane(); | // Pane pane = new Pane(); | ||
Riadok 104: | Riadok 120: | ||
Scene scene = new Scene(grid); | Scene scene = new Scene(grid); | ||
− | ... | + | // ... |
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
Riadok 136: | Riadok 152: | ||
Podobne môžeme do mriežky umiestniť aj ďalšie ovládacie prvky: | Podobne môžeme do mriežky umiestniť aj ďalšie ovládacie prvky: | ||
<syntaxhighlight lang="java"> | <syntaxhighlight lang="java"> | ||
− | Label | + | Label lblNumber1 = new Label("Prvý argument:"); |
− | grid.add( | + | grid.add(lblNumber1, 0, 1); |
− | + | ||
− | Label | + | Label lblNumber2 = new Label("Druhý argument:"); |
− | grid.add( | + | grid.add(lblNumber2, 0, 2); |
− | + | ||
− | Label | + | Label lblOperation = new Label("Operácia:"); |
− | grid.add( | + | grid.add(lblOperation, 0, 3); |
− | + | ||
Label lblResultText = new Label("Výsledok:"); | Label lblResultText = new Label("Výsledok:"); | ||
grid.add(lblResultText, 0, 5); | grid.add(lblResultText, 0, 5); | ||
− | + | ||
Label lblResult = new Label("0"); | Label lblResult = new Label("0"); | ||
grid.add(lblResult, 1, 5); | grid.add(lblResult, 1, 5); | ||
− | + | ||
− | TextField | + | TextField tfNumber1 = new TextField(); |
− | grid.add( | + | grid.add(tfNumber1, 1, 1); |
− | + | ||
− | TextField | + | TextField tfNumber2 = new TextField(); |
− | grid.add( | + | grid.add(tfNumber2, 1, 2); |
− | + | ||
− | ComboBox | + | ComboBox<String> cbOperation = new ComboBox<>(); |
− | grid.add( | + | grid.add(cbOperation, 1, 3); |
− | + | cbOperation.getItems().addAll("+", "-", "*", "/"); | |
− | + | cbOperation.setValue("+"); | |
− | + | ||
Button btnOK = new Button("Počítaj!"); | Button btnOK = new Button("Počítaj!"); | ||
grid.add(btnOK, 1, 4); | grid.add(btnOK, 1, 4); | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | Novým prvkom je tu „vyskakovací zoznam” <tt>[https://openjfx.io/javadoc/ | + | Novým prvkom je tu „vyskakovací zoznam” <tt>[https://openjfx.io/javadoc/21/javafx.controls/javafx/scene/control/ComboBox.html ComboBox<T>]</tt> prvkov typu <tt>T</tt>. Jeho metóda <tt>getItems</tt> 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 <tt>setValue</tt> nastaví aktuálne zvolenú možnosť. (V takomto východzom stave nemožno do <tt>ComboBox</tt>-u zadávať text manuálne; v prípade potreby je ale možné túto možnosť aktivovať metódou <tt>setEditable</tt> s parametrom <tt>true</tt>.) |
Pre účely ladenia ešte môžeme zviditeľniť deliace čiary mriežky nasledujúcim spôsobom: | Pre účely ladenia ešte môžeme zviditeľniť deliace čiary mriežky nasledujúcim spôsobom: | ||
Riadok 180: | Riadok 196: | ||
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ť): | 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ť): | ||
<syntaxhighlight lang="java"> | <syntaxhighlight lang="java"> | ||
− | + | tfNumber1.setPrefWidth(300); | |
− | + | tfNumber2.setPrefWidth(300); | |
− | + | cbOperation.setPrefWidth(300); | |
</syntaxhighlight> | </syntaxhighlight> | ||
Tiež si môžeme všimnúť, že medzi jednotlivými políčkami mriežky nie sú žiadne medzery. To vyriešime napríklad nasledovne: | Tiež si môžeme všimnúť, že medzi jednotlivými políčkami mriežky nie sú žiadne medzery. To vyriešime napríklad nasledovne: | ||
Riadok 193: | Riadok 209: | ||
import javafx.geometry.*; | import javafx.geometry.*; | ||
− | ... | + | // ... |
− | grid.setPadding(new Insets(10,20,10,20)); // horny okraj 10 pixelov, pravy 20, dolny 10, lavy 20 | + | grid.setPadding(new Insets(10, 20, 10, 20)); // horny okraj 10 pixelov, pravy 20, dolny 10, lavy 20 |
</syntaxhighlight> | </syntaxhighlight> | ||
[[Image:Kalkulacka4.png|thumb|340px|Vzhľad aplikácie po nastavení okrajov.]] | [[Image:Kalkulacka4.png|thumb|340px|Vzhľad aplikácie po nastavení okrajov.]] | ||
− | Trieda [https://openjfx.io/javadoc/ | + | Trieda [https://openjfx.io/javadoc/21/javafx.graphics/javafx/geometry/Insets.html <tt>Insets</tt>] (skratka od angl. ''Inside Offsets'') reprezentuje iba súbor štyroch hodnôt pre „veľkosti okrajov” a je definovaná v balíku <tt>javafx.geometry</tt>. 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 | Ď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 | ||
Riadok 209: | Riadok 225: | ||
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: | 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: | ||
<syntaxhighlight lang="java"> | <syntaxhighlight lang="java"> | ||
− | // | + | // tfNumber1.setPrefWidth(300); |
− | // | + | // tfNumber2.setPrefWidth(300); |
− | // | + | // cbOperation.setPrefWidth(300); |
</syntaxhighlight> | </syntaxhighlight> | ||
− | Cielený efekt dosiahneme naplnením zoznamu „obmedezní pre jednotlivé stĺpce”, ktorý si každá oblasť typu <tt>GridPane</tt> 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 [https://openjfx.io/javadoc/ | + | Cielený efekt dosiahneme naplnením zoznamu „obmedezní pre jednotlivé stĺpce”, ktorý si každá oblasť typu <tt>GridPane</tt> 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 [https://openjfx.io/javadoc/21/javafx.graphics/javafx/scene/layout/ColumnConstraints.html <tt>ColumnConstraints</tt>]. (Analogicky je možné nastavovať „obmedzenia” aj pre jednotlivé riadky.) |
<syntaxhighlight lang="java"> | <syntaxhighlight lang="java"> | ||
ColumnConstraints cc = new ColumnConstraints(); | ColumnConstraints cc = new ColumnConstraints(); | ||
Riadok 222: | Riadok 238: | ||
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”: | 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”: | ||
<syntaxhighlight lang="java"> | <syntaxhighlight lang="java"> | ||
− | + | cbOperation.setMaxWidth(Double.MAX_VALUE); | |
</syntaxhighlight> | </syntaxhighlight> | ||
Riadok 228: | Riadok 244: | ||
[[Image:Kalkulacka5.png|thumb|340px|Vzhľad aplikácie po dokončení návrhu rozloženia ovládacích prvkov v mriežke.]] | [[Image:Kalkulacka5.png|thumb|340px|Vzhľad aplikácie po dokončení návrhu rozloženia ovládacích prvkov v mriežke.]] | ||
<syntaxhighlight lang="java"> | <syntaxhighlight lang="java"> | ||
− | GridPane.setHalignment( | + | GridPane.setHalignment(lblNumber1, HPos.RIGHT); |
− | GridPane.setHalignment( | + | GridPane.setHalignment(lblNumber2, HPos.RIGHT); |
− | GridPane.setHalignment( | + | GridPane.setHalignment(lblOperation, HPos.RIGHT); |
GridPane.setHalignment(lblResultText, HPos.RIGHT); | GridPane.setHalignment(lblResultText, HPos.RIGHT); | ||
GridPane.setHalignment(lblResult, HPos.RIGHT); | GridPane.setHalignment(lblResult, HPos.RIGHT); | ||
GridPane.setHalignment(btnOK, HPos.RIGHT); | GridPane.setHalignment(btnOK, HPos.RIGHT); | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | S | + | 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: |
<syntaxhighlight lang="java"> | <syntaxhighlight lang="java"> | ||
// grid.setGridLinesVisible(true); | // grid.setGridLinesVisible(true); | ||
Riadok 243: | Riadok 259: | ||
==== <tt>BorderPane</tt> a <tt>VBox</tt> ==== | ==== <tt>BorderPane</tt> a <tt>VBox</tt> ==== | ||
− | Pridáme teraz tlačidlá „<tt>Zmaž</tt>” a „<tt>Skonči</tt>”. Mohli by sme ich samozrejme umiestniť napríklad do ďalšieho stĺpca mriežky <tt>grid</tt>. Tu si však ukážeme odlišný prístup – namiesto oblasti typu <tt>GridPane</tt> použijeme ako koreňový uzol scény oblasť typu [https://openjfx.io/javadoc/ | + | Pridáme teraz tlačidlá „<tt>Zmaž</tt>” a „<tt>Skonči</tt>”. Mohli by sme ich samozrejme umiestniť napríklad do ďalšieho stĺpca mriežky <tt>grid</tt>. Tu si však ukážeme odlišný prístup – namiesto oblasti typu <tt>GridPane</tt> použijeme ako koreňový uzol scény oblasť typu [https://openjfx.io/javadoc/21/javafx.graphics/javafx/scene/layout/BorderPane.html <tt>BorderPane</tt>]. 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 <tt>grid</tt>: | 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 <tt>grid</tt>: | ||
Riadok 254: | Riadok 270: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | 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 <tt>[https://openjfx.io/javadoc/ | + | 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 <tt>[https://openjfx.io/javadoc/21/javafx.graphics/javafx/scene/layout/VBox.html VBox]</tt>, do ktorej sa jednotlivé uzly vkladajú vertikálne jeden pod druhý: |
<syntaxhighlight lang="java"> | <syntaxhighlight lang="java"> | ||
VBox right = new VBox(); // Vytvorenie oblasti typu VBox | VBox right = new VBox(); // Vytvorenie oblasti typu VBox | ||
− | right.setPadding(new Insets(10, 20, 10, 60)); // Nastavenie okrajov (v poradi horny, pravy, lavy | + | 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.setSpacing(10); // Vertikalne medzery medzi vkladanymi uzlami | ||
right.setAlignment(Pos.BOTTOM_LEFT); // Zarovnanie obsahu oblasti vertikalne nadol a horizontalne dolava | right.setAlignment(Pos.BOTTOM_LEFT); // Zarovnanie obsahu oblasti vertikalne nadol a horizontalne dolava | ||
Riadok 271: | Riadok 287: | ||
right.getChildren().add(btnExit); | right.getChildren().add(btnExit); | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | [[Image:Kalkulacka6.png|thumb|400px|Vzhľad aplikácie po pridaní pravej časti.]] | + | [[Image:Kalkulacka6.png|thumb|400px|Vzhľad aplikácie po pridaní prvkov v pravej časti okna.]] |
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 <tt>right</tt> 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 <tt>right</tt>: | 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 <tt>right</tt> 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 <tt>right</tt>: | ||
<syntaxhighlight lang="java"> | <syntaxhighlight lang="java"> | ||
Riadok 282: | Riadok 298: | ||
Okrem <tt>GridPane</tt>, <tt>BorderPane</tt> a <tt>VBox</tt> existuje v JavaFX aj niekoľko ďalších oblastí umožňujúcich (polo)automaticky spravovať rozloženie jednotlivých uzlov: | Okrem <tt>GridPane</tt>, <tt>BorderPane</tt> a <tt>VBox</tt> existuje v JavaFX aj niekoľko ďalších oblastí umožňujúcich (polo)automaticky spravovať rozloženie jednotlivých uzlov: | ||
− | * [https://openjfx.io/javadoc/ | + | * [https://openjfx.io/javadoc/21/javafx.graphics/javafx/scene/layout/HBox.html <tt>HBox</tt>]: ide o horizontálnu obdobu <tt>VBox</tt>-u. |
− | * [https://openjfx.io/javadoc/ | + | * [https://openjfx.io/javadoc/21/javafx.graphics/javafx/scene/layout/StackPane.html <tt>StackPane</tt>]: 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ď.). |
− | * [https://openjfx.io/javadoc/ | + | * [https://openjfx.io/javadoc/21/javafx.graphics/javafx/scene/layout/FlowPane.html <tt>FlowPane</tt>]: 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. |
− | * [https://openjfx.io/javadoc/ | + | * [https://openjfx.io/javadoc/21/javafx.graphics/javafx/scene/layout/TilePane.html <tt>TilePane</tt>]: udržiava „dlaždice” rovnakej veľkosti. |
− | * [https://openjfx.io/javadoc/ | + | * [https://openjfx.io/javadoc/21/javafx.graphics/javafx/scene/layout/AnchorPane.html <tt>AnchorPane</tt>]: umožňuje ukotvenie prvkov na danú pozíciu. |
=== Kalkulačka: oživenie aplikácie === | === 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 <tt>btnOK</tt> | + | 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 <tt>btnOK</tt>. |
<syntaxhighlight lang="java"> | <syntaxhighlight lang="java"> | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
public class Calculator extends Application { | 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 ( | + | switch (operation) { |
case "+": | case "+": | ||
return arg1 + arg2; | return arg1 + arg2; | ||
Riadok 317: | Riadok 330: | ||
return arg1 / arg2; | return arg1 / arg2; | ||
default: | default: | ||
− | throw new | + | throw new IllegalArgumentException(); |
} | } | ||
} | } | ||
− | ... | + | // ... |
@Override | @Override | ||
public void start(Stage primaryStage) { | public void start(Stage primaryStage) { | ||
− | ... | + | // ... |
− | btnOK.setOnAction( | + | btnOK.setOnAction(event -> { |
try { | try { | ||
− | + | lblResult.setText(Double.toString(calculate( | |
− | + | cbOperation.getValue(), | |
− | + | Double.parseDouble(tfNumber1.getText()), | |
− | + | Double.parseDouble(tfNumber2.getText())))); | |
− | |||
− | |||
} catch (NumberFormatException exception) { | } catch (NumberFormatException exception) { | ||
lblResult.setText("Výnimka!"); | lblResult.setText("Výnimka!"); | ||
Riadok 341: | Riadok 352: | ||
}); | }); | ||
− | ... | + | // ... |
} | } | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | Podobne môžeme pridať aj funkcionalitu zostávajúcich dvoch tlačidiel | + | Podobne môžeme pridať aj funkcionalitu zostávajúcich dvoch tlačidiel. |
<syntaxhighlight lang="java"> | <syntaxhighlight lang="java"> | ||
@Override | @Override | ||
public void start(Stage primaryStage) { | public void start(Stage primaryStage) { | ||
− | ... | + | // ... |
− | btnClear.setOnAction( | + | btnClear.setOnAction(event -> { |
− | + | tfNumber1.clear(); | |
− | + | tfNumber2.clear(); | |
− | + | cbOperation.setValue("+"); | |
lblResult.setText("0"); | lblResult.setText("0"); | ||
}); | }); | ||
− | btnExit.setOnAction( | + | btnExit.setOnAction(event -> { |
− | Platform.exit(); | + | Platform.exit(); |
}); | }); | ||
− | + | ||
− | ... | + | // ... |
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
Riadok 396: | Riadok 407: | ||
<syntaxhighlight lang="java"> | <syntaxhighlight lang="java"> | ||
@Override | @Override | ||
− | + | public void start(Stage primaryStage) { | |
− | ... | + | // ... |
lblHeader.setId("header"); | lblHeader.setId("header"); | ||
lblResult.setId("result"); | lblResult.setId("result"); | ||
− | ... | + | // ... |
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
Riadok 423: | Riadok 434: | ||
<syntaxhighlight lang="java"> | <syntaxhighlight lang="java"> | ||
− | |||
− | |||
import javafx.application.*; | import javafx.application.*; | ||
import javafx.stage.*; | import javafx.stage.*; | ||
Riadok 432: | Riadok 441: | ||
import javafx.event.*; | import javafx.event.*; | ||
import javafx.geometry.*; | import javafx.geometry.*; | ||
− | |||
− | |||
− | |||
public class Calculator extends Application { | public class Calculator extends Application { | ||
− | public double calculate(String | + | /** |
− | switch ( | + | * 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 "+": | case "+": | ||
return arg1 + arg2; | return arg1 + arg2; | ||
Riadok 449: | Riadok 462: | ||
return arg1 / arg2; | return arg1 / arg2; | ||
default: | default: | ||
− | throw new | + | throw new IllegalArgumentException(); |
} | } | ||
} | } | ||
− | + | ||
@Override | @Override | ||
public void start(Stage primaryStage) { | public void start(Stage primaryStage) { | ||
GridPane grid = new GridPane(); | GridPane grid = new GridPane(); | ||
− | + | ||
− | + | Label lblHeader = new Label("Zadajte vstupné hodnoty:"); | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | Label lblHeader = new Label("Zadajte vstupné hodnoty:"); | ||
grid.add(lblHeader, 0, 0, 2, 1); | grid.add(lblHeader, 0, 0, 2, 1); | ||
lblHeader.setId("header"); | lblHeader.setId("header"); | ||
− | + | ||
− | Label | + | Label lblNumber1 = new Label("Prvý argument:"); |
− | grid.add( | + | grid.add(lblNumber1, 0, 1); |
− | GridPane.setHalignment( | + | GridPane.setHalignment(lblNumber1, HPos.RIGHT); |
− | + | ||
− | Label | + | Label lblNumber2 = new Label("Druhý argument:"); |
− | grid.add( | + | grid.add(lblNumber2, 0, 2); |
− | GridPane.setHalignment( | + | GridPane.setHalignment(lblNumber2, HPos.RIGHT); |
− | + | ||
− | Label | + | Label lblOperation = new Label("Operácia:"); |
− | grid.add( | + | grid.add(lblOperation, 0, 3); |
− | GridPane.setHalignment( | + | GridPane.setHalignment(lblOperation, HPos.RIGHT); |
− | + | ||
Label lblResultText = new Label("Výsledok:"); | Label lblResultText = new Label("Výsledok:"); | ||
grid.add(lblResultText, 0, 5); | grid.add(lblResultText, 0, 5); | ||
GridPane.setHalignment(lblResultText, HPos.RIGHT); | GridPane.setHalignment(lblResultText, HPos.RIGHT); | ||
− | + | ||
Label lblResult = new Label("0"); | Label lblResult = new Label("0"); | ||
grid.add(lblResult, 1, 5); | grid.add(lblResult, 1, 5); | ||
GridPane.setHalignment(lblResult, HPos.RIGHT); | GridPane.setHalignment(lblResult, HPos.RIGHT); | ||
lblResult.setId("result"); | lblResult.setId("result"); | ||
− | + | ||
− | TextField | + | TextField tfNumber1 = new TextField(); |
− | grid.add( | + | grid.add(tfNumber1, 1, 1); |
− | + | ||
− | TextField | + | TextField tfNumber2 = new TextField(); |
− | grid.add( | + | grid.add(tfNumber2, 1, 2); |
− | + | ||
− | ComboBox | + | ComboBox<String> cbOperation = new ComboBox<>(); |
− | grid.add( | + | grid.add(cbOperation, 1, 3); |
− | + | cbOperation.getItems().addAll("+", "-", "*", "/"); | |
− | + | cbOperation.setValue("+"); | |
− | + | cbOperation.setMaxWidth(Double.MAX_VALUE); | |
− | + | ||
Button btnOK = new Button("Počítaj!"); | Button btnOK = new Button("Počítaj!"); | ||
grid.add(btnOK, 1, 4); | grid.add(btnOK, 1, 4); | ||
GridPane.setHalignment(btnOK, HPos.RIGHT); | 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(); | VBox right = new VBox(); | ||
right.setPadding(new Insets(10, 20, 10, 60)); | right.setPadding(new Insets(10, 20, 10, 60)); | ||
right.setSpacing(10); | right.setSpacing(10); | ||
right.setAlignment(Pos.BOTTOM_LEFT); | right.setAlignment(Pos.BOTTOM_LEFT); | ||
− | + | ||
Button btnClear = new Button("Zmaž"); | Button btnClear = new Button("Zmaž"); | ||
right.getChildren().add(btnClear); | right.getChildren().add(btnClear); | ||
btnClear.setMaxWidth(Double.MAX_VALUE); | btnClear.setMaxWidth(Double.MAX_VALUE); | ||
− | + | ||
Button btnExit = new Button("Skonči"); | Button btnExit = new Button("Skonči"); | ||
right.getChildren().add(btnExit); | right.getChildren().add(btnExit); | ||
btnExit.setMaxWidth(Double.MAX_VALUE); | btnExit.setMaxWidth(Double.MAX_VALUE); | ||
− | + | ||
+ | |||
BorderPane border = new BorderPane(); | BorderPane border = new BorderPane(); | ||
border.setCenter(grid); | border.setCenter(grid); | ||
border.setRight(right); | border.setRight(right); | ||
− | + | ||
− | + | ||
− | + | btnOK.setOnAction(event -> { | |
− | |||
− | |||
− | btnOK.setOnAction( | ||
try { | try { | ||
− | + | lblResult.setText(Double.toString(calculate( | |
− | + | cbOperation.getValue(), | |
− | + | Double.parseDouble(tfNumber1.getText()), | |
− | + | Double.parseDouble(tfNumber2.getText())))); | |
− | |||
− | |||
} catch (NumberFormatException exception) { | } catch (NumberFormatException exception) { | ||
lblResult.setText("Výnimka!"); | lblResult.setText("Výnimka!"); | ||
} | } | ||
}); | }); | ||
− | + | ||
− | btnClear.setOnAction( | + | btnClear.setOnAction(event -> { |
− | + | tfNumber1.setText(""); | |
− | + | tfNumber2.setText(""); | |
− | + | cbOperation.setValue("+"); | |
lblResult.setText("0"); | lblResult.setText("0"); | ||
}); | }); | ||
− | + | ||
− | btnExit.setOnAction( | + | btnExit.setOnAction(event -> { |
Platform.exit(); | Platform.exit(); | ||
}); | }); | ||
− | + | ||
+ | |||
+ | Scene scene = new Scene(border); | ||
+ | scene.getStylesheets().add("styles.css"); | ||
+ | |||
primaryStage.setScene(scene); | primaryStage.setScene(scene); | ||
primaryStage.setTitle("Kalkulačka"); | primaryStage.setTitle("Kalkulačka"); | ||
Riadok 596: | Riadok 610: | ||
=== Základné princípy programovania riadeného 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 | + | 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 <tt>javafx.event</tt>. |
− | + | 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 <tt>MyEvent</tt> reprezentujúcu jednoduchú udalosť obsahujúcu nejakú správu o sebe. | ||
<syntaxhighlight lang="java"> | <syntaxhighlight lang="java"> | ||
− | package | + | package events; |
import javafx.event.*; | import javafx.event.*; | ||
− | |||
− | public class | + | 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 | |
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | 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 | 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 | this.message = message; // Nastavime spravu zodpovedajucu nasej udalosti | ||
} | } | ||
− | + | ||
− | public String getMessage() { // Metoda, ktora vrati spravu zodpovedajucu udalosti | + | public String getMessage() { // Metoda, ktora vrati spravu zodpovedajucu udalosti |
return message; | return message; | ||
} | } | ||
Riadok 634: | Riadok 636: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | Spracovávateľ JavaFX udalostí typu <tt>T</tt> sa vyznačuje tým, že implementuje rozhranie [https://openjfx.io/javadoc/ | + | Spracovávateľ JavaFX udalostí typu <tt>T</tt> sa vyznačuje tým, že implementuje rozhranie [https://openjfx.io/javadoc/21/javafx.base/javafx/event/EventHandler.html <tt>EventHandler<T></tt>]. S týmto rozhraním sme sa stretli už minule a vieme, že vyžaduje implementáciu jedinej metódy <tt>handle</tt> (čo okrem iného umožňuje nahradiť inštancie takýchto tried lambda výrazmi). Vytvorme teda jednoduchú triedu <tt>MyEventHandler</tt> pre spracovávateľa udalostí <tt>MyEvent</tt>. |
<syntaxhighlight lang="java"> | <syntaxhighlight lang="java"> | ||
− | class MyEventHandler implements EventHandler<MyEvent> { | + | package events; |
+ | |||
+ | import javafx.event.*; | ||
+ | |||
+ | public class MyEventHandler implements EventHandler<MyEvent> { | ||
@Override | @Override | ||
public void handle(MyEvent event) { | public void handle(MyEvent event) { | ||
Riadok 645: | Riadok 651: | ||
Potrebujeme ešte triedu <tt>MyEventSender</tt>, ktorá bude schopná udalosti typu <tt>MyEvent</tt> vytvárať. Tá bude zo všetkých najkomplikovanejšia. Musí totiž: | Potrebujeme ešte triedu <tt>MyEventSender</tt>, ktorá bude schopná udalosti typu <tt>MyEvent</tt> vytvárať. Tá bude zo všetkých najkomplikovanejšia. Musí totiž: | ||
− | * Uchovávať zoznam <tt> | + | * Uchovávať zoznam <tt>eventHandlers</tt> vš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 <tt> | + | * Poskytovať metódu <tt>addEventHandler</tt> pridávajúcu spracúvateľa udalosti. (Tá sa podobá napríklad na metódu <tt>setOnAction</tt> inštancie triedy <tt>Button</tt> s tým rozdielom, že metóda <tt>setOnAction</tt> nepridáva ''ďalšieho'' spracúvateľa, ale nastavuje ''nového jediného'' spracúvateľa. Aj <tt>Button</tt> však poskytuje metódu <tt>addEventHandler</tt>, ktorá je dokonca o niečo všeobecnejšia, než bude tá naša.) |
− | * Poskytovať metódu <tt>fireAction</tt>, ktorá udalosť spustí. To si vyžaduje zavolať metódu <tt>handle</tt> všetkých | + | * Poskytovať metódu <tt>fireAction</tt>, ktorá udalosť spustí. To si vyžaduje zavolať metódu <tt>handle</tt> všetkých spracúvateľov zo zoznamu <tt>eventHandlers</tt>. |
<syntaxhighlight lang="java"> | <syntaxhighlight lang="java"> | ||
− | class MyEventSender { | + | package events; |
− | private | + | |
− | private | + | import java.util.*; |
− | + | import javafx.event.*; | |
+ | |||
+ | public class MyEventSender { | ||
+ | private String name; | ||
+ | private List<EventHandler<MyEvent>> eventHandlers; // Zoznam spracovavatelov udalosti | ||
+ | |||
public MyEventSender(String name) { | public MyEventSender(String name) { | ||
this.name = name; | this.name = name; | ||
− | + | eventHandlers = new ArrayList<>(); | |
} | } | ||
− | + | ||
public String getName() { | public String getName() { | ||
− | return | + | return name; |
} | } | ||
− | + | ||
− | public void | + | /* Metoda pridavajuca spracovavatela udalosti: */ |
− | + | public void addEventHandler(EventHandler<MyEvent> handler) { | |
+ | eventHandlers.add(handler); | ||
} | } | ||
− | + | ||
− | public void fireAction(int type) { | + | /* Metoda spustajuca udalost: */ |
+ | public void fireAction(int type) { | ||
MyEvent event = new MyEvent(this, "UDALOST " + type); | MyEvent event = new MyEvent(this, "UDALOST " + type); | ||
− | for (EventHandler<MyEvent> | + | for (EventHandler<MyEvent> handler : eventHandlers) { |
− | + | handler.handle(event); | |
} | } | ||
} | } | ||
Riadok 675: | Riadok 688: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | Môžeme teraz ešte upraviť triedu <tt>MyEventHandler</tt> tak, aby využívala metódu <tt>MyEventSender | + | Môžeme teraz ešte upraviť triedu <tt>MyEventHandler</tt> tak, aby využívala metódu <tt>getName</tt> inštancie triedy <tt>MyEventSender</tt>. |
<syntaxhighlight lang="java"> | <syntaxhighlight lang="java"> | ||
− | class MyEventHandler implements EventHandler<MyEvent> { | + | public class MyEventHandler implements EventHandler<MyEvent> { |
@Override | @Override | ||
public void handle(MyEvent event) { | public void handle(MyEvent event) { | ||
Riadok 689: | Riadok 702: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | + | Trieda s metódou <tt>main</tt> potom môže vyzerať napríklad nasledovne. | |
<syntaxhighlight lang="java"> | <syntaxhighlight lang="java"> | ||
− | public static void main(String[] args) { | + | package events; |
+ | |||
+ | public class SimpleEvents { | ||
+ | |||
+ | public static void main(String[] args) { | ||
MyEventSender sender1 = new MyEventSender("prvy"); | MyEventSender sender1 = new MyEventSender("prvy"); | ||
MyEventSender sender2 = new MyEventSender("druhy"); | MyEventSender sender2 = new MyEventSender("druhy"); | ||
− | + | ||
MyEventHandler handler = new MyEventHandler(); | MyEventHandler handler = new MyEventHandler(); | ||
− | sender1. | + | sender1.addEventHandler(handler); |
− | sender2. | + | sender2.addEventHandler(event -> |
− | System.out.println("Spracuvavam udalost " + event.getMessage() + " inym sposobom.") | + | System.out.println("Spracuvavam udalost " + event.getMessage() + " inym sposobom.")); |
− | + | sender2.addEventHandler(handler); | |
− | sender2. | + | sender2.addEventHandler(event -> |
− | sender2. | + | System.out.println("Spracuvavam udalost " + event.getMessage() + " este inym sposobom.")); |
− | System.out.println("Spracuvavam udalost " + event.getMessage() + " este inym sposobom.") | + | |
− | |||
− | |||
sender1.fireAction(1000); | sender1.fireAction(1000); | ||
− | sender2.fireAction(2000); | + | sender2.fireAction(2000); |
} | } | ||
+ | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
=== Konzumácia udalostí === | === Konzumácia udalostí === | ||
− | V triede [https://openjfx.io/javadoc/ | + | V triede [https://openjfx.io/javadoc/21/javafx.base/javafx/event/Event.html <tt>Event</tt>] sú okrem iného definované dve špeciálne metódy: <tt>consume()</tt> a <tt>isConsumed()</tt>. 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 <tt>MyEventHandler</tt> tak, aby pri spracovaní udalosti túto udalosť aj rovno skonzumovala; triedu <tt>MyEventSender</tt> naopak upravíme tak, aby metódy <tt>handle</tt> jednotlivých spracúvateľov volala len kým ešte udalosť nie je skonzumovaná. |
<syntaxhighlight lang="java"> | <syntaxhighlight lang="java"> | ||
− | class MyEventHandler implements EventHandler<MyEvent> { | + | public class MyEventHandler implements EventHandler<MyEvent> { |
@Override | @Override | ||
public void handle(MyEvent event) { | public void handle(MyEvent event) { | ||
Riadok 725: | Riadok 741: | ||
} | } | ||
} | } | ||
+ | </syntaxhighlight> | ||
− | + | <syntaxhighlight lang="java"> | |
− | + | public class MyEventSender { | |
− | class MyEventSender { | ||
− | ... | + | // ... |
public void fireAction(int type) { | public void fireAction(int type) { | ||
MyEvent event = new MyEvent(this, "UDALOST " + type); | MyEvent event = new MyEvent(this, "UDALOST " + type); | ||
− | for (EventHandler<MyEvent> | + | for (EventHandler<MyEvent> handler : eventHandlers) { |
− | + | handler.handle(event); | |
if (event.isConsumed()) { | if (event.isConsumed()) { | ||
break; | break; | ||
Riadok 742: | Riadok 758: | ||
} | } | ||
− | ... | + | // ... |
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
Riadok 750: | Riadok 766: | ||
=== JavaFX: udalosti myši === | === 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 [https://openjfx.io/javadoc/ | + | * Udalosti nejakým spôsobom súvisiace s myšou (napríklad stlačenie alebo uvoľnenie tlačidla) v JavaFX reprezentuje trieda [https://openjfx.io/javadoc/21/javafx.graphics/javafx/scene/input/MouseEvent.html <tt>MouseEvent</tt>]. |
* Obsahuje napríklad metódy <tt>getButton()</tt>, <tt>getSceneX()</tt>, <tt>getSceneY()</tt> umožňujúce získať informácie o danej udalosti. | * Obsahuje napríklad metódy <tt>getButton()</tt>, <tt>getSceneX()</tt>, <tt>getSceneY()</tt> umožňujúce získať informácie o danej udalosti. | ||
Vytváranie a spracovanie udalostí myši v JavaFX funguje nasledovne: | 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). | * 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. | * 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 [https://docs.oracle.com/javafx/2/events/processing.htm tu]). | * Celkovo je predávanie udalostí k ďalším uzlom relatívne komplikovaný proces (viac detailov [https://docs.oracle.com/javafx/2/events/processing.htm tu]). | ||
=== JavaFX: udalosti klávesnice === | === JavaFX: udalosti klávesnice === | ||
− | * Udalosti súvisiace s klávesnicou v JavaFX reprezentuje trieda [https://openjfx.io/javadoc/ | + | * Udalosti súvisiace s klávesnicou v JavaFX reprezentuje trieda [https://openjfx.io/javadoc/21/javafx.graphics/javafx/scene/input/KeyEvent.html <tt>KeyEvent</tt>]. |
* Kľúčovou metódou tejto triedy je <tt>getCode</tt>, ktorá vracia kód stlačeného tlačidla klávesnice. | * Kľúčovou metódou tejto triedy je <tt>getCode</tt>, 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 <tt>requestFocus()</tt>. | * Udalosť sa vytvorí na uzle, ktorý má tzv. fokus – každý uzol oň môže požiadať metódou <tt>requestFocus()</tt>. | ||
Riadok 766: | Riadok 782: | ||
== Časovač: pohybujúci sa kruh == | == Časovač: pohybujúci sa kruh == | ||
− | V balíku <tt>javafx.animation</tt> je definovaná ''abstraktná'' trieda [https://openjfx.io/javadoc/ | + | V balíku <tt>javafx.animation</tt> je definovaná ''abstraktná'' trieda [https://openjfx.io/javadoc/21/javafx.graphics/javafx/animation/AnimationTimer.html <tt>AnimationTimer</tt>], ktorá umožňuje „periodické” vykonávanie určitej udalosti (zakaždým, keď sa nanovo prekreslí obsah scény). Obsahuje implementované metódy <tt>start()</tt> a <tt>stop()</tt> a abstraktnú metódu s hlavičkou |
<syntaxhighlight lang="java"> | <syntaxhighlight lang="java"> | ||
abstract void handle(long now) | abstract void handle(long now) | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | Prekrytím tejto metódy v podtriede dediacej od <tt>AnimationTimer</tt> možno špecifikovať udalosť, ktorá sa bude „periodicky” vykonávať. Jej | + | Prekrytím tejto metódy v podtriede dediacej od <tt>AnimationTimer</tt> možno špecifikovať udalosť, ktorá sa bude „periodicky” vykonávať. Jej vstupným argumentom je „časová pečiatka” <tt>now</tt> 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. | ''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. | ||
<syntaxhighlight lang="java"> | <syntaxhighlight lang="java"> | ||
− | |||
− | |||
import javafx.application.*; | import javafx.application.*; | ||
import javafx.stage.*; | import javafx.stage.*; | ||
import javafx.scene.*; | import javafx.scene.*; | ||
import javafx.scene.layout.*; | import javafx.scene.layout.*; | ||
− | |||
− | |||
import javafx.scene.shape.*; | import javafx.scene.shape.*; | ||
import javafx.scene.paint.*; | import javafx.scene.paint.*; | ||
Riadok 790: | Riadok 802: | ||
public class MovingCircle extends Application { | public class MovingCircle extends Application { | ||
− | enum MoveDirection { // Vymenovany typ reprezentujuci mozne smery pohybu kruhu | + | private enum MoveDirection { // Vymenovany typ reprezentujuci mozne smery pohybu kruhu |
− | UP, | + | UP, |
RIGHT, | RIGHT, | ||
DOWN, | DOWN, | ||
LEFT | LEFT | ||
− | }; | + | } |
− | + | ||
+ | private Scene scene; // Scena hlavneho okna aplikacie | ||
+ | private Circle circle; // Pohybujuci sa kruh | ||
private MoveDirection moveDirection; // Aktualny smer pohybu kruhu | private MoveDirection moveDirection; // Aktualny smer pohybu kruhu | ||
− | + | ||
− | // Metoda, ktora | + | // Metoda, ktora posunie kruh circle smerom moveDirection o pocet pixelov delta: |
− | private void moveCircle( | + | private void moveCircle(double delta) { |
− | double newX | + | double newX, newY; |
− | |||
switch (moveDirection) { | switch (moveDirection) { | ||
case UP: | case UP: | ||
Riadok 809: | Riadok 822: | ||
circle.setCenterY(newY); | circle.setCenterY(newY); | ||
} else { // V opacnom pripade zmen smer o 180 stupnov | } else { // V opacnom pripade zmen smer o 180 stupnov | ||
− | + | moveDirection = MoveDirection.DOWN; | |
} | } | ||
break; | break; | ||
Riadok 817: | Riadok 830: | ||
circle.setCenterY(newY); | circle.setCenterY(newY); | ||
} else { // V opacnom pripade zmen smer o 180 stupnov | } else { // V opacnom pripade zmen smer o 180 stupnov | ||
− | + | moveDirection = MoveDirection.UP; | |
} | } | ||
break; | break; | ||
Riadok 825: | Riadok 838: | ||
circle.setCenterX(newX); | circle.setCenterX(newX); | ||
} else { // V opacnom pripade zmen smer o 180 stupnov | } else { // V opacnom pripade zmen smer o 180 stupnov | ||
− | + | moveDirection = MoveDirection.RIGHT; | |
} | } | ||
break; | break; | ||
Riadok 833: | Riadok 846: | ||
circle.setCenterX(newX); | circle.setCenterX(newX); | ||
} else { // V opacnom pripade zmen smer o 180 stupnov | } else { // V opacnom pripade zmen smer o 180 stupnov | ||
− | + | moveDirection = MoveDirection.LEFT; | |
} | } | ||
break; | break; | ||
} | } | ||
− | } | + | } |
− | + | ||
− | Color | + | private Color randomColour(Random random) { |
return Color.color(random.nextDouble(), random.nextDouble(), random.nextDouble()); | return Color.color(random.nextDouble(), random.nextDouble(), random.nextDouble()); | ||
} | } | ||
Riadok 846: | Riadok 859: | ||
public void start(Stage primaryStage) { | public void start(Stage primaryStage) { | ||
Pane pane = new Pane(); | Pane pane = new Pane(); | ||
− | + | ||
− | + | scene = new Scene(pane, 400, 400); | |
− | + | ||
Random random = new Random(); | Random random = new Random(); | ||
− | + | ||
double radius = 20; // Fixny polomer kruhu | double radius = 20; // Fixny polomer kruhu | ||
double x = radius + (random.nextDouble() * (scene.getWidth() - 2 * radius)); // Nahodna pociatocna x-ova suradnica 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 | 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); | pane.getChildren().add(circle); | ||
− | circle.setFill( | + | circle.setFill(randomColour(random)); |
− | + | ||
moveDirection = MoveDirection.values()[random.nextInt(4)]; // Nahodne zvoleny pociatocny smer pohybu | moveDirection = MoveDirection.values()[random.nextInt(4)]; // Nahodne zvoleny pociatocny smer pohybu | ||
− | + | ||
circle.requestFocus(); // Kruh dostane fokus, aby mohol reagovat na klavesnicu | circle.requestFocus(); // Kruh dostane fokus, aby mohol reagovat na klavesnicu | ||
− | circle.setOnKeyPressed( | + | circle.setOnKeyPressed(event -> { // Nastavime reakciu kruhu na stlacenie klavesy |
− | switch ( | + | switch (event.getCode()) { |
case UP: // Ak bola stlacena niektora zo sipok, zmenime podla nej smer | case UP: // Ak bola stlacena niektora zo sipok, zmenime podla nej smer | ||
moveDirection = MoveDirection.UP; | moveDirection = MoveDirection.UP; | ||
Riadok 876: | Riadok 889: | ||
moveDirection = MoveDirection.LEFT; | moveDirection = MoveDirection.LEFT; | ||
break; | break; | ||
− | } | + | } |
}); | }); | ||
− | + | ||
AnimationTimer animationTimer = new AnimationTimer() { // Vytvorenie casovaca | AnimationTimer animationTimer = new AnimationTimer() { // Vytvorenie casovaca | ||
private long lastMoveTime = 0; // Casova peciatka posledneho pohybu kruhu | private long lastMoveTime = 0; // Casova peciatka posledneho pohybu kruhu | ||
− | private long | + | private long lastColourChangeTime = 0; // Casova peciatka poslednej zmeny farby kruhu |
− | + | ||
@Override | @Override | ||
− | public void handle(long now) { | + | public void handle(long now) { |
// Ak bol kruh naposledy posunuty pred viac ako 20 milisekundami, posun ho o 5 pixelov | // Ak bol kruh naposledy posunuty pred viac ako 20 milisekundami, posun ho o 5 pixelov | ||
− | if (now - lastMoveTime >= 20000000) { | + | if (now - lastMoveTime >= 20000000) { |
− | moveCircle( | + | moveCircle(5); |
lastMoveTime = now; | lastMoveTime = now; | ||
} | } | ||
// Ak sa farba kruhu naposledy zmenila pred viac ako 500 milisekundami, zmen ju nahodne | // Ak sa farba kruhu naposledy zmenila pred viac ako 500 milisekundami, zmen ju nahodne | ||
− | if (now - | + | if (now - lastColourChangeTime >= 500000000) { |
− | circle.setFill( | + | circle.setFill(randomColour(random)); |
− | + | lastColourChangeTime = now; | |
} | } | ||
} | } | ||
}; | }; | ||
animationTimer.start(); // Spusti casovac | animationTimer.start(); // Spusti casovac | ||
− | + | ||
primaryStage.setScene(scene); | primaryStage.setScene(scene); | ||
primaryStage.setTitle("Pohyblivý kruh"); | primaryStage.setTitle("Pohyblivý kruh"); | ||
Riadok 913: | Riadok 926: | ||
== Odkazy == | == Odkazy == | ||
− | * [https://openjfx.io/javadoc/ | + | * [https://openjfx.io/javadoc/21/javafx.graphics/javafx/scene/doc-files/cssref.html Dokumentácia k JavaFX CSS štýlom.] |
Aktuálna revízia z 10:52, 28. apríl 2024
Obsah
Oznamy
- Riešenia druhej domácej úlohy je potrebné odovzdať najneskôr do začiatku zajtrajších cvičení.
- Krátko po skončení tejto prednášky bude na testovači zverejnené zadanie tretej domácej úlohy s odovzdávaním do utorka 14. mája 2024, 9:50 – čiže do začiatku trinástych cvičení. Úloha bude za 6 bodov + 2 bonusové body za prípadné vylepšenia.
- Počas zajtrajších cvičení (od 9:50 do 11:20) bude prebiehať štvrtý test zameraný na látku z prvých desiatich týždňov. Body z testu bude možné získať iba v prípade prítomnosti na cvičeniach v miestnosti I-H6.
- V AISe sú vypísané nasledujúce termíny skúšok (všetky v miestnosti F1-248):
- Piatok, 17. mája o 10:00 (predtermín).
- Piatok, 24. mája o 10:00.
- Piatok, 31. mája o 10:00.
- Piatok, 14. júna o 10:00 (predposledný skúškový termín, predovšetkým pre opravné termíny).
- Piatok, 21. júna o 10:00 (posledný skúškový termín, predovšetkým pre druhé opravné termíny).
- Prihlasovanie na skúšky je otvorené od dnešného poludnia.
- Na každý z termínov je potrebné prihlásiť sa najneskôr 24 hodín vopred.
- Každý z termínov je pre najviac 18 študentov.
- Pri prihlasovaní na skúškový termín nerozhoduje, či má ísť o riadny, prvý opravný, alebo druhý opravný termín. Myslite ale na to, aby vám vždy zostal dostatočný počet termínov aj na prípadné opravy (nie je teda napríklad rozumné využiť niektorý z posledných dvoch termínov skúšky ako riadny).
- Skúšky sa môžu zúčastniť všetci, čo zo semestra získajú aspoň 20 bodov (a budú teda mať aspoň teoretickú šancu na úspešné absolvovanie predmetu).
- Skúška bude pozostávať z:
- Písomnej časti (cca. 30 minút) za maximálne 10 bodov. (Bude potrebné poznať teóriu z prednášok a rozumieť jej.)
- Praktickej časti (cca. 90 minút) za maximálne 20 bodov spočívajúcej v riešení jednej programátorskej úlohy s odovzdávaním na testovač.
- Ústnej časti, ktorej čas a miesto konania budú upresnené v deň skúšky (treba počítať s tým, že všetky tri časti skúšky môžu dohromady trvať celý deň).
- Na úspešné absolvovanie skúšky je potrebné:
- Získať z písomnej a praktickej časti dohromady aspoň 15 bodov.
- Uspieť na ústnej časti skúšky.
- Spôsob, ktorým bude určené výsledné hodnotenie, je uvedený v pravidlách.
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 Pane budeme 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íklad resources. Následne v IntelliJ na tento adresár kliknime pravou myšou a označme ho prostredníctvom možnosti Mark Directory as --> Resources Root. Súbor styles.css ulož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 triedy Parent; tá je priamou podtriedou triedy Node. Medzi takéto triedy okrem Pane patria aj triedy pre ovládacie prvky ako napríklad Button alebo Label. Nepatria medzi ne napríklad triedy pre geometrické útvary (trieda Shape a 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ódy getChildren().add pre 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 obdobu VBox-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 eventHandlers vš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 addEventHandler pridávajúcu spracúvateľa udalosti. (Tá sa podobá napríklad na metódu setOnAction inštancie triedy Button s tým rozdielom, že metóda setOnAction nepridáva ďalšieho spracúvateľa, ale nastavuje nového jediného spracúvateľa. Aj Button však poskytuje metódu addEventHandler, 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ódu handle všetkých spracúvateľov zo zoznamu eventHandlers.
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);
}
}