Programování v JavaFX: základní GUI, uživatelské události a nové okno

Jaromír Vojtaj 17. 9. 2015

Minulý třetí díl seriálu byl věnován vytvoření a úpravě kostry ukázkové aplikace, ve které budeme postupně postupně demonstrovat možnosti vývoje aplikací JavaFX. V dnešním čtvrtém dílu se zaměříme hlavně na vytvoření akcí a reakcí na uživatelské události GUI a prvního specializovaného okna pro ukázkový příklad.

Ještě než se pustíme do práce na vytvoření akcí a reakcí, krátce se vrátíme k úpravě vzhledu spuštěné aplikace. Již dříve bylo uvedeno, že při práci v JFXSB je možné získat náhledy oken s různým vzhledem. První obrázek první galerie ukazuje, že při výběru menu Preview → JavaFX Theme je k dispozici celkem devět možností, z toho osm funkčních. Kdo má zájem, může si vyzkoušet, které téma by se k jeho aplikaci nejvíce hodilo a nebo které se mu prostě líbí. Vzhledem k tomu, že se nám taková možnost nabízí, bylo by asi vhodné si ukázat, jak takové nastavení vzhledu provést v rámci aplikace, tedy v samotném kódu. Za tímto účelem si otevřeme IJI, náš zkušební příklad a soubor mainApp.java. Nebudeme dlouho napínat a rovnou konstatujeme, že programové nastavení vzhledu je možné a to sice prostřednictvím metody

setUserAgentStylesheet(String url)

Více je možné se dočíst v nápovědě: Application Stylesheet. Zde se kromě jiného dočteme, že jsou k dispozici pouze dvě témata (na rozdíl od JFXSB) – výchozí MODENA a volitelný CASPIAN. Na dalších dvou obrázcích první galerie je vidět, jak se projeví zařazení kódu na vzhledu hlavního okna aplikace.

setUserAgentStylesheet(Application.STYLESHEET_MODENA);
setUserAgentStylesheet(Application.STYLESHEET_CASPIAN);

Na čtvrtém obrázku v první galerii je pak vidět, jak vypadá kontextová nápověda IJI k této konkrétní metodě. Jak je z obrázků patrné, rozdíly nejsou nijak dramatické, ale přece jenom jsou rozpoznatelné. Když už jsme narazili na úpravu vzhledu aplikace, bylo by vhodné zmínit i možnost přiřazení konkrétního CSS souboru programově. Celou metodu nebudeme zkoušet ani komentovat, ale pro případné zájemce uvedeme pouze ukázku kódu:

primaryStage.getScene().getStylesheets().add("cesta k souboru.css");

Tímto příkladem bychom asi mohli ukončit úpravu vzhledu aplikace a přejít k definování uživatelských akcí. Ještě to ale neuděláme a podíváme se na jednu zajímavou možnost, která souvisí s oběma úlohami. Při tvorbě GUI aplikací se ve většině případů (a celkem logicky a automaticky) používá „běžný“ vzhled oken s titulkovým pruhem a ovládacími ikonami. Na tom by určitě nebylo nic špatného, to je jasné. Vezměme ale následující situaci: máme nějaké okno a chceme u něj definovat uživatelskou akci při jeho zavření. To se samozřejmě dá udělat, ale narazíme zde na problém. Když budeme definovat např. tlačítko na uzavření okna, tak do reakce vložíme příslušnou funkci či proceduru. Co se ale stane, když uživatel nezavře okno definovaným tlačítkem, ale ovládací ikonou okna („křížkem“)? Pokud bychom chtěli ošetřit i tuto variantu, musíme se o to v kódu postarat. Máme ale i jinou možnost? Když už se tady o tom tak zeširoka mluví, tak je asi jasné, že máme. Můžeme totiž využít náš kořenový kontejner (Stage) a jeho metodu pro nastavení stylu. Kód by mohl vypadat např. takto:

primaryStage.initStyle(StageStyle.UNDECORATED);

