Hlavní navigace

Programování v JavaFX: zobrazení a formátování dat z databázové tabulky

Jaromír Vojtaj 1. 10. 2015

Minulý pátý díl seriálu byl zaměřen na krátký úvod do problematiky databází a konkrétně PG, připravili jsme si v něm data pro první ukázkový příklad a začali postupně tvořit potřebné GUI i výkonný kód. V dnešním šestém dílu dokončíme potřebný kód pro základní zobrazení dat z tabulky ve formuláři.

V minulém dílu jsme vytvořili základní funkci, která načítá data z databázové tabulky a ukládá je v příslušném formátu do lokální proměnné. Zatím je nám ale tato funkce k ničemu, i kdybychom získaná data přiřadili k vytvořenému widgetu. Chybí nám totiž zobrazení jednotlivých sloupců, resp. propojení sloupců, vytvořených ve widgetu TableView a „reálných“ sloupců z tabulky v databázi. Jak již bylo uvedeno v minulém díle, je procedura pro inicializaci sloupců tabulky poměrně komplikovaná. Počet řádků tomu sice moc neodpovídá, ale posuďte sami:

        private void initCol1() {
            col_ID.setCellValueFactory(new Callback<TableColumn.CellDataFeatures<ObservableList<String>, String>, ObservableValue<String>>() {
                @Override
                public ObservableValue<String> call(TableColumn.CellDataFeatures<ObservableList<String>, String> param) {
                    return new SimpleStringProperty(param.getValue().get(0));
                } }); }

Výkonné řádky jsou zde prakticky jenom tři a zase nám s nimi hodně pomůže IJI. Vytvoříme si novou proceduru a když zadáme část příkazu pro první sloupec tabulky (stačí se dopracovat k new Callback), IJI doplní kostru příkazu. My pak musíme udělat dvě změny:

  • na místech otazníků doplnit kód ObservableList<String> a String podle vzoru
  • místo příkazu return null; vložit kód podle příkladu

Pokud toto vše a přesně provedeme, nemáme ještě vyhráno. IJI se totiž brání neodbytným hlášením o chybě. Problém není ale v naší nové funkci, ale v deklaraci tabulky a sloupců, které pocházejí z JFXSB. Na netu se dá najít více možností a my z nich použijeme jednu a změníme deklarace takto:

@FXML private TableView<ObservableList<ObservableList>> table_Obce;
@FXML private TableColumn<ObservableList<String>, String> col_ID;

Pak chybové hlášení zmizí a my můžeme začít uvažovat o tom, jestli by už nešlo data zobrazit. Zatím to ale ještě neuděláme a podíváme se na třetí bod z našeho minule uvedeného seznamu problémů – zobrazení různých typů dat. Když se podíváme na kód pro vytvoření tabulky obce tak zjistíme, že sloupce jsou postupně typu CHAR, SmallInt, VARCHAR, SmallInt, VARCHAR, VARCHAR. Toto nastavení samozřejmě kopíruje také funkce pro čtení a ukládání dat (dataView), kde se rozdíly projeví při načítání dat do řádku. Před chvílí vytvořená funkce a doplněná deklarace tabulky a prvního sloupce s tím musí samozřejmě korespondovat. Z tohoto důvodu se podíváme na druhý sloupec v pořadí, kde je celočíselná hodnota. Změníme tedy jeho deklaraci takto:

@FXML private TableColumn<ObservableList<String>, Integer> col_Ckraje;

a vytvoříme novou proceduru pro inicializaci druhého sloupce tabulky:

    private void initCol2() {
        col_Ckraje.setCellValueFactory(new Callback<TableColumn.CellDataFeatures<ObservableList<String>, Integer>, ObservableValue<Integer>>() {
            @Override
            public ObservableValue<Integer> call(TableColumn.CellDataFeatures<ObservableList<String>, Integer> param) {
                return new SimpleObjectProperty(param.getValue().get(1));
            } }); }

Tato druhá procedura řeší nás problém se zobrazením různých typů dat v prvním a druhém sloupci tabulky a my se tím pádem můžeme pomalu začít zajímat o to, jak provést skutečné zobrazení. K tomu budeme potřebovat volání jak vytvořených funkcí a procedur, tak další příkazy. Pro jistotu si na to vytvoříme extra proceduru včetně všech potřebných volání:

private void showData() {
        final ObservableList data = dataView("SELECT * FROM obce;");    //1
        initCol1();                         //2
        initCol2();                         //3
        table_Obce.setItems(data);                  //4
        }

Poslední věc, která nám ještě zbývá je volání této procedury, která má tyto funkce:

  1. do deklarované lokální proměnné příslušného typu se ukládá volání funkce s konkrétním zněním SQL dotazu
  2. volá se procedura pro inicializaci prvního sloupce
  3. volá se procedura pro inicializaci druhého sloupce
  4. k deklarované tabulce se přiřazují data odpovídajícího typu

