Programovanie (2) v Jave
1-INF-166, letný semester 2023/24

Prednášky · Pravidlá · Softvér · Testovač
· Vyučujúcich predmetu možno kontaktovať mailom na adresách uvedených na hlavnej stránke. Hromadná mailová adresa zo zimného semestra v letnom semestri nefunguje.
· JavaFX: cesta k adresáru lib je v počítačových učebniach /usr/share/openjfx/lib.


Letný semester, prednáška č. 12

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

Oznamy

Zložitejšie ovládacie prvky a aplikácie s viacerými oknami: jednoduchý textový editor

Cieľom tohto textu je demonštrovať použitie niektorých zložitejších ovládacích prvkov v JavaFX (ako napríklad Menu, RadioButton, či ListView<T>) a štandardných dialógov (Alert resp. FileChooser), tvorbu aplikácií s viacerými oknami, ako aj mechanizmus tzv. vlastností.

Urobíme tak na ukážkovej aplikácii jednoduchého textového editora zvládajúceho nasledujúce úkony:

  • Vytvorenie prázdneho textového dokumentu a jeho následná modifikácia.
  • Vytvorenie textového dokumentu pozostávajúceho z nejakého fixného počtu náhodných cifier (nezmyselná funkcionalita slúžiaca len na ukážku možností triedy Menu).
  • Načítanie textu z používateľom zvoleného textového súboru (v našom prípade budeme predpokladať kódovanie UTF-8).
  • Uloženie textu do súboru.
  • V prípade požiadavky na zatvorenie neuloženého súboru výzva na jeho uloženie.
  • Do určitej miery aj zmena fontu, ktorým sa text vypisuje.

Základ aplikácie

Ako koreňovú oblasť hlavného okna aplikácie zvolíme inštanciu triedy BorderPane, s ktorou sme sa stretli už minule. Vzhľadom na o niečo väčší rozsah našej aplikácie sa navyše zdá rozumné nepracovať výhradne s lokálnymi premennými metódy start, ale dôležitejšie ovládacie prvky uchovávať ako premenné inštancie samotnej hlavnej triedy Editor, čo umožňí ich neskoršiu modifikáciu z rôznych pomocných metód. Takto si okrem iného budeme uchovávať aj referenciu primaryStage na hlavné okno aplikácie.

Základ programu tak môže vyzerať napríklad nasledovne.

package editor;

import javafx.application.*;
import javafx.stage.*;
import javafx.scene.*;
import javafx.scene.layout.*;
import javafx.scene.control.*;

public class Editor extends Application {

    private Stage primaryStage;

