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 č. 13

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

Oznamy

Jednoduchý textový editor: pokračovanie

Otvárací a ukladací dialóg (FileChooser)

Implementujeme teraz kľúčovú funkcionalitu textového editora – metódy openAction, saveAction a saveAsAction realizujúce otváranie resp. ukladanie textových súborov.

Na výber súboru môžeme pri oboch typoch akcií využiť preddefinovaný dialóg FileChooser.

Pomocné metódy realizujúce výber súboru na otvorenie resp. uloženie môžu s jeho použitím vyzerať napríklad nasledovne.

public class Editor extends Application {    
    // ...

    /**
     * Pomocna metoda, ktora vytvori dialog typu FileChooser a prednastavi niektore jeho vlastnosti.
     * @return Vytvoreny dialog typu FileChooser.
     */
    private FileChooser prepareFileChooser() {
        FileChooser fileChooser = new FileChooser();                                // Vytvorenie dialogoveho okna
        fileChooser.setInitialDirectory(new File(System.getProperty("user.dir")));  // Nastavenie vychodzieho adresara
        fileChooser.getExtensionFilters().addAll(                                   // Filtre na pripony
                new FileChooser.ExtensionFilter("Textové súbory (*.txt)", "*.txt"),
                new FileChooser.ExtensionFilter("Všetky súbory (*.*)", "*.*"));
        return fileChooser;
    }

    /**
     * Pomocna metoda realizujuca vyber suboru na otvorenie.
     * @return Vybrany subor alebo null v pripade, ze ziaden subor vybrany nebol.
     */
    private File chooseFileToOpen() {
        FileChooser fileChooser = prepareFileChooser();
        fileChooser.setTitle("Otvoriť");
        return fileChooser.showOpenDialog(primaryStage);
    }

    /**
     * Pomocna metoda realizujuca vyber suboru na ulozenie.
     * @return Vybrany subor alebo null v pripade, ze ziaden subor vybrany nebol.
     */
    private File chooseFileToSave() {
        FileChooser fileChooser = prepareFileChooser();
        fileChooser.setTitle("Uložiť");
        return fileChooser.showSaveDialog(primaryStage);
    }
    
    // ...
}

Môžeme teraz pristúpiť k samotnej implementácii otváracích a ukladacích metód. Na čítanie a zápis do textového súboru použijeme statické metódy triedy Files z balíka java.nio.file, ktoré načítajú kompletný obsah súboru do reťazca resp. zapíšu obsah reťazca do súboru. Takýto prístup síce nie je úplne vhodný pre veľmi veľké súbory, no na manipuláciu s nimi by bolo potrebné našu aplikáciu vylepšiť aj v iných ohľadoch – v prvom rade by sme v rámci ovládacieho prvku typu TextArea nemali uchovávať kompletný obsah textového súboru. Použijeme preto teraz spomenuté jednoduché riešenie, hoci rovnako dobre by sme mohli použiť napríklad aj BufferedReader a PrintStream. U všetkých textových súborov predpokladáme kódovanie UTF-8.

// ...

import java.nio.file.*;

// ...

public class Editor extends Application {    
    // ...
    
    private String loadFromFile(File file) throws IOException {
        return Files.readString(Paths.get(file.getPath()));
    }

    private void writeToFile(File file, String s) throws IOException {
        Files.writeString(Paths.get(file.getPath()), s);
    }

    // ...

    private boolean openAction() {
        if (saveBeforeClosingAlert()) {                        // Ak sa podarilo zavriet dokument
            File file = chooseFileToOpen();                    // Vyber subor na otvorenie
            if (file != null) {                                // Ak bol nejaky subor vybrany ...
                try {
                    textArea.setText(loadFromFile(file));      // ... vypis jeho obsah do textArea
                    updateOpenedFileInformation(file, false);  // Aktualizuj informacie o otvorenom subore
                } catch (IOException e) {
                    System.err.println("Nieco sa pokazilo.");
                }
                return true;                                   
            } else {
                return false;
            }
        } else {
            return false;
        }
    }

