Programovanie (1) v C/C++
1-INF-127, ZS 2024/25

Úvod · Pravidlá · Prednášky · Softvér · Testovač
· Kontaktujte nás pomocou e-mailovej adresy E-prg.png (bude odpovedať ten z nás, kto má príslušnú otázku na starosti alebo kto má práve čas).
· Prosíme študentov, aby si pravidelne čítali e-maily na @uniba.sk adrese alebo aby si tieto emaily preposielali na adresu, ktorú pravidelne čítajú.


Letný semester, prednáška č. 11

Z Programovanie
Skočit na navigaci Skočit na vyhledávání

Oznamy

  • Štvrtú domácu úlohu je potrebné odovzdať do stredy 27. apríla, 13:10.
  • Stredajšie cvičenia nebudú, keďže sa v tomto čase koná Študentská vedecká konferencia. Na testovači ale bude zverejnených niekoľko nebodovaných úloh so zameraním na JavaFX.
  • Dnes po prednáške bude zverejnené zadanie piatej domácej úlohy, ktorú bude potrebné odovzdať do stredy 11. mája, 13:10.

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.
Výsledný vzhľad 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 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
Vzhľad aplikácie po nastavení okrajov.

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(Integer.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:

Vzhľad aplikácie po dokončení návrhu rozloženia ovládacích prvkov v mriežke.
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);
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 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(Integer.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 actionListeners všetkých spracová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 spracovávateľ; hoci je to najčastejší prípad, nebýva to vždy tak).
  • Poskytovať metódu addActionListener pridávajúcu spracovávateľa udalosti. (Tá sa podobá napríklad na metódu Button.setOnAction s tým rozdielom, že Button.setOnAction nepridáva ďalšieho spracovávateľa, ale pridáva nového jediného spracovávateľa. Aj Button však poskytuje metódu addEventHandler, ktorá je dokonca o niečo všeobecnejšia, než bude naša metóda addActionListener).
  • Poskytovať metódu fireAction, ktorá udalosť spustí. To si vyžaduje zavolať metódu handle všetkých spracovávateľov zo zoznamu actionListeners.
package events;

import java.util.*;
import javafx.event.*;

public class MyEventSender {
    private String name;
    private ArrayList<EventHandler<MyEvent>> actionListeners;  // Zoznam spracovavatelov udalosti

    public MyEventSender(String name) {
        this.name = name;
        actionListeners = new ArrayList<>();
    }

    public String getName() {
        return this.name;
    }

    public void addActionListener(EventHandler<MyEvent> handler) {   // Metoda pridavajuca spracovavatela udalosti
        actionListeners.add(handler);
    }

    public void fireAction(int type) {                               // Metoda spustajuca udalost
        MyEvent event = new MyEvent(this, "UDALOST " + type);
        for (EventHandler<MyEvent> eventHandler : actionListeners) {
            eventHandler.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.addActionListener(handler);
        sender2.addActionListener((MyEvent event) -> {
            System.out.println("Spracuvavam udalost " + event.getMessage() + " inym sposobom.");
        });
        sender2.addActionListener(handler);
        sender2.addActionListener((MyEvent 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 spracová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 spracová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> eventHandler : actionListeners) {
            eventHandler.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).
  • Spracová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 MoveDirection moveDirection;         // Aktualny smer pohybu kruhu

    // Metoda, ktora na scene scene posunie kruh circle smerom moveDirection o pocet pixelov delta:
    private void moveCircle(Scene scene, Circle circle, MoveDirection moveDirection, double delta) {
        double newX;
        double 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 
                    this.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   
                    this.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
                    this.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
                    this.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 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 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(scene, circle, moveDirection, 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);
    }
}

Odkazy