Prakticky žádná větší aplikace se neobejde bez zobrazování a modifikace
větších datových celků. Obvykle bývají reprezentovány jako různé seznamy,
stromy a tabulky. Framework Swing nám nabízí v tomto ohledu velice užitečný
a snadno použitelný soubor prostředků.
31.8.2006 06:00 | Lukáš Jelínek | přečteno 39225×
Než přejdeme ke konkrétním objektovým třídám, dovolil bych si nejprve představit obecný mechanismus práce s daty, který je odděluje od vlastního GUI. Jsou to tzv. modely. Model není nic jiného než implementace rozhraní, přes které grafická komponenta přistupuje k datům pro jejich zobrazení a úpravy. Komponenta tedy nemusí vůbec znát detaily implementace, vystačí si s rozhraním.
K čemu je to dobré? Použití modelů výrazně zlepšuje variabilitu implementace a umožňuje důsledné oddělení GUI od backendu. Máme-li například tabulku, mohou být data uložena jak přímo v aplikaci (v kolekcích apod.), tak třeba i v databázi, klidně i s přímým přístupem (bez mezičlánku v podobě úložiště v aplikaci). Současně lze manipulace nad daty provádět také jinak než přes GUI, aniž by se na způsobu uložení dat cokoli měnilo.
Praktická realizace modelů ve Swingu je řešena tak, že máme jednak základní
rozhraní (např. TableModel
), pak abstraktní implementaci (obsahuje často
používanou funkcionalitu pro práci s daty - např. AbstractTableModel
), a
nakonec výchozí implementaci modelu (např. DefaultTableModel
- používá se
v případech, kdy nepoužijeme implementaci vlastní). Někdy je hierarchie
složitější (obsahuje např. neproměnné a proměnné modely), komponenty
navíc pracují s více modely, protože kromě uložení dat lze obdobně
využít modely i pro další účely (třeba pro vlastnosti sloupců nebo pro výběry).
Důležitou vlastností modelů je oznamování událostí. Dojde-li ke změně v datech, model to oznámí všech přihlášeným odběratelům. Již jsem to zmiňoval někdy dříve, ale opět to připomínám. Využívá se standardní mechanismus práce s událostmi (tedy vytvoření instance objektu události a její předání v argumentu metody pro obsluhu dané události).
Můžeme v zásadě rozlišovat dva druhy seznamů: obyčejné (v okénku je nějaký počet prvků, některé z nich mohou být "vybrané") a rozbalovací, tzv. combo boxy (normálně je zobrazen jen jediný prvek, při rozbalení se teprve zobrazí další). Každý z těchto seznamů má svoji třídu a svůj model. Podívejme se na ně blíže.
Na úvod bych chtěl říci, že ačkoli seznamy obsahují metody pro operace nad daty, pokud máme přímo k dispozici referenci k modelu, je lepší volat metody modelu. Tím spíš, že model obecně nemusí podporovat všechny operace, které lze prostřednictvím seznamu volat.
Je reprezentován třídou JList
a jako model používá rozhraní ListModel
.
Rozhraní modelu má pouze 4 metody, pro přidání/odebrání odběratele událostí,
pro zjištění délky seznamu a pro získání určitého prvku seznamu. Text prvku
se získává pomocí metody toString()
příslušného prvku, v seznamu mohou bez
problémů být i prvky různých typů.
Co se týká samotného vykreslování, seznam nemá vlastní posuvníky (je to podobné
jako např. u JTextArea
). Proto ho téměř vždy umísťujeme do komponenty
JScrollPane
, která posuvníky poskytne. Třída seznamu disponuje obrovským
množstvím metod (což dokazuje, jak velké pole působnosti zde máme) - z
prostorových důvodů se ovšem podíváme jen na několik málo z nich. Zde je
krátký příklad, jak se seznamem pracovat:
DefaultListModel m = new DefaultListModel(); m.addElement("Prvek 1"); m.addElement("Prvek 2"); m.addElement("Prvek 3"); JList list = new JList(m); list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); JScrollPane sp = new JScrollPane(list, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); ... Object o = list.getSelectedValue();
V příkladu nejprve vytvoříme instanci výchozího datového modelu a vložíme
do ní prvky. Pak zkonstruujeme seznam (s použitím daného modelu), tomu
nastavíme režim výběru (pouze jednotlivé položky) a vložíme ho do komponenty
s posuvníky. V reálné aplikaci bychom případně pak nastavovali velikost,
pozici apod. Poslední řádek příkladu pak ukazuje, jak zjistit, který prvek
seznamu je vybrán. Tato metoda vrací přímo příslušný prvek (případně null
);
pokud potřebujeme index, použije se getSelectedIndex()
. Jsou k dispozici i
metody pro vícečetné výběry, ale to v tomto příkladu nemá význam.
Co by se stalo, pokud bychom přidali do modelu nějaký prvek? Výchozí implementace
na to reaguje zavoláním metody fireIntervalAdded()
z abstraktní třídy
AbstractListModel
, což vyústí v distribuci objektu události. Událost obdrží
též instance třídy JList
a proto překreslí seznam. Podobně by se to chovalo
při odebrání jednoho nebo více prvků anebo při jejich změně. Pokud se tato
změna děje uvnitř objektu prvku, není ji ovšem tento model schopen detekovat.
Ještě upřesním, že DefaultListModel
je vlastně obal na třídu Vector
,
se vším, co k tomu patří - proto je mnohdy lepší použít vlastní implementaci
modelu, hlavně při častých změnách uvnitř seznamu.
Tuto komponentu potřebujeme možná ještě častěji než normální seznam. Je
tvořena třídou JComboBox
a modelem ComboBoxModel
(potomek rozhraní ListModel
- obsahuje navíc podporu pro výběr prvku, který je zobrazen v nerozbaleném
stavu). Potomkem ComboBoxModel
je MutableComboBoxModel
, umožňující přidávání
a odebírání prvků.
Combo box se z hlediska použití příliš neliší od běžného seznamu. Protože ovšem zobrazuje (v nerozbaleném stavu) jen jednu položku, do GUI se začleňuje podobným způsobem jako textové pole. Rozbalený seznam má v případě potřeby posuvník, není třeba se o to starat. Výběr může samozřejmě obsahovat pouze jediný prvek.
Rozlišujeme dva druhy rozbalovacích seznamů - editovatelné a needitovatelné.
U needitovatelných se pracuje pouze s výběrem prvku. Editovatelný seznam
umožňuje upravit vybranou položku, která však implicitně zůstává mimo vlastní
seznam - v modelu se tedy neobjeví a metoda getSelectedIndex()
vrací -1.
Potřebujeme-li, aby se upravený prvek objevil v seznamu, musíme se o to
postarat vlastními silami.
Následující příklad ukazuje základy práce s rozbalovacím seznamem:
String sa[] = { "Prvek 1", "Prvek 2", "Prvek 3", "Prvek 4" }; DefaultComboBoxModel m = new DefaultComboBoxModel(sa); JComboBox cb = new JComboBox(m); cb.setEditable(true); cb.addItemListener(new ItemListener() { public void itemStateChanged(ItemEvent e) { if (e.getStateChange() == ItemEvent.SELECTED) { ... } } });
Nejdřív opět vytvoříme výchozí model. Tento příklad ukazuje jiný způsob než minule - konstruktoru se předává připravené pole prvků. Pak zkonstruujeme seznam a označíme ho jako editovatelný. Další úsek kódu slouží k reakci na změnu výběru. Když uživatel vybere prvek ze seznamu (nebo upraví ten, který je vybraný), provedeme zvolenou akci. Reagovat lze samozřejmě i na zrušení výběru (odvybrání).
Zatímco seznamy byly ve své podstatě velmi jednoduché, stromy jsou o něco složitější. Stromové datové struktury asi každý zná, stromové zobrazení v GUI rovněž. Proto by snad nikdo neměl mít problémy s pochopením toho, jak swingovská implementace stromů funguje.
Začneme modelem. Základem je rozhraní TreeModel
, obsahující (kromě známých
metod pro správu odběratelů událostí) metody pro zjištění kořene stromu,
počtu potomků určitého uzlu, přístup k potomkovi přes index, zjištění indexu
potomka a dotaz, zda je uzel listem. Dále je tu také metoda valueForPathChanged()
,
volaná při změně hodnoty uzlu ve stromě. Často používáme výchozí implementaci,
DefaultTreeModel
, obsahující ještě řadu dalších metod.
Nelze opomenout třídu TreePath
. Představuje cestu, kterou musíme projít
od kořene k nějakému uzlu. Je např. předávána do výše uvedené metody
valueForPathChanged()
, ale hodí se i v jiných případech.
Ačkoli rozhraní TreeModel
pracuje s obecnými objekty, ve třídě DefaultTreeModel
se používají specializované třídy - implementace rozhraní TreeNode
. Má to
své důvody. Pokud bychom totiž neuchovávali informace o stromové hierarchii
v objektech, musel by to dělat model a to není příliš systémové. Rozhraní
TreeNode
má potřebné metody, které umožňují zjistit informace o rodičovi
a případných potomcích. Často se používá rozhraní MutableTreeNode
, které
přidává navíc ještě manipulační operace.
K dispozici je též výchozí implementace, DefaultMutableTreeNode
. V mnoha
případech si s ní vystačíme, tím ovšem vyvstává problém, jak tam napojit
data. Jednoduše - pomocí odkazu na uživatelský objekt. Pokud je odkaz nastaven,
přebírá se zobrazovaný text z tohoto objektu (metoda toString()
).
Třída JTree
, představující grafickou komponentu pro kreslení stromu, patří
mezi nejsložitější třídy ve Swingu. Má obrovské množství metod, umožňujících
provádět se stromem všemožné operace. Popisovat je nemá cenu, raději si na
příkladu ukážeme, jak se se stromem pracuje:
void addFiles(File dir, DefaultMutableTreeNode parent) { File fa[] = dir.listFiles(); for (int i=0; i<fa.length; i++) { File f = fa[i]; Arrays.sort(fa); DefaultMutableTreeNode node = new DefaultMutableTreeNode(f.getName()); parent.add(node); if (f.isDirectory()) { addFiles(f, node); } } } File home = new File(System.getProperty("user.home")); DefaultMutableTreeNode root = new DefaultMutableTreeNode(home.getName()); addFiles(home, root); DefaultTreeModel m = new DefaultTreeModel(root); JTree tree = new JTree(m); JScrollPane sp = new JScrollPane(tree, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
Příklad ukazuje zobrazení adresářového stromu (pod domovským adresářem) s rekurzivní implementací. Připomínám, že takto by se s adresářovým stromem pracovat nemělo, může být velice rozsáhlý a jeho načtení by trvalo dlouho. Vhodnější je načítat obsah adresáře až v okamžiku, kdy se rozbaluje. Zde jsem ovšem pro jednoduchost zvolil načtení celého stromu naráz.
Podívejme se nejdřív na metodu addFiles()
. Ta se volá rekurzivně (nebyl by
samozřejmě problém to převést na iterační řešení) a zajišťuje zpracování
obsahu adresáře, tedy vytvoření podstromu tohoto adresáře. Asi není co příliš
vysvětlovat, prostě se získá seznam souborů, ten se abecedně seřadí a
jednotlivé soubory se přidají do stromu jako potomci aktuálního uzlu. Pokud je
daný soubor adresářem, metoda se na něj opět zavolá.
Při samotném použití pak zjistíme domovský adresář (získáme ho z vlastností systému), vytvoříme pro něj kořenový uzel a zavoláme výše popsanou metodu. Až je logický strom vytvořen, zkonstruujeme pro něj model a ten předáme konstruktoru grafické komponenty stromu. Zbývá už jen vložit komponentu do oblasti s posuvníky.
Když už jsem zmínil to postupné načítání při rozbalování, naznačím ještě, jak
by se to dělalo. Existuje rozhraní TreeWillExpandListener
, které slouží ke
zpracování událostí generovaných stromem před tím, než se rozbalí nebo sbalí
větev pod uzlem. Vytvoříme si tedy implementaci rozhraní a zaregistrujeme ji
u stromu:
tree.addTreeWillExpandListener(new TreeWillExpandListener() { public void treeWillExpand(TreeExpansionEvent e) throws ExpandVetoException { DefaultMutableTreeNode node = (DefaultMutableTreeNode) e.getPath().getLastPathComponent(); ... } public void treeWillCollapse(TreeExpansionEvent e) throws ExpandVetoException {} });
Příklad pracuje s anonymní třídou. Implementovat musíme samozřejmě obě metody
rozhraní, i když používáme jen jednu z nich. Objekt události obsahuje cestu
k uzlu, který bude rozbalen. Lze snadno získat přímo tento uzel, jak je
v příkladu ukázáno. Pokud bychom ale takto implementovali načítání adresářového
stromu, takto jednoduché by to nebylo. Buď bychom museli jako uživatelský
objekt stromu použít něco jiného než obyčejný řetězec s název souboru
(objekt File
použíte nejde - ve stromě by se zobrazovaly celé cesty), anebo
cestu k načítanému adresáři sestavit z komponent cesty k uzlu (TreePath
).
Posledním objektem v tomto dílu seriálu bude tabulka, představovaná třídou
JTable
. Nemusím snad připomínat, že základem budou opět modely, jeden pro
samotnou tabulku, druhý pro sloupce.
Model tabulky má metody pro zjištění počtu řádků a sloupců, získání a uložení hodnoty buňky, zjištění názvu a třídy sloupce (viz dále), a zjištění editovatelnosti buňky. Třída sloupce má zásadní význam - ovlivňuje totiž zobrazování a případné úpravy buněk v daném sloupci (různé např. pro textové řetězce a pravdivostní hodnoty). Sloupcový model umožňuje velice detailní ovlivnění toho, jak se bude se sloupci tabulky zobrazovat. Bohužel na tu dostatečný popis není dost prostoru, proto ho musím vynechat.
Pro případy, kdy si vytváříme vlastní model pro tabulková data, je nejlepší
použít abstraktní třídu AbstractTableModel
. Povinně se implementují pouze
tři metody (getRowCount()
, getColumnCount()
a getValueAt()
) - pokud si
vystačíme s read-only režimem, nepotřebujeme pro základní funkcionalitu nic dalšího.
Pro editovatelné buňky musíme předefinovat metody setValueAt()
a isCellEditable()
.
Obvykle je ale vhodné předefinovat i další metody (názvy sloupců, jejich třídy
atd.). Uložení dat je plně v naší režii, při jejich změně se musíme postarat
o zavolání příslušných metod fireXXX()
, aby byli informováni odběratelé
událostí. K implementaci vlastního tabulkového modelu se ještě vrátíme později,
až se dostaneme k problematice databází.
V jednodušších případech si lze vystačit s výchozí implementací modelu, třídou
DefaultTableModel
. Ta používá pro tabulkové řádky kolekce typu Vector
, a
ukládá je opět do instance Vector
. Totéž platí i pro názvy sloupců. Model
automaticky generuje události při změnách v datech, změny uvnitř datových
objektů však samozřejmě není schopen detekovat. Všechny buňky jsou v tomto
modelu editovatelné.
Pro metody třídy JTable
platí totéž, co jsem uvedl u předchozích grafických
komponent - je jich mnoho a popisovat je nemá smysl. Proto bude lepší si je
opět ukázat na příkladu:
DefaultTableModel m = new DefaultTableModel(); m.addColumn("Název"); m.addColumn("Hodnota"); Properties p = System.getProperties(); Iterator<Object> it = p.keySet().iterator(); while (it.hasNext()) { String key = (String) it.next(); String row[] = { key, p.getProperty(key) }; m.addRow(row); } JTable table = new JTable(m); JScrollPane sp = new JScrollPane(table, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
Příklad ukazuje tabulku vlastností systému. V prvním sloupci je název vlastnosti, ve druhém její hodnota. S vlastnostmi systému jsme se setkali jak v tomto článku, tak i v některých dřívějších - jsou to textové hodnoty popisující prostředí, v němž běží program (např. domovský a pracovní adresář, oddělovač adresářových cest, kódování znaků a mnoho dalších parametrů). Hodnoty se zobrazují neseřazené.
V příkladu se nejprve vytvoří prázdný model a přidají se do něj dva sloupce.
Pak se postupně přidávají jednotlivé hodnoty - získávají se pomocí iterátoru.
Další kroky jsou pak triviální (a stejné jako u přechozích příkladů),
vytvoří se instance JTable
a ta se vloží do grafického kontejneru s posuvníky.
Kdo si příklad vyzkouší ("omáčka" je pochopitelně vynechána a každý si ji
doplní sám - např. na základě příkladů z přechozích kapitol), možná zjistí, že
může obsah kterékoli buňky bez problémů změnit (dvojklikem na buňce se
spustí editace). To je přesně v souladu s tím, jak jsem popsal chování třídy
DefaultTableModel
. Změněné hodnoty se pochopitelně nijak nepromítnou do
nastavení příslušných systémových vlastností.
Příští kapitola naváže na tuto a bude věnována vlastním zobrazovačům (rendererům) a editorům. Při zobrazování a úpravách hodnot v tabulkách a dalších grafických objektech totiž nejsme zdaleka omezeni jen tím, co nám tyto komponenty nabízejí. Můžeme si vytvořit vlastní třídy, které se budou chovat tak, jak právě potřebujeme. Potřebujeme třeba zobrazovat záporná čísla červeně nebo při editaci kontrolovat formát vkládaných dat. Obojí, a ještě mnoho dalšího, lze bez problémů zařídit.