    private boolean saveAction() {
        if (openedFile == null) {                                // Ak nebol otvoreny ziaden subor ... 
            return saveAsAction();                               // ... realizuj to iste ako pri "Ulozit ako"
        } else {                                                 // V opacnom pripade ...
            try {
                writeToFile(openedFile, textArea.getText());     // ... prepis aktualne otvoreny subor
                updateOpenedFileInformation(openedFile, false);  // Aktualizuj informacie o otvorenom subore
            } catch (IOException e) {
                System.err.println("Nieco sa pokazilo.");
            }
            return true;
        }
    }    
    
    private boolean saveAsAction() {
        File file = chooseFileToSave();                    // Vyber subor, do ktoreho sa ma ukladat
        if (file != null) {                                // Ak bol nejaky subor vybrany ...
            try {
                writeToFile(file, textArea.getText());     // ... zapis don obsah textArea
                updateOpenedFileInformation(file, false);  // Aktualizuj informacie o otvorenom subore
            } catch (IOException e) {
                System.err.println("Nieco sa pokazilo.");
            }
            return true;
        } else {
            return false;
        }
    }

    // ...
}

Vlastné dialógy (aplikácie s viacerými oknami)

Na rozdiel od dialógov na výber súboru JavaFX neobsahuje ako štandardnú súčasť žiaden dialóg na výber fontu (hoci viaceré takéto dialógy možno nájsť v externých knižniciach). Vytvoríme si teda dialóg vlastný (nebude úplne dokonalý), čo využijeme predovšetkým ako príležitosť na demonštráciu niekoľkých ďalších aspektov práce s JavaFX:

  • Dialóg na výber fontu bude realizovaný pomocou ďalšieho okna (Stage) aplikácie; ukážeme si teda, ako možno tvoriť aplikácie s viacerými oknami.
  • Tento dialóg navyše miestami schválne navrhneme trochu suboptimálne, čo nám umožní demonštrovať použitie ďalších dvoch ovládacích prvkov v JavaFX: ListView<T> a predovšetkým RadioButton.

Dialóg na výber fontu budeme reprezentovať inštanciou samostatnej triedy FontDialog s metódou showFontDialog, ktorá tento dialóg zobrazí a po jeho ukončení vráti informácie o vybratom fonte. Návratovou hodnotou tejto metódy bude inštancia novovytvorenej triedy FontAttributes, ktorá bude slúžiť ako „obal” pre niektoré atribúty fontu (z inštancií triedy Font, ktorá nie je navrhnutá úplne najideálnejším spôsobom, sa niektoré z týchto atribútov zisťujú pomerne ťažko).

package editor;

import javafx.scene.text.*;

public class FontAttributes {
    private String family;
    private FontWeight weight;
    private FontPosture posture;
    private double size;

    public String getFamily() {
        return family;
    }

    public FontWeight getWeight() {
        return weight;
    }

    public FontPosture getPosture() {
        return posture;
    }

    public double getSize() {
        return size;
    }

    public FontAttributes(String family, FontWeight weight, FontPosture posture, double size) {
        this.family = family;
        this.weight = weight;
        this.posture = posture;
        this.size = size;
    }
}
package editor;

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

public class FontDialog {
    private Stage stage;

    public FontDialog() {
        // Zatial iba zaklad implementacie.

        BorderPane borderPane = new BorderPane();

        Scene scene = new Scene(borderPane);

        stage = new Stage();
        stage.initStyle(StageStyle.UTILITY);             // Prejavi sa v systemovych ikonach okna
        stage.initModality(Modality.APPLICATION_MODAL);  // Pocas zobrazenia okna sa nebude dat pristupovat k ostatnym oknam
        stage.setScene(scene);
        stage.setTitle("Formát písma");
    }

