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:
- 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
- volá se procedura pro inicializaci prvního sloupce
- volá se procedura pro inicializaci druhého sloupce
- 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");
JavaFX: zobrazení dat z tabulky ve widgetu
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í:
- povedlo se!
- jsou zobrazené sloupce pro identifikátor a číslo kraje
- oba sloupce jsou zarovnané na levý okraj
- řádky v tabulce střídají barvu
- 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
- tabulka zabírá na šířku skoro celý prostor formuláře, na výšku ale nikoliv
- pomocí myši můžeme měnit šířku sloupců
- jsou k dispozici funkční vodorovné i svislé skrolovací pruhy
- 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í:
- žádné formátování, položka se nechá jako načtený řetězec
- řetězcová položka se zprava ořízne
- 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
- položka datum/čas se převádí na v našich končinách „běžný“ formát DD.MM.RRRR
- čí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.