Programovanie (1) v C/C++
1-INF-127, ZS 2024/25
Letný semester, prednáška č. 12: Rozdiel medzi revíziami
(Vytvorená stránka „== 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…“) |
|||
(3 medziľahlé úpravy od rovnakého používateľa nie sú zobrazené.) | |||
Riadok 1: | Riadok 1: | ||
== Oznamy == | == Oznamy == | ||
+ | |||
+ | * Tretiu domácu úlohu treba odovzdať najneskôr ''do utorka 14. mája 2024, 9:50'' – čiže do začiatku trinástych cvičení. | ||
+ | * Počas zajtrajších cvičení (od 9:50 do 11:20) bude prebiehať piaty test zameraný na látku z prvých jedenástich týždňov. Body z testu bude možné získať iba v prípade prítomnosti na cvičeniach v miestnosti I-H6. | ||
+ | * Krátko po dnešnej prednáške bude na testovači zverejnených niekoľko úloh, ktoré možno využiť ako prípravu na skúšku (pôjde o zadania, ktoré sa v rámci praktickej časti skúšky objavili v minulých rokoch). | ||
== Zložitejšie ovládacie prvky a aplikácie s viacerými oknami: jednoduchý textový editor == | == Zložitejšie ovládacie prvky a aplikácie s viacerými oknami: jednoduchý textový editor == | ||
− | Cieľom | + | Cieľom tejto a nasledujúcej prednášky je demonštrovať použitie niektorých zložitejších ovládacích prvkov v JavaFX (ako napríklad [https://openjfx.io/javadoc/21/javafx.controls/javafx/scene/control/Menu.html <tt>Menu</tt>], [https://openjfx.io/javadoc/21/javafx.controls/javafx/scene/control/RadioButton.html <tt>RadioButton</tt>], či [https://openjfx.io/javadoc/21/javafx.controls/javafx/scene/control/ListView.html <tt>ListView<T></tt>]) a štandardných dialógov ([https://openjfx.io/javadoc/21/javafx.controls/javafx/scene/control/Alert.html <tt>Alert</tt>] resp. [https://openjfx.io/javadoc/21/javafx.graphics/javafx/stage/FileChooser.html <tt>FileChooser</tt>]), 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: | Urobíme tak na ukážkovej aplikácii jednoduchého textového editora zvládajúceho nasledujúce úkony: | ||
Riadok 15: | Riadok 19: | ||
=== Základ aplikácie === | === Základ aplikácie === | ||
− | Ako koreňovú oblasť hlavného okna aplikácie zvolíme inštanciu triedy [https://openjfx.io/javadoc/ | + | Ako koreňovú oblasť hlavného okna aplikácie zvolíme inštanciu triedy [https://openjfx.io/javadoc/21/javafx.graphics/javafx/scene/layout/BorderPane.html <tt>BorderPane</tt>], 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 <tt>start</tt>, ale dôležitejšie ovládacie prvky uchovávať ako premenné inštancie samotnej hlavnej triedy <tt>Editor</tt>, čo umožňí ich neskoršiu modifikáciu z rôznych pomocných metód. Takto si okrem iného budeme uchovávať aj referenciu <tt>primaryStage</tt> na hlavné okno aplikácie. |
Základ programu tak môže vyzerať napríklad nasledovne. | Základ programu tak môže vyzerať napríklad nasledovne. | ||
Riadok 99: | Riadok 103: | ||
=== Ovládací prvok <tt>TextArea</tt> === | === Ovládací prvok <tt>TextArea</tt> === | ||
− | 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 [https://openjfx.io/javadoc/ | + | 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 [https://openjfx.io/javadoc/21/javafx.controls/javafx/scene/control/TextArea.html <tt>TextArea</tt>] a my inštanciu tejto triedy zvolíme za stredovú časť koreňovej oblasti <tt>border</tt>. |
Podobne ako vyššie budeme referenciu <tt>textArea</tt> na prvok typu <tt>TextArea</tt> uchovávať ako premennú inštancie triedy <tt>Editor</tt>. Navyše si v premenných inštancie triedy <tt>Editor</tt> budeme pamätať aj kľúčové atribúty fontu, ktoré vhodne inicializujeme. Na font použitý v priestore <tt>textArea</tt> tieto atribúty aplikujeme v pomocnej metóde <tt>applyFont</tt>, ktorú zavoláme hneď po inicializácii premennej <tt>textArea</tt>. | Podobne ako vyššie budeme referenciu <tt>textArea</tt> na prvok typu <tt>TextArea</tt> uchovávať ako premennú inštancie triedy <tt>Editor</tt>. Navyše si v premenných inštancie triedy <tt>Editor</tt> budeme pamätať aj kľúčové atribúty fontu, ktoré vhodne inicializujeme. Na font použitý v priestore <tt>textArea</tt> tieto atribúty aplikujeme v pomocnej metóde <tt>applyFont</tt>, ktorú zavoláme hneď po inicializácii premennej <tt>textArea</tt>. | ||
Riadok 158: | Riadok 162: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | Aby sme takúto akciu vedeli vykonať po každej zmene textového obsahu priestoru <tt>textArea</tt>, využijeme mechanizmus takzvaných ''vlastností''. Pod ''vlastnosťou'' sa v JavaFX rozumie trieda implementujúca generické rozhranie [https://openjfx.io/javadoc/ | + | Aby sme takúto akciu vedeli vykonať po každej zmene textového obsahu priestoru <tt>textArea</tt>, využijeme mechanizmus takzvaných ''vlastností''. Pod ''vlastnosťou'' sa v JavaFX rozumie trieda implementujúca generické rozhranie [https://openjfx.io/javadoc/21/javafx.base/javafx/beans/property/Property.html <tt>Property<T></tt>] a možno si ju predstaviť ako „značne pokročilý obal pre nejakú hodnotu typu <tt>T</tt>”. Rozhranie <tt>Property<T></tt> je rozšírením rozhrania [https://openjfx.io/javadoc/21/javafx.base/javafx/beans/value/ObservableValue.html <tt>ObservableValue<T></tt>] reprezentujúceho hodnoty typu <tt>T</tt>, 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 <tt>ObservableValue<T></tt> 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 <tt>EventHandler<T></tt>, ale inštancie tried implementujúcich rozhranie [https://openjfx.io/javadoc/ | + | 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 <tt>ObservableValue<T></tt> 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 <tt>EventHandler<T></tt>, ale inštancie tried implementujúcich rozhranie [https://openjfx.io/javadoc/21/javafx.base/javafx/beans/value/ChangeListener.html <tt>ChangeListener<T></tt>]. To vyžaduje implementáciu jedinej metódy |
<syntaxhighlight lang="java"> | <syntaxhighlight lang="java"> | ||
void changed(ObservableValue<? extends T> observable, T oldValue, T newValue) | void changed(ObservableValue<? extends T> observable, T oldValue, T newValue) | ||
Riadok 166: | Riadok 170: | ||
ktorá sa vykoná pri zmene hodnoty „obalenej” inštanciou <tt>observable</tt> z <tt>oldValue</tt> na <tt>newValue</tt>. 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 <tt>ChangeListener<T></tt> ako „spracúvateľa zmeny” pre inštanciu <tt>observable</tt> rozhrania <tt>ObservableValue<T></tt> vykonáme pomocou metódy <tt>observable.addListener</tt>. | ktorá sa vykoná pri zmene hodnoty „obalenej” inštanciou <tt>observable</tt> z <tt>oldValue</tt> na <tt>newValue</tt>. 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 <tt>ChangeListener<T></tt> ako „spracúvateľa zmeny” pre inštanciu <tt>observable</tt> rozhrania <tt>ObservableValue<T></tt> vykonáme pomocou metódy <tt>observable.addListener</tt>. | ||
− | Vráťme sa teraz k nášmu textovému editoru: textový obsah priestoru <tt>textArea</tt> je reprezentovaný ako vlastnosť, ktorú môžeme získať volaním metódy <tt>textArea.textProperty()</tt>. Ide tu o inštanciu triedy [https://openjfx.io/javadoc/ | + | Vráťme sa teraz k nášmu textovému editoru: textový obsah priestoru <tt>textArea</tt> je reprezentovaný ako vlastnosť, ktorú môžeme získať volaním metódy <tt>textArea.textProperty()</tt>. Ide tu o inštanciu triedy [https://openjfx.io/javadoc/21/javafx.base/javafx/beans/property/StringProperty.html <tt>StringProperty</tt>] implementujúcej rozhranie <tt>Property<String></tt>. Môžeme tak pre ňu zaregistrovať „spracúvateľa zmien” pomocou metódy <tt>addListener</tt>, ktorej jediným argumentom bude inštancia takéhoto spracúvateľa. To možno realizovať pomocou anonymnej triedy |
<syntaxhighlight lang="java"> | <syntaxhighlight lang="java"> | ||
import javafx.beans.value.*; | import javafx.beans.value.*; | ||
Riadok 203: | Riadok 207: | ||
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: | 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 [https://openjfx.io/javadoc/ | + | * Do hlavného okna aplikácie sa umiestní ovládací prvok typu [https://openjfx.io/javadoc/21/javafx.controls/javafx/scene/control/MenuBar.html <tt>MenuBar</tt>], ktorý reprezentuje priestor, v ktorom sa budú jednotlivé ponuky zobrazovať. Každý <tt>MenuBar</tt> si udržiava zoznam ponúk v ňom umiestnených. |
− | * Každá ponuka (ako napríklad <tt>Súbor</tt>, <tt>Formát</tt>, ...) je reprezentovaná inštanciou triedy [https://openjfx.io/javadoc/ | + | * Každá ponuka (ako napríklad <tt>Súbor</tt>, <tt>Formát</tt>, ...) je reprezentovaná inštanciou triedy [https://openjfx.io/javadoc/21/javafx.controls/javafx/scene/control/Menu.html <tt>Menu</tt>], ktorá si okrem iného pamätá zoznam všetkých položiek danej ponuky. |
− | * Položka ponuky je reprezentovaná inštanciou triedy [https://openjfx.io/javadoc/ | + | * Položka ponuky je reprezentovaná inštanciou triedy [https://openjfx.io/javadoc/21/javafx.controls/javafx/scene/control/MenuItem.html <tt>MenuItem</tt>]. Každej položke možno napríklad pomocou metódy <tt>setOnAction</tt> priradiť akciu, ktorá sa má vykonať po jej zvolení používateľom. |
* Trieda <tt>Menu</tt> je podtriedou triedy <tt>MenuItem</tt>, z čoho okrem iného vyplýva, že položkou ponuky môže byť aj ďalšia podponuka. | * Trieda <tt>Menu</tt> je podtriedou triedy <tt>MenuItem</tt>, 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 [https://openjfx.io/javadoc/ | + | * Špeciálne položky ponúk sú reprezentované inštanciami tried [https://openjfx.io/javadoc/21/javafx.controls/javafx/scene/control/CheckMenuItem.html <tt>CheckMenuItem</tt>] (takúto položku ponuky možno zvolením zaškrtnúť resp. odškrtnúť) a [https://openjfx.io/javadoc/21/javafx.controls/javafx/scene/control/SeparatorMenuItem.html <tt>SeparatorMenuItem</tt>] (reprezentuje vodorovnú čiaru na vizuálne oddelenie častí ponuky). |
V našej aplikácii teraz vytvoríme <tt>MenuBar</tt> s dvojicou ponúk <tt>Súbor</tt> a <tt>Formát</tt> s nasledujúcou štruktúrou. | V našej aplikácii teraz vytvoríme <tt>MenuBar</tt> s dvojicou ponúk <tt>Súbor</tt> a <tt>Formát</tt> s nasledujúcou štruktúrou. | ||
Riadok 321: | Riadok 325: | ||
Ď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 <tt>TextArea</tt> už prichádza s prednastavenou kontextovou ponukou. Chceli by sme teraz túto ponuku nahradiť vlastnou, obsahujúcou rovnaké dve položky ako ponuka <tt>mFormat</tt> (budeme však musieť tieto položky vytvárať nanovo, pretože každá položka môže patriť iba do jedinej ponuky). | Ď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 <tt>TextArea</tt> už prichádza s prednastavenou kontextovou ponukou. Chceli by sme teraz túto ponuku nahradiť vlastnou, obsahujúcou rovnaké dve položky ako ponuka <tt>mFormat</tt> (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 [https://openjfx.io/javadoc/ | + | Jediný rozdiel oproti tvorbe hlavnej ponuky bude spočívať v použití inštancie triedy [https://openjfx.io/javadoc/21/javafx.controls/javafx/scene/control/ContextMenu.html <tt>ContextMenu</tt>]. Tú následne pomocou metódy <tt>setContextMenu</tt> priradíme ako kontextovú ponuku ovládaciemu prvku <tt>textArea</tt>. Výsledný vzhľad kontextovej ponuky je na obrázku vpravo. |
[[Image:Textovyeditor2.png|thumb|Výsledný vzhľad kontextovej ponuky.|250px]] | [[Image:Textovyeditor2.png|thumb|Výsledný vzhľad kontextovej ponuky.|250px]] | ||
Riadok 425: | Riadok 429: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | * Kým obojstranné previazanie vlastností realizujeme metódou <tt>bindBidirectional</tt>, jednostranné previazanie možno realizovať metódou <tt>bind</tt>. Vlastnosť typu implementujúceho rozhranie <tt>[https://openjfx.io/javadoc/ | + | * Kým obojstranné previazanie vlastností realizujeme metódou <tt>bindBidirectional</tt>, jednostranné previazanie možno realizovať metódou <tt>bind</tt>. Vlastnosť typu implementujúceho rozhranie <tt>[https://openjfx.io/javadoc/21/javafx.base/javafx/beans/property/Property.html Property<T>]</tt> pritom možno jednostranne previazať nielen s inou vlastnosťou typu implementujúceho toto rozhranie, ale možno ju metódou <tt>bind</tt> naviazať aj na ľubovoľnú hodnotu typu implementujúceho rozhranie [https://openjfx.io/javadoc/21/javafx.base/javafx/beans/value/ObservableValue.html <tt>ObservableValue<T></tt>]. |
− | * 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 [https://openjfx.io/javadoc/ | + | * 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 [https://openjfx.io/javadoc/21/javafx.base/javafx/beans/binding/Bindings.html <tt>Bindings</tt>]. |
* Viac sa o vlastnostiach a ich previazaní možno dočítať napríklad v [https://docs.oracle.com/javase/8/javafx/properties-binding-tutorial/binding.htm tomto tutoriáli]. | * Viac sa o vlastnostiach a ich previazaní možno dočítať napríklad v [https://docs.oracle.com/javase/8/javafx/properties-binding-tutorial/binding.htm tomto tutoriáli]. | ||
Riadok 437: | Riadok 441: | ||
</gallery> | </gallery> | ||
− | Na zobrazenie takejto výzvy využijeme jeden z jednoduchých štandardných dialógov – inštanciu triedy [https://openjfx.io/javadoc/ | + | Na zobrazenie takejto výzvy využijeme jeden z jednoduchých štandardných dialógov – inštanciu triedy [https://openjfx.io/javadoc/21/javafx.controls/javafx/scene/control/Alert.html <tt>Alert</tt>]. |
<syntaxhighlight lang="java"> | <syntaxhighlight lang="java"> | ||
// ... | // ... | ||
Riadok 487: | Riadok 491: | ||
// ... | // ... | ||
− | private final int N = 1000; // V metode newRandomAction budeme generovat N riadkov o N nahodnych cifrach | + | private static final int N = 1000; // V metode newRandomAction budeme generovat N riadkov o N nahodnych cifrach |
// ... | // ... | ||
Riadok 531: | Riadok 535: | ||
=== Ďalšie typy jednoduchých dialógov === | === Ďalšie typy jednoduchých dialógov === | ||
− | V JavaFX možno využívať aj ďalšie preddefinované jednoduché dialógy – od dialógu <tt>Alert</tt> s odlišným <tt>AlertType</tt> až po dialógy ako [https://openjfx.io/javadoc/ | + | V JavaFX možno využívať aj ďalšie preddefinované jednoduché dialógy – od dialógu <tt>Alert</tt> s odlišným <tt>AlertType</tt> až po dialógy ako [https://openjfx.io/javadoc/21/javafx.controls/javafx/scene/control/TextInputDialog.html <tt>TextInputDialog</tt>] alebo [https://openjfx.io/javadoc/21/javafx.controls/javafx/scene/control/ChoiceDialog.html <tt>ChoiceDialog</tt>]. |
* Viac sa o preddefinovaných jednoduchých dialógoch v JavaFX možno dočítať napríklad [https://code.makery.ch/blog/javafx-dialogs-official/ tu]. | * Viac sa o preddefinovaných jednoduchých dialógoch v JavaFX možno dočítať napríklad [https://code.makery.ch/blog/javafx-dialogs-official/ tu]. | ||
Aktuálna revízia z 09:51, 5. máj 2024
Obsah
- 1 Oznamy
- 2 Zložitejšie ovládacie prvky a aplikácie s viacerými oknami: jednoduchý textový editor
- 2.1 Základ aplikácie
- 2.2 Ovládací prvok TextArea
- 2.3 Vlastnosti a spracovanie ich zmeny
- 2.4 Hlavné ponuky (MenuItem, Menu a MenuBar)
- 2.5 Kontextové ponuky (ContextMenu)
- 2.6 Priradenie udalostí k jednotlivým položkám ponúk
- 2.7 Previazanie vlastností
- 2.8 Jednoduché dialógy (Alert)
- 2.9 Ďalšie typy jednoduchých dialógov
- 2.10 Zatvorenie hlavného okna aplikácie „krížikom”
Oznamy
- Tretiu domácu úlohu treba odovzdať najneskôr do utorka 14. mája 2024, 9:50 – čiže do začiatku trinástych cvičení.
- Počas zajtrajších cvičení (od 9:50 do 11:20) bude prebiehať piaty test zameraný na látku z prvých jedenástich týždňov. Body z testu bude možné získať iba v prípade prítomnosti na cvičeniach v miestnosti I-H6.
- Krátko po dnešnej prednáške bude na testovači zverejnených niekoľko úloh, ktoré možno využiť ako prípravu na skúšku (pôjde o zadania, ktoré sa v rámci praktickej časti skúšky objavili v minulých rokoch).
Zložitejšie ovládacie prvky a aplikácie s viacerými oknami: jednoduchý textový editor
Cieľom tejto a nasledujúcej prednášky 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
// ...
}
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.
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 static 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.