    /**
     * Metoda, ktora zobrazi dialogove okno stage, pricom vychodzie nastavenie ovladacich prvkov bude urcene parametrom
     * oldFontAttributes. Po ukonceni dialogu vrati atributy fontu vybrane pouzivatelom.
     * @param oldFontAttributes Atributy fontu, ktorym sa text zobrazoval doposial. Na zaklade nich sa nastavia
     *                          vlastnosti zobrazovanych ovladacich prvkov dialogu.
     * @return                  Nova instancia triedy FontAttributes reprezentujuca atributy fontu vybraneho v dialogu
     *                          pouzivatelom. V pripade ukoncenia dialogu inym sposobom, nez potvrdenim, je navratovou
     *                          hodnotou metody referencia null.
     */
    public FontAttributes showFontDialog(FontAttributes oldFontAttributes) {
        // Zatial iba zaklad implementacie.

        stage.showAndWait(); // Od metody show sa showAndWait lisi tym, ze zvysok kodu sa vykona az po zavreti okna
        return new FontAttributes("Tahoma", FontWeight.NORMAL, FontPosture.REGULAR, 16); // Neskor nahradime zmysluplnym kodom
    }
}

V hlavnej triede Editor vytvoríme inštanciu triedy FontDialog a implementujeme metódu fontAction.

public class Editor extends Application {
    // ...

    private FontDialog fontDialog; 

    // ...

    private boolean fontAction() {
        FontAttributes fontAttributes = fontDialog.showFontDialog(
                new FontAttributes(fontFamily, fontWeight, fontPosture, fontSize));
        if (fontAttributes != null) {
            fontFamily = fontAttributes.getFamily();
            fontWeight = fontAttributes.getWeight();
            fontPosture = fontAttributes.getPosture();
            fontSize = fontAttributes.getSize();
            applyFont();
        }
        return true;
    }

    // ...

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

        fontDialog = new FontDialog();

        // ...
    }
}

Ovládacie prvky ListView a RadioButton

Výsledný vzhľad dialógového okna.

Pridáme teraz do nášho dialógového okna jednotlivé ovládacie prvky tak, aby dialóg vyzeral ako na obrázku vpravo. Okrem dvojice tlačidiel pozostáva dialóg z ovládacích prvkov, s ktorými sme sa doposiaľ nestretli:

  • Z jedného „zoznamu” typu ListView<T>. Kľúčovou vlastnosťou inštancie triedy ListView<T> je jej selectionModel, ktorý hovorí o móde výberu jednotlivých prvkov. My si vystačíme s východzím nastavením tejto vlastnosti, pri ktorom možno zo zoznamu vybrať najviac jeden prvok. Prístup k vybraným prvkom zoznamu je však vždy potrebné realizovať prostredníctvom metódy getSelectionModel vracajúcej inštanciu generickej triedy MultipleSelectionModel<T>, ktorá je vlastnosťou selectionModel „obalená”.
  • Z viacerých tlačidiel typu RadioButton. Toto pomenovanie je zvolené na základe toho, že pri ich typickom použití zvolenie jedného z tlačidiel „na diaľku” vypína doposiaľ zvolené tlačidlo v danej skupine.

Rozmiestnenie jednotlivých ovládacích prvkov na scéne realizujeme podobne ako na jedenástej prednáške; tentokrát však v konštruktore triedy FontDialog.

// ...

import java.util.*;
import javafx.geometry.*;
import javafx.collections.*;

// ...

public class FontDialog {
    // ...
    
    private ListView<String> lvFamilies;
    private ArrayList<RadioButton> rbSizes;
    private RadioButton rbRegular;
    private RadioButton rbBold;
    private RadioButton rbItalic;
    private RadioButton rbBoldItalic;
    
