Kapitola 1. Tabulky

Obsah

1.1. Základní informace
1.2. Základní princip
1.2.1. Primitivní použití
1.3. Vylepšování funkčnosti
1.3.1. Rolování řádek a sloupců
1.3.2. Zobrazení záhlaví
1.4. Uživatelsky konfigurovatelné zobrazování hodnot
1.4.1. Datová vrstva informuje prezentační vrstvu o typech dat
1.4.2. Využití vlastního zobrazovače
1.5. Práce s jednotlivými sloupci na úrovni prezentační vrstvy
1.6. Práce se záhlavím
1.7. Změna hodnot v tabulce
1.7.1. Editace základních datových typů
1.7.2. Použití editoru na bázi DefaultCellEditor
1.7.3. Vytvoření vlastního editoru na bázi libovolné komponenty
1.8. Řazení v tabulce
1.9. Data jsou organizována jako na sobě nezávislá pole
1.9.1. Ukázka vzájemného provázání dat
1.9.2. Ukázka načítání dat ze souboru
1.9.3. Data jsou součástí třídy implementující TableModel
1.9.4. Data se počítají za běhu

1.1. Základní informace

  • 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

1.2. Základní princip

  • je nutné mít dvou (tří -- podle toho jak je na to díváte) stupňovou organizaci

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

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

1.2.1. Primitivní použití

  • 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()

1.2.1.1. Vrstva vlastních dat

  • 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)  
    }
  };
}

1.2.1.2. Vrstva metod datové vrstvy

  • 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];
  }
}

1.2.1.3. Prezentační vrstva

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

1.3. Vylepšování funkčnosti

1.3.1. Rolování řádek a sloupců

  • 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

    Poznámka

    Sloupce se dají přehazovat.

1.3.2. Zobrazení záhlaví

  • 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];
  }
}

1.4. Uživatelsky konfigurovatelné zobrazování hodnot

  • 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

1.4.1. Datová vrstva informuje prezentační vrstvu o typech dat

  • 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

1.4.2. Využití vlastního zobrazovače

Poznámka

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

1.4.2.1. Zobrazovač je potomkem třídy DefaultTableCellRenderer

  • 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);
  }
}

1.4.2.2. Zobrazovač implementuje rozhraní TableCellRenderer

  • 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;
  }
}

1.5. Práce s jednotlivými sloupci na úrovni prezentační vrstvy

  • 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;
  }
...

1.6. Práce se záhlavím

  • 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ů

1.7. Změna hodnot v tabulce

Poznámka

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.

1.7.1. Editace základních datových typů

  • 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;
  }
...

1.7.2. Použití editoru na bázi DefaultCellEditor

  • 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

    Poznámka

    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;
        }
      }
    }

1.7.2.1. Využití instance JComboBox přímo v prezentační vrstvě

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

1.7.2.2. Využití třídy DefaultCellEditor pro konstrukci vlastní třídy

  • 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());
...

1.7.3. Vytvoření vlastního editoru na bázi libovolné komponenty

  • 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;
  } 
}

1.8. Řazení v tabulce

  • není nutné implementovat, protože v Java Tutoriál je připravená třída TableSorter

    Poznámka

    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;
  }
...

1.9. Data jsou organizována jako na sobě nezávislá pole

  • č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

1.9.1. Ukázka vzájemného provázání dat

  • 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

    Poznámka

    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();
  }
}

1.9.2. Ukázka načítání dat ze souboru

  • 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

    Poznámka

    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í

1.9.3. Data jsou součástí třídy implementující TableModel

  • 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;
  }
...

1.9.4. Data se počítají za běhu

  • 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);
      }
    ...