    public void start(Stage primaryStage) {
        this.primaryStage = primaryStage;

        BorderPane borderPane = new BorderPane();
        Scene scene = new Scene(borderPane);

        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Predpokladajme, že titulok hlavného okna má obsahovať text Textový editor, za ktorým v zátvorke nasleduje názov momentálne otvoreného súboru (alebo informácia o tom, že dokument nie je uložený v žiadnom súbore). Za zátvorkou sa navyše bude zobrazovať znak * v prípade, že sa obsah dokumentu od jeho posledného uloženia zmenil.

V danom momente otvorený súbor si budeme pamätať v premennej openedFile; v prípade, že nie je otvorený žiaden súbor, bude obsahom tejto premennej referencia null. Premenná openedFileChanged bude rovná true práve vtedy, keď od posledného uloženia dokumentu došlo k jeho zmene alebo keď dokument nie je uložený v žiadnom súbore. Metóda updateOpenedFileInformation dostane dvojicu premenných s rovnakým významom a nastaví podľa nich premenné openedFile a openedFileChanged; vhodným spôsobom pritom upraví aj titulok hlavného okna. Z metódy start budeme volať updateOpenedFileInformation(null, true), keďže po spustení aplikácie nebude dokument uložený v žiadnom súbore.

// ...

import java.io.*;

// ...

public class Editor extends Application {

    private File openedFile; 
    private boolean openedFileChanged; 
    
    // ...
    
    private void updateOpenedFileInformation(File file, boolean hasChanged) {
        openedFile = file;
        openedFileChanged = hasChanged;
        updatePrimaryStageTitle();
    }

    private void updatePrimaryStageTitle() {
        StringBuilder title = new StringBuilder("Textový editor (");
        if (openedFile == null) {
            title.append("neuložené v žiadnom súbore");
        } else {
            title.append(openedFile.getName());
        }
        title.append(")");
        if (openedFileChanged) {
            title.append("*");
        }
        primaryStage.setTitle(title.toString());
    }
    
    @Override
    public void start(Stage primaryStage) {
        // ...

        updateOpenedFileInformation(null, true);
        
        // ... 
    }
}

Ovládací prvok TextArea

Môžeme pokračovať pridaním kľúčového ovládacieho prvku našej aplikácie – priestoru na písanie samotného textu. Takýto ovládací prvok má v JavaFX názov TextArea a my inštanciu tejto triedy zvolíme za stredovú časť koreňovej oblasti border.

Podobne ako vyššie budeme referenciu textArea na prvok typu TextArea uchovávať ako premennú inštancie triedy Editor. Navyše si v premenných inštancie triedy Editor budeme pamätať aj kľúčové atribúty fontu, ktoré vhodne inicializujeme. Na font použitý v priestore textArea tieto atribúty aplikujeme v pomocnej metóde applyFont, ktorú zavoláme hneď po inicializácii premennej textArea.

// ...

import javafx.scene.text.*;
import javafx.geometry.*;

// ...

public class Editor extends Application {
    // ...

    private TextArea textArea;
    
    private String fontFamily = "Tahoma";
    private FontWeight fontWeight = FontWeight.NORMAL;
    private FontPosture fontPosture = FontPosture.REGULAR;
    private double fontSize = 16;

    // ...

    private void applyFont() {
        textArea.setFont(Font.font(fontFamily, fontWeight, fontPosture, fontSize));
    }

    // ...
    
    @Override
    public void start(Stage primaryStage) {
        // ...

        textArea = new TextArea();
        borderPane.setCenter(textArea);
        textArea.setPadding(new Insets(5, 5, 5, 5));
        textArea.setPrefWidth(1000);
        textArea.setPrefHeight(700);
        applyFont();

        // ... 
    }
}

Vlastnosti a spracovanie ich zmeny

Chceli by sme teraz pomocou metódy updateOpenedFileInformation prestaviť premennú openedFileChanged na true zakaždým, keď sa v textovom poli udeje nejaká zmena (viditeľný efekt to bude mať až po implementácii ukladania do súboru; po vhodných dočasných zmenách v našom programe ale môžeme funkčnosť nasledujúceho kódu testovať už teraz).

To znamená: zakaždým, keď sa zmení obsah priestoru textArea, potrebujeme vykonať nasledujúcu metódu:

private void handleTextAreaChange() {
    if (!openedFileChanged) {
        updateOpenedFileInformation(openedFile, true);
    }
}

Aby sme takúto akciu vedeli vykonať po každej zmene textového obsahu priestoru textArea, využijeme mechanizmus takzvaných vlastností. Pod vlastnosťou sa v JavaFX rozumie trieda implementujúca generické rozhranie Property<T> a možno si ju predstaviť ako „značne pokročilý obal pre nejakú hodnotu typu T”. Rozhranie Property<T> je rozšírením rozhrania ObservableValue<T> reprezentujúceho hodnoty typu T, ktorých zmeny možno v určitom presne definovanom zmysle sledovať.

Podobne ako sme k ovládacím prvkom pridávali spracúvateľov udalostí, možno k vlastnostiam a ďalším inštanciám rozhrania ObservableValue<T> pridávať „spracúvateľov zmien” volaných zakaždým, keď sa zmení nimi „obalená” hodnota. Týmito spracúvateľmi však teraz nebudú inštancie tried implementujúcich rozhranie EventHandler<T>, ale inštancie tried implementujúcich rozhranie ChangeListener<T>. To vyžaduje implementáciu jedinej metódy

void changed(ObservableValue<? extends T> observable, T oldValue, T newValue)

ktorá sa vykoná pri zmene hodnoty „obalenej” inštanciou observable z oldValue na newValue. Ide pritom o funkcionálne rozhranie, takže na jeho implementáciu možno použiť aj lambda výrazy. Registráciu inštancie rozhrania ChangeListener<T> ako „spracúvateľa zmeny” pre inštanciu observable rozhrania ObservableValue<T> vykonáme pomocou metódy observable.addListener.

Vráťme sa teraz k nášmu textovému editoru: textový obsah priestoru textArea je reprezentovaný ako vlastnosť, ktorú môžeme získať volaním metódy textArea.textProperty(). Ide tu o inštanciu triedy StringProperty implementujúcej rozhranie Property<String>. Môžeme tak pre ňu zaregistrovať „spracúvateľa zmien” pomocou metódy addListener, ktorej jediným argumentom bude inštancia takéhoto spracúvateľa. To možno realizovať pomocou anonymnej triedy

import javafx.beans.value.*;

// ...

public void start(Stage primaryStage) {
    // ...

    textArea.textProperty().addListener(new ChangeListener<>() {
        @Override
        public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
            handleTextAreaChange();
        }
    });

    // ...
}

alebo alternatívne prostredníctvom lambda výrazu

public void start(Stage primaryStage) {
    // ...

    textArea.textProperty().addListener((observable, oldValue, newValue) -> handleTextAreaChange());

    // ...
}

Poznámky:

  • Textový obsah priestoru typu TextArea je v JavaFX iba jednou z obrovského množstva vlastností, na ktorých zmenu možno reagovať. Ovládacie prvky typicky ponúkajú veľké množstvo vlastností, o ktorých sa možno dočítať v dokumentácii (ako príklady uveďme napríklad text alebo font tlačidla resp. textového popisku, rozmery okna, atď.).
  • V prípade, že nejaký ovládací prvok ponúka vlastnosť, ku ktorej sa pristupuje metódou cokolvekProperty, typicky ponúka aj metódu getCokolvek, ktorá vráti hodnotu obalenú danou vlastnosťou. V prípade, že možno meniť hodnotu danej vlastnosti, môže byť k dispozícii aj metóda setCokolvek.
  • Vlastnosti navyše možno medzi sebou aj vzájomne previazať (napríklad veľkosť kruhu vykresleného na scéne možno previazať s veľkosťou okna tak, aby bol polomer kruhu rovný tretine menšieho z rozmerov okna). S príkladom previazania vlastností sa stretneme nižšie.

Hlavné ponuky (MenuItem, Menu a MenuBar)

Kľúčovou súčasťou mnohých aplikácií bývajú hlavné ponuky (menu). V JavaFX ich do aplikácie možno pridať nasledujúcim spôsobom:

  • Do hlavného okna aplikácie sa umiestní ovládací prvok typu MenuBar, ktorý reprezentuje priestor, v ktorom sa budú jednotlivé ponuky zobrazovať. Každý MenuBar si udržiava zoznam ponúk v ňom umiestnených.
  • Každá ponuka (ako napríklad Súbor, Formát, ...) je reprezentovaná inštanciou triedy Menu, ktorá si okrem iného pamätá zoznam všetkých položiek danej ponuky.
  • Položka ponuky je reprezentovaná inštanciou triedy MenuItem. Každej položke možno napríklad pomocou metódy setOnAction priradiť akciu, ktorá sa má vykonať po jej zvolení používateľom.
  • Trieda Menu je podtriedou triedy MenuItem, z čoho okrem iného vyplýva, že položkou ponuky môže byť aj ďalšia podponuka.
  • Špeciálne položky ponúk sú reprezentované inštanciami tried CheckMenuItem (takúto položku ponuky možno zvolením zaškrtnúť resp. odškrtnúť) a SeparatorMenuItem (reprezentuje vodorovnú čiaru na vizuálne oddelenie častí ponuky).

V našej aplikácii teraz vytvoríme MenuBar s dvojicou ponúk Súbor a Formát s nasledujúcou štruktúrou.

Súbor (Menu)                                    Formát (Menu)
|                                               |
|- Nový (Menu) --- Prázdny súbor (MenuItem)     |- Písmo... (MenuItem)
|               |                               |  
|               |- Náhodné cifry (MenuItem)     |- Zalamovať riadky (CheckMenuItem)
|  
|- Otvoriť... (MenuItem)
|
|- Uložiť (MenuItem)
|
|- Uložiť ako... (MenuItem) 
|
|--------------- (SeparatorMenuItem)
|
|- Koniec (MenuItem)               

Vytvorenie takýchto ponúk realizujeme nasledujúcim kódom (v ktorom ponuky a ich položky reprezentujeme ako premenné inštancie triedy Editor, kým MenuBar vytvárame iba lokálne v metóde start).

// ...

public class Editor extends Application {
    // ...

    private Menu mFile;
    private Menu mFileNew;
    private MenuItem miFileNewEmpty;
    private MenuItem miFileNewRandom;
    private MenuItem miFileOpen;
    private MenuItem miFileSave;
    private MenuItem miFileSaveAs;
    private MenuItem miFileExit;
    private Menu mFormat;
    private MenuItem miFormatFont;
    private CheckMenuItem miFormatWrap;   

    // ...

    @Override
    public void start(Stage primaryStage) {   
        // ...

        MenuBar menuBar = new MenuBar();
        borderPane.setTop(menuBar);
        
        mFile = new Menu("Súbor");
        mFileNew = new Menu("Nový");
        miFileNewEmpty = new MenuItem("Prázdny súbor");
        miFileNewRandom = new MenuItem("Náhodné cifry");
        mFileNew.getItems().addAll(miFileNewEmpty, miFileNewRandom);
        miFileOpen = new MenuItem("Otvoriť...");
        miFileSave = new MenuItem("Uložiť");
        miFileSaveAs = new MenuItem("Uložiť ako...");
        miFileExit = new MenuItem("Koniec");
        mFile.getItems().addAll(mFileNew, miFileOpen, miFileSave, miFileSaveAs, new SeparatorMenuItem(), miFileExit); 
        
        mFormat = new Menu("Formát");
        miFormatFont = new MenuItem("Písmo...");
        miFormatWrap = new CheckMenuItem("Zalamovať riadky");
        miFormatWrap.setSelected(false);                        // Nie je nutne, kedze false je tu vychodzia hodnota
        mFormat.getItems().addAll(miFormatFont, miFormatWrap);
        
        menuBar.getMenus().addAll(mFile, mFormat);   

        // ...
    } 
}

K dôležitejším položkám môžeme priradiť aj klávesové skratky.

// ...

import javafx.scene.input.*;

// ...

@Override
public void start(Stage primaryStage) {
    // ...

    miFileNewEmpty.setAccelerator(new KeyCodeCombination(KeyCode.N, KeyCombination.CONTROL_DOWN)); // Ctrl + N
    miFileOpen.setAccelerator(new KeyCodeCombination(KeyCode.O, KeyCombination.CONTROL_DOWN));     // Ctrl + O
    miFileSave.setAccelerator(new KeyCodeCombination(KeyCode.S, KeyCombination.CONTROL_DOWN));     // Ctrl + S

    // ...
}
Vzhľad aplikácie po pridaní panelu s ponukami.

V rámci metódy updateOpenedFileInformation ešte môžeme zabezpečiť, aby položka miFileSave bola aktívna práve vtedy, keď má argument hasChanged hodnotu true (v opačnom prípade nie je čo ukladať).

private void updateOpenedFileInformation(File file, boolean hasChanged) { 
    // ...

    miFileSave.setDisable(!hasChanged);

    // ...
}

Výsledný vzhľad aplikácie je na obrázku vpravo.

Kontextové ponuky (ContextMenu)

Ďalším užitočným typom ponúk sú kontextové (resp. vyskakovacie) ponuky, ktoré sa zobrazia po kliknutí na nejaký ovládací prvok pravou myšou. Všimnime si, že TextArea už prichádza s prednastavenou kontextovou ponukou. Chceli by sme teraz túto ponuku nahradiť vlastnou, obsahujúcou rovnaké dve položky ako ponuka mFormat (budeme však musieť tieto položky vytvárať nanovo, pretože každá položka môže patriť iba do jedinej ponuky).

Jediný rozdiel oproti tvorbe hlavnej ponuky bude spočívať v použití inštancie triedy ContextMenu. Tú následne pomocou metódy setContextMenu priradíme ako kontextovú ponuku ovládaciemu prvku textArea. Výsledný vzhľad kontextovej ponuky je na obrázku vpravo.

Výsledný vzhľad kontextovej ponuky.
public class Editor extends Application {
    // ...
    