    public FontDialog() {
        // ...
        
        GridPane grid = new GridPane();
        borderPane.setCenter(grid);
        grid.setPadding(new Insets(10, 15, 10, 15));
        grid.setAlignment(Pos.CENTER);
        grid.setHgap(10);

        lvFamilies = new ListView<>();
        grid.add(lvFamilies, 0, 0);
        lvFamilies.setItems(FXCollections.observableList(Font.getFamilies()));
        lvFamilies.setPrefHeight(200);

        rbSizes = new ArrayList<>();
        for (int i = 8; i <= 24; i += 2) {
            rbSizes.add(new RadioButton(Integer.toString(i)));
        }

        VBox rbBox1 = new VBox();
        rbBox1.setSpacing(10);
        rbBox1.setPadding(new Insets(0, 20, 0, 10));
        rbBox1.getChildren().addAll(rbSizes);
        grid.add(rbBox1, 1, 0);

        rbRegular = new RadioButton("Obyčajný");
        rbBold = new RadioButton("Tučný");
        rbItalic = new RadioButton("Kurzíva");
        rbBoldItalic = new RadioButton("Tučná kurzíva");

        VBox rbBox2 = new VBox();
        rbBox2.setSpacing(20);
        rbBox2.setPadding(new Insets(0, 10, 0, 20));
        rbBox2.getChildren().addAll(rbRegular, rbBold, rbItalic, rbBoldItalic);
        grid.add(rbBox2, 2, 0);

        TilePane bottom = new TilePane();
        borderPane.setBottom(bottom);
        bottom.setPadding(new Insets(15, 15, 15, 15));
        bottom.setAlignment(Pos.BASELINE_RIGHT);
        bottom.setHgap(10);

        Button btnOK = new Button("Potvrdiť");
        btnOK.setMaxWidth(Double.MAX_VALUE);
        Button btnCancel = new Button("Zrušiť");
        btnCancel.setMaxWidth(Double.MAX_VALUE);
        bottom.getChildren().addAll(btnOK, btnCancel);
        
        // ...
    }
    
    // ...   
}

Skupiny tlačidiel typu RadioButton (ToggleGroup)

Po otvorení dialógu vytvoreného vyššie zisťujeme, že je možné „zaškrtnúť” ľubovoľnú podmnožinu tlačidiel typu RadioButton. Ak totiž nepovieme inak, každé z nich tvorí osobitnú skupinu.

V nasledujúcom zabezpečíme, aby bolo možné „zaškrtnúť” najviac jedno z tlačidiel v zozname rbSizes a najviac jedno zo zvyšných tlačidiel. Urobíme to tak, že každé z tlačidiel pridáme do jednej z dvoch skupín reprezentovaných inštanciami triedy ToggleGroup.

public class FontDialog {
    // ...
    
    private ToggleGroup toggleGroup1;        
    private ToggleGroup toggleGroup2;        

    // ...

    public FontDialog() {
        // ...

        toggleGroup1 = new ToggleGroup();           

        /* Cyklus for, v ktorom sa pridavaju prvky do zoznamu rbSizes, upravime nasledovne: */
        for (int i = 8; i <= 24; i += 2) {
            RadioButton rb = new RadioButton(Integer.toString(i));
            rb.setToggleGroup(toggleGroup1);       
            rbSizes.add(rb);
        } 

        // ...

        toggleGroup2 = new ToggleGroup();           
         
        rbRegular.setToggleGroup(toggleGroup2);     
        rbBold.setToggleGroup(toggleGroup2);        
        rbItalic.setToggleGroup(toggleGroup2);      
        rbBoldItalic.setToggleGroup(toggleGroup2);  

        // ...
    }

    // ...     
}

Dokončenie dialógu na výber fontu

Pridajme najprv funkcionalitu jednotlivých tlačidiel dialógu na výber fontu.

public class FontDialog {
    // ...

    private boolean confirmed;
    
    private void okAction() {
        confirmed = true;
        stage.close();
    }
    
    private void cancelAction() {
        stage.close();
    }

    public FontDialog() {
        // ...
        
        btnOK.setOnAction(event -> okAction());
        btnCancel.setOnAction(event -> cancelAction());

        // ...
    }

    // ...
}

