Java

CardLayout



Pracując z komponentami Swing dość szybko zorientujemy się, że ich ułożeniem można sterować na kilka sposobów. Najprostszym z nich jest ręczne ustawianie każdego z komponentów w kontenerze. By mieć taką możliwość musimy wyzerować Layout Managera, danego kontenera:
container.setLayout(null);

Takie podejście ma wiele wad. Największą z nich jest konieczność ręcznego wpisywania położeń dla wszystkich komponentów w kontenerze. Jak wiadomo każdy nadmiarowy kod jest przyczyną nowych ciekawych błędów. Błędów chcemy unikać.
W tym artykule przedstawiony będzie java.awt.CardLayout1 (CL).

Podstawy ogólnie


Jeżeli chcesz zrozumieć jak działają LM zapoznaj się z BorderLayout#id-Podstawy-ogólnie.

CardLayout Informacje podstawowe


CardLayout jest specyficznym menadżerem wyglądu. Nie zarządza on rozmieszczeniem komponentów, ale ich widocznością. Jest szczególnie przydatny wszędzie tam, gdzie chcemy uzyskać efekt podobny do tego znanego z komponentu JTabbedPane, ale na obszarze całego okna.
Sposób użycia CL jest dość prosty. Poniższy program demonstruje działanie tego manadżera:
package pl.koziolekweb.programmers.java.cardlayout;
 
import java.awt.BorderLayout;
import java.awt.CardLayout;
import java.awt.Dimension;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
 
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextArea;
 
public class CardLayoutApp {
 
        public static void main(String[] args) {
 
                JFrame frame = new JFrame("CardLayout");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setSize(300, 300);
 
                JComboBox box = new JComboBox(new String[] { "1", "2" });
 
                CardLayout cardLayout = new CardLayout();
                JPanel cards = new JPanel(cardLayout);
                cards.add(new MyPanel(cardLayout), "1");
                cards.add(new MyPanel(cardLayout), "2");
 
                box.addItemListener(new Pager(cardLayout, cards));
 
                frame.getContentPane().setLayout(new BorderLayout());
 
                frame.getContentPane().add(box, BorderLayout.NORTH);
                frame.getContentPane().add(cards, BorderLayout.CENTER);
                frame.pack();
                frame.setVisible(true);
        }
 
        static class Pager implements ItemListener {
 
                private CardLayout cardLayout;
                private JPanel cards;
 
                public Pager(CardLayout cardLayout, JPanel cards) {
                        this.cardLayout = cardLayout;
                        this.cards = cards;
                }
 
                @Override
                public void itemStateChanged(ItemEvent e) {
                        cardLayout.show(cards, (String) e.getItem());
                }
 
        }
}
 
/**
 * Klasa reprezentuje abstrakcyjny komponent stworzony przez użytkownika.
 * 
 * @author koziolek
 * 
 */
class MyPanel extends JPanel {
 
        private static final long serialVersionUID = 1L;
 
        public MyPanel(CardLayout cardLayout) {
                super(new BorderLayout());
                init();
        }
 
        private void init() {
                JTextArea area = new JTextArea();
                add(area, BorderLayout.CENTER);
                setPreferredSize(new Dimension(300, 300));
        }
 
}

Klasa MyPanel reprezentuje w tym przypadku dowolny komponent stworzony przez użytkownika.

CardLayout sposób użycia


CardLayout nie służy do zarządzania położeniem komponentów, ale ich widocznością w ramach kontenera w którym się znajdują. Jest to o tyle ważne, że determinuje sposób i miejsce użycia CL.
  1. CardLayout powinien być wykorzystywany wszędzie tam gdzie chcemy w jednym oknie obsługiwać wiele różnych widoków.
  2. CardLayout należy używać zamiast tworzenia kilku instancji JFrame, różniących się tylko częścią elementów, a mających wspólne elementy takie jak pasek menu, menu boczne itp.
  3. CardLayout nie zastępuje JTabbetPane ponieważ jest na wyższym poziomie jeśli chodzi o abstrakcję interfejsu użytkownika.

Tworzenie CardLayout


Jeżeli chcemy używać CL musimy mieć co najmniej dwa komponenty. Pierwszy z nich to panel, w którym będą wyświetlane poszczególne "ekrany". To właśnie ten komponent będzie miał nadany CL jak o layout. Drugim komponentem jest komponent, który jest źródłem zdarzeń, które będą powodować zmianę ekranu. Może to być lista rozwijana, jak w pierwszym przykładzie, może to również być przycisk, albo inny komponent, który może odbierać zdarzenia. Oba te elementy umieszczamy w dowolnym innym komponencie. Dodatkowo tworzymy jeszcze zbiór "ekranów".
Przyjrzyjmy się jak od strony praktycznej wygląda taki proces. Stworzymy prostą aplikację składającą się z panelu na którym będą wyświetlane informacje o bieżącej karcie oraz dwóch przycisków, które będą pozwalały na poruszanie się pomiędzy kartami.
package pl.koziolekweb.programmers.java.cardlayout;
 
import java.awt.BorderLayout;
import java.awt.CardLayout;
import java.awt.Dimension;
import java.awt.GridLayout;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
 
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
 
public class CardLayoutApp2 {
 
        class MyPanel extends JPanel {
 
                private static final long serialVersionUID = 1L;
 
                public MyPanel(String title) {
                        super();
                        JLabel jLabel = new JLabel(title);
                        add(jLabel);
                }
        }
 
        public static void main(String[] args) {
                CardLayoutApp2 app = new CardLayoutApp2();
                app.init();
                app.show();
        }
 
        private JFrame mainFrame;
        private JButton prev;
        private JButton next;
        private JPanel buttonPanel;
 
        private JPanel contentPanel;
 
        private CardLayout cardLayout;
 
        public CardLayoutApp2() {
                mainFrame = new JFrame("CardLayout demo 2");
                prev = new JButton("<<");
                next = new JButton(">>");
                buttonPanel = new JPanel();
                contentPanel = new JPanel();
 
                mainFrame.setSize(new Dimension(300, 300));
                mainFrame.setLayout(new BorderLayout());
                mainFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 
                buttonPanel.setLayout(new GridLayout(1, 2));
                buttonPanel.add(prev, 0);
                buttonPanel.add(next, 1);
 
                mainFrame.add(buttonPanel, BorderLayout.NORTH);
                mainFrame.add(contentPanel, BorderLayout.CENTER);
 
        }
 
        private void init() {
                cardLayout = new CardLayout();
                contentPanel.setLayout(cardLayout);
                for (int i = 0; i < 5; i++) {
                        contentPanel.add(new MyPanel("Karta numer: " + i), "" + i);
                }
 
                prev.addMouseListener(new MouseAdapter() {
 
                        @Override
                        public void mouseClicked(MouseEvent e) {
                                cardLayout.previous(contentPanel);
                        }
 
                });
 
                next.addMouseListener(new MouseAdapter() {
 
                        @Override
                        public void mouseClicked(MouseEvent e) {
                                cardLayout.next(contentPanel);
                        }
 
                });
        }
 
        private void show() {
                mainFrame.pack();
                mainFrame.setVisible(true);
        }
}

W liniach 17-26 tworzymy klasę wewnętrzną reprezentującą "ekran". W praktyce jest to zazwyczaj klasa stworzona przez użytkownika i rozszerzająca jeden z komponentów takich jak JPanel, JTable czy inny. Linie 28-32 uruchamiają aplikację.  Następnie w konstruktorze inicjowane są poszczególne elementy okna aplikacji. Dla nas jednak najbardziej interesująca jest metoda init()(linie 63-87).
Najpierw tworzony jest CardLayout i dodawany do contentPanel. Potem do panelu dodajemy pięć paneli MyPanel, które będą wyświetlać swój numer. Bardzo ważne jest to, że metoda add() musi jako drugi argument przyjąć unikalny ciąg znaków. Służy on do identyfikacji komponentu przez CL. Jeżeli parametr nie będzie podany lub nie będzie to String otrzymamy komunikat:
<quote>Exception in thread "main" java.lang.IllegalArgumentException: cannot add to layout: constraint must be a string</quote>
Następnie do przycisków dodajemy obiekty nasłuchujące, które przełączają widoczny panel za pomocą metod next() i previous().
Ostania metoda wyświetla okno.

Inne sposoby poruszania się w CardLayout


W poprzednim punkcie poznaliśmy metodę next() i previous(). CardLayout Udostępnia też inne metody poruszania się po panelach.

  1. metoda next(Container parent) - powoduje przejście do kolejnego elementu w panelu. Jeżeli obecnie wyświetlony jest ostatni element to zostanie wyświetlony pierwszy element.
  2. metoda previoud(Container parent) - powoduje przejście do poprzedniego elementu w panelu. Jeżeli obecnie wyświetlony jest pierwszy element to zostanie wyświetlony ostatni element.
  3. metoda first(Container parent) - powoduje przejście do pierwszego elementu.
  4. metoda last(Container parent) - powoduje przejście do ostatniego elementu.
  5. metoda show(Container parent, String name) - powoduje wyświetlenie elementu, który został dodany do panelu pod określoną nazwą.

We wszystkich tych metodach parametr parent oznacza komponent dla którego zastosowano CL. Jest to dość niewygodna konstrukcja, ale wynika z faktu, iż komponenty mogą jako parametr w konstruktorze przyjmować menadżery wyglądu. Nie ma więc gwarancji, że przekazując LM komponent nie przekażemy wartości null.
Istnieje jednak metoda pozwalająca na obejście tego problemu poprzez rozszerzenie CL i dodanie metod pozbawionych parametru parent, za to nasza implementacja będzie przyjmować jako parametr w konstruktorze komponent do którego należy.
Przykładowa implementacja takiej klasy wygląda następująco:
class MyCardLayout extends CardLayout{
        private static final long serialVersionUID = 1L;
        private Container parent;
 
        public MyCardLayout(Container parent) {
                super();
                if(parent == null)
                        throw new IllegalArgumentException("Parent component can not be null!");
                this.parent = parent;
        }
 
        public void first() {
                super.first(parent);
        }
 
        public void last() {
                super.last(parent);
        }
 
        public void next() {
                super.next(parent);
        }
 
        public void previous() {
                super.previous(parent);
        }
 
        public void show(String name) {
                super.show(parent, name);
        }
 
}

Sprawdzanie warunku w konstruktorze chroni nas przed sytuacją, którą opisałem.

Podsumowanie


CardLayout jest bardzo dobrym wyborem jeżeli chcemy uniknąć tworzenia wielu okien lub samemu implementować podobną funkcjonalność. Jest niezastąpiony wszędzie tam gdzie w jednym panelu chcemy mieć możliwość dodawania wielu różnych komponentów pogrupowanych w osobnych "ekranach".

Zobacz też


BorderLayout
BoxLayout
FlowLayout
GridLayout
GridBagLayout
GroupLayout
SpringLayout  

[1] Pełna dokumentacja klasy BorderLayout w javie 1.5 znajduje się pod adresem: http://java.sun.com/j2se/1.5.0/docs/api/java/awt/CardLayout.html

4 komentarze

Koziołek 2010-01-12 13:48

Siądę wieczorkiem i poprawię co trzeba zatem. Będzie kategoria Swing i przeniesione artykuły.

Coldpeer 2010-01-12 13:38

Pamiętam pamiętam :) Zrobiłem kategorię, trzeba tylko odpowiednie teksty edytować dodając owe {{Cat}}. Na początek właściwie nie trzeba nic więcej tam opisywać, robić ładnego pogrupowania tak jak to jest w głównej C# czy Delphi - tym można zająć się kiedyś, przy okazji ;)

Koziołek 2010-01-12 09:31

Jestem za... W sumie w tej Javie miałem kiedyś ambityny plan przygotowania struktury kategorii... no ale jak wiadomo chcieliśmy dobrze wyszło jak zawsze.

Coldpeer 2010-01-12 01:08

Koziołku, co myślisz o tym, żeby teksty *Layout oraz ogólnie związane ze Swingiem przypisywać* również do kategorii Swing?

  • ) poprzez {{Cat:Java/Swing}}