    private ContextMenu contextMenu;
    private MenuItem cmiFormatFont;
    private CheckMenuItem cmiFormatWrap;

    // ...

    @Override
    public void start(Stage primaryStage) {
        // ...       
        
        contextMenu = new ContextMenu();
        textArea.setContextMenu(contextMenu);
        cmiFormatFont = new MenuItem("Formát písma...");
        cmiFormatWrap = new CheckMenuItem("Zalamovať riadky");
        cmiFormatWrap.setSelected(false);
        contextMenu.getItems().addAll(cmiFormatFont, cmiFormatWrap);

        // ...
    } 
}

Priradenie udalostí k jednotlivým položkám ponúk

Môžeme teraz k jednotlivým položkám ponúk (okrem položiek typu CheckMenuItem) priradiť ich funkcionalitu, ktorá bude zatiaľ pozostávať z volania metód s takmer prázdnym telom. Všetky tieto metódy budú mať návratový typ boolean, pričom výstupná hodnota bude hovoriť o tom, či sa zamýšľaná akcia podarila alebo nie – táto črta sa nám zíde neskôr.

public class Editor extends Application {
    // ...
    
    private boolean newEmptyAction() {
        return true;  // Neskor nahradime zmysluplnym telom metody
    }
    
    private boolean newRandomAction() {
        return true;
    }
    
    private boolean openAction() {
        return true;
    }
    
    private boolean saveAction() {
        return true;
    }    
    
    private boolean saveAsAction() {
        return true;
    }
    
    private boolean exitAction() {
        return true;
    }    
    
    private boolean fontAction() {
        return true;
    }

    // ...