Teraz už len ostáva implementovať metódu showFontDialog, ktorá:

  • Podľa vstupných parametrov – atribútov doposiaľ zvoleného fontu – nastaví predvolené hodnoty v dialógu.
  • Otvorí dialóg metódou stage.showAndWait (vykonávanie programu sa teda zablokuje, až kým sa dialóg nezavrie; tým sa táto metóda líši od metódy show).
  • V prípade, že dialóg skončil potvrdením (confirmed == true), vráti na výstupe atribúty fontu na základe tých zvolených v dialógu.

Jej implementácia môže byť napríklad nasledovná.

public class FontDialog {
    // ...

    public FontAttributes showFontDialog(FontAttributes oldFontAttributes) {
        /* Nastav predvoleny font podla oldFontAttributes: */  
 
        lvFamilies.getSelectionModel().select(oldFontAttributes.getFamily());
        rbSizes.get((int)((oldFontAttributes.getSize() - 8) / 2)).setSelected(true);
        if (oldFontAttributes.getWeight() == FontWeight.NORMAL) {
            if (oldFontAttributes.getPosture() == FontPosture.REGULAR) {
                rbRegular.setSelected(true);
            } else if (oldFontAttributes.getPosture() == FontPosture.ITALIC) {
                rbItalic.setSelected(true);
            }
        } else if (oldFontAttributes.getWeight() == FontWeight.BOLD) {
            if (oldFontAttributes.getPosture() == FontPosture.REGULAR) {
                rbBold.setSelected(true);
            } else if (oldFontAttributes.getPosture() == FontPosture.ITALIC) {
                rbBoldItalic.setSelected(true);
            } 
        }

    
        /* Otvor dialogove okno: */
 
        confirmed = false;
        stage.showAndWait();


        /* Ak dialog skoncil potvrdenim, vrat zvolene hodnoty na vystupe: */

        if (confirmed) {
            String newFamily = "";
            FontWeight newWeight;
            FontPosture newPosture;
            double newSize = 0;
            newFamily = lvFamilies.getSelectionModel().getSelectedItem();
            if (rbRegular.isSelected() || rbItalic.isSelected()) {
                newWeight = FontWeight.NORMAL;
            } else {
                newWeight = FontWeight.BOLD;
            }
            if (rbRegular.isSelected() || rbBold.isSelected()) {
                newPosture = FontPosture.REGULAR;
            } else {
                newPosture = FontPosture.ITALIC;
            }
            int i = 8;
            for (RadioButton rb : rbSizes) {
                if (rb.isSelected()) {
                    newSize = i;
                }
                i += 2;
            }
            return new FontAttributes(newFamily, newWeight, newPosture, newSize);
        } else {
            return null;
        }
    }    
}

Cvičenia

  • Rozšírte textový editor o možnosť nastavovania farby fontu a farby výplne textového priestoru. Zísť sa tu môže ovládací prvok ColorPicker.
  • Ukončenie aplikácie sme implementovali tak, že pokiaľ bol dokument od posledného uloženia zmenený, zobrazí sa výzva na jeho uloženie; pokiaľ zmenený nebol, aplikácia sa priamo ukončí. Upravte aplikáciu tak, aby sa aj v druhom prípade zobrazila výzva na potvrdenie ukončenia programu (avšak bez toho, aby sa aplikácia pýtala na uloženie dokumentu).

Textový editor: kompletný zdrojový kód

Viacoknové aplikácie v JavaFX: minimalistický príklad

Zakončime teraz tému tvorby aplikácií v JavaFX príkladom aplikácie s dvoma oknami, ktorý je o niečo jednoduchší, než ten vyššie. Hlavné okno aplikácie bude pozostávať z jedného tlačidla a jedného textového popisku. Po stlačení tlačidla sa zobrazí druhé okno s dvoma tlačidlami (Áno resp. Nie). Každé z týchto tlačidiel toto druhé okno zavrie. Ak sa tak stane tlačidlom Áno, v textovom popisku hlavného okna sa objaví text Áno; ak je druhé okno zavreté tlačidlom Nie alebo „krížikom”, v textovom popisku sa objaví text Nie.

