Obsah
Swing poskytuje širokou podporu pro tabulkové zobrazení
základní třída javax.swing.JTable
podpůrné třídy (7) a rozhraní (4) jsou v balíku
javax.swing.table
kromě nich jsou v balíku javax.swing
další podpůrné
třídy a rozhraní společné i pro JTree
CellEditor
ListCellRenderer
ListSelectionModel
Renderer
AbstractCellEditor
dohromady umožňují vytvořit téměř jakkoliv složitou tabulku s libovolnými funkcemi
je nutné mít dvou (tří -- podle toho jak je na to díváte) stupňovou organizaci
na první (datové) úrovni je datový model
typicky splňuje rozhraní
javax.swing.table.TableModel
je to rozhraní mezi datovou a prezentační vrstvou
datový model může být složen z více vrstev:
ze třídy implementující rozhraní
TableModel
typicky se třída datové úrovně dědí od
AbstractTableModel
pak se překrývá jen několik málo metod
tato třída je nutná, protože se předává do prezentační úrovně
viz dále třídu
PrimitivniDatovyModel
z vlastních dat získaných libovolným způsobem
toto je pak ta „třetí“ vrstva
není nezbytná, odkud se data berou není podstatné
v jednodušších případech jsou data organizována
ve dvourozměrném poli typu Object
není to ale podmínkou – viz příklady dále
viz dále třída Data
naprosto nejjednodušší je použít
DefaultTableModel
pak odpadá implementace jakékoliv metody
pro její omezené možnosti se však příliš nepoužívá
všechny hodnoty musejí být v paměti
potíže se změnou hodnot
na druhé (prezentační) úrovni je JTable
jako
vizuální komponenta
typicky v konstruktoru přebírá datový model
new JTable(new TableModel());
Swing nepoužívá termíny datová a prezentační vrstva, ale model a view -- resp. nepoužívá ani termín view
výpis jen základních datových typů a řetězců bez nároků na vzhled
pokud se vyskytnou v buňkách tabulky objekty, musejí mít
vhodně překrytou toString()
zde jsou použita statická pole, ale principiálně na tom vůbec nezáleží
podstatné je, aby data vyhovovala metodám datové vrstvy (viz další sekce)
všechny položky jsou objekty jako např. Integer
(tedy nikoliv jen zakladní datové typy jako např.
int
)
lze využít boxing (automatický
wrapper) – viz
false
u jablek
není to ale vhodné všude, protože často potřebujeme pro
netriviální zobrazení odlišit typy dat, např.
Integer
od Double
import java.util.*;
public class Data {
public static final int NAZEV = 0;
public static final int JEDNOTKOVA_CENA = 1;
public static final int VAHA = 2;
public static final int DATUM_SPOTREBY = 3;
public static final int DOVOZ = 4;
public static String[] zahlavi = {
"Název", "Jednotková cena", "Váha",
"Datum spotřeby", "Dovoz"
};
public static Object[][] hodnoty = {
{ "jablka", new Integer(10), new Double(2.5),
new GregorianCalendar(2005, Calendar.MAY, 1),
false // vyuziva boxing
},
{ "banány", new Integer(25), new Double(2),
new GregorianCalendar(2005, Calendar.MAY, 2),
new Boolean(true)
},
{ "grapefruity", new Integer(19), new Double(0.75),
new GregorianCalendar(2005, Calendar.MAY, 3),
new Boolean(true)
},
{ "švestky sušené", new Integer(32), new Double(1.8),
new GregorianCalendar(2005, Calendar.MAY, 4),
new Boolean(false)
}
};
}
při použití DefaultTableModel
není
potřebná
typicky se ale používá dědění od
AbstractTableModel
import javax.swing.table.*; public class PrimitivniDatovyModel extends AbstractTableModel { public int getRowCount() { return Data.hodnoty.length; } public int getColumnCount() { return Data.hodnoty[0].length; } public Object getValueAt(int row, int column) { return Data.hodnoty[row][column]; } }
import javax.swing.*; import javax.swing.table.*; public class PrimitivniZobrazeni extends JFrame { private JComponent nastaveniTabulky() { JTable tabTB = new JTable(new DefaultTableModel( Data.hodnoty, Data.zahlavi)); // JTable tabTB = new JTable(new PrimitivniDatovyModel()); return tabTB; } private PrimitivniZobrazeni() { super("PrimitivniZobrazeni"); this.add(nastaveniTabulky()); this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); this.setSize(400, 90); this.setVisible(true); } public static void main(String[] args) { new PrimitivniZobrazeni(); } }
nedokonalosti primitivního řešení
všechny hodnoty se vypisují jako řetězce (přes
toString()
) a jsou zarovnány vlevo
to viditelně vadí u výpisu datumu
výpis datumu neposkytuje očekávanou informaci
není zobrazeno záhlaví
všechny sloupce mají stejnou šířku
šířku sloupců není možné měnit
není možné rolovat řádky
typické přidání důležité funkčnosti pomocí velmi jednoduché změny
využije se JScrollPane
změna jen v prezentační vrstvě
pro změnu šířky sloupců se využívají konstanty z
JTable
ty mj. udávají, jak se bude měnit šířka jednotlivých sloupců, změníme-li tažením myší šířku jednoho sloupce
podle potřeby se automaticky použije vodorovný i svislý posuvník
AUTO_RESIZE_OFF
– změna šířky jen jednoho
sloupce
další nastavení používají jen svislý posuvník
AUTO_RESIZE_ALL_COLUMNS
– změna všech
sloupců
AUTO_RESIZE_LAST_COLUMN
– změna jednoho
sloupce a posunutí všech vpravo
AUTO_RESIZE_NEXT_COLUMN
– změna jen dvou
sloupců
AUTO_RESIZE_SUBSEQUENT_COLUMNS
–
proporcionální změna všech sloupců vpravo
public class RolovaniZobrazeni extends JFrame { private JComponent nastaveniTabulky() { JTable tabTB = new JTable(new PrimitivniDatovyModel()); // tabTB.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); // tabTB.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS); // tabTB.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN); // tabTB.setAutoResizeMode(JTable.AUTO_RESIZE_NEXT_COLUMN); // tabTB.setAutoResizeMode(JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS); JScrollPane rolSP = new JScrollPane(tabTB); return rolSP; } private RolovaniZobrazeni() { super("RolovaniZobrazeni"); this.add(nastaveniTabulky()); this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); this.setSize(400, 100); this.setVisible(true); } public static void main(String[] args) { new RolovaniZobrazeni(); } }
zobrazí se záhlaví sloupců, ale jen jako písmena
Sloupce se dají přehazovat.
nutná změna datové vrstvy
přibude implementace getColumnName(int
column)
prezentační vrstva se nemění
public class ZahlaviZobrazeni extends JFrame { private JComponent nastaveniTabulky() { JTable tabTB = new JTable(new ZahlaviDatovyModel()); ... } public class ZahlaviDatovyModel extends AbstractTableModel { public int getRowCount() { ... public int getColumnCount() { ... public Object getValueAt(int row, int column) { ... public String getColumnName(int column) { return Data.zahlavi[column]; } }
primitivní zobrazení většinou nevyhovuje
jsou tři možnosti zlepšení
vždy se využívají zobrazovače (renderer) sloupců, které lze individuálně nastavit
základní datové typy mají připraveny defaultní zobrazovače
stačí pouze stanovit, jakého typu jsou objekty v daném sloupci
metoda Class<?> getColumnClass(int
column)
prezentační vrstva se nemění (příklad TypoveSloupceZobrazeni.java)
public class TypoveSloupceDatovyModel extends AbstractTableModel { public int getRowCount() { ... public int getColumnCount() { ... public Object getValueAt(int row, int column) { ... public String getColumnName(int column) { ... public Class<?> getColumnClass(int column) { if (column == Data.JEDNOTKOVA_CENA) { // return Integer.class; return Number.class; } if (column == Data.VAHA) { return Double.class; // return Number.class; } if (column == Data.DOVOZ) { return Boolean.class; } return String.class; } }
pro čísla lze použít Number.class
je ale lepší specifikovat typ co nejpřesněji – zde
Double.class
zobrazuje desetinnou čárku (přebírá nastavení z
platného Locale),
Number.class
tečku
boolean
se zobrazuje jako
JCheckBox
vše ostatní jako String
Tam, kde nám vyhovují defaultní zobrazovače, je předchozí postup ponecháván beze změny.
možnost specifikovat způsob zobrazení zcela podle našich představ
umístění, zarovnání, barvy, fonty, formáty, ...
v datové vrstvě se většinou změní pouze:
public Class<?> getColumnClass(int column) { return getValueAt(0, column).getClass(); }
v prezentační vrstvě se nastaví pro každou třídu objektů v libovolném sloupci instance vlastního zobrazovače
není-li explicitně specifikován zobrazovač, použije se
defaultní – viz první příklad pro Integer
import java.util.GregorianCalendar; import javax.swing.*; public class VlastniZobrazovacZobrazeni extends JFrame { private JComponent nastaveniTabulky() { JTable tabTB = new JTable(new VlastniZobrazovacDatovyModel()); tabTB.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); tabTB.setDefaultRenderer(String.class, new VypisRetezce()); tabTB.setDefaultRenderer(Double.class, new VypisVahy()); tabTB.setDefaultRenderer(GregorianCalendar.class, new VypisDatumu()); JScrollPane rolSP = new JScrollPane(tabTB); return rolSP; } ...
způsob má malou nevýhodu v tom, že např. všechny případné
sloupce typu Double
se formátují stejně
řešení viz dále
vlastní zobrazovač může být potomek různých tříd
nejjednodušší
používá se pro zarovnání a změnu barev (je to potomek
JLabel
)
nelze pracovat s hodnotou buňky
import java.awt.*; import javax.swing.*; import javax.swing.table.*; public class VypisRetezce extends DefaultTableCellRenderer { VypisRetezce() { this.setHorizontalAlignment(SwingConstants.CENTER); this.setVerticalAlignment(SwingConstants.CENTER); this.setBackground(Color.green); } }
je to potomek libovolné komponenty, který navíc implementuje
rozhraní TableCellRenderer
lze nastavit naprosto cokoliv i v závislosti na sloupci a řádce
např. dva Double
v různých sloupcích se
mohou zobrazovat různě
dále lze nastavit různé zobrazení pro vybranou či nevybranou buňku nebo buňku s fokusem
import java.awt.*;
import java.util.*;
import java.text.*;
import javax.swing.*;
import javax.swing.table.*;
public class VypisVahy extends JLabel
implements TableCellRenderer {
static DecimalFormat df;
static {
NumberFormat nf = NumberFormat.getNumberInstance(
new Locale("cs", "CZ"));
df = (DecimalFormat) nf;
df.applyPattern("#,##0.00");
}
VypisVahy() {
this.setHorizontalAlignment(JLabel.RIGHT);
this.setFont(new Font("Dialog", Font.PLAIN, 12));
this.setOpaque(true);
}
public Component getTableCellRendererComponent(JTable table,
Object value, boolean isSelected,
boolean hasFocus, int row, int column) {
double vaha = ((Double) value).doubleValue();
String s = df.format(vaha) + " ";
this.setText(s);
if (isSelected == true) {
this.setForeground(Color.red);
this.setBackground(Color.blue);
}
else {
this.setForeground(Color.black);
this.setBackground(Color.white);
}
return this;
}
}
zde je použita komponenta JLabel
nastavuje se za všech okolností český způsob vypisování desetinné čárky a odsazení řádů mezerou, výpis je na dvě desetinná místa s nevýznamovými nulami
v konstruktoru se nastavuje zarovnání doprava a normální font
metoda getTableCellRendererComponent()
je z
rozhraní TableCellRenderer
umožňuje získat hodnotu zobrazované buňky, její pozici na řádce a sloupci a informaci o tom, zda je vybrána a má klávesnicový fokus
hodnotu lze libovolně zformátovat
zde je u vybrané buňky změna barvy popředí
chceme-li nastavit i barvu pozadí, musí být komponenta nastavena na neprůhlednou
this.setOpaque(true);
pro zobrazení hodnoty možné použít libovolnou vhodnou
komponentu, málokdy však použijeme něco jiného než
JLabel
nebo JCheckBox
pro výpis datumu je použit stejný postup
import java.awt.*; import java.util.*; import java.text.*; import javax.swing.*; import javax.swing.table.*; public class VypisDatumu extends JLabel implements TableCellRenderer { static SimpleDateFormat sdf = new SimpleDateFormat("d.MM.yyyy"); VypisDatumu() { this.setFont(new Font("MonoSpaced", Font.BOLD, 12)); this.setHorizontalAlignment(JLabel.RIGHT); this.setForeground(Color.blue); this.setBackground(Color.yellow); this.setOpaque(true); } public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { String s = sdf.format(((GregorianCalendar) value).getTime()); this.setText(s); return this; } }
na tabulku lze pohlížet jako na pole sloupců
objekty sloupců lze získat na prezentační vrstvě jako
instance TableColumn
každému sloupci pak lze přiřadit množství vlastností
nejužívanější jsou nastavení šířky sloupce
setMinWidth(40);
– méně nelze
zmenšit
setPreferredWidth(50);
– zobrazená
šířka
setMaxWidth(100);
– více nelze
roztáhnout
sloupci lze nastavit zobrazovač
nesouvisí vůbec s informacemi o třídě poskytovanými datovou vrstvou
lze používat různé způsoby zobrazení nastavené jen na prezentační úrovni
je to nejvhodnější způsob
public class NastaveniSloupceZobrazeni extends JFrame { private JComponent nastaveniTabulky() { JTable tabTB = new JTable(new ZahlaviDatovyModel()); tabTB.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS); TableColumn tc = tabTB.getColumnModel().getColumn(Data.VAHA); tc.setMinWidth(40); tc.setPreferredWidth(50); tc.setMaxWidth(100); tc.setCellRenderer(new VypisVahy()); JScrollPane rolSP = new JScrollPane(tabTB); return rolSP; } ...
potřebujeme vypsat záhlaví jinak, než standardním fontem do jedné řádky
princip je stejný, jako při použití formátovače hodnot ve sloupcích
pouze se použije setHeaderRenderer()
public class VlastniZahlaviZobrazeni extends JFrame {
private JComponent nastaveniTabulky() {
JTable tabTB = new JTable(new ZahlaviDatovyModel());
tabTB.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
VypisZahlavi z = new VypisZahlavi();
TableColumnModel tcm = tabTB.getColumnModel();
for (int i = 0, n = tcm.getColumnCount(); i < n; i++) {
TableColumn tc = tcm.getColumn(i);
tc.setHeaderRenderer(z);
}
JScrollPane rolSP = new JScrollPane(tabTB);
return rolSP;
}
...
zobrazovač záhlaví se vytváří principiálně zcela stejně, jako zobrazovač normálních buněk
import java.awt.*; import javax.swing.*; import javax.swing.table.*; public class VypisZahlavi extends JPanel implements TableCellRenderer { public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { this.removeAll(); String s = (String) value; String[] radky = s.split(" "); // kazde slovo na nove radce this.setLayout(new GridLayout(radky.length, 1)); for (int i = 0; i < radky.length; i++) { JLabel l = new JLabel(radky[i], JLabel.CENTER); // l.setFont(new Font("Dialog", Font.PLAIN, 12)); // l.setForeground(Color.red); // l.setBackground(Color.blue); // l.setOpaque(true); this.add(l); } LookAndFeel.installBorder(this, "TableHeader.cellBorder"); return this; } }
volání this.removeAll();
je nezbytné
bez ní by se texty smíchaly do jednoho
metoda getTableCellRendererComponent()
je
totiž volána pro záhlaví všech sloupců
U všech dále popisovaných způsobů dojde k překreslení změněného obsahu buňky, tj. k viditelnému provedení akce, až po kliknutí na jinou buňku. Potvrzení změny stiskem <Enter> nestačí. Návod na změnu tohoto chování viz dále a též v Java Tutorial.
pro základní datové typy např. Integer
,
Double
, Boolean
nebo String
je změna jednoduchá – nemusí se připravovat žádný editor
je třeba pouze změnit datovou vrstvu přidáním metod:
boolean isCellEditable(int row, int
column)
pokyn pro prezentační vrstvu, aby této buňce dovolila změnu
v příkladu (kód níže) je změna zakázána pro sloupec „Název“
void setValueAt(Object value, int row, int
column)
datová vrstva provedenou změnu uloží
žádná další akce (např. v prezentační vrstvě) není třeba
pokud jsou v prezentační vrstvě použity vlastní zobrazovače, nijak to nevadí
jednoduchost je zaplacena některými potížemi
položky, které je možno měnit, se vybírají dvojklikem
to není příliš vhodné chování, protože běžně očekáváme pouze jedno kliknutí
při zadávání reálného čísla je nutné jako desetinný oddělovač použít tečku, nikoliv zobrazovanou čárku
public class ZmenaDatovyModel extends AbstractTableModel {
public int getRowCount() { ...
public int getColumnCount() { ...
public Object getValueAt(int row, int column) { ...
public String getColumnName(int column) { ...
public Class<?> getColumnClass(int column) { ...
public boolean isCellEditable(int row, int column) {
if (column == Data.NAZEV) {
return false;
}
return true;
}
public void setValueAt(Object value, int row, int column) {
Data.hodnoty[row][column] = value;
}
}
v prezentační vrstvě nejsou nutná žádná speciální nastavení
public class ZmenaZobrazeni extends JFrame { private JComponent nastaveniTabulky() { JTable tabTB = new JTable(new ZmenaDatovyModel()); tabTB.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); tabTB.setDefaultRenderer(Double.class, new VypisVahy()); tabTB.setDefaultRenderer(GregorianCalendar.class, new VypisDatumu()); JScrollPane rolSP = new JScrollPane(tabTB); return rolSP; } ...
pro nejčastější předpokládané způsoby editace lze využít třídy
DefaultCellEditor
tyto způsoby editace jsou pro hodnoty typu:
zapnuto/vypnuto – využívá se
JCheckBox
výběr z nabídnutých možností – využívá se
JComboBox
obecný řetězec – využívá se JTextField
nebo často jeho potomek
JFormattedTextField
DefaultCellEditor
dokáže pracovat pouze s
nimi
DefaultCellEditor
se používá v prezentační vrstvě
pro editaci hodnot v konkrétních sloupcích
je to častý případ použití, protože v různých sloupcích mohou být hodnoty stejných datových typů, ale my potřebujeme, aby se editovaly rozdílně
pro konkrétní sloupec lze zvolený editor nastavit metodou
setCellEditor()
existují dva základní způsoby použití – viz následující příklady
následující příklady pro změnu položky ve sloupci
Dovoz
budou mít s využitím JComboBox
tuto
funkčnost
Použití JComboBox
pro hodnoty typu
zapnuto/vypnuto je ve skutečnosti zbytečný
„luxus“.
aby bylo možné tuto funkčnost dosáhnout, musí se oproti
předchozímu příkladu pozměnit metoda setValueAt()
v
datové vrstvě
public class ZmenaDatovyModel extends AbstractTableModel { public int getRowCount() { ... public void setValueAt(Object value, int row, int column) { if (value instanceof String && column == Data.DOVOZ) { if (value.toString().equals("ano") == true) { Data.hodnoty[row][column] = new Boolean(true); } else { Data.hodnoty[row][column] = new Boolean(false); } } else { Data.hodnoty[row][column] = value; } } }
v prezentační vrstvě vytvoříme komponentu
JComboBox
jako část editoru, která se předá do
konstruktoru DefaultCellEditor
pomocí metody setCellEditor()
ze třídy
TableColumn
pak editor přiřadíme konkrétnímu
sloupci
public class ZmenaZobrazeni extends JFrame {
private JComponent nastaveniTabulky() {
JTable tabTB = new JTable(new ZmenaDatovyModel());
tabTB.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
tabTB.setDefaultRenderer(Double.class, new VypisVahy());
tabTB.setDefaultRenderer(GregorianCalendar.class,
new VypisDatumu());
tabTB.setDefaultEditor(GregorianCalendar.class,
new DatumEditor());
TableColumn tc = tabTB.getColumnModel().getColumn(Data.DOVOZ);
JComboBox dovozJCB = new JComboBox();
dovozJCB.addItem("ano");
dovozJCB.addItem("NE");
tc.setCellEditor(new DefaultCellEditor(dovozJCB));
...
předchozí způsob, kdy se přímo v prezentační vrstvě připraví část editoru, je nekoncepční – míchají se do sebe různé věci
třídu vlastního editoru lze připravit děděním třídy
DefaultCellEditor
základní použití s komponentami
JTextField
, JCheckBox
a
JComboBox
je jednoduché
ukázka sofistikovaného využití je uvedena v Java Tutorial
třída editoru
import javax.swing.*;
public class DovozEditorDefault extends DefaultCellEditor {
public DovozEditorDefault() {
super(new JComboBox());
JComboBox jcb = (JComboBox) this.getComponent();
jcb.addItem("ano");
jcb.addItem("NE");
}
}
prezentační vrstva
public class ZmenaZobrazeni extends JFrame {
private JComponent nastaveniTabulky() {
JTable tabTB = new JTable(new ZmenaDatovyModel());
tabTB.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
tabTB.setDefaultRenderer(Double.class, new VypisVahy());
tabTB.setDefaultRenderer(GregorianCalendar.class,
new VypisDatumu());
TableColumn tc = tabTB.getColumnModel().getColumn(Data.DOVOZ);
tc.setCellEditor(new DovozEditorDefault());
...
nestačí-li nám předchozí způsoby, lze (stejně jako u zobrazovačů (rendererů)) vytvořit třídu velmi obecného editoru
vnitřek editoru může tvořit libovolná vhodná komponenta
editor musí být třída implementující rozhraní
TableCellEditor
a potažmo i rozhraní
CellEditor
zde je celkem 8 metod včetně řešení událostí
je to obecné, ale poněkud zdlouhavé řešení
pro zjednodušení se běžně používá následující postup:
je vhodné dědit od
javax.swing.AbstractCellEditor
, která odstíní
od nutnosti řešit problémy s událostmi
stačí implementovat metodu
getCellEditorValue()
vrací nově nastavenou hodnotu do metody
setValueAt()
datové vrstvy
dále třída musí implementovat rozhraní
javax.swing.table.TableCellEditor
konkrétně metodu
getTableCellEditorComponent()
v ní se stanoví, jak výsledek práce editoru změní hodnotu vybrané (měněné) buňky datové vrstvy
daný editor musíme nastavit na prezentační úrovni jedním ze dvou základních způsobů
jako defaultní pro všechny sloupce, ve kterých se objekt dané třídy vyskytuje (podle datového typu)
pro konkrétní sloupec (jako v předchozím případě)
public class ZmenaZobrazeni extends JFrame { private JComponent nastaveniTabulky() { JTable tabTB = new JTable(new ZmenaDatovyModel()); tabTB.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); tabTB.setDefaultRenderer(Double.class, new VypisVahy()); tabTB.setDefaultRenderer(GregorianCalendar.class, new VypisDatumu()); // prvni zpusob tabTB.setDefaultEditor(Boolean.class, new DovozEditor()); // druhy zpusob // TableColumn tc = tabTB.getColumnModel().getColumn(Data.DOVOZ); // tc.setCellEditor(new DovozEditor());
pro editaci dovozu použijeme (jako dříve) komponentu
JComboBox
funkčnost bude stejná, jako v předchozích případech
import java.awt.*; import javax.swing.*; import javax.swing.table.*; public class DovozEditor extends AbstractCellEditor implements TableCellEditor { private JComboBox dovozCB; public DovozEditor() { dovozCB = new JComboBox(); dovozCB.addItem("ano"); // index 0 dovozCB.addItem("NE"); // index 1 } public Object getCellEditorValue() { return dovozCB.getSelectedItem(); } public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { boolean b = (Boolean) value; dovozCB.setSelectedIndex(b == true ? 0 : 1); return dovozCB; } }
pro editaci datumu použijeme komponentu
JTextField
import java.awt.*; import java.text.SimpleDateFormat; import java.util.GregorianCalendar; import javax.swing.*; import javax.swing.table.*; public class DatumEditor extends AbstractCellEditor implements TableCellEditor { static SimpleDateFormat sdf = new SimpleDateFormat("d.MM.yyyy"); private JTextField datumTF; public DatumEditor() { datumTF = new JTextField(); datumTF.setFont(new Font("MonoSpaced", Font.BOLD, 12)); datumTF.setHorizontalAlignment(JLabel.RIGHT); datumTF.setForeground(Color.black); } public Object getCellEditorValue() { String[] udaje = datumTF.getText().split("\\D"); int den = Integer.parseInt(udaje[0]); int mesic = Integer.parseInt(udaje[1]); int rok = Integer.parseInt(udaje[2]); GregorianCalendar gc = new GregorianCalendar(rok, mesic - 1, den); return gc; } public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { GregorianCalendar gc = (GregorianCalendar) value; String s = sdf.format(gc.getTime()); datumTF.setText(s); return datumTF; } }
není nutné implementovat, protože v Java Tutoriál je připravená
třída TableSorter
Měla být součástí Java Core API od 1.6, ale není.
jedná se o návrhový vzor dekorátor pro
TableModel
pro řazení stačí jen klikat na záhlaví
řadí vzestupně i sestupně
po Ctrl-click je možné sekundární řazení podle dalšího sloupce
import java.util.GregorianCalendar;
import javax.swing.*;
public class RazeniZobrazeni extends JFrame {
private JComponent nastaveniTabulky() {
TableSorter sorter = new TableSorter(new ZmenaDatovyModel());
JTable tabTB = new JTable(sorter);
sorter.setTableHeader(tabTB.getTableHeader());
tabTB.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
tabTB.setDefaultRenderer(Double.class, new VypisVahy());
tabTB.setDefaultRenderer(GregorianCalendar.class,
new VypisDatumu());
tabTB.setDefaultEditor(GregorianCalendar.class,
new DatumEditor());
JScrollPane rolSP = new JScrollPane(tabTB);
return rolSP;
}
...
často se stane, že data nejsou homogenní a/nebo není vhodné, aby byla uložena v jednom dvourozměrném poli
první dva příklady budou dodržovat dosud používanou architekturu tří tříd, tj. (1) data, (2) třída s metodami datové vrstvy čili tzv. model a (3) prezentační vrstva
v těchto příkladech je ukázáno, jak použít v jednom sloupci nehomogenní data (viz „suma“ ve sloupci „Jednotková cena“)
poslední sloupec „Cena“ je kompletně vypočítáván z údajů „Jednotková cena“ a „Váha“
stejně tak jsou vypočítávány buňky sumy vah a sumy ceny
tabulka bude vypadat takto:
třetí příklad sloučí obě třídy datové vrstvy do jedné
čtvrtý jej modifikuje k dokonalosti
třída DataVypocet
má místo jednoho dvourozměrného
pole několik jednorozměrných polí představujících jednotlivé
sloupce
tato pole spolu vzájemně fyzicky nesouvisejí
logická souvislost (tj. spojení do jedné tabulky) se
provede až ve třídě
ZmenaVypocetDatovyModel
třída je doplněna o čtyři statické metody:
prepoctiCenu()
– vypočítá cenu zboží
součinem jednotkové ceny a váhy
sumaVah()
– vypočítá celkovou váhu
zboží
sumaCen()
– vypočítá celkovou cenu
zboží
pocatecniVypocetDat()
– provede všechny
předchozí výpočty
public class DataVypocet { public static final int NAZEV = 0; public static final int JEDNOTKOVA_CENA = 1; public static final int VAHA = 2; public static final int CENA = 3; public static String[] zahlavi = { "Název", "Jednotková cena", "Váha", "Cena" }; public static String[] nazev = { "jablka", "banány", "grapefruity", "švestky sušené", "" }; // musi byt Object kvuli posledni sume public static Object[] jednotkovaCena = { new Integer(10), new Integer(25), new Integer(19), new Integer(32), "suma" }; public static Double[] vaha = { new Double(2.5), new Double(2), new Double(0.75), new Double(1.8), new Double(0) }; public static Double[] cena = { new Double(0), new Double(0), new Double(0), new Double(0), new Double(0) }; public static void prepoctiCenu(int radka) { cena[radka] = ((Integer) jednotkovaCena[radka]) * ((Double) vaha[radka]); } public static void sumaVah() { double sumaV = 0.0; for (int i = 0; i < vaha.length - 1; i++) { sumaV += (Double) vaha[i]; } vaha[vaha.length - 1] = new Double(sumaV); } public static void sumaCen() { double sumaC = 0.0; for (int i = 0; i < cena.length - 1; i++) { sumaC += (Double) cena[i]; } cena[cena.length - 1] = new Double(sumaC); } // pocatecni vypocet cen a obou sum public static void pocatecniVypocetDat() { for (int i = 0; i < cena.length - 1; i++) { prepoctiCenu(i); } sumaVah(); sumaCen(); } }
třída ZmenaVypocetDatovyModel
má oproti všem
dosud uváděným příkladům výrazně změněnou metodu
getValueAt()
v této metodě se logicky spojují jednotlivé fyzicky nezávislé pole (tj. sloupce) dat
podobná změna je i v metodě
setValueAt()
zde je navíc volána metoda
fireTableCellUpdated()
, která zabezpečí, že po
editaci některé buňky prezentační vrstva aktualizuje i buňku
fyzicky nesouvisející (není na stejné řádce)
import javax.swing.table.*; public class ZmenaVypocetDatovyModel extends AbstractTableModel { public int getRowCount() { return DataVypocet.nazev.length; } public int getColumnCount() { return DataVypocet.zahlavi.length; } public Object getValueAt(int row, int column) { switch (column) { case DataVypocet.NAZEV: return DataVypocet.nazev[row]; case DataVypocet.JEDNOTKOVA_CENA: return DataVypocet.jednotkovaCena[row]; case DataVypocet.VAHA: return DataVypocet.vaha[row]; case DataVypocet.CENA: return DataVypocet.cena[row]; } return null; } public String getColumnName(int column) { return DataVypocet.zahlavi[column]; } public Class<?> getColumnClass(int column) { return getValueAt(0, column).getClass(); } public boolean isCellEditable(int row, int column) { if (row == DataVypocet.nazev.length - 1) { return false; } if (column == DataVypocet.JEDNOTKOVA_CENA || column == DataVypocet.VAHA) { return true; } return false; } public void setValueAt(Object value, int row, int column) { switch (column) { case DataVypocet.JEDNOTKOVA_CENA: DataVypocet.jednotkovaCena[row] = (Integer) value; DataVypocet.prepoctiCenu(row); DataVypocet.sumaCen(); this.fireTableCellUpdated(DataVypocet.cena.length - 1, DataVypocet.CENA); break; case DataVypocet.VAHA: DataVypocet.vaha[row] = (Double) value; DataVypocet.sumaVah(); this.fireTableCellUpdated(DataVypocet.vaha.length - 1, DataVypocet.VAHA); DataVypocet.prepoctiCenu(row); DataVypocet.sumaCen(); this.fireTableCellUpdated(DataVypocet.cena.length - 1, DataVypocet.CENA); break; } } }
třída VypisDoubleVypocet
, jako zobrazovač hodnot
typu Double
, je velmi podobná dříve používané třídě
VypisVahy
import java.awt.*; import java.util.*; import java.text.*; import javax.swing.*; import javax.swing.table.*; public class VypisDoubleVypocet extends JLabel implements TableCellRenderer { static DecimalFormat df; static { NumberFormat nf = NumberFormat.getNumberInstance( new Locale("cs", "CZ")); df = (DecimalFormat) nf; df.applyPattern("#,##0.00"); } VypisDoubleVypocet() { this.setHorizontalAlignment(JLabel.RIGHT); this.setFont(new Font("Dialog", Font.PLAIN, 12)); } public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { double vaha = ((Double) value).doubleValue(); String s = df.format(vaha) + " "; this.setText(s); return this; } }
třída ZmenaZobrazeniVypocet
je velmi podobná
dříve uváděným třídám prezentační vrstvy
Volání metody DataVypocet.pocatecniVypocetDat()
by mělo být spíše v konstruktoru třídy
ZmenaVypocetDatovyModel
. Zde je uvedeno proto, aby
mohl snadno navazovat další příklad.
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
public class ZmenaZobrazeniVypocet extends JFrame {
private JComponent nastaveniTabulky() {
DataVypocet.pocatecniVypocetDat();
JTable tabTB = new JTable(new ZmenaVypocetDatovyModel());
tabTB.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
tabTB.setDefaultRenderer(Double.class,
new VypisDoubleVypocet());
JScrollPane rolSP = new JScrollPane(tabTB);
return rolSP;
}
private ZmenaZobrazeniVypocet() {
super("ZmenaZobrazeniVypocet");
this.add(nastaveniTabulky());
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setSize(400, 180);
this.setVisible(true);
}
public static void main(String[] args) {
new ZmenaZobrazeniVypocet();
}
}
všechny dosud uváděné příklady měly zobrazovaná data napevno nastavená ve třídě datové vrstvy
to je nerealistický příklad, protože zobrazovaná data se nejčastěji načtou ze souboru nebo z databáze -- zde příklad se souborem:
soubor s názvem jidlo.txt
má
obsah
10;jablka;2.5 25;banány;2 19;grapefruity;0.75 32;švestky sušené;1.8 15;hrušky;1.5 22;pomeranče;3.2 40;fíky;0.5
pro načítání stačí pouze dodat třídu
DataVypocetSoubor
, která údaje ze souboru
načte
tak nahradí přednastavené hodnoty ve třídě
DataVypocet
řádky ze souboru se parsují a jednotlivé údaje se
ukládají do pomocných seznamů
(ArrayList
)
tím se nemusíme starat o velikost souboru
po načtení se seznamy převedou na pole metodou
toArray()
jejím parametrem je pole nulové velikosti typu převáděného pole – „trik“ z kolekcí
díky tomuto postupu není vůbec potřebná změna ve
třídách DataVypocet
a
ZmenaVypocetDatovyModel
import java.io.BufferedReader; import java.io.FileReader; import java.util.ArrayList; public class DataVypocetSoubor { public static void nactiSoubor(String jmeno) { ArrayList<Object> jednotkovaCenaAr = new ArrayList<Object>(); ArrayList<String> nazevAr = new ArrayList<String>(); ArrayList<Double> vahaAr = new ArrayList<Double>(); try { BufferedReader bfr = new BufferedReader( new FileReader(jmeno)); String radka; while ((radka = bfr.readLine()) != null) { String[] polozky = radka.split(";"); jednotkovaCenaAr.add(new Integer(polozky[0])); nazevAr.add(polozky[1]); vahaAr.add(new Double(polozky[2])); } bfr.close(); } catch (Exception e) { e.printStackTrace(); } // pridani radky pro sumy jednotkovaCenaAr.add("suma"); nazevAr.add(""); vahaAr.add(new Double(0)); // prevod na pole DataVypocet.nazev = nazevAr.toArray(new String[0]); DataVypocet.jednotkovaCena = jednotkovaCenaAr.toArray(new Object[0]); DataVypocet.vaha = vahaAr.toArray(new Double[0]); // vytvoreni pole pro cenu DataVypocet.cena = new Double[DataVypocet.vaha.length]; } }
v prezentační vrstvě je nutné zavolat metodu pro načtení ze souboru
Ve skutečném příkladě by se tato činnost řešila v přetíženém
konstruktoru třídy ZmenaVypocetDatovyModel
, kterému
by se jako skutečný parametr předal název souboru.
public class ZmenaZobrazeniVypocet extends JFrame {
private JComponent nastaveniTabulky() {
DataVypocetSoubor.nactiSoubor("jidlo.txt");
DataVypocet.pocatecniVypocetDat();
JTable tabTB = new JTable(new ZmenaVypocetDatovyModel());
tabTB.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
tabTB.setDefaultRenderer(Double.class,
new VypisDoubleVypocet());
JScrollPane rolSP = new JScrollPane(tabTB);
return rolSP;
}
...
program zobrazí
může se stát, že data nebudeme načítat ze souboru, ale potřebujeme je vytvořit programově v určité velikosti
pak je vhodné mít tato data rovnou ve třídě implementující
TableModel
zbavíme se jedné třídy datové vrstvy
příklad připraví matici o zadaném počtu řádků a sloupců a naplní ji celými čísly
v záhlaví budou čísla sloupců
zobrazí se např.:
datová vrstva
import javax.swing.table.*; public class VypoctenoDatovyModel extends AbstractTableModel { private int nRadek; private int nSloupcu; private String[] zahlavi; private Integer[][] hodnoty; public VypoctenoDatovyModel(int nRadek, int nSloupcu) { this.nRadek = nRadek; this.nSloupcu = nSloupcu; hodnoty = new Integer[nRadek][nSloupcu]; zahlavi = new String[nSloupcu]; naplneniHodnot(); naplneniZahlavi(); } private void naplneniHodnot() { for (int i = 0; i < nRadek; i++) { for (int j = 0; j < nSloupcu; j++) { hodnoty[i][j] = (i + 1) * 10 + (j + 1); } } } private void naplneniZahlavi() { for (int j = 0; j < nSloupcu; j++) { zahlavi[j] = "" + (j + 1) + "."; } } public int getRowCount() { return nRadek; } public int getColumnCount() { return nSloupcu; } public Object getValueAt(int row, int column) { return hodnoty[row][column]; } public String getColumnName(int column) { return zahlavi[column]; } }
prezentační vrstva
public class VypoctenoZobrazeni extends JFrame {
private JComponent nastaveniTabulky() {
JTable tabTB = new JTable(new VypoctenoDatovyModel(5, 4));
tabTB.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
JScrollPane rolSP = new JScrollPane(tabTB);
return rolSP;
}
...
reálně se často stává, že data nejsou připravená ve dvourozměrném poli, tak jak jsme dosud viděli
příslušné dvouroměrné pole se vytváří až když je potřeba tj.
při volání getRowCount
, getColumnCount
nebo getValueAt
dokonce není ani potřeba plýtvat pamětí a toto dvouroměrné
pole "vytvářet až když je potřeba" -- stačí jen chytře
naimplementovat getValueAt
:
...
public Object getValueAt(int row, int column) {
// return hodnoty[row][column];
return (row + 1) * 10 + (column + 1);
}
...