    @Override
    public void start(Stage primaryStage) {
        // ...

        miFileNewEmpty.setOnAction(event -> newEmptyAction());
        miFileNewRandom.setOnAction(event -> newRandomAction());
        miFileOpen.setOnAction(event -> openAction());
        miFileSave.setOnAction(event -> saveAction());
        miFileSaveAs.setOnAction(event -> saveAsAction());
        miFileExit.setOnAction(event -> exitAction());
        miFormatFont.setOnAction(event -> fontAction());

        // ...

        cmiFormatFont.setOnAction(event -> fontAction());

        // ...
    }
}

Previazanie vlastností

Na implementáciu funkcionality položiek miFormatWrap a cmiFormatWrap použijeme ďalšiu črtu vlastností – možnosť ich (obojstranného) previazania. Pri zmene niektorej z vlastností sa potom automaticky zmenia aj všetky vlastnosti s ňou previazané. V našom prípade navzájom previažeme vlastnosti hovoriace o zaškrtnutí položiek miFormatWrap a cmiFormatWrap a tiež vlastnosť hovoriacu o zalamovaní riadkov v textovom priestore textArea.

@Override
public void start(Stage primaryStage) {
    // ...

    textArea.wrapTextProperty().bindBidirectional(miFormatWrap.selectedProperty());
    textArea.wrapTextProperty().bindBidirectional(cmiFormatWrap.selectedProperty());

    // ...
}
  • Kým obojstranné previazanie vlastností realizujeme metódou bindBidirectional, jednostranné previazanie možno realizovať metódou bind. Vlastnosť typu implementujúceho rozhranie Property<T> pritom možno jednostranne previazať nielen s inou vlastnosťou typu implementujúceho toto rozhranie, ale možno ju metódou bind naviazať aj na ľubovoľnú hodnotu typu implementujúceho rozhranie ObservableValue<T>.
  • Previazanie vlastností možno využiť aj v rôzličných ďalších situáciách. Užitočným cvičením môže byť napísať aplikáciu, v ktorej hlavnom okne je vykreslený kruh, ktorého polomer ostáva rovný jednej tretine menšieho z rozmerov okna (a to aj v prípade, že sa rozmery okna zmenia). Pri tejto úlohe sa zídu metódy triedy Bindings.
  • Viac sa o vlastnostiach a ich previazaní možno dočítať napríklad v tomto tutoriáli.

Jednoduché dialógy (Alert)

Naším najbližším cieľom teraz bude implementácia metód newEmptyAction, newRandomAction a exitAction. Spoločnou črtou týchto akcií je, že vyžadujú zatvorenie práve otvoreného dokumentu. V takom prípade by sme ale chceli – pokiaľ boli v práve otvorenom dokumente vykonané nejaké neuložené zmeny – zobraziť výzvu na uloženie dokumentu ako na obrázku nižšie.

Na zobrazenie takejto výzvy využijeme jeden z jednoduchých štandardných dialógov – inštanciu triedy Alert.

// ...

import java.util.*;                                                   // Kvoli triede Optional

// ...

public class Editor extends Application {

    // ...

    private boolean saveBeforeClosingAlert() {
        if (openedFileChanged) {                                      // Ak dokument nie je ulozeny
            Alert alert = new Alert(Alert.AlertType.CONFIRMATION);    // Vytvor dialog typu Alert, variant CONFIRMATION
            alert.setTitle("Uložiť súbor?");                          // Nastav titulok dialogu
            alert.setHeaderText(null);                                // Dialog nebude mat ziaden "nadpis"
            alert.setContentText("Uložiť zmeny v súbore?");           // Nastav text dialogu

            ButtonType buttonTypeYes = new ButtonType("Áno");         // Vytvor instancie reprezentujuce typy tlacidiel
            ButtonType buttonTypeNo = new ButtonType("Nie");

            // Vdaka druhemu argumentu sa bude ako tlacidlo "Zrusit" spravat aj "krizik" v dialogovom okne vpravo hore:
            ButtonType buttonTypeCancel = new ButtonType("Zrušiť", ButtonBar.ButtonData.CANCEL_CLOSE);

            alert.getButtonTypes().setAll(buttonTypeYes, buttonTypeNo, buttonTypeCancel); // Nastav typy tlacidiel

            Optional<ButtonType> result = alert.showAndWait();        // Zobraz dialog a cakaj, kym sa zavrie
            if (result.get() == buttonTypeYes) {                      // Pokracuj podla typu stlaceneho tlacidla
                return saveAction();
            } else if (result.get() == buttonTypeNo) {
                return true;
            } else {
                return false;
            }
        } else {                                                      // Ak je dokument ulozeny, netreba robit nic
            return true;
        }
    }
    