Základom aplikácie môže byť nasledujúci kód, v ktorom sú ovládacie prvky rozmiestnené v hlavnom okne.

package nejakaaplikacia;

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

public class NejakaAplikacia extends Application {

    private Stage primaryStage;
    private Button btnOpenDialog;
    private Label lblResult;

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

        GridPane grid = new GridPane();
        grid.setAlignment(Pos.CENTER);
        grid.setPadding(new Insets(20,100,20,100));
        grid.setHgap(10);
        grid.setVgap(10);

        btnOpenDialog = new Button("Otvor dialóg");
        grid.add(btnOpenDialog, 0, 0);

        lblResult = new Label("");
        grid.add(lblResult, 0, 1);

        Scene scene = new Scene(grid);

        primaryStage.setTitle("Hlavné okno");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

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

Vytvorenie druhého okna a jeho zobrazenie po stlačení tlačidla btnOpenDialog potom môžeme priamočiaro implementovať napríklad nasledovne.

public class NejakaAplikacia extends Application {
    // ...
    
    private Stage dialogStage;
    private Button btnYes;    
    private Button btnNo;
    
    private boolean result;    

    @Override
    public void start(Stage primaryStage) {
        // ...
        
        btnOpenDialog.setOnAction(event -> {
            result = false;
            dialogStage.showAndWait();
            if (result) {
                lblResult.setText("Áno");
            } else {
                lblResult.setText("Nie");
            }
        });      

        // ...

        dialogStage = new Stage();               
        
        HBox hb = new HBox();                    
        
        hb.setSpacing(10);
        hb.setPadding(new Insets(10,10,10,10));
        
        btnYes = new Button("Áno");              
        btnYes.setOnAction(event -> {  
            result = true;
            dialogStage.close();
        });
        btnNo = new Button("Nie");               
        btnNo.setOnAction(event -> dialogStage.close());
        hb.getChildren().addAll(btnYes, btnNo);
        
        Scene dialogScene = new Scene(hb, 120, 50);
        
        dialogStage.setScene(dialogScene);                     
        dialogStage.setTitle("Dialóg");                        
        dialogStage.initModality(Modality.APPLICATION_MODAL);  
        dialogStage.initStyle(StageStyle.UTILITY);             

        // ...
    }
}

O niečo elegantnejším prístupom je však vytvorenie samostatnej triedy (napríklad CustomDialog) pre dialógové okno. Do konštruktora tejto triedy môžeme presunúť všetok kód rozmiestňujúci ovládacie prvky dialógového okna.

package nejakaaplikacia;

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

public class CustomDialog {
    private Stage dialogStage;
    private Button btnYes;
    private Button btnNo;

    private boolean result;

    public CustomDialog() {
        dialogStage = new Stage();

        HBox hb = new HBox();

        hb.setSpacing(10);
        hb.setPadding(new Insets(10,10,10,10));

        btnYes = new Button("Áno");
        btnYes.setOnAction(event -> {
            result = true;
            dialogStage.close();
        });
        btnNo = new Button("Nie");
        btnNo.setOnAction(event -> dialogStage.close());
        hb.getChildren().addAll(btnYes, btnNo);

        Scene dialogScene = new Scene(hb, 120, 50);

        dialogStage.setScene(dialogScene);
        dialogStage.setTitle("Dialóg");
        dialogStage.initModality(Modality.APPLICATION_MODAL);
        dialogStage.initStyle(StageStyle.UTILITY);
    }

    public String showCustomDialog() {
        result = false;
        dialogStage.showAndWait();
        if (result) {
            return "Áno";
        } else {
            return "Nie";
        }
    }
}
public class NejakaAplikacia extends Application {
    // ...     

    private CustomDialog dialog;
    
    @Override
    public void start(Stage primaryStage) {
        // ...

        dialog = new CustomDialog();
        btnOpenDialog.setOnAction(event -> lblResult.setText(dialog.showCustomDialog()));        

        // ...
    }
}