Uvedené příkazy jsou dostatečné k tomu, aby se ve widgetu zobrazily první dva sloupce naší tabulky. Takto vytvořenou proceduru budeme opět volat při otevření formuláře a uděláme to trochu „komplikovaněji“ (zakomentujeme řádek pro kontrolu připojení k databázi a přesuneme ji do nového volání. To se spustí pouze tehdy, když je připojení aktivní, jinak se hlásí chyba):

//System.out.println(logOK());
        if(logOK())
            showData();
            else
            System.out.println("FALSE");

Teď je vše již připravené k překladu a spuštění aplikace. Když otevřeme příslušný formulář, tak zjistíme, že se nám oba první sloupce skutečně zobrazí. První obrázek galerie nám ukazuje několik skutečností:

  1. povedlo se!
  2. jsou zobrazené sloupce pro identifikátor a číslo kraje
  3. oba sloupce jsou zarovnané na levý okraj
  4. řádky v tabulce střídají barvu
  5. druhý obrázek v galerii ukazuje, že řádek vybraný kliknutím myši a řádek, nad kterým je aktuálně kurzor myši, mají také jiné barvy
  6. tabulka zabírá na šířku skoro celý prostor formuláře, na výšku ale nikoliv
  7. pomocí myši můžeme měnit šířku sloupců
  8. jsou k dispozici funkční vodorovné i svislé skrolovací pruhy
  9. pokud budeme chvíli listovat daty dolů, zjistíme, že jsou řazena podle hodnot v prvním sloupci

Zkusíme se na některé z těchto zjištění podívat. Jako první vyřešíme zbytečně malou výšku widgetu. Otevřeme si v JFXSB příslušný soubor a pomocí „očka“ na spodní straně widgetu TableView zvětšíme jeho výšku tak, jak je to patrné na třetím obrázku galerie. Změnu uložíme a rovnou zkusíme nové spuštění aplikace v IJI (nemusíme nic měnit v kódu, protože tyto úpravy se aktivují spolu s načítáním FXML souboru při otevření příslušného formuláře). Jak ukazuje čtvrtý obrázek v galerii, výsledek je hned zajímavější.

Řazení záznamů jsme doposud nijak neřešili, a tak je celkem logické, že jsou řazené podle primárního klíče. Pro nás by ale bylo určitě zajímavější, kdyby se seznam řadil podle čísel okresů a krajů. I zde je pomoc velmi jednoduchá. Stačí změnit SQL příkaz při volání funkce a výsledek je pak viditelný na pátém obrázku galerie (sloupce s číslem okresu zatím zobrazený nemáme, ale to příkladu nijak neškodí):

final ObservableList data = dataView("SELECT * FROM obce ORDER BY ckraje, cokresu;");

Dále si zkusíme nějak upravit vzhled widgetu pomocí CSS. To už jsme dříve dělali, takže si otevřeme soubor main.css a přidáme dva řádky kódu a změnu uložíme. V aplikaci není nutné dělat nic, jenom spustit a otevřít formulář. Výsledek je viditelný na šestém obrázku v galerii (šířky sloupců a jejich neúplné nadpisy zatím řešit nebudeme):

.table-view .column-header { -fx-font-size: 14pt;}
.table-cell { -fx-font-size: 12pt; -fx-font-weight: bold}

Poslední věc, na kterou se z výše uvedeného seznamu zaměříme, je zarovnání hodnot ve sloupcích. To může být někdy důležité a docela určitě to patří k zajištění lepší přehlednosti při zobrazování různých typů dat. Řešení je podobné proceduře pro inicializaci sloupců, ale kód je bohužel ještě komplikovanější. Podobně jako u inicializace sloupců si necháme od IJI vygenerovat kostru. Příjemná změna je v tom, že když máme nově deklarovaný sloupec, tak se automaticky doplní příslušné typy parametrů a my je nemusíme doplňovat ručně. Možný výsledek vypadá asi nějak takto (pro první sloupce tabulky a pouze počáteční řádky:)

    public void alignCol1() {
            colID.setCellFactory(new Callback<TableColumn<ObservableList<String>, String>, TableCell<ObservableList<String>, String>>() {
                @Override
                public TableCell<ObservableList<String>, String> call(TableColumn<ObservableList<String>, String> param) {

Potud je kód úplně stejný jako pro inicializaci prvního sloupce. Pak ale nastává uvedená komplikace a je nutné přidat dalších 5 řádků kódu a doplnit návratovou hodnotu funkce:

TableCell<ObservableList<String>, String> tc = new TableCell<ObservableList<String>, String>() {
           @Override
           public void updateItem(String item, boolean empty) {
               if (item != null) { setText(item); } } };
           tc.setAlignment(Pos.CENTER);
           return tc;
           } }); }

