LINUXSOFT.cz
Username: Password:     
    CZ UK PL

> Java (27) - seznamy, stromy, tabulky

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 | read 39360×

DISCUSSION   

Modely a jejich smysl

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).

Seznamy ve Swingu

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.

Obyčejný seznam

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.

Rozbalovací seznam

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í).

Stromy

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).

Tabulky

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í.

Vlastní zobrazení - proč ne?

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.

 

DISCUSSION

For this item is no comments.

Add comment is possible for logged registered users.
> Search Software
> Search Google
1. Pacman linux
Download: 4881x
2. FreeBSD
Download: 9068x
3. PCLinuxOS-2010
Download: 8565x
4. alcolix
Download: 10950x
5. Onebase Linux
Download: 9662x
6. Novell Linux Desktop
Download: 0x
7. KateOS
Download: 6249x

1. xinetd
Download: 2414x
2. RDGS
Download: 937x
3. spkg
Download: 4762x
4. LinPacker
Download: 9969x
5. VFU File Manager
Download: 3199x
6. LeftHand Mała Księgowość
Download: 7204x
7. MISU pyFotoResize
Download: 2813x
8. Lefthand CRM
Download: 3564x
9. MetadataExtractor
Download: 0x
10. RCP100
Download: 3123x
11. Predaj softveru
Download: 0x
12. MSH Free Autoresponder
Download: 0x
©Pavel Kysilka - 2003-2024 | mailatlinuxsoft.cz | Design: www.megadesign.cz