Programovanie (2) v Jave
1-INF-166, letný semester 2023/24
Letný semester, prednáška č. 10
Oznamy
- Stredajšie cvičenia budú aj napriek dekanskému voľnu (účasť je samozrejme dobrovoľná, rovnako ako aj na ostatných cvičeniach, na ktorých sa nepíše test). Bude zverejnených niekoľko nebodovaných úloh zameraných na látku z dnešnej prednášky (úlohy na JavaFX sa nebudú odovzdávať na testovač).
- Štvrtú domácu úlohu treba odovzdať do pondelka 26. apríla, 9:00 a tretiu bonusovú úlohu do stredy 28. apríla, 11:30.
- Najneskôr na budúcej prednáške, prosím, dajte vedieť o prípadných námietkach proti vypísaným termínom skúšky.
Lambda výrazy
Príklad: komparátor ako lambda výraz
Pripomeňme si z piatej prednášky zostupné triedenie zoznamu celých čísel pomocou metódy Collections.sort, ktorá ako druhý parameter berie komparátor, čiže inštanciu nejakej triedy implementujúcej rozhranie Comparator<? super Integer>. Videli sme pritom tri možné spôsoby, ako komparátor definovať:
- Vytvoriť „bežnú” triedu implementujúcu rozhranie Comparator<Integer> a metódu sort zavolať pre novovytvorenú inštanciu tejto triedy.
- To isté s použitím lokálnej triedy:
import java.util.*; public class Trieda { public static void main(String[] args) { class DualComparator implements Comparator<Integer> { @Override public int compare(Integer o1, Integer o2) { return o2.compareTo(o1); } } List<Integer> a = new ArrayList<>(); a.add(6); a.add(1); a.add(3); a.add(2); a.add(3); Collections.sort(a, new DualComparator()); System.out.println(a); } }
- Definíciu triedy a vytvorenie jej inštancie spojiť do jediného príkazu s využitím mechanizmu anonymných tried:
import java.util.*; public class Trieda { import java.util.*; public class Trieda { public static void main(String[] args) { List<Integer> a = new ArrayList<>(); a.add(6); a.add(1); a.add(3); a.add(2); a.add(3); Collections.sort(a, new Comparator<>() { @Override public int compare(Integer o1, Integer o2) { return o2.compareTo(o1); } }); System.out.println(a); } }
Princíp a syntax lambda výrazov v Jave
Ďalšie príklady použitia lambda výrazov
Úvod do JavaFX
JavaFX je knižnica, ktorú možno využiť ako nástroj na tvorbu aplikácií s grafickým používateľským rozhraním (GUI). Namiesto konzolových aplikácií teda budeme v nasledujúcich niekoľkých prednáškach vytvárať aplikácie grafické (typicky pozostávajúce z jedného alebo niekoľkých okien s ovládacími prvkami, akými sú napríklad tlačidlá, textové polia, a podobne).
- Staršia verzia JavaFX bola priamo súčasťou Java SE 8 a mala dobrú podporu v NetBeans (ide o verzie nainštalované v počítačových učebniach).
- Novšie verzie Java SE už JavaFX neobsahujú, ale táto je vyvíjaná ako samostatný modul, ktorý je potrebné aj samostatne nainštalovať. Podpora v NetBeans je slabšia.
- Viac o inštalácii JavaFX pre novšie verzie Java SE a NetBeans (návod popisuje aj vytvorenie JavaFX projektu v novších verziách NetBeans).
- Dokumentácia a tutoriály pre JavaFX 8.
- Dokumentácia a tutoriály pre JavaFX 14.
Vytvorenie aplikácie s jedným grafickým oknom
Minimalistickú JavaFX aplikáciu zobrazujúcu jedno prázdne okno o 300 krát 250 pixeloch s titulkom „Hello, World!” vytvoríme nasledovne:
package aplikacia;
import javafx.application.*;
import javafx.stage.*;
import javafx.scene.*;
import javafx.scene.layout.*;
public class Aplikacia extends Application {
@Override
public void start(Stage primaryStage) {
Pane pane = new Pane();
Scene scene = new Scene(pane, 300, 250);
primaryStage.setTitle("Hello, World!");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Uvedený kód si teraz rozoberme:
- Hlavná trieda JavaFX aplikácie (tzn. trieda obsahujúca metódu main) sa vyznačuje tým, že dedí od abstraktnej triedy Application definovanej v balíku javafx.application, ktorý je potrebné importovať.
- Každá trieda dediaca od triedy Application musí implementovať jej abstraktnú metódu start, ktorej argumentom je objekt primaryStage typu Stage reprezentujúci hlavné grafické okno aplikácie (trieda Stage je definovaná v balíku javafx.stage, ktorý je potrebné importovať). Metóda start sa vykoná hneď po spustení aplikácie. V rámci metódy start sa typicky vytvárajú jednotlivé ovládacie prvky aplikácie a špecifikujú sa ich vlastnosti.
- V našom prípade je kľúčovým riadkom metódy start volanie primaryStage.show(), ktorým zobrazíme hlavné okno aplikácie. Bez tohto volania by aplikácia bežala „na pozadí”.
- Volaním primaryStage.setTitle("Hello, World!") nastavíme titulok hlavného okna na text „Hello, World!”.
- Uvedené dva riadky často stačia na zobrazenie grafického okna s titulkom „Hello, World!” a „náhodne” zvolenou veľkosťou. V závislosti od systému sa však môže stať aj to, že sa nezobrazí nič – grafické okno totiž zatiaľ nič neobsahuje a systém nemá ako „rozumne” vypočítať jeho veľkosť; môže teda túto situáciu vyhodnotiť aj tak, že ešte nie je čo zobraziť.
- Zvyšnými riadkami už len hovoríme, že „obsahom” hlavného okna má byť prázdna oblasť o veľkosti 300 krát 250 pixelov:
- Kontajnerom pre obsah okna je trieda Scene. Ide tu o analógiu s divadelnou terminológiou: okno zodpovedá javisku; na javisku následne možno umiestniť scénu pozostávajúcu z jednotlivých rekvizít. Scénu scene možno oknu primaryStage priradiť volaním primaryStage.setScene(scene). Trieda Scene je definovaná v balíku javafx.scene, ktorý je potrebné importovať.
- Scéna je interpretovaná ako hierarchický strom uzlov (detaily neskôr), pričom uzlami môžu byť napríklad oblasti, ale aj ovládacie prvky ako napríklad tlačidlá, či textové polia. Volaním konštruktora Scene scene = Scene(pane, 300, 250) vytvoríme scénu o rozmeroch 300 krát 250 pixelov, ktorej koreňovým uzlom je objekt pane; ten bude v našom prípade reprezentovať prázdnu oblasť.
- Volaním konštruktora Pane pane = new Pane() vytvoríme novú oblasť pane. Tá môže neskôr slúžiť ako kontajner pre pridávanie rôznych ovládacích prvkov a podobne. Trieda Pane je definovaná v balíku javafx.scene.layout, ktorý je potrebné importovať.
- Metóda main JavaFX aplikácie typicky pozostáva z jediného riadku, v ktorom sa volá statická metóda launch triedy Application. Tá sa postará o vytvorenie inštancie našej triedy Aplikacia, o vytvorenie hlavného grafického okna aplikácie, ako aj o následné zavolanie metódy start, ktorá dostane vytvorené okno ako argument.
Okno s niekoľkými jednoduchými ovládacími prvkami
Podbne ako v príklade vyššie vytvorme aplikáciu pozostávajúcu s jediného grafického okna:
package aplikacia;
import javafx.application.*;
import javafx.stage.*;
import javafx.scene.*;
import javafx.scene.layout.*;
public class Aplikacia extends Application {
@Override
public void start(Stage primaryStage) {
Pane pane = new Pane();
Scene scene = new Scene(pane, 340, 100);
primaryStage.setTitle("Zadávanie textu");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Pridáme teraz do hlavného okna niekoľko ovládacích prvkov tak, ako na obrázku vpravo. Naším cieľom bude vytvorenie aplikácie umožňujúcej zadať text, ktorý sa pri kliknutí na tlačidlo OK zjaví v textovom popisku červenej farby. Ovládacie prvky ako textové pole alebo tlačidlo sú definované v balíku javafx.scene.control, ktorý je tak nutné importovať. Podobne na prácu s fontmi budeme potrebovať balík javafx.scene.text a na prácu s farbami balík javafx.scene.paint.
Začnime s pridaním textového popisku „Zadaj text”. Takéto textové popisky sú v JavaFX reprezentované triedou Label, pričom popisok label1 obsahujúci nami požadovaný text vytvoríme nasledovne:
Label label1 = new Label("Zadaj text:");
Rovnako dobre by sme mohli použiť aj konštruktor bez argumentov, ktorý je ekvivalentný volaniu konštruktora s argumentom "" – text popisku label1 možno upraviť aj neskôr volaním label1.setText("Nový text").
Po jeho vytvorení ešte musíme popisok label1 pridať do našej scény – presnejšie do oblasti pane, ktorá je jej koreňovým uzlom (čo znamená, že všetky ostatné uzly budú umiestnené v tejto oblasti). Vytvorený popisok label1 teda pridáme do zoznamu synov oblasti pane nasledujúcim volaním:
pane.getChildren().add(label1);
Následne môžeme upraviť niektoré vlastnosti vytvoreného popisku, ako napríklad jeho pozíciu a font:
label1.setFont(Font.font("Tahoma", FontWeight.BOLD, 12));
label1.setLayoutX(20);
label1.setLayoutY(10);
Analogicky vytvoríme aj ostatné komponenty:
TextField textField = new TextField();
pane.getChildren().add(textField);
textField.setFont(Font.font("Tahoma", FontWeight.BOLD, 12));
textField.setLayoutX(20);
textField.setLayoutY(30);
textField.setPrefWidth(300);
Label label2 = new Label("(Zatiaľ nebolo zadané nič)");
pane.getChildren().add(label2);
label2.setFont(Font.font("Tahoma", 12));
label2.setTextFill(Color.RED);
label2.setLayoutX(20);
label2.setLayoutY(70);
Button button = new Button("OK");
pane.getChildren().add(button);
button.setFont(Font.font("Tahoma", FontWeight.BOLD, 12));
button.setLayoutX(280);
button.setLayoutY(60);
button.setPrefWidth(40);
button.setPrefHeight(30);
Uvedený spôsob grafického návrhu scény má však hneď dva zásadné nedostatky:
- Môžeme si všimnúť, že takéto pevné rozloženie ovládacích prvkov na scéne nevyzerá dobre, keď zmeníme veľkosť okna. Provizórne môžeme tento problém vyriešiť tým, že menenie rozmerov okna jednoducho zakážeme: primaryStage.setResizable(false). Takéto riešenie má však ďaleko od ideálneho. Odporúčaným prístupom je využiť namiesto triedy Pane niektorú z jej „inteligentnejších” podtried umožňujúcich (polo)automatické škálovanie scény v závislosti od veľkosti okna. V takom prípade sa absolútne súradnice ovládacích prvkov zvyčajne vôbec nenastavujú.
- Formát jednotlivých ovládacích prvkov (ako napríklad font alebo farba) by sa po správnosti nemal nastavovať priamo v zdrojovom kóde. Namiesto toho je odporúčaným prístupom využitie štýlov definovaných v pomocných súboroch JavaFX CSS. Takto je možné meniť formátovanie bez väčších zásahov do zdrojového kódu.
Obidvoma týmito problematikami sa budeme zaoberať v rámci nasledujúcej prednášky.
Oživenie ovládacích prvkov (spracovanie udalostí)
Dokončime našu jednoduchú aplikáciu so zadávaním textu pridaním jej kľúčovej funkcionality: po stlačení tlačidla OK (t.j. button) sa má do „červeného” popisku prekopírovať text zadaný používateľom do textového poľa.
Po stlačení tlačidla button je systémom (Java Virtual Machine) vygenerovaná tzv. udalosť, ktorá je v tomto prípade typu ActionEvent. Udalosť je teda akýsi objekt nesúci informáciu o tom, že bolo stlačené dané tlačidlo. Každé tlačidlo – objekt typu Button – má navyše k dispozícii (zdedenú) metódu
public final void setOnAction(EventHandler<ActionEvent> value)
umožňujúcu „zaregistrovať” pre dané tlačidlo jeho spracovávateľa udalostí typu ActionEvent. Ním môže byť ľubovoľná trieda implementujúca rozhranie EventHandler<ActionEvent>, ktoré vyžaduje implementáciu jedinej metódy
void handle(ActionEvent event)
Po zaregistrovaní objektu eventHandler ako spracovávateľa udalostí ActionEvent pre tlačidlo button volaním
button.setOnAction(eventHandler);
sa po každom stlačení tlačidla button vykoná metóda eventHandler.handle.
Nami požadovanú funkcionalitu tlačidla button tak vieme vyjadriť napríklad pomocou lokálnej triedy ButtonActionEventHandler:
import javafx.event.*;
...
public void start(Stage primaryStage) {
...
class ButtonActionEventHandler implements EventHandler<ActionEvent> {
@Override
public void handle(ActionEvent event) {
label2.setText(textField.getText());
}
}
EventHandler<ActionEvent> eventHandler = new ButtonActionEventHandler();
button.setOnAction(eventHandler);
...
}
Skrátene môžeme to isté napísať s použitím anonymnej triedy:
public void start(Stage primaryStage) {
...
button.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
label2.setText(textField.getText());
}
});
...
}
Ak si navyše uvedomíme, že rozhranie EventHandler pozostáva z jedinej metódy, môžeme tento zápis ešte ďalej zjednodušiť použitím lambda výrazu:
public void start(Stage primaryStage) {
...
button.setOnAction((ActionEvent e) -> {
label2.setText(textField.getText());
});
...
}
Podrobnejšie sa spracúvaním udalostí v JavaFX budeme zaoberať na nasledujúcej prednáške.
Geometrické útvary
Špeciálnym typom uzlov, ktoré možno umiestňovať do scén, sú geometrické útvary ako napríklad Circle, Rectangle, Arc, Ellipse, Line, Polygon, atď. Všetky útvary dedia od spoločnej abstraktnej nadtriedy Shape. Sú definované v balíku javafx.scene.shape, ktorý je nutné na prácu s nimi importovať.
Aj keď útvary nevedia vyvolať udalosť typu ActionEvent, môžu vyvolávať udalosti iných typov. Napríklad kliknutie na útvar myšou vyústi v udalosť typu MouseEvent (definovanú v balíku javafx.scene.input) a spracovávateľa takejto udalosti možno pre útvar shape zaregistrovať pomocou metódy shape.setOnMouseClicked.
Nasledujúci kód vykreslí „tabuľku” o 10 krát 10 útvaroch, pričom pre každý sa náhodne určí, či pôjde o štvorec, alebo o kruh. Farba každého z útvarov sa taktiež určí náhodne. Navyše po kliknutí myšou na ktorýkoľvek z útvarov sa jeho farba náhodne zmení.
package aplikacia;
import javafx.application.*;
import javafx.stage.*;
import javafx.scene.*;
import javafx.scene.layout.*;
import javafx.scene.shape.*;
import javafx.scene.paint.*;
import javafx.scene.input.*;
import java.util.*;
public class Aplikacia extends Application {
Color randomColor(Random random) {
return Color.color(random.nextDouble(), random.nextDouble(), random.nextDouble());
}
@Override
public void start(Stage primaryStage) {
Pane pane = new Pane();
Random random = new Random();
for (int i = 0; i <= 9; i++) {
for (int j = 0; j <= 9; j++) {
Shape shape;
boolean isSquare = random.nextBoolean();
Color color = randomColor(random);
if (isSquare) {
shape = new Rectangle(i * 60 + 5, j * 60 + 5, 50, 50);
} else {
shape = new Circle(i * 60 + 30, j * 60 + 30, 25);
}
shape.setFill(color);
shape.setOnMouseClicked((MouseEvent e) -> {
shape.setFill(randomColor(random));
});
pane.getChildren().add(shape);
}
}
Scene scene = new Scene(pane, 600, 600);
primaryStage.setTitle("Geometrické útvary");
primaryStage.setResizable(false);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}