Do importů se ještě doplní potřebná knihovna:

import javafx.geometry.Pos;

Pak už nezbývá než doplnit na příslušném místě volání nové funkce (v proceduře showData se přidá 1 řádek):

alignCol1();

Aplikace se přeloží a spustí a na posledním obrázku galerie je vidět, jak vypadá tabulka s daty. Pro lepší „umělecký dojem“ byl první sloupec rozšířen jako důkaz o zarovnání jeho hodnot na střed. V další fázi by samozřejmě bylo možné do tabulky přidat další inicializované sloupce, zarovnat je a ukázat tak celou sadu dat. My to ale dělat nebudeme a zaměříme se na trochu jinou věc. Když se zamyslíme na předchozími funkcemi a procedurami, tak je asi hned každému jasné jedno: řešení je sice funkční, ale nijak extra šikovné. Pro každou tabulku v aplikaci se musí dělat extra funkce pro získání dat, všechny sloupce se musejí jednotlivě inicializovat, zarovnávat apod. Při našich zkušebních příkladech to problém není, ale pro nějakou větší reálnou aplikaci už samozřejmě ano. Proto se zkusíme zaměřit na nějaké obecnější řešení. Vzhledem k tomu, že to budeme dělat na jiném formuláři, dáme do přílohy aktuální kód třídy, která se jediná měnila: samexam1.java.

Nový formulář vytvoříme snadno. Otevřeme si JFXSB a současný soubor samExam1.fxml. Uložíme ho pod nový názvem samExam2.fxml. V záložce Controller nastavíme název budoucí třídy na:

jfxapp.samexam2

a změnu uložíme. To nám bude prozatím stačit, protože všechny ostatní záležitost pro GUI máme nastavené z předchozího příkladu. Vytvoříme si novou kostru kontroléru, zkopírujeme a přejdeme do IJI. Zde vytvoříme novou třídu s názvem samexam2.java a vložíme do ní kostru kontroléru ze schránky. Do třídy mainForm pak přidáme proceduru pro otevření dalšího formuláře a její volání příslušným tlačítkem:

private void showExam2(Stage primaryStage) throws  Exception {
        Parent root = FXMLLoader.load(getClass().getResource("/samExam2.fxml"));
        primaryStage.setTitle("Ukázková úloha č. 2");
        primaryStage.setScene(new Scene(root, 1000, 730));
        primaryStage.initModality(Modality.APPLICATION_MODAL);
        primaryStage.show(); }
@FXML void btn_E2_Click(ActionEvent event) throws Exception { showExam2(new Stage()); }

Pro jistotu zkusíme aplikaci přeložit, spustit a otevřít nové okno. Pokud jsme někde neudělali nějakou zákeřnou chybu, mělo by se to podařit. Tímto máme připravenou půdu pro to, abychom se mohli zamyslet na tím, jak řešit a vyřešit výše uvedené záležitosti. Jako první si vezmeme na paškál funkci viewData a hlavně tu část, kde se do řádku ukládají jednotlivé hodnoty podle svých typů:

row.add(rs.getString(1));
row.add(rs.getInt(2));
row.add(rs.getString(3));
row.add(rs.getInt(4));
row.add(rs.getString(5));
row.add(rs.getString(6));

Pokud se nad tímto kódem zamyslíme, tak je jeho nějaká „automatizace“ docela komplikovaná tím, že se zde používají speciální funkce get+konkrétní typ dat. Přesto ale určitá možnost existuje, i když je také trochu komplikovaná. Je totiž založena na tom, že všechny položky se načítají jako text a v rámci funkce se převádějí na jiné typy dle potřeby. Není to řešení úplně ideální, ale je funkční a pro větší aplikace určitě přínosné. K řešení se využije načítání všech typů dat s tím, že se převedou na typ String a následně se formátují podle potřeby. K tomu je třeba použít nový parametr volání, ve kterém budou požadované typy dat. Zjednoduší se také deklarace jednotlivých sloupců tabulky, protože budou všechny stejného typu String. Další podrobnosti budou uvedeny při rozboru příslušného kódu. Já osobně využívám většinou pět typů dat, resp. jejich formátování:

  1. žádné formátování, položka se nechá jako načtený řetězec
  2. řetězcová položka se zprava ořízne
  3. položka se formátuje jako desetinné číslo s požadovaným počtem desetinných míst, používá se externí formátovací funkce
  4. položka datum/čas se převádí na v našich končinách „běžný“ formát DD.MM.RRRR
  5. číselné položky „blízké nule“ se zobrazují jako prázdný řetězec