    // ...
}

Dialóg typu Alert by ideálne mal obsahovať práve jedno tlačidlo, ktorého „dáta” sú nastavené na CANCEL_CLOSE. V opačnom prípade sa „krížik” v pravom hornom rohu dialógového okna nemusí správať korektne (v niektorých prípadoch sa dialógové okno po kliknutí naň dokonca ani nemusí zavrieť). Toto obmedzenie sa ale dá eliminovať ďalšími nastaveniami dialógu.

Môžeme teraz pristúpiť k implementácii spomínaných troch akcií.

public class Editor extends Application {
    // ...

    private final int N = 1000;  // V metode newRandomAction budeme generovat N riadkov o N nahodnych cifrach

    // ...

    private boolean newEmptyAction() {               
        if (saveBeforeClosingAlert()) {               // Pokracuj len ak sa podarilo zavriet dokument  
            updateOpenedFileInformation(null, true);  // Nebude teraz otvoreny ziaden subor...
            textArea.clear();                         // Zmazeme obsah textoveho priestoru textArea
            return true;
        } else {
            return false;
        }
    }

    private boolean newRandomAction() {
        if (newEmptyAction()) {                           // Skus vytvorit novy subor a pokracuj len, ak sa to podarilo
            StringBuilder builder = new StringBuilder();  // Vygeneruj retazec o N x N nahodnych cifrach
            Random random = new Random();
            for (int i = 1; i <= N; i++) {
                for (int j = 1; j <= N; j++) {
                    builder.append(random.nextInt(10));
                }
                builder.append(System.lineSeparator());
            }
            textArea.setText(builder.toString());         // Vypis vygenerovany retazec do textoveho priestoru textArea
            return true;
        } else {
            return false;
        }
    }

    private boolean exitAction() {
        if (saveBeforeClosingAlert()) {   // Pokracuj len ak sa podarilo zavriet dokument         
            Platform.exit();              // Ukonci aplikaciu
            return true;
        } else {
            return false;
        }
    }    
}

Ďalšie typy jednoduchých dialógov

V JavaFX možno využívať aj ďalšie preddefinované jednoduché dialógy – od dialógu Alert s odlišným AlertType až po dialógy ako TextInputDialog alebo ChoiceDialog.

  • Viac sa o preddefinovaných jednoduchých dialógoch v JavaFX možno dočítať napríklad tu.

Zatvorenie hlavného okna aplikácie „krížikom”

Metódu exitAction(), ktorá sa vykoná zakaždým, keď používateľ zvolí v hlavnej ponuke možnosť Súbor --> Koniec, sme implementovali tak, aby sa najprv zobrazila prípadná výzva na uloženie súboru. Táto výzva pritom v niektorých prípadoch môže ukončeniu aplikácie aj zamedziť (napríklad keď používateľ klikne na tlačidlo Zrušiť alebo keď bude tlačidlom Zrušiť ukončený ukladací dialóg).

Ak ale používateľ aplikáciu zavrie kliknutím na „krížik” v pravom hornom rohu okna, aplikácia sa zavrie bez akejkoľvek ďalšej akcie. Chceli by sme pritom, aby sa vykonali rovnaké operácie, ako pri ukončení aplikácie pomocou položky hlavnej ponuky. To môžeme zariadiť nastavením spracúvateľa udalosti, ktorá vznikne pri požiadavke na zatvorenie okna. Ak túto udalosť v rámci jej spracovania skonzumujeme, zatvoreniu okna sa v konečnom dôsledku zamedzí.

public class Editor extends Application {
    // ...

    private boolean handleStageCloseRequest(WindowEvent event) {
        if (saveBeforeClosingAlert()) {  // Ak sa podarilo zavriet subor
            return true;
        } else {                         // Ak sa nepodarilo zavriet subor ... 
            event.consume();             // ... nechceme ani zavriet okno 
            return false;
        }
    }

    // ...

    @Override
    public void start(Stage primaryStage) {
        // ...
        
        primaryStage.setOnCloseRequest(event -> handleStageCloseRequest(event));
    
        // ...
    }
}


Zvyšnú funkcionalitu aplikácie implementujeme nabudúce.