Více si o možnostech nastavení stylu můžeme přečíst např. zde: Stage Style. Z nápovědy je zřejmé, že jsou k dispozici 4 styly – DECORATED, UNDECORATED, TRANSPARENT, UTILITY. Podrobnější popis není třeba, ale na sérii dalších 4 obrázků v první galerii si ukážeme, jaký mají jednotlivé styly vliv na vzhled hlavního okna aplikace. A když už jsme v těch změnách, tak si konečně změníme i název hlavního okna, aby korespondoval s naším záměrem. Z obrázků je patrné, že nějaké změny viditelné jsou. Je třeba upozornit na to, že vzhled oken může být v různých prostředích a operačních systémech různý a také se od sebe mohou lišit nastavené styly. Na posledním obrázku první galerie vidíme opět kontextovou nápovědu IJI, kde se ukazuje ještě další styl – UNIFIED. Obrázek okna tohoto stylu nemá smysl zde uvádět, protože je úplně stejný jako u stylu DECORATED (toto je základní styl, který se použije vždy, když není v kódu nějaký jiný příkaz).

Tímto konstatováním ukončíme hrátky se vzhledem aplikace a přesuneme se k uživatelským akcím a reakcím. Jako první si popíšeme možnosti ukončení aplikace pomocí definovaného tlačítka. Již z předchozích kroků máme ve třídě mainForm.java definovanou akci při stisku tlačítka (btn_Quit_Click). Bylo by samozřejmě možné přidat výkonné příkazy přímo do deklarace akce. My to ale uděláme trochu jinak, abychom si zachovali větší přehlednost kódu. Proto si vytvoříme novou metodu a na tu se v akci odkážeme. Zatím vytvoříme pouze kostru metody:

private void onQuit() {

}

Následně si trochu si upravíme i deklaraci akce a vložíme do ní odkaz na novu metodu:

@FXML void btn_Quit_Click(ActionEvent event) { onQuit(); }

Nyní máme již všechno připravené a můžeme se pokusit aplikaci ukončit. Jako první (hlavně pro uživatele Javy) by asi bylo vyzkoušet zavření hlavního okna. Při tomto pokusu ale zjistíme, že žádná taková metoda není k dispozici. Ono je to trochu logické, protože vlastně nemáme žádné okno, ale používáme typ Stage. Zkusíme tedy zavřít Stage. Problém je ale v tom, že nemáme žádnou definovanou metodu a tudíž se nemáme při pokusu o zavření na co odkazovat. Naštěstí i zde existuje řešení, i když trochu komplikovanější:

Stage stage = (Stage) contentPane.getScene().getWindow();
stage.close();
System.out.println("Aplikace byla řádně ukončena");

Posledního řádku si nebudeme všímat, protože pouze vypisuje do konzole informaci o tom, že byla aplikace ukončena. Funkci definujeme pro využití v dalších krocích. Jinak má příkaz dva výkonné řádky. Na tom prvním si definujeme novou Stage pomocí kořenového widgetu AnchorPane, kterému přidělíme „status okna“. V druhém příkazu už pak můžeme Stage jako „okno“ také zavřít. Pokud vyzkoušíme překlad, spuštění a ukončení aplikace příslušným tlačítkem tak zjistíme, že vše řádně a uspokojivě funguje. Pokud bychom se ale trochu podívali na zkušenosti a rady ohledně ukončení aplikací, najdeme jiné řešení. Já osobně používám zavření Stage pouze v jednom konkrétním případě, když chci po otevření nového okna v aplikaci zavřít to stávající (např. po zadání hesla pro přístup do aplikace se otevře hlavní okno a okno s loginem se zavře. Pokud se ukončí hlavní okno, ukončí se aplikace a pro nové přihlášení je nutné ji znovu spustit). Ukážeme si tedy i doporučené řešení pro ukončení aplikace JavaFX, kde si vystačíme s jedním příkazem:

Platform.exit();

Při pátrání na netu můžeme najít ještě další řešení, kde se doplňuje další příkaz:

System.exit(0);

Při zkoušce zjistíme, že se aplikace řádně ukončí včetně výpisu do konzole. Nyní by asi bylo vhodné si ukázat, jak řešit situaci při zavření okna pomocí ikony křížku. Prozatím sice ukončení samozřejmě funguje, ale žádná uživatelská zpráva se při ukončení neobjeví. Na problém samozřejmě narazíme zase s tím, že ve třídě mainForm není žádná Stage deklarovaná. Ono to ani není třeba, protože můžeme vše ošetřit pomocí příslušného handleru. Přejdeme tedy do třídy mainApp a za aktuálně poslední příkaz přidáme další:

