PV168/Swing 2: Porovnání verzí

Z FI WIKI
Přejít na: navigace, hledání
m (Stránka PV168/cvičení 9 přemístěna na stránku PV168/Swing 2)
(Kontrola úlohy č. 6: odstraněno)
Řádka 1: Řádka 1:
== Kontrola úlohy č. 6 ==
 
Během cvičení bude opravena úloha č. 6 (návrh GUI pro aplikaci).
 
 
 
== Princip MVC ==
 
== Princip MVC ==
 
[[MVC]] je návrhový vzor pro architekturu GUI aplikací. Základním principem je rozdělení datového modelu aplikace, uživatelského rozhraní a řídicí logiky do tří nezávislých komponent. Důsledkem je možnost snadné výměny některé z těchto tří komponent bez vlivu na ostatní dvě komponenty, snadnější údržba a vyšší flexibilita i znovupoužitelnost jednotlivých kompoent.
 
[[MVC]] je návrhový vzor pro architekturu GUI aplikací. Základním principem je rozdělení datového modelu aplikace, uživatelského rozhraní a řídicí logiky do tří nezávislých komponent. Důsledkem je možnost snadné výměny některé z těchto tří komponent bez vlivu na ostatní dvě komponenty, snadnější údržba a vyšší flexibilita i znovupoužitelnost jednotlivých kompoent.

Verze z 18. 2. 2011, 19:39

Princip MVC

MVC je návrhový vzor pro architekturu GUI aplikací. Základním principem je rozdělení datového modelu aplikace, uživatelského rozhraní a řídicí logiky do tří nezávislých komponent. Důsledkem je možnost snadné výměny některé z těchto tří komponent bez vlivu na ostatní dvě komponenty, snadnější údržba a vyšší flexibilita i znovupoužitelnost jednotlivých kompoent.

Všechny prvky uživatelského rozhraní ve Swingu využívají architekturu MVC. My si tento princip budeme demostrovat na příkladě komponenty JTable.

Komponenta JTable

Komponenta JTable umožňuje zobrazovat a editovat data ve formě tabulky. Kromě vlastní třídy JTable (sloužící jako controller) zde figuruje několik dalších komponent:

  • TableModel reprezentuje a poskytuje data, která jsou v tabulce zobrazena. Lze použít DefaultTableModel, který ukládá data v paměti pomocí kolekcí typu Vektor, ale obvykle vytváříme vlastní implementaci modelu, který zpřístupňuje data z aplikační vrstvy naší aplikace.
  • TableCellRenderer definuje způsob zobrazení dat v jednotlivých políčkách tabulky, přičemž jedna tabulka může používat více objektů TableCellRenderer (např. pro různé sloupce nebo různé typy dat). Standardně se používá DefaultTableCellRenderer (je postavený na komponentě JLabel), ale v případě potřeby si můžeme vytvořit vlastní implementaci (buď rozšířením třídy DefaultTableCellRenderer, nebo implementací jiné třídy implementující rozhraní TableCellRenderer).
  • TableCellEditor definuje způsob, jak editovat obsah políček tabulky. Pokud chceme pro editaci využít komponentu JCheckBox, JComboBox nebo JTextField, obvykle si vystačíme s instancí existující třídy DefaultCellEditor.

Následující komponenty jsou zde uvedeny pouze pro úplnost; obvykle není nutné se jimi zabývat a ani my se jim nebudeme věnovat podrobněji:

  • TableRowSorter je komponenta zodpovědná za filtrování a řazení dat. Buď můžeme použít přímo instanci třídy TableRowSorter, nebo můžeme vytvořit potomka této třídy a modifikovat tak způsob filtrování nebo řazení dat.
  • TableColumnModel definuje jednotlivé sloupce a jejich chování. Obvykle není nutné vytvářet vlastní TableColumnModel, protože se automaticky vytváří instance třídy DefaultTableColumnModel a tato instance je ve většině případů zcela vyhovující.
  • TableColumn reprezentuje sloupec v tabulce.
  • JTableHeader je komponenta, která reprezentuje hlavičku tabulky. Jedná se o první řádek, který obsahuje názvy sloupců, umožňuje měnit jejich velikost apod. S touto komponentou obvykle vůbec nepřijdeme do styku, neboť její instance se vytváří automaticky a není důvod ji jakkoliv měnit.
  • ListSelectionModel je komponenta zodpovědná za způsob výběru řádků v tabulce. Standardní implementace DefaultListSelectionModel podporuje tři režimy výběru (což je většinou naprosto dostačující):
    • SINGLE_SELECTION — vždy je vybrán maximálně jeden řádek;
    • SINGLE_INTERVAL_SELECTION — může být vybráno více řádků, ale ty tvoří vždy souvislý interval;
    • MULTIPLE_INTERVAL_SELECTION — může být vybráno více libovolných řádků.

