Miałem niedawno okazję dłużej popracować nad projektami Javy w NetBeans 6.5 (wcześniej poważniej NetBeans używałem tylko do Grails). Środowisko to nie było dotychczas moim ulubionym, głównie ze względu na wydajność, co do której można mieć spore zastrzerzenia, jednak zauważyłem, że ma ono również mnóstwo zalet. Pierwszą jaką dostrzegłem jest to, że projekty generowane przez to IDE są naprawdę ładnie utworzone, wykorzystują standardowe narzędzia Javy, takie jak np. Ant i nie uzależniają użytkownika od używania „jedynego i właściwego” IDE. NetBeans może zauroczyć również stopniem dopracowania swoich elementów – jak już coś wspiera, to wspiera naprawdę dobrze. Przykładem tu może być sposób, w jaki można za pomocą tego środowiska utworzyć i zarządzać aplkacjami wykorzystującymi Web Services w aplikacji – zarówno klientem, jak i usługą wykorzystującącymi API JAX-WS. Kolejny (lecz na pewno nie ostatni) plus należy się NetBeans za właściwie bezkonkurencyjny edytor wizualny aplikacji wykorzystujących Swing.

I o Swingu właśnie będzie ten wpis (nieco przydługi, ostrzegam).

Nie do końca podoba mi się domyślna filozofia okien dialogowych w Javie. Np. rozmiar – niby można zmieniać rozmiar „świeżo” utworzonego okienka dialogowego rozciągając je, ale menadżery okien nie pozwalają na jego maksymalizację (brakuje bowiem przycisku maksymalizacji). Wiem, że takie jest założenie okienek dialogowych, ale wyczuwam tu pewną niekonsekwencję. Ponadto „goły” JDialog jest praktycznie pozbawiony funkcjonalności, np. brakuje mu domyślnych zachowań takich jak potwierdzanie i anulowanie, reakcję na przycisk Escape i Enter – te braki sprawiają, że każda implementacja okienka dialogowego musi być poprzedzona żmudnym wykonywaniem tych banalnych czynności.

Postanowiłem więc wykonać coś na wzór bazy dla okna dialogowego, która to baza będzie wyposażona we wszystkie cechy, które wg mnie posiadać powinna każda implementacja JDialog. W takiej bazie będę umieszczał różne komponenty – w zależności od tego, jakiego dialogu potrzebuję.

szkic

Chcę uzyskać bazę dla dialogu, w której będę umieszczał przeróżne panele – począwszy od prostych, informacyjnych (wtedy taki dialog przypominałby funkconalnością JOptionPane), skończywszy na całkiem zaawansowanych funkcjonalnie formularzach edycji wykorzystujących panele, które przygotuję sobie osobno (dzięki temu będę mógł wykorzystać panele także w innych sytuacjach). Wszystkie jednak dialogi cechować się mają wspólnymi właściwościami: dialog można zatwierdzić (submit), albo odrzucić (cancel). Wciśnięcie Escape na aktywnym oknie powoduje jego zamknięcie (funkcjonalność identyczna do wciśnięcia przycisku Anuluj), a Enter – zatwierdzenie (kliknięcie OK).

W tym celu zbudowałem JDialog w NetBeans wybierając z menu projektu New->JDialog Form i nazwałem go DialogBase. W edytorze wizualnym umieściłem dwa przyciski (nazwałem je jBtnOK i jBtnCancel), oraz jeden JPanel (jPanelMain), z ustawionym layoutem BorderLayout – w tym panelu będę umieszczał przeróżne panele „implementacyjne” – w zależności od przeznaczenia mojego dialogu.

Na początek, domyślny konstruktor i obsługa własności panel (czyli obiekt panelu, który umieszczamy w oknie):

    private JPanel panel;
 
    public DialogBase(JPanel panel) {
        this( null, true );
        setLocationRelativeTo(null);
        setPanel(panel);
    }
 
    protected void setPanel(JPanel panel) {
        this.panel = panel;
        jPanelMain.removeAll();
        jPanelMain.add( panel, BorderLayout.CENTER );
        panel.setVisible( true );
        Dimension d = new Dimension(panel.getPreferredSize());
        this.setSize( d.width, d.height+70 );
    }
 
    protected JPanel getPanel() {
        return panel;
    }

Metoda setPanel() ustawia własność panel oraz zajmuje się osadzeniem go w oknie dialogu i ustawieniem odpowiedniego rozmiaru okna (wysokość zwiększam o wielkość przycisków na dole okna).

Aby wiedzieć w jaki sposób moje okno dialogowe zostało zamknięte, wyposażam je we własność exitStatus:

    public static final int DLG_EXIT_CANCEL = 0;
    public static final int DLG_EXIT_OK = 1;
 
    private int exitStatus = DLG_EXIT_CANCEL;
 
    // getter i setter

Domyślne zamknięcie okna poskutkuje zapamiętaniem statusu „Cancel”.

Czas na obsługę klawisza Escape i Enter. Posiłkując się ciekawym na ten temat artykułem, wzbogaciłem mój DialogBase o następujący kod:

    @Override
    protected JRootPane createRootPane() {
        JRootPane theRootPane = new JRootPane();
 
        KeyStroke escStroke = KeyStroke.getKeyStroke( KeyEvent.VK_ESCAPE, 0 );
        theRootPane.registerKeyboardAction( new ActionListener() {
            public void actionPerformed( ActionEvent actionEvent ) {
                doCancel();
            }
        }, escStroke, JComponent.WHEN_IN_FOCUSED_WINDOW );
 
        KeyStroke entStroke = KeyStroke.getKeyStroke( KeyEvent.VK_ENTER, 0 );
        theRootPane.registerKeyboardAction( new ActionListener() {
            public void actionPerformed( ActionEvent actionEvent ) {
                doSubmit();
            }
        }, entStroke, JComponent.WHEN_IN_FOCUSED_WINDOW );
 
        return theRootPane;
    }