primaryStage.setOnCloseRequest(new EventHandler() {
    @Override
    public void handle(WindowEvent event) {
        Platform.exit();
        System.out.println("Aplikace byla řádně ukončena");
        }
        });

Při psaní podobných komplikovanějších příkazů opět oceníme práci IJI – po zadání textu new EventHandler nám nejen doplní zbytek, ale vytvoří kompletní kostru příkazu a na nás už jsou jenom dva výkonné řádky. Pokud provedeme zkoušku, zjistíme funkčnost zavření jak tlačítkem, tak ikonou křížku. Pokud bychom požadovali nějaké složitější akce při ukončení, je samozřejmě možné proceduru onQuit() vložit tak, aby bylo možné se na ni odkázat odkudkoliv a využít její kód. Pro tuto chvíli bychom mohli ukončení aplikace považovat za vyřešené a můžeme se pustit do další akce. Ve spodní části hlavního okna máme další tlačítko, které má klasickou funkci – otvírá další okno s informacemi o aplikaci (O programu…). My ale zatím nebudeme využívat další okno (novou Stage), ale podíváme se na další variantu, kterou nám JavaFX nabízí. Touto variantou jsou tzv. dialogy, což je speciální typ oken různého vzhledu a zaměření, které se používají v GUI aplikacích pro zobrazení nějakých uživatelských hlášení. Každého by asi napadlo (hlavně podle zkušeností třeba z Javy), že to není a nemůže být žádný problém. Ale on mohl a poměrně dlouhou dobu také byl! Jak nám říká nápověda, dialogy se staly standardní součástí JavaFX až od verze 8u40: JavaFX Dialogs.

Do té doby bylo nutné používat nějaké jiné možnosti, kterých nebylo zase až tak moc. Jedna z celkem použitelných se nachází např. zde: DialogFX

Autor projekt stále vyvíjí a je tak k dispozici další, rozšířená varianta: MonologFX

My si v našem seriálu představíme jak oficiální verzi dialogů pro JavaFX 8, tak projekt DialogFX. Začneme tím jednodušším řešením a vytvoříme si novou proceduru ve třídě mainForm:

private void onAbout() {

    }

Podle vzoru v nápovědě začneme tvořit příkaz pro nové dialogové okno (třída Alert). První obrázek druhé galerie nám ukazuje, že je k dispozici celkem 5 typů dialogu – CONFIRMATION, ERROR, INFORMATION, NONE, WARNING. Dalších 5 obrázků ve druhé galerii ukazuje vzhled jednotlivých typů oken v daném pořadí. Pro zobrazení byl použit velmi jednoduchý kód:

Alert dialog =new Alert(Alert.AlertType.CONFIRMATION);
dialog.showAndWait();

Pro zobrazení musíme také samozřejmě upravit akci pro tlačítko a přidat do něj odkaz na novou proceduru:

@FXML void btn_About_Click(ActionEvent event) { onAbout(); }

Z obrázků je patrné, že i když je celé prostředí komplet v češtině, tak se v dialogu lokalizované texty automaticky neobjevují. Jinak je asi jasné, že pro naše účely je nejvhodnější typ dialogu INFORMATION, takže si tento typ upravíme a přizpůsobíme:

Alert dialog =new Alert(Alert.AlertType.INFORMATION);
dialog.setTitle("O programu...");
dialog.setHeaderText("Ukázková aplikace JavaFX");
dialog.setContentText("Seriál článků pro root.cz");
dialog.showAndWait();

Sedmý obrázek druhé galerie pak ukazuje, jak vypadá takto sestavený dialog. Pro krátkou ukázku by to mohlo takto stačit. Kdo má zájem, může si podrobně prostudovat přiloženou webovou stránku, kde je k dispozici podstatně více a složitějších příkladů a možností. My se přesuneme k již zmíněnému projektu DialogFX a ukážeme si zase trochu jiné varianty. Před samotným kódováním musíme udělat dva důležité kroky:

  • z příslušné stránky stáhnout třídy a umístit je do adresáře src naší aplikace. Je také nutné změnit název balíčku na jfxapp
  • z toho samého zdroje stáhnout příslušné ikony a umístit je do adresáře GUI-Files