Význam tohoto řešení je v již zmiňované obecnosti, kde není předem omezen počet sloupců a jejich typů. Další důležitou vlastností je možnost formátovat zobrazení tabulkových dat podle potřeb tvůrců aplikací či ještě lépe jejich uživatelů. Tohle už určitě za nějakou tu námahu stojí. Pokud si dále porovnáme obě funkce pro inicializaci prvních dvou sloupců tabulky, tak zjistíme, že zde je situace mnohem lepší a jednodušší. Odlišnosti jsou pouze ve třech věcech:

  • název sloupce, kterého se inicializace týká
  • potřebné typy proměnných (jak bylo uvedeno v předchozím odstavci, budou nově všechny stejné!)
  • pořadí sloupce v tabulce

Z tohoto výčtu je jasné, že zde je velký prostor pro vytvoření univerzální funkce, které stačí pouze dodat parametr s názvy inicializovaných sloupců. Velmi obdobná je situace při zarovnávání hodnot při zobrazení ve sloupcích. Zde je navíc pouze jedna jediná vlastnost: nějak se musí deklarovat a zajistit pro daný sloupec potřebné zarovnání. Navíc se nemůže použít standardní hodnota (vlevo), ale je nutné vždy uvádět jedno z možností nalevo – na střed – napravo. Ani jedno zde navržené řešení už nebudeme v dnešním dílem dál rozebírat ani řešit a necháme to do dílu příštího. Dnes si pouze připravíme funkci pro výše uvedený formát desetinných čísel. K tomu budeme potřebovat jednak vlastní funkci a také import příslušných knihoven:

private NumberFormat nform2 = new DecimalFormat("#,##0.00");
import java.text.DecimalFormat;
import java.text.NumberFormat;

V dnešním dílu jsme se zaměřili na kód potřebný pro základní zobrazení dat z tabulky ve formuláři a nastínili možná obecnější řešení této problematiky. V příštím dílu budeme obecnější řešení implementovat a komentovat a konečně si ukážeme kompletní obsah tabulky ukázkových dat.

Našli jste v článku chybu?

22. 10. 2015 18:43

Vizte první díl seriálu, je to autorova zkratka – IntelliJ IDEA.

22. 10. 2015 7:47

prosím, bude li kdokoli tak laskav a napíše mi co je to IJI, moc mne potěší. Ač jsem v Javě napsal nějaké programy, nikdy jsem pojem IJI neslyšel..

Vitalia.cz: Znáte „černý detox“? Ani to nezkoušejte

Znáte „černý detox“? Ani to nezkoušejte

Root.cz: 250 Mbit/s po telefonní lince, když máte štěstí

250 Mbit/s po telefonní lince, když máte štěstí

Vitalia.cz: Paštiky plné masa ho zatím neuživí

Paštiky plné masa ho zatím neuživí

Podnikatel.cz: Přehledná titulka, průvodci, responzivita

Přehledná titulka, průvodci, responzivita

Měšec.cz: U levneELEKTRO.cz už reklamaci nevyřídíte

U levneELEKTRO.cz už reklamaci nevyřídíte

Podnikatel.cz: EET: Totálně nezvládli metodologii projektu

EET: Totálně nezvládli metodologii projektu

DigiZone.cz: Sony KD-55XD8005 s Android 6.0

Sony KD-55XD8005 s Android 6.0

DigiZone.cz: Recenze Westworld: zavraždit a...

Recenze Westworld: zavraždit a...

Vitalia.cz: Chtějí si léčit kvasinky. Lék je jen v Německu

Chtějí si léčit kvasinky. Lék je jen v Německu

Vitalia.cz: Jsou čajové sáčky toxické?

Jsou čajové sáčky toxické?

120na80.cz: Horní cesty dýchací. Zkuste fytofarmaka

Horní cesty dýchací. Zkuste fytofarmaka

Vitalia.cz: Baletky propagují zdravotní superpostel

Baletky propagují zdravotní superpostel

Měšec.cz: Jak vymáhat výživné zadarmo?

Jak vymáhat výživné zadarmo?

Root.cz: Certifikáty zadarmo jsou horší než za peníze?

Certifikáty zadarmo jsou horší než za peníze?

Vitalia.cz: To není kašel! Správná diagnóza zachrání život

To není kašel! Správná diagnóza zachrání život

Lupa.cz: Google měl výpadek, nejel Gmail ani YouTube

Google měl výpadek, nejel Gmail ani YouTube

DigiZone.cz: Flix TV má set-top box s HEVC

Flix TV má set-top box s HEVC

Podnikatel.cz: Babiše přesvědčila 89letá podnikatelka?!

Babiše přesvědčila 89letá podnikatelka?!

Vitalia.cz: Dáte si jahody s plísní?

Dáte si jahody s plísní?

Podnikatel.cz: Chtějte údaje k dani z nemovitostí do mailu

Chtějte údaje k dani z nemovitostí do mailu