Na první pohled se může zdát tato dekompozice složitá, protože v ní figuruje více komponent, ve skutečnosti se ale jedná o příklad velmi dobře navržené dekompozice. Celý problém je rozdělen do více tříd, přičemž každá třída plní pouze jednu svoji úlohu. Toto řešení přináší zjednodušení jednotlivých komponent, jejich snazší ladění i údržbu, vysokou flexibiltu a snadnou rozšiřitelnost. Jednotlivé komponenty mohou být flexibilně kombinovány a nahrazovány různými implementacemi, aniž by to ovlivnilo ostatní komponenty. Jediným spojujícím prvkem je controller v podobě komponenty JTable.

Jak si navíc za chvilku ukážeme, není třeba všechny výše zmíněné komponenty implementovat nebo používat, protože vždy existuje výchozí implementace, jejíž instance jsou vytvářeny automaticky a jsou v mnoha případech dostačující.

Práce s komponentou JTable

Příprava entit pro model

Založte si nový projekt typu Java Application. Vytvořte třídy Fuel a Car:

public enum Fuel {
    DIESEL, GAS, LPG, CNG
}
public class Car {
 
    private int id;
    private String type;
    private String registrationNumber;
    private Color color;
    private Fuel fuel;
 
    public Car(int id, String type, String registrationNumber, Color color, Fuel fuel) {
        this.id = id;
        this.type = type;
        this.registrationNumber = registrationNumber;
        this.color = color;
        this.fuel = fuel;
    }
}

Pomocí příkazu Insert Code (Alt + I) vygenerujte pro všechny atributy třídy Car get/set metody. Pokud je to třeba, opravte importy pomocí funkce Fix Imports (Ctrl + Shift + I).

Jednoduchý model

Nejjednodušší způsob jak implementovat vlastní model je rozšířit abstraktní třídu AbstractTableModel. V tomto případě postačí překrýt tři abstraktní metody:

public class CarsTableModel extends AbstractTableModel {
 
    private List<Car> cars = new ArrayList<Car>();
 
    @Override
    public int getRowCount() {
        return cars.size();
    }
 
    @Override
    public int getColumnCount() {
        return 5;
    }
 
    @Override
    public Object getValueAt(int rowIndex, int columnIndex) {
        Car car = cars.get(rowIndex);
        switch (columnIndex) {
            case 0:
                return car.getId();
            case 1:
                return car.getRegistrationNumber();
            case 2:
                return car.getType();
            case 3:
                return car.getColor();
            case 4:
                return car.getFuel();
            default:
                throw new IllegalArgumentException("columnIndex");
        }
    }
}

Pokud je to třeba, opravte importy pomocí funkce Fix Imports (Ctrl + Shift + I).

Všimněte si, že v případě neplatného indexu sloupečku vyhazujeme výjimku. To je správný postup, jak reagovat na porušení kontraktu metody getValueAt(…). Nesnažte se v podobných případech vracet null, nebo nějakou implicitní hodnotu. Pokud nastane situace, která by nastat neměla, měli bychom vždy vyhodit výjimku, protože v opačném případě případnou chybu maskujeme a výrazně zvyšujeme obtížnost jejího nalezení. U indexu řádku žádnou explicitní kontrolu dělat nemusíme, protože výjimku nám v případě indexu mimo rozsah automaticky vyhodí metoda List.get(int).

Vytvoření okna s tabulkou

Nyní si vytvoříme okno, do něhož umístíme tabulku. Vytvořte nový soubor typu JFrame (V menu File vyberte položku New File… a v kategorii Swing GUI Forms vyberte JFrame Form). Třídu okna nazvěte CarsFrame. Pomocí GUI návrháře do něj vložte komponentu JTable a roztáhněte ji tak, aby zabírala celou plochu okna.

Nyní klikněte pravým tlačítkem myši na komponentě JTable, v kontextovém menu vyberte položku Change Variable Name … a přejmenujte tabulku na jTableCars. Dále si v tomto kontextovém menu vyberte položku Properties a v okně s vlastnostmi komponenty jTableCars najděte řádek s vlastností model. Nyní klikněte na tlačítko , které se nachází na pravém okraji řádku. Zobrazí se okno s nastavením modelu pro naši tabulku. V horní části okna se nachází combo box, kde je vybrána hodnota Table model customizer. Změňte ji na custom code a jako argument metody jTableCars.setModel(…) vepište kód new CarsTableModel().

Potvrďte změny pomocí tlačítka Ok a zavřete okno s vlastnostmi tabulky.

Nyní zkuste projekt zkompilovat a spustit. Aby se spouštěla správná třída, je nutné, aby byla ve vlastnostech projektu nastavena Main Class na třídu CarsFrame (tato položka se nastavuje v záložce Run).

Zobrazí se nám okno s tabulkou s pěti sloupečky, ale žádným řádkem, protože v našem modelu nejsou žádná data.

Přidání dat do modelu

Abychom mohli do modelu přidávat nějaká data, vytvoříme v něm novou metodu addCar(…)­:

public void addCar(Car car) {
    cars.add(car);      
}

Do konstruktoru třídy CarsFrame přidáme za volání metody initComponents() tento kód:

CarsTableModel model = (CarsTableModel) jTableCars.getModel();
model.addCar(new Car(1,"Renault Clio","2B6 7895",Color.RED,Fuel.GAS));
model.addCar(new Car(2,"Škoda 120","2A1 9999",Color.PINK,Fuel.LPG));

Nyní zkuste projekt zkompilovat a spustit. Měla by se zobrazit tabulka s dvěma přidanými auty.

Názvy sloupců

Nyní se pokusíme zobrazení dat vylepšit. Začneme nastavením vhodnějších názvů sloupců místo stávajících názvů A, B, C, D, E.

Do modelu přidejte metodu

@Override
public String getColumnName(int columnIndex) {
    switch (columnIndex) {
        case 0:
            return "Id";
        case 1:
            return "RegistrationNumber";
        case 2:
            return "Type";
        case 3:
            return "Color";
        case 4:
            return "Fuel";
        default:
            throw new IllegalArgumentException("columnIndex");
    }
}

Nyní zkuste projekt zkompilovat a spustit. Měla by se zobrazit tabulka se správnými nadpisy sloupců.

Poznámka: Pokud bychom chtěli názvy sloupců lokalizovat, nemělo by se tak dít modifikací modelu, ale nastavením vlastní implementace komponenty TableCellRenderer pro hlavičky sloupců.

Notifikace změn

Data poskytovaná modelem se mohou samozřejmě měnit a v takovém případě je třeba o těchto změnách informovat komponentu JTable, aby byla příslušná políčka tabulky překreslena.

Do okna CarsFrame přidejte tlačítko (JButton). Budete si na něj muset zřejmě udělat místo tak, že zmenšíte plochu, kterou zabírá naše komponenta jTableCars. Po přidání tlačítka ale komponentu tabulky opět roztáhněte tak, aby se při zvětšení okna zvětšila i plocha obsazená komponentou tabulky.

Titulek tlačítka nastavte na Add random car (položka Edit Text v kontextovém menu) a jeho jméno opravte na jButtonAddCar (položka Change Variable Name … v kontextovém menu). Pak na tlačítko dvakrát klikněte a měla by se vám automaticky vytvořit metoda jButtonAddCarActionPerformed(…), jejíž obsah můžete editovat. Komentář

// TODO add your handling code here: 

nahraďte kódem

CarsTableModel model = (CarsTableModel) jTableCars.getModel();
model.addCar(randomCar());

Do třídy CarsFrame dále přidejte následující kód:

private Random random = new Random();
private static final String[] CAR_TYPES = { "Škoda Felicia", "Škoda Octavia",
    "Ford Focus", "Ford Fiesta", "Renault Clio", "Porsche Carrera" };
private static final Color[] CAR_COLORS = { Color.BLACK, Color.BLUE,
    Color.RED, Color.GREEN, Color.PINK, Color.WHITE, Color.YELLOW };
 
private Car randomCar() {
    return new Car(
        random.nextInt(10000),
        CAR_TYPES[random.nextInt(CAR_TYPES.length)],
        "2C1 " + (random.nextInt(9000) + 1000),
        CAR_COLORS[random.nextInt(CAR_COLORS.length)],
        Fuel.values()[random.nextInt(Fuel.values().length)]);
}

Pokud je to třeba, opravte importy pomocí funkce Fix Imports (Ctrl + Shift + I).

Nyní zkuste projekt zkompilovat a spustit. Pokud se pokusíme přidat další auto, nic se nestane.

Problém spočívá v tom, že při přidání auta se o tom zatím komponenta JTable nedozví. Jako řešení by nás mohlo napadnou nějak explicitně vynutit překresleni tabulky zavoláním nějaké vhodné metody třídy JTable. To však není správné řešení.

Správné řešení spočívá ve využití mechanismu zasílání událostí. Když komponenta JTable dostane přiřazen nějaký model, automaticky u tohoto modelu zaregistruje svůj event listener (konkrétně TableModelListener. Díky tomu pak model může komponentu JTable informovat o změně dat tím, že jí zašle TableModelEvent.

Pokud je náš model potomkem třídy AbstractTableModel, je zaslání příslušné zprávy velmi jednoduché, neboť stačí zavolat některou z metod fire<typZměny>, které nám třída AbstractTableModel poskytuje. V našem případě stačí upravit metodu addCar(…) takto:

public void addCar(Car car) {
    cars.add(car);
    int lastRow = cars.size() - 1;
    fireTableRowsInserted(lastRow, lastRow);
}

Nyní zkuste projekt zkompilovat a spustit. Pokud se pokusíme přidat další auto, mělo by se objevit v tabulce.

Definice typu sloupce

Aby mohla komponenta JTable vybrat vhodný TableCellRenderer, potřebuje vědět, jakého typu jsou data v určitém sloupci. Typ dat se nesnaží zjišťovat dynamicky, ale spoléhá na informace dodané modelem. Implementace metody AbstractTableModel vrací vždy hodnotu Object.class, takže je obvykle vhodné tuto metodu překrýt:

@Override
public Class<?> getColumnClass(int columnIndex) {
    switch (columnIndex) {
        case 0:
            return Integer.class;
        case 1:
        case 2:
            return String.class;
        case 3:
            return Color.class;
        case 4:
            return Fuel.class;
        default:
            throw new IllegalArgumentException("columnIndex");
    }
}

Pokud je to třeba, opravte importy pomocí funkce Fix Imports (Ctrl + Shift + I).

Nyní zkuste projekt zkompilovat a spustit. Všimněte si, že se první sloupec začal zarovnávat vpravo. To je tím, že JTable nyní s hodnotami v tomto sloupci nyní nakládá jako s hodnotami typu Integer a používá pro ně vhodnější renderer.

TableCellRenderer

Nyní se pokusíme vylepšit způsob zobrazování informace o barvě. K tomu budeme potřebovat implementovat vlastní TableCellRenderer. Protože nám bude stačit změnit barvu pozadí a vynechat text, nebudeme implementovat úplně nový TableCellRenderer, ale pouze rozšíříme DefaultTableCellRenderer.

public class ColorCellRenderer extends DefaultTableCellRenderer {
 
    @Override
    public Component getTableCellRendererComponent(JTable table, Object value,
            boolean isSelected, boolean hasFocus, int row, int column) {
 
        super.getTableCellRendererComponent(table, value, isSelected,
                hasFocus, row, column);
        setBackground((Color) value);
        setText("");
        return this;
    }
 
}

Pokud je to třeba, opravte importy pomocí funkce Fix Imports (Ctrl + Shift + I).

Abychom pochopili, co výše uvedený kód dělá, je nutné si vysvětlit, jak funguje DefaultTableCellRenderer. Jedná se o potomka třídy JLabel, který je upraven tak, aby šel efektivně použít pro opakované vykreslování jednotlivých políček tabulky. Pokud je potřeba vykreslit políčko tabulky, JTable zavolá metodu getTableCellRendererComponent(…). Tato metoda na základě svých parametrů nastaví příslušné vlastnosti (properties) komponenty JLabel (např. barvu popředí/pozadí, text, rámeček pro políčko s focusem, apod.) a nakonec vrátí sebe sama (čili this).

Naše implementace komponenty TableCellRenderer potřebuje pouze mírně modifikovat chování třídy DefaultTableCellRenderer, proto nejdříve zavolá zděděnou implementaci metody getTableCellRendererComponent(…). Pak pouze opraví barvu pozadí podle hodnoty z modelu (tu získá prostřednictvím parametru value) a text nastaví na prázdný řetězec.

Nyní musíme komponentě JTable sdělit, že má pro typ Color používat náš ColorCellRenderer. V GUI návrháři klikněte pravým tlačítkem myši na komponentu jTableCars a v kontextovém menu vyberte položku Customize Code. Zobrazí se okno, které vám umožní modifikovat kód generovaný GUI návrhářem. Za řádek

jTableCars.setModel(new CarsTableModel());

vložte následující kód:

jTableCars.setDefaultRenderer(Color.class, new ColorCellRenderer());

Nyní zkuste projekt zkompilovat a spustit. Měla by se zobrazit tabulka, která místo textového popisu barvy zobrazuje přímo tuto barvu.


Editace dat

Pokud budeme chtít umožnit také editaci dat v tabulce, musíme rozšířit náš model tak, aby umožnil modifikaci dat. Bude třeba překrýt další dvě metody:

@Override
public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
    Car car = cars.get(rowIndex);
    switch (columnIndex) {
        case 0:
            car.setId((Integer) aValue);
            break;
        case 1:
            car.setRegistrationNumber((String) aValue);
            break;
        case 2:
            car.setType((String) aValue);
            break;
        case 3:
            car.setColor((Color) aValue);
            break;
        case 4:
            car.setFuel((Fuel) aValue);
            break;
        default:
            throw new IllegalArgumentException("columnIndex");
    }
    fireTableCellUpdated(rowIndex, columnIndex);
}
 
@Override
public boolean isCellEditable(int rowIndex, int columnIndex) {
    switch (columnIndex) {
        case 1:
        case 2:
            return true;
        case 0:
        case 3:
        case 4:
            return false;
        default:
            throw new IllegalArgumentException("columnIndex");
    }
}

Nyní zkuste projekt zkompilovat a spustit. Měla by se zobrazit tabulka, která umožní editaci hodnot ve druhém a třetím sloupečku.

TableCellEditor

Prozatím umíme editovat hodnoty typu string, ale chtěli bychom samozřejmě umět editovat i např. hodnoty typu Fuel. Pokud chceme změnit způsob editace dat, musíme k tomu použít vlastní TableCellEditor. Protože budeme pro editaci paliva používat JComboBox, nemusíme implementovat novou třídu, ale pouze použijeme novou instanci třídy TableCellEditor.

V GUI návrháři klikněte pravým tlačítkem myši na komponentu jTableCars a v kontextovém menu vyberte položku Customize Code. Za řádek

jTableCars.setDefaultRenderer(Color.class, new ColorCellRenderer());

vložte následující fragment kódu

JComboBox fuelComboBox = new JComboBox();
for (Fuel f : Fuel.values()) {
    fuelComboBox.addItem(f);
}
jTableCars.getColumnModel().getColumn(4).setCellEditor(new DefaultCellEditor(fuelComboBox));

Pokud je to třeba, opravte importy pomocí funkce Fix Imports (Ctrl + Shift + I).

Také nezapomeňte upravit model tak, aby byl pátý sloupeček editovatelný.

Nyní zkuste projekt zkompilovat a spustit. Měla by se zobrazit tabulka, která umožní editaci hodnot v posledním sloupečku.


Důležité rady na závěr

  • Případnou lokalizaci názvů sloupců nebo hodnot dat (např. pro enumy) nikdy nedělejte v modelu, ale řešte to v příslušné komponentě CellRenderer.
  • Pokud pracujete s indexem řádku nebo sloupce, dávejte si pozor, jestli se jedná o index v modelu, nebo ve view. Komponenta JTable umožňuje pomocí myši (Drag & Drop) nebo i programově měnit pořadí sloupců, nebo sloupce skrývat. Index sloupečku podle zobrazení potom nemusí odpovídat jeho indexu v modelu. Použítí komponenty TableRowSorter zase mění pořadí řádků, takže pak index řádku podle zobrazení nemusí odpovídat indexu řádku v modelu. Pokud potřebujete z indexu v modelu získat index ve view, nebo naopak, použijte metody JTable.convertColumnIndexToModel(int), JTable.convertRowIndexToModel(int), JTable.convertColumnIndexToView(int) nebo JTable.convertRowIndexToView(int).
  • Nezapomínejte, že všechny manipulace s GUI (včetně zpracování události) musí probíhat ve vlákně Event Dispatcher, takže všechna volání metod fire<typZměny> musí taktéž probíhat v tomto vlákně.

Další zdroje informací

Cenným zdrojem informací o jednotlivých komponentách Swingu a jejich použití je Swing Tutorial (http://java.sun.com/docs/books/tutorial/uiswing/). Užitečným zdrojem informací o textových komponentách včetně tvorby vlastních modelů nebo view je také bakalářská práce Romana Šroma (http://is.muni.cz/th/139856/fi_b/).