Já pro vlastní potřeby využívám starší verzi DialogFX, kde jsou pouze 4 typy dialogů – ACCEPT, ERROR, INFO, QUESTION. V nejnovější verzi je doplněn ještě další typ WARNING. Pro náš příklad použiju svoji verzi, která je již upravená a také lokalizovaná do češtiny. Kompletní kód je pro jistotu umístěn v příloze DialogFX.java.

Teď už je vše připravené a můžeme se pustit do samotného kódování. Uděláme to ale trochu obecněji než v minulém příkladu. Kdybychom si blíže prostudovali třídu DialogFX, tak zjistíme, že typ QUESTION je odlišný od třech dalších typů. Abychom je tedy mohli volat všechny, vytvoříme si dvě nové procedury:

private void dialogMessage(final String text, final String title, final char type) {
        DialogFX dfx = new DialogFX();
        dfx.setTitleText(title);
        dfx.setMessage(text);
        switch (type) { case 'A': dfx.setType(DialogFX.Type.ACCEPT); break;
                        case 'E': dfx.setType(DialogFX.Type.ERROR); break;
                        case 'I': dfx.setType(DialogFX.Type.INFO); break;
        } dfx.showDialog();
    }

V této proceduře se zadává titulek okna, vnitřní text a typ dialogu/okna.

private Integer questMessage(final String text, final String title) {
        DialogFX dfx = new DialogFX();
        dfx.setType(DialogFX.Type.QUESTION);
        dfx.setTitleText(title);
        dfx.setMessage(text);
        return dfx.showDialog();
    }

Toto není vlastně procedura, ale funkce, která vrací hodnotu podle toho, jak bylo dialogové okno ukončeno. Na dalších třech obrázcích druhé galerie jsou postupně vidět okna typu ACCEPT, ERROR, INFO. V proceduře onAbout() zakomentujeme předchozí kód pro dialogy a přidáme pouze jeden řádek kódu:

dialogMessage("Ukázková aplikace JavaFX", "O programu", 'A');

Poslední obrázek ve druhé galerii pak ukazuje typ QUESTION včetně lokalizace tlačítek a zobrazení návratové hodnoty (pro Ano=0, pro Ne=1). Kód je zde:

Integer qm = questMessage("Ukázková aplikace JavaFX", "O programu");
System.out.println(qm.toString());

Tímto bychom ukončili nastavení reakcí na dvě spodní tlačítka (ukončení aplikace a zobrazení dialogového okna). Dosavadní kód je v přílohách mainApp.java a ma­inForm.java. Pro úplnou jistotu je v příloze i příslušný FXML soubor: rootForm.fxml.

Spodní část formuláře máme tedy úspěšně vyřešenou a můžeme se pustit do té horní. V ní je celkem 8 tlačítek pro přechod (otevření příslušných oken) do jednotlivých ukázkových příkladů. Ještě než se pustíme do konkrétní práce na prvním takovém příkladu, ukážeme si obecný postup, jak nová okna vytvářet, otvírat a zavírat. Toto jsou běžné akce u složitějších GUI aplikací, které se málokdy skládají pouze z jediného okna. Proto je vhodné mít nějaký jednoduchý recept, jak toho dosáhnout. V rámci toho si také ukážeme, jaké typy oken/formulářů je možné použít a jak se jejich použití odlišuje. Než začneme, tak si v JFXSB připravíme velmi jednoduchý pokusný formulář, který bude obsahovat pouze kořenový widget AnchorPane. Podrobnosti nemá smysl uvádět, takže jenom odkážeme na první obrázek třetí galerie, ze kterého je vše zřejmé. Rozdíl bude pouze jeden jediný – při kopírování kostry kontroléru použijeme variantu Full. Kód je velmi jednoduchý, jak to ostatně ukazuje druhý obrázek ve třetí galerii.

V našem úsilí se můžeme inspirovat v proceduře mainApp, kde se otvírá hlavní okno aplikace, a hlavně – vznikla automaticky v IJI. Zde vypadá deklarace příslušné procedury takto:

public void start(Stage primaryStage) throws Exception { }

Pro náš konkrétní účel si tedy ve třídě mainForm vytvoříme novou proceduru:

private void showExam1(Stage primaryStage) throws Exception {

}

Pro volání nově vytvořené procedury musíme doplnit kód pro akci při stisku příslušného tlačítka (při této úpravě musíme dbát na to, abychom zajistili stejný typ a parametry při volání i deklaraci procedury):

@FXML void btn_E1_Click(ActionEvent event) throws Exception { showExam1(new Stage()); }

Tím máme vše připravené a můžeme vytvořit novou třídu, která bude představovat požadovaný kontrolér. Na balíčku ve struktuře projektu klikneme pravým tlačítkem myši a použijeme volbu New → Java Class. Jak ukazuje třetí obrázek třetí galerie, otevře se formulář, kde je možné zadat jméno nové třídy a dokonce (i když toho nyní nevyužijeme) změnit její typ. Při zadání názvu třídy musíme brát ohled na to, jaký název jsme v předchozích krocích zadali do formuláře JFXSB. Po zadání a stisku OK se vytvoří a otevře nová třída, která má nějakou základní strukturu (ta se nechá změnit či nastavit v rámci nastavení IJI). My ale celý kód smažeme a použijeme naši kopii kostry z JFXSB. Po provedení hlásí IJI dva problémy (pozná se vždy ve struktuře projektu, kdy jsou třídy s nějakým zádrhelem červeně podtržené vlnovkou. To samé platí pro konkrétní řádek či příkaz v samotném kódu). První je příkaz @FXML, který se jeví jako neznámý. Stačí ale na něj najet kurzorem (stačí samozřejmě vybrat jeden ze tří výskytů toho samého kódu), stisknout známou (a napovídanou…) kombinaci Alt+Enter a vybrat první položku ze seznamu – Import class. Tato akce zajistí import příslušné třídy či knihovny a rázem je po problému.

Další chyba je na řádku, kde se v kódu nachází název třídy. Nápověda nám říká, že se neshodují názvy třídy v kódu a námi zadaný název (bylo to uděláno schválně, aby bylo jasné, že i jedno velké písmeno může být problém…). Již dříve probraná možnost přejmenování zde bohužel nefunguje, takže použijeme volbu Refactor → Rename File a zadáme název tak, jak to ukazuje čtvrtý obrázek ve třetí galerii. Tlačítkem Refactor změnu potvrdíme a problémy jsou vyřešené. Novou třídu opustíme a vrátíme se do třídy mainForm a doplníme její volání. Opět použijeme známý kód a pouze změníme název, titulek a velikost volaného okna:

Parent root = FXMLLoader.load(getClass().getResource("/samExam1.fxml"));
primaryStage.setTitle("Ukázková úloha č. 1");
primaryStage.setScene(new Scene(root, 1000, 730));
primaryStage.show();

V daném případě můžeme velmi dobře použít prostou kopii potřebného úseku kódu. Pokud se ho budeme snažit vložit na nové místo (zde dokonce do jiné třídy), může se objevit okno, které vidíme na pátém obrázku třetí galerie. Zde opět velmi dobře pracuje IJI a nabízí import všech potřebných tříd a knihoven, které jsou v kopírovaném kódu potřeba. Proto po odsouhlasení kódu IJI nehlásí žádnou chybu. Z kódu můžeme (ale nemusíme) odstranit nastavení vzhledu. Pak zkusíme kód přeložit a příslušným tlačítkem nové okno otevřít. Povedenou akci nám dokazuje šestý obrázek ve třetí galerii. Tím se můžeme přesunout k tomu, co už zde bylo dříve naznačeno: kromě jiného se dá nastavit typ okna, který budeme otevírat. Jeden typ už jsme si představili – dialogová okna. „Běžná okna“ můžeme rozdělit na dva typy podle vlastnosti, které se říká modalita. Máme tedy k dispozici okna modální a nemodální.

Pokud neurčíme jinak, automaticky se nové okno otvírá jako nemodální. Dalo by se samozřejmě najít pojednání o tom, v čem je rozdíl, ale my si to ukážeme na jednoduchém příkladu. Naši aplikaci přeložíme a otevřeme nové okno. Máme na ploše dvě okna – mainForm a samexam1. Pokud odsuneme okno s příkladem stranou, objeví se nám hlavní okno aplikace. Bez problémů se můžeme dostat k jeho prvkům a využít je, např. můžeme aplikaci ukončit pomocí příslušného tlačítka. V rámci toho se samozřejmě zavře i druhé otevřené okno. Abychom mohlo otevřít okno modální, musíme použít příslušný kód:

primaryStage.initModality(Modality.);

Nápověda podle posledního obrázku třetí galerie nám říká, že máme k dispozici tři varianty:

widgety

  • APPLICATION_MODAL
  • NONE
  • WINDOW_MODAL

Volba NONE samozřejmě znamená nemodální okno. Dva typy modálních oken se liší v tom, jestli se modalita vztahuje pouze na nejbližší rodičovské okno (WINDOW) nebo na všechna rodičovská okna (APPLICATION). Tuto variantu si vybereme my, aplikaci přeložíme a otevřeme okno s příkladem. Pokud se opět pokusíme dostat do hlavního okna aplikace, nebude to beze změny velikosti vůbec možné (modální okno totiž neumožní běžným způsobem překrytí oknem mateřský, i když dojde k předání fokusu). A už vůbec není možné použít jakýkoliv ovládací prvek mateřského okna! Dokud tedy nedojde k uzavření okna s příkladem, není možné mateřské (hlavní) okno zavřít a ukončit aplikaci. Snad je z jednoduchého popisu a příkladů jasné, jak se jednotlivé typy oken od sebe liší a uživatelé budou schopni si vybrat takový typ, který se bude momentálně nejvíce hodit pro daný účel.

V dnešním dílu jsme se zaměřili hlavně na vytvoření akcí a reakcí na uživatelské události GUI a vytvoření a otevření prvního specializovaného okna pro ukázkový příklad. V příštím dílu se pustíme do krátkého úvodu do PG, připravíme si data pro první příklad a začneme tvořit potřebné GUI i kód.

Našli jste v článku chybu?
Vitalia.cz: Test dětských svačinek: Tyhle ne!

Test dětských svačinek: Tyhle ne!

Lupa.cz: Blíží se konec Wi-Fi sítí bez hesla?

Blíží se konec Wi-Fi sítí bez hesla?

Lupa.cz: Patička e-mailu závazná jako vlastnoruční podpis?

Patička e-mailu závazná jako vlastnoruční podpis?

Vitalia.cz: Antibakteriální mýdla nepomáhají, spíš škodí

Antibakteriální mýdla nepomáhají, spíš škodí

DigiZone.cz: Rapl: seriál, který vás smíří s ČT

Rapl: seriál, který vás smíří s ČT

Podnikatel.cz: Udělali jsme velkou chybu, napsal Čupr

Udělali jsme velkou chybu, napsal Čupr

Vitalia.cz: dTest odhalil ten nejlepší kečup

dTest odhalil ten nejlepší kečup

DigiZone.cz: Nova opět stahuje „milionáře“

Nova opět stahuje „milionáře“

DigiZone.cz: Technisat připravuje trojici DAB

Technisat připravuje trojici DAB

Podnikatel.cz: Tyto pojmy k #EET byste měli znát

Tyto pojmy k #EET byste měli znát

DigiZone.cz: Digi Slovakia zařazuje stanice SPI

Digi Slovakia zařazuje stanice SPI

Lupa.cz: Jak levné procesory změnily svět?

Jak levné procesory změnily svět?

Lupa.cz: Cimrman má hry na YouTube i vlastní doodle

Cimrman má hry na YouTube i vlastní doodle

120na80.cz: Nejsilnější alergeny jsou pryč

Nejsilnější alergeny jsou pryč

Vitalia.cz: Tradiční čínská medicína a rakovina

Tradiční čínská medicína a rakovina

Podnikatel.cz: „Lex Babiš“ Babišovi paradoxně pomůže

„Lex Babiš“ Babišovi paradoxně pomůže

DigiZone.cz: Parlamentní listy: kde končí PR...

Parlamentní listy: kde končí PR...

Vitalia.cz: Jak Ondra o astma přišel

Jak Ondra o astma přišel

Lupa.cz: Další Češi si nechali vložit do těla čip

Další Češi si nechali vložit do těla čip

Vitalia.cz: Tesco nabízí desítky tun jídla zdarma

Tesco nabízí desítky tun jídla zdarma