Jak widzisz, wykorzystuję tu tajemnicze metody doSubmit() i doCancel(). Co to za metody?

    private void doSubmit() {
        if( onSubmit() ) {
            setExitStatus( DLG_EXIT_OK );
            dispose();
        }
    }
 
    private void doCancel() {
        if( onCancel() ) {
            setExitStatus( DLG_EXIT_CANCEL );
            dispose();
        }
    }
 
    protected boolean onSubmit() {
        return true;
    }
 
    protected boolean onCancel() {
        return true;
    }

Obie metody działają dość podobnie: jeśli metoda onSubmit() (lub analogicznie onCancel()) zwróci true, ustawiamy odpowiedni exitStatus i zamykamy okno. Zarówno doSubmit() jak i doCancel() wiążę też za pomocą zdarzenia ActionEvent z przyciskami (wystarczy dwukrotnie kliknąć na przycisku w edytorze wizualnym i wyedytować wygenerowaną przez NetBeans pustą metodę):

    private void jBtnCancelActionPerformed(java.awt.event.ActionEvent evt) {
        doCancel();
    }
 
    private void jBtnOKActionPerformed(java.awt.event.ActionEvent evt) {
        doSubmit();
    }

OK, ale po co nam te trywialne metody onSubmit() i onCancel()? Wykorzystamy je np. do walidacji w przyszłych implementacjach naszych okien dialogowych (jeśli zdecydujemy się na ich przesłonięcie, jeśli nie – niczego złego nie zrobią ;) ).

Wygląda na to, że nasza baza jest gotowa. Użyjmy jej więc na jakimś przykładzie – np. formularzu logowania.

W NetBeans, tworzymy nowy JPanel Form, nazywamy go PanelLogin i dodajemy odpowiednie elementy interfejsu użytkownika (jak na załączonym wyżej szkicu), oczywiście to ten panel będziemy osadzać w bazie dialogu, więc nie dodajemy już przycisków OK i Anuluj, bo nie musimy. Element JTextField, zawierający nazwę użytkownika nazywamy jTxtUsername, a JPasswordField (z hasłem) – jTxtPassword.
Dodajemy dwie publiczne metody do pobrania wprowadzonych nazwy użytkownika i hasła:

    public String getUsername() {
        return jTxtUsername.getText();
    }
 
    public String getPassword() {
        return new String(jTxtPassword.getPassword());
    }

A teraz nasz dialog logowania.
W NetBeans tworzę nową klasę Javy (New->Java Class), a nie dialog(!) i nazywam ją DialogLogin. Nasza nowa klasa musi dziedziczyć po DialogBase. Ponadto, nasz dialog powinien zwracać obiekt użytkownika (jakiejś klasy np. User, której szczegóły nie są tu istotne) jeśli logowanie się powiedzie. W tym celu powołujemy własność returnValue (wraz z getterem i setterem). Przy okazji dodaję też licznik prób logowania – proste umożliwienie trzykrotnej pomyłki przy logowaniu.

public class DialogLogin extends DialogBase {
    private User returnValue;
    private int tries = 3;
 
    public DialogLogin() {
        super(new PanelLogin());
    }
}

Czas na obsługę logiki biznesowej naszego dialogu. Anulowanie dialogu zostawiamy bez zmian – po prostu zamykamy okno. Jednak musimy zaimplementować obsługę zatwierdzania formularza. W tym celu przesłaniamy naszą metodę onSubmit():

    @Override
    protected boolean onSubmit() {
        PanelLogin thePanel = ( PanelLogin ) this.getPanel();
 
        User u = App.login( thePanel.getUsername(), thePanel.getPassword() );
        if( u == null ) {
            JOptionPane.showMessageDialog( this, "Nie udało się zalogować");
            return (--tries<=0);
        }
 
        returnValue = u;
        return true;
    }

Jeśli App.login() (przyjmijmy, że ta metoda odpowiada za autoryzację aplikacji) zwróci null, to znaczy, ze nie zalogowaliśmy się do aplikacji – dialog nie zamknie się, ale pokaże nam info o nieprawidłowych parametrach logowania (i tak do momentu aż wartość tries osiągnie wartość mniejszą lub równą zeru). Jeśli użytkownik jest prawidłowy, dialog zamknie się ustawiwszy uprzednio własność returnValue.

Powinno działać. Sprawdźmy:

    public static void main(String[] args) {
        try {
            UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() );
        }
        catch( Exception ex ) {}
 
        DlgLogin dlg = new DlgLogin();
        dlg.setVisible( true );
        System.err.println("Logowanie zakończone: "+dlg.getExitStatus());
        if( dlg.getExitStatus() == DialogBase.DLG_EXIT_OK ) {
            if( dlg.getReturnValue() != null ) {
                System.err.println("W porządku, użytkownik prawidłowy");
            }
            else {
                System.err.println("Brak dostępu!");
            }
        }
        else {
            System.err.println("Anulowano!");
        }
    }

Voila!

We wpisie użyłem ikon Free Business Icons opublikowanych na licencji Creative Commons oraz Silk na licencji LGPL.