Skip to content

10. [TD]: Implementacja warstwy [ui] z wykorzystaniem interfej u Swing

Słowa kluczowe: architektura wielowarstwowa, Spring, wstrzykiwanie zależności, biblioteka komponentów Swing.

10.1. Support

W warstwie [UI] chcemy zbudować interfejs graficzny Swing. NetBeans posiada narzędzie [Matisse] do tworzenia takich interfejsów Swing, które przewyższa możliwości oferowane przez Eclipse. Interfejsy Swing są stopniowo zastępowane przez interfejsy JavaFx. NetBeans i Eclipse wykorzystują to samo narzędzie do tworzenia tych ostatnich. Jeśli więc tworzymy interfejsy JavaFx, możemy zachować Eclipse w całej architekturze warstwowej.

NetBeans może otworzyć dowolny projekt Maven. Wykorzystamy więc poprzedni projekt Maven i dodamy do niego interfejs Swing. W [2] wczytujemy (File / Open project) projekty Maven trzech warstw, które stworzyliśmy w Eclipse. Następnie kompilujemy ich pliki binarne [3]. Opcje [Build] i [Clean and Build] kompilują plik binarny projektu, do którego są zastosowane. Pliki te są umieszczane w folderze [target] [4-5] projektu:

Opcja [Clean] usuwa ten folder [target]. Opcja [Build] odtwarza go. Doświadczenie pokazuje, że w przypadku wystąpienia nieoczekiwanych problemów pierwszą rzeczą, jaką należy zrobić, jest uruchomienie polecenia [Clean and Build] na projekcie, aby upewnić się, że pracujemy z jego najnowszą wersją. Jest to szczególnie konieczne w przypadku plików konfiguracyjnych, których modyfikacja nie powoduje automatycznej rekompilacji podczas uruchamiania projektu. Należy wówczas wymusić tę rekompilację za pomocą polecenia [Clean and Build], aby ich nowe wersje zostały zainstalowane w folderze [target].

10.2. Działanie aplikacji

Wróćmy do ogólnej architektury aplikacji [Elections]:

Zajmujemy się teraz nową implementacją warstwy [ui]. Jedyną implementacją zrealizowaną na razie jest interfejs konsolowy. Teraz tworzymy interfejs graficzny.

Użytkownik będzie miał do dyspozycji następujący interfejs do interakcji z aplikacją [Elections]:

Interfejs graficzny znajduje się w warstwie [ui]. To właśnie on zapewnia interakcję z użytkownikiem.

  • Podczas uruchamiania aplikacja konsolowa [main] tworzy instancje trzech warstw aplikacji za pomocą Springa. Odbywa się to jeszcze przed wyświetleniem interfejsu graficznego. Również w tej fazie inicjalizacji warstwa [dao] dostarcza informacje charakteryzujące wybory (liczba mandatów do obsadzenia, próg wyborczy, listy startujące w wyborach). Jeśli ta faza inicjalizacji zakończy się niepowodzeniem (na przykład z powodu braku dostępu do danych), na konsoli wyświetlany jest komunikat o błędzie, a interfejs graficzny nie jest wyświetlany.
  • Jeśli odczyt danych przebiegł pomyślnie, wyświetlany jest interfejs graficzny zawierający następujące informacje (patrz zrzut ekranu powyżej):
    • liczba miejsc do obsadzenia w (2)
    • próg wyborczy w (3)
    • identyfikatory i nazwy list wyborczych w (4)
  • następnie użytkownik przypisuje każdej liście wyborczej liczbę głosów za pomocą pól 4 (id – nazwa), 5 (głosy), 6 (dodaj).

Image

  • Następnie można skorzystać z linku (10), aby obliczyć liczbę mandatów:

Image

  • link [Enregistrer] (12) pozwala zapisać wyniki w źródle danych.

10.3. Klasa [ElectionsSwing] implementująca warstwę [ui]

10.3.1. Projekt NetBeans

Uwaga: W punkcie 22.4 opisano, jak pobrać NetBeans.

Ostateczny projekt aplikacji w środowisku NetBeans będzie miał następującą postać: [1]. Należy go skompilować, postępując zgodnie z instrukcjami zawartymi w [2-5]:

Należy upewnić się, że projekt jest skonfigurowany do kompilacji za pomocą JDK 1.8 [1-6]:

10.3.2. Konfiguracja Maven

Nowy projekt [elections-swing-metier-dao-jdbc] będzie oparty na poprzednim projekcie [elections-console-metier-dao-jdbc]. W tym celu dodajemy zależność Maven w następujący sposób: [1-3]:

10.3.3. Tworzenie interfejsu graficznego

Aby utworzyć interfejs graficzny, możemy postępować w następujący sposób:

  • [1]: dodanie obiektu do pakietu [elections.ui.service]
  • [2]: wybór opcji [JFrame Form] w kategorii [Swing GUI Forms]
  • [4]: nadanie nazwy klasie
  • [5]: pakiet klasy.
  • Zakończ pracę kreatora.
  • [6]: wygenerowana klasa
  • [7]: klasa [AbstractElectionsSwing] w trybie [Design]
  • [8]: zakładka [Navigator], która wyświetla drzewo [9] komponentów okna
  • [10]: zakładka [Properties], która wyświetla właściwości komponentu [jFrame] wybranego w [9]
  • [11]: [JFrame] jest kontenerem komponentów. Komponenty te można rozmieszczać w kontenerze zgodnie z różnymi regułami pozycjonowania o nazwie layouts. W tym przypadku wybieramy layout, [Free Design] oraz [14], które umożliwiają swobodne rozmieszczanie komponentów w kontenerze.

Elementy te znajdujemy na pasku narzędzi o nazwie Palette:

  • [1]: paleta
  • [2]: komponent JLabel został umieszczony w kontenerze komponentów
  • Po kliknięciu prawym przyciskiem myszy na nim uzyskuje się dostęp do różnych właściwości: jego nazwy [4], tekstu [3] lub menedżerów zdarzeń [5]. Używamy [3] do ustawienia tekstu [6].
  • [1]: zakładka [Properties] komponentu [JLabel] zapewnia dostęp do jego właściwości: jego położenia poziomego [2], pozycję pionową [3], czcionkę tekstu [4] oraz sam tekst [5].

Gdy komponent zostanie umieszczony i skonfigurowany w interfejsie graficznym, a wykonana praca zostanie zapisana (Ctrl-S), w klasie [AbstractElectionsSwing] generowany jest kod:

 
 

Nie należy modyfikować tego wyszarzonego kodu, ponieważ zostanie on usunięty i ponownie wygenerowany przy następnym zapisaniu. Wszelkie wprowadzone zmiany zostałyby wówczas utracone.

Samouczek dotyczący tworzenia interfejsu graficznego za pomocą NetBeans można znaleźć w plikach URL i [https://netbeans.org/kb/docs/java/quickstart-gui.html?print=yes#design] (listopad 2015 r.).

Teraz tworzymy następujący interfejs:

Elementy interfejsu są następujące:

nr
typ
nazwa
rola
1
JMenuBar
jMenuBar1
menu
2
JLabel
jLabelSAP
liczba wolnych miejsc
3
JLabel
jLabelSE
próg wyborczy
4
JComboBox
jComboBoxNomsListes
lista nazw list wyborczych biorących udział w wyborach
5
JTextField
jTextFieldVoixListe
liczba głosów oddanych na daną listę
6
JLabel
jLabelAjouter
aby dodać listę do (8)
7,8
(JScrollPane, JList)
jListNomsVoix
nazwy i głosy list
9
JLabel
jLabelSupprimer
aby usunąć z (8) listę wybraną w (8)
10
JLabel
jLabelCalculer
w celu obliczenia wyników wyborów
11
JLabel
jLabelEffacer
w celu usunięcia wyników wyborów
12
JLabel
jLabelEnregistrer
w celu zapisania wyników wyborów
13,14
(JScrollPane, JList)
jListResultats
w celu wyświetlenia wyników wyborów
15,16
(JScrollPane,
JTextPane)
jTextPaneMessages
aby wyświetlić komunikaty dotyczące monitorowania

Adnotacja (JScrollPane, JList) [13-14] ma na celu wskazanie, że po upuszczeniu komponentu [JList] w oknie jest on automatycznie umieszczany w komponencie [JScrollPane], który umożliwia przewijanie listy. To właśnie komponent [JScrollPane] umożliwia wyświetlenie wszystkich elementów listy, podczas gdy w danym momencie widoczna jest tylko ograniczona ich liczba. To samo dotyczy komponentów [JTextPane] i [15-16].

Menu można utworzyć w następujący sposób:

  • [1, 2]: komponent [Menu Bar] jest umieszczany w oknie
  • [3]: domyślnie wygenerowane menu, takie jak przedstawione w zakładce [Navigator]
  • [4,5,6]: klikając prawym przyciskiem myszy na opcję menu, można:
    • zmienić jej tekst ([4]) oraz nazwę ([5])
    • zarządzać jej zdarzeniami [6]
  • [7]: żądane menu

Żądane menu wygląda następująco:

poziom 1
poziom 2
Wybory
 
 
Quitter
Listy
 
 
Ajouter
 
Supprimer
Wyniki
 
 
Calculer
 
Effacer
 
Enregistrer
O nas
 

Interfejs graficzny można przetestować w dowolnym momencie:

 

Podczas tworzenia interfejsu należy przypisać obsługę zdarzeń do niektórych etykiet i menu [Ajouter, Effacer, ...]. Oto jak to zrobić:

  • [1]: kliknij prawym przyciskiem myszy na komponent, dla którego chcesz zarządzać zdarzeniem
  • [2]: wybierz opcję [Events]
  • [3]: wybierz kategorię zdarzeń
  • [4]: wybierz zdarzenie, którym chcesz zarządzać

Kod Java wygenerowany w wyniku tej operacji jest następujący:


    jLabelCalculer.addMouseListener(new java.awt.event.MouseAdapter() {
      public void mouseClicked(java.awt.event.MouseEvent evt) {
        jLabelCalculerMouseClicked(evt);
      }
    });
...

  private void jLabelCalculerMouseClicked(java.awt.event.MouseEvent evt) {
    // TODO dodaj tutaj swój kod obsługi:
}
  • wiersze 1–5: do komponentu jLabelCalculer dodano menedżera zdarzeń. Metoda addMouseListener oczekuje jako parametr klasy implementującej następujący interfejs MouseListener:
 

Interfejs MouseListener jest implementowany przez różne klasy, w tym klasę MouseAdapter. Klasa ta implementuje pięć metod interfejsu MouseListener, jednak metody te nie wykonują żadnych działań. Należy zatem utworzyć klasę pochodną tej klasy, aby zaimplementować wybraną metodę lub metody. Zostało to zrobione w powyższym kodzie i przedstawione poniżej:


    jLabelCalculer.addMouseListener(new java.awt.event.MouseAdapter() {
      public void mouseClicked(java.awt.event.MouseEvent evt) {
        jLabelCalculerMouseClicked(evt);
      }
});

Powyższy kod wykorzystuje technikę klasy anonimowej opisaną w punkcie 2.5 kursu [ref1].

W wierszu 1 parametrem metody addMouseListener jest klasa anonimowa, definiowana dynamicznie. Jest to instancja klasy pochodnej klasy MouseAdapter (wiersz 1), w której przekształcono metodę mouseClicked (wiersze 2–4), aby wykonała określoną czynność.

Metoda jLabelCalculerMouseClicked, zwana wierszem 3, jest zdefiniowana w następujący sposób:


  private void jLabelCalculerMouseClicked(java.awt.event.MouseEvent evt) {
    // TODO dodaj tutaj swój kod obsługi:
}

Programista obsługuje zdarzenie „MouseClicked”, umieszczając kod w tej metodzie.

Wszystkie procedury obsługi zdarzeń są generowane przez NetBeans w ten sposób. Programista może pominąć linie kodu wygenerowane przez NetBeans, aby powiązać metodę ze zdarzeniem komponentu. Może po prostu umieścić swój kod w linii 2 powyżej. Oto przykład:


  private void jLabelCalculerMouseClicked(java.awt.event.MouseEvent evt) {
    System.out.println("Mouse Clicked");
}

Jeśli uruchomimy interfejs graficzny i klikniemy na link [Calculer], na konsoli pojawi się komunikat:

  • [1]: dwukrotnie klikamy etykietę [Calculer]
  • [2]: menedżer tego zdarzenia został uruchomiony i wygenerował komunikaty mouseClicked w konsoli NetBeans.

Komponenty [jComboBoxNomsListes, jListNomsVoix, jListResultats] są zadeklarowane w następujący sposób:


protected javax.swing.JComboBox jComboBoxNomsListes;
protected javax.swing.JList jListNomsVoix;
protected javax.swing.JList jListResultats;

Komponenty te są listami, które zazwyczaj są parametryzowane przez typ T: typ elementów modelu wyświetlanego przez komponenty. Typ T może być dowolny. Wartość wyświetlana w komponencie listy jest typu [String]. Domyślnie do wyświetlania używana jest wówczas metoda [T.toString()]. Aby uzyskać większą kontrolę nad tym, co zostanie wyświetlone, typem T będzie tutaj typ String. Zatem poprawna deklaracja naszych list wygląda następująco:


protected javax.swing.JComboBox<String> jComboBoxNomsListes;
protected javax.swing.JList<String> jListNomsVoix;
protected javax.swing.JList<String> jListResultats;

Wynik ten uzyskujemy poprzez zmianę jednej z właściwości komponentu:

10.3.4. Rozdzielenie kodu

Wróćmy do struktury naszej aplikacji:

Klasa [AbstractElectionsSwing] musi implementować powyższą warstwę [ui]. Jej kod wygenerowany przez NetBeans zawiera na razie wyłącznie kod zarządzający oknem oraz procedury obsługi zdarzeń, które na razie nie wykonują żadnych czynności. Powyżej widać, że klasa [AbstractElectionsSwing] będzie musiała obsługiwać komunikację z warstwą [métier]. Obsługa ta będzie realizowana w procedurach obsługi zdarzeń. Aby uporządkować strukturę kodu, postanowiono umieścić go w dwóch klasach:

  • [AbstractElectionsSwing], która pozostanie w stanie, w jakim została wygenerowana przez NetBeans, z niewielkimi zmianami. Klasa ta sama w sobie nie będzie obsługiwać żadnych zdarzeń. Obsługa zdarzeń będzie pusta i zadeklarowana jako abstrakcyjna. Zostanie ona zaimplementowana przez klasę pochodną od [AbstractElectionsSwing].
  • [ElectionsSwing] – klasa wywodząca się z [AbstractElectionsSwing], która zaimplementuje wszystkie procedury obsługi zdarzeń.

Tego rodzaju rozdzielenie nie jest niczym niezwykłym. Można je znaleźć na przykład na stronach internetowych ASP.NET (wersja inna niż MVC). Projekt NetBeans rozwija się w następujący sposób:

 

Z kolei kod klasy [AbstractElectionsSwing] ewoluuje w następujący sposób:


public abstract class AbstractElectionsSwing {
....
    private void jMenuItemCalculerActionPerformed(java.awt.event.ActionEvent evt) {
        doCalculer();
    }

...

    private void jLabelCalculerMouseClicked(java.awt.event.MouseEvent evt) {
        if (jLabelCalculer.isEnabled()) {
            doCalculer();
        }
    }

....
    // obsługa zdarzeń
    abstract protected void doSupprimer();

    abstract protected void doCalculer();

    abstract protected void doQuitter();

    abstract protected void doEffacer();

    abstract protected void doEnregistrer();

    abstract protected void doAjouter();

    abstract protected void doInformer();

    abstract protected void doMajLabelAjouter();

    abstract protected void doMajLabelSupprimer();
...
}
  • wiersz 1: klasa jest zadeklarowana jako abstrakcyjna
  • wiersze 3–5: obsługa kliknięcia opcji menu [jMenuItemCalculer]. Widać, że obsługa zdarzenia jest przekazywana do metody doCalculer z wiersza 19. Metoda ta nie jest zaimplementowana i jest zadeklarowana jako abstrakcyjna. Zaimplementuje ją klasa pochodna [ElectionsSwing];
  • wiersze 9–13: procedurę obsługi zdarzenia clic dla etykiety [jLabelCalculer]. Kliknięcie zawsze wywołuje zdarzenie, niezależnie od tego, czy komponent [jLabel] jest aktywny (enabled=true), czy nieaktywny (enabled=false). W tym miejscu upewniamy się, że jest on rzeczywiście aktywny, aby przetworzyć zdarzenie;
  • wiersze 15 i kolejne: ta technika delegowania obsługi zdarzeń do metody abstrakcyjnej jest stosowana we wszystkich handlerach zdarzeń.

Klasa [ElectionsSwing], wywodząca się z klasy [AbstractElectionsSwing], implementuje wszystkie obsługujące zdarzenia, które nie zostały zaimplementowane przez klasę [AbstractElectionsSwing]:


package elections.ui.service;;
...
public class ElectionsSwing extends AbstractElectionsSwing {

    // obsługa zdarzeń

    @Override
    protected void doInformer() {
...
    }

    @Override
    protected void doAjouter() {
    ...
    }

    @Override
    protected void doCalculer() {
    ...
    }

    @Override
    protected void doEffacer() {
    ...
    }

    @Override
    protected void doEnregistrer() {
...
    }

    @Override
    protected void doQuitter() {
        System.exit(0);
    }

    @Override
    protected void doSupprimer() {
...
    }

    @Override
    protected void doMajLabelAjouter() {
...
    }

    @Override
    protected void doMajLabelSupprimer() {
...
    }

}
  • wiersz 3: [ElectionsSwing] wywodzi się z [AbstractElectionsSwing]
  • wiersze 7–50: procedury obsługi zdarzeń okna graficznego

Metody klasy pochodnej [ElectionsSwing] będą operować na komponentach klasy nadrzędnej [AbstractElectionsSwing]. Obecnie komponenty te mają zakres private, co uniemożliwia klasie potomnej [ElectionsSwing] dostęp do nich:


private JMenuItem jMenuItemAPropos = null;

private JLabel jLabelAjouter = null;

Aby rozwiązać ten problem, należy ustawić zakres komponentów interfejsu graficznego na [protected]:

  • ustawić atrybut [protected] na [3];

10.3.5. Implementacja interfejsu [IElectionsUI]

Wróćmy do struktury naszej aplikacji:

Powyżej warstwa [ui] musi udostępniać interfejs [IElectionsUI] obiektowi [main]:


package elections.ui.service;

public interface IElectionsUI {
    /**
     * lance le dialogue avec l'utilisateur
     */
    public void run();
}

Interfejs ten został zdefiniowany w projekcie [elections-console-metier-dao-jdbc] i opisany w paragrafie 9.4. Ponieważ projekt ten jest zależnością projektu [swing], interfejs ten jest znany.

Ponieważ klasa [AbstractElectionsSwing] stała się klasą abstrakcyjną, nie może już być instancjonowana przez Spring. Teraz instancjonowana musi być klasa [ElectionsSwing]. Klasa [ElectionsSwing] musi implementować interfejs [IElectionsUI]. Jej kod zmienia się zatem w następujący sposób:


public class ElectionsSwing extends AbstractElectionsSwing implements IElectionsUI {

    // metoda run interfejsu [ElectionsUI]
    public void run() {
...
    }

  • wiersz 1: klasa [ElectionsSwing] implementuje interfejs [IElectionsUI]
  • wiersze 4–6: metoda [run] tego interfejsu

Co powinna robić metoda run? Wyświetlić okno graficzne. Jak to zrobić? Można kierować się metodą [main], którą wygenerował NetBeans w klasie [AbstractElectionsSwing] i która wykonuje to, co jest pożądane:


public static void main(String args[]) {
    /* Ustaw wygląd i styl Nimbus */
    //<editor-fold defaultstate="collapsed" desc=" Kod ustawień wyglądu i zachowania (opcjonalnie) ">
    /* Jeśli Nimbus (wprowadzony w Javie SE 6) nie jest dostępny, należy zachować domyślny wygląd i styl.
         * For details see http://download.oracle.com/javase/tutorial/uiswing/lookandfeel/plaf.html 
     */
    try {
      for (javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.getInstalledLookAndFeels()) {
        if ("Nimbus".equals(info.getName())) {
          javax.swing.UIManager.setLookAndFeel(info.getClassName());
          break;
        }
      }
    } catch (ClassNotFoundException ex) {
      java.util.logging.Logger.getLogger(NewJFrame.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
    } catch (InstantiationException ex) {
      java.util.logging.Logger.getLogger(NewJFrame.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
    } catch (IllegalAccessException ex) {
      java.util.logging.Logger.getLogger(NewJFrame.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
    } catch (javax.swing.UnsupportedLookAndFeelException ex) {
      java.util.logging.Logger.getLogger(NewJFrame.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
    }
    //</editor-fold>

    /* Utwórz i wyświetl formularz */
    java.awt.EventQueue.invokeLater(new Runnable() {
      public void run() {
        new NewJFrame().setVisible(true);
      }
    });
  }

Konstruktor [AbstractElectionsSwing] użyty w linii 28 wygląda następująco:


  public AbstractElectionsSwing() {
    initComponents();
}
  • linia 2: metoda [initComponents] jest metodą prywatną wygenerowaną przez generator interfejsu graficznego. Nie można zmieniać jej kodu.

Metoda [run] klasy [ElectionsSwing] mogłaby zatem wyglądać następująco:


  @Override
  public void run() {
    // wyświetlanie interfejsu graficznego
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        init();
        setVisible(true);
      }
    });
}
  • wiersz 6: interfejs graficzny jest inicjowany za pomocą metody [init]. W tym miejscu chcielibyśmy wywołać metodę [initComponents] z klasy nadrzędnej, ale jest ona prywatna. W związku z tym w klasie nadrzędnej [AbstractElectionsSwing] dodajemy następującą metodę [init]:

  protected void init(){
    initComponents();
}
  • (ciąg dalszy)
    • ponieważ znajduje się w klasie [AbstractElectionsSwing], metoda [init] ma dostęp do prywatnej metody [initComponents] tej samej klasy;
    • ponieważ posiada atrybut [protected], jest widoczna w klasie potomnej [ElectionsSwing];
  • wiersz 7: interfejs graficzny zostaje udostępniony;

Uwaga: po dodaniu metody [run] do klasy [ElectionsSwing] można usunąć metodę [main] z klasy abstrakcyjnej [AbstractElectionsSwing].

10.3.6. Klasa wykonywalna

Wróćmy do struktury naszej aplikacji:

Chcielibyśmy, aby Spring utworzył instancję warstwy [ui] tak samo, jak miało to miejsce, gdy była ona implementowana przez aplikację console. W tym celu klasa implementacyjna [ElectionsSwing] musi zawierać odwołanie do warstwy [métier]:


@Component
public class ElectionsSwing extends AbstractElectionsSwing implements IElectionsUI{

  // informacje o warstwie [métier]
  @Autowired
  private IElectionsMetier metier;
...
  • wiersz 1: klasa [ElectionsSwing] jest komponentem Spring;
  • wiersze 5–6: wstrzyknięcie przez Spring odniesienia do warstwy [métier];

Interfejs graficzny jest uruchamiany poprzez wykonanie następującej klasy [BootElectionsSwing]:

  

package elections.ui.boot;

import elections.ui.service.IElectionsUI;

public class BootElectionsSwing extends AbstractBootElections {
    public static void main(String[] arguments) {
        new BootElectionsSwing().run();
    }

    @Override
    protected IElectionsUI getUI() {
        return ctx.getBean("electionsSwing", IElectionsUI.class);
    }
}

Podobny kod omówiliśmy w punkcie 9.5, wyjaśniając kod klas [AbstractBootElections] i [BootElectionsConsole]. W wierszu 12 pobieramy bean o nazwie [electionsSwing], który odpowiada standardowej nazwie Spring dla klasy [ElectionsSwing].

10.3.7. Inicjalizacja interfejsu graficznego

Po wyświetleniu interfejsu graficznego niektóre z jego komponentów zostały zainicjowane:

Image

Jak widać powyżej:

  • że lista rozwijana została wypełniona nazwami list wyborczych;
  • że podano liczbę mandatów do obsadzenia oraz próg wyborczy;
  • że niektóre linki zostały wyłączone;
  • że na dole okna wyświetlany jest komunikat o pomyślnym zakończeniu operacji;

Kiedy nastąpią te inicjalizacje? Mogą one nastąpić dopiero po zainicjowaniu pola [electionsMetier] klasy [ElectionsSwing]. Nazwy list zostaną bowiem pobrane z warstwy [metier]. Inicjalizacja tego pola zostanie przeprowadzona przez Spring w następującej kolejności:

  • wykorzystanie konstruktora bez parametrów klasy [ElectionsSwing];
  • wstrzyknięcie zależności, w tym przypadku odwołania do warstwy [métier];
  • wywołanie metody [run] klasy [ElectionsSwing]:

    @Override
    public void run() {
        // wyświetla się interfejs graficzny
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                init();
                setVisible(true);
            }
        });
}
  • w wierszu 12 określiliśmy, że wywołujemy metodę [init] klasy nadrzędnej, która narysuje elementy interfejsu graficznego. Przedefiniujemy tę metodę lokalnie w klasie [ElectionsSwing]. To właśnie w tej metodzie zainicjujemy, tym razem z danymi, elementy okna (listy rozwijane, etykiety):

Lokalna metoda [init] mogłaby mieć następujący szkielet:


public class ElectionsSwing extends AbstractElectionsSwing implements IElectionsUI {

    ...
  // odwołanie do warstwy [métier]
  @Autowired
  private IElectionsMetier metier;

    // inicjalizacje
    public void init() {
      // generowanie komponentów przez klasę nadrzędną
    super.init();
    
    // pobieranie list z warstwy [metier]
        ...
        // nazwy list są powiązane z polem rozwijanym jComboBoxNomsListes
        ...
        // oraz parametry wyborów
        ...
        // inicjuje się etykiety powiązane z tymi dwiema informacjami
        ...
        // komunikat o pomyślnym zakończeniu
        ...
        // inicjalizacja stanu niektórych komponentów formularza
...
}

Warto zwrócić uwagę na wywołanie w wierszu 11 metody [init] z klasy nadrzędnej.

10.3.8. Klasa [Utilitaires]

W klasie [Utilitaires] zgromadzono szereg statycznych metod pomocniczych:

 

Klasa [Utilitaires] ma następującą postać:


package istia.st.elections.ui;

import javax.swing.JLabel;
import javax.swing.JMenuItem;

//klasa pomocnicza
class Utilitaires {
    // zarządzanie stanem tablicy etykiet
    public static void setEnabled(JLabel[] labels, boolean value) {
        for (int i = 0; i < labels.length; i++) {
            labels[i].setEnabled(value);
        }
    }

    // zarządzanie stanem tablicy opcji menu
    public static void setEnabled(JMenuItem[] menuItems, boolean value) {
        ...
    }

}
  • wiersz 9: metoda setEnabled ustawia stan komponentów JLabel zdefiniowanych w tablicy. Metoda setEnabled komponentu JLabel umożliwia włączenie lub wyłączenie komponentu JLabel.

Zadanie do wykonania: na wzór metody setEnabled z wiersza 9 (,) napisz metodę setEnabled z wiersza 16, która wykonuje tę samą czynność dla komponentów JMenuItem.


10.3.9. Kod klasy [ElectionsSwing]

Przypomnijmy ogólną strukturę klasy [ElectionsSwing]:


package istia.st.elections.ui;

...
public class ElectionsSwing extends AbstractElectionsSwing implements IElectionsUI {

  // odwołanie do warstwy [métier]
  @Autowired
  private IElectionsMetier metier;


    // inicjalizacje
    public void init() {
    ...
    }

    // procedury obsługi zdarzeń

    @Override
    protected void doInformer() {
...
    }

    @Override
    protected void doAjouter() {
...
    }

    @Override
    protected void doCalculer() {
    ...
    }

    @Override
    protected void doEffacer() {
...
    }

    @Override
    protected void doEnregistrer() {
...
    }

    @Override
    protected void doQuitter() {
        System.exit(0);
    }

    @Override
    protected void doSupprimer() {
...
    }

    @Override
    protected void doMajLabelAjouter() {
    ...
    }

    @Override
    protected void doMajLabelSupprimer() {
...
    }

}

Przeanalizujemy metody tej klasy po kolei.

10.3.9.1. Metoda [init]

Wróćmy do interfejsu graficznego:

Cele metody [init] to:

  • wypełnić pole kombi [4] danymi identyfikacyjnymi i nazwami list w formacie [id - nom]
  • wyświetlić komunikat o pomyślnym zakończeniu w polu [15]
  • zainicjować etykiety [2] i [3]
  • wyłączenie niektórych linków

Szkielet metody [init] mógłby wyglądać następująco:


@Override
    protected void init() {
        // generowanie komponentów przez klasę nadrzędną
        super.init();
        // lokalne inicjalizacje
        modèleNomsVoix = new DefaultListModel<>();
        jListNomsVoix.setModel(modèleNomsVoix);
        modèleRésultats = new DefaultListModel<>();
        jListResultats.setModel(modèleRésultats);
        String info;
        try {
            // pobieranie list z warstwy [métier]
            listes = ...
            // powiązanie nazw list z polem kombi jComboBoxNomsListes
            ...
            // oraz parametry wyborów
            int nbSiegesAPourvoir = ...
            double seuilElectoral = ...
            // inicjuje się etykiety powiązane z tymi dwiema informacjami
            ...
            // komunikat o pomyślnym zakończeniu
            info = "Source de données lue avec succès";
        } catch (ElectionsException ex1) {
            // odnotowuje się błąd
            info = getInfoForException("Les erreurs suivantes se sont produites :", ex1);
        } catch (RuntimeException ex2) {
            // odnotowuje się błąd
            info = getInfoForException("Les erreurs suivantes se sont produites :", ex2);
        }
        // wyświetla się informacja
        jTextPaneMessages.setText(info);
        jTextPaneMessages.setCaretPosition(0);
        // stan formularza
        Utilitaires.setEnabled(new JLabel[] { jLabelAjouter, jLabelCalculer, jLabelEnregistrer, jLabelSupprimer }, false);
        Utilitaires.setEnabled(
                new JMenuItem[] { jMenuItemAjouter, jMenuItemCalculer, jMenuItemEnregistrer, jMenuItemSupprimer }, false);
        // wyśrodkowanie okna
        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
        Dimension frameSize = getSize();
        if (frameSize.height > screenSize.height) {
            frameSize.height = screenSize.height;
        }
        if (frameSize.width > screenSize.width) {
            frameSize.width = screenSize.width;
        }
        setLocation((screenSize.width - frameSize.width) / 2, (screenSize.height - frameSize.height) / 2);
    }

    private String getInfoForException(String message, ElectionsException ex) {
        // wyświetla się komunikat
        StringBuffer info = new StringBuffer(String.format("%s -------------\n", message));
        info.append(String.format("Code erreur : %d\n", ex.getCode()));
        // wyświetla się lista błędów
        for (String erreur : ex.getErreurs()) {
            info.append(String.format("-- %s\n", erreur));
        }
        return info.toString();
    }

    private String getInfoForException(String message, RuntimeException ex) {
        // wyświetla komunikat
        StringBuffer info = new StringBuffer(String.format("%s -------------\n", message));
        // wyświetla stos wyjątków
        Throwable cause = ex;
        while (cause != null) {
            info.append(String.format("-- %s\n", cause.getMessage()));
            cause = cause.getCause();
        }
        return info.toString();
    }

Zadanie: uzupełnij kod metody [init].


Materiały do przeczytania w ramach kursu: komponenty JTextField, JLabel

Warto wiedzieć:

Komponent JList wyświetla dane zawarte w szablonie. Domyślnie szablon ten jest typu DefaultListModel (wiersze 2 i 3). Obiekt typu DefaultListModel zachowuje się nieco podobnie do typu ArrayList:

  • aby dodać obiekt typu o do szablonu:

[DefaultListModel].addElement(Object o);

W tej aplikacji obiekt o będzie zawsze typu String.

  • aby usunąć element nr i z szablonu:

[DefaultListModel].remove(int i);
  • Aby uzyskać element nr i z szablonu:

[DefaultListModel].elementAt(int i);

Aby dodać element do zestawu [jComboBoxNomsListes], należy użyć metody [addItem]:


jComboBoxNomsListes.addItem(chaîne de caractères)

Komponent JTextPane posiada metody getText() i setText() służące do odczytu i zapisu wyświetlanego tekstu.

10.3.9.2. Zarządzanie stanem łącza [Ajouter]

Link [Ajouter] [6] jest aktywny tylko wtedy, gdy pole [5] zawierające głosy nie jest puste. W klasie [AbstractElectionsSwing] moduł obsługujący ruchy kursora w polu [5] wygląda następująco:


  private void jTextFieldVoixListeCaretUpdate(javax.swing.event.CaretEvent evt) {
    doMajLabelAjouter()
}

Wiersz 2 wywołuje metodę [doMajLabelAjouter] klasy [ElectionsSwing].


    protected void doMajLabelAjouter() {
        // ustawianie stanu etykiety [jLabelAjouter]
        ...
        // ustala się stan menu [jMenuItemAjouter]
        ...
}

Zadanie: uzupełnij kod metody [doMajLabelAjouter].


10.3.9.3. Przypisz głosy do każdej listy

Dla każdej listy kandydującej z (4) postępuje się w następujący sposób:

  • wybór listy z (4)
  • wprowadzenie liczby głosów w (5)
  • potwierdzenie poprzez kliknięcie linku [Ajouter]

Błędy wprowadzenia danych są sygnalizowane tak, jak pokazano w poniższym przykładzie:

Image

Jeśli liczba głosów jest prawidłowa, lista zostaje dodana do komponentu (8), liczba głosów jest usuwana, a link [Ajouter] znika:

W klasie [AbstractElectionsSwing] procedurą obsługującą kliknięcie linku [Ajouter] jest następująca:


    private void jLabelAjouterMouseClicked(java.awt.event.MouseEvent evt) {
        if (jLabelAjouter.isEnabled()) {
            doAjouter();
        }
}

Wiersz 3 wywołuje metodę [doAjouter] klasy [ElectionsSwing]:


  // szablony list JList
  private DefaultListModel<String> modèleNomsVoix = null;
  private DefaultListModel<String> modèleRésultats = null;

  // listy biorące udział w konkursie
  private ListeElectorale[] listes;

  // listy wprowadzone przez użytkownika
  private final List<ListeElectorale> listesSaisies = new ArrayList<>();
  private ListeElectorale[] tListesSaisies;
...
  @Override
  protected void doAjouter() {
    // czy liczba głosów jest prawidłowa?
    ...
    // jeśli wystąpi błąd, należy go zgłosić
    if (erreur) {
      JOptionPane.showMessageDialog(null, "Nombre de voix incorrect", "Elections : erreur",
              JOptionPane.INFORMATION_MESSAGE);
      jTextFieldVoixListe.requestFocus();
      // powrót do interfejsu graficznego
      return;
    }
    // brak błędu – zapisujemy listę
    listesSaisies.add(...);
    modèleNomsVoix.addElement(...);
    // kasujemy liczbę głosów
    jTextFieldVoixListe.setText("");
    // stan formularza (menu, etykiety)
...
}
  • wiersz 25: za każdym razem, gdy użytkownik dodaje głosy do listy i zatwierdza swój wybór, lista ta jest umieszczana w polu [listesSaisies] z wiersza 9. Zostanie tam zapisana lista wraz z informacjami [id, version, nom, voix]. Trzy pierwsze informacje pochodzą z list zapisanych początkowo w tablicy w wierszu 6. Metoda [getSelectedIndex] pola kombi pozwala ustalić indeks wybranej listy;

Zadanie: uzupełnij kod metody [doAjouter].


10.3.9.4. Zarządzanie stanem linku [Supprimer]

Łącze [Supprimer] [9] jest aktywne tylko wtedy, gdy w [8] zaznaczono jakiś element.

W klasie [AbstractElectionsSwing] procedurą reagującą na kliknięcie elementu z listy [8] jest następująca:


  private void jListNomsVoixValueChanged(javax.swing.event.ListSelectionEvent evt) {
    doMajLabelSupprimer();
}

Wiersz 2 wywołuje metodę [doMajLabelSupprimer] klasy [ElectionsSwing].


    @Override
    protected void doMajLabelSupprimer() {
            // włączamy etykietę [jLabelSupprimer] i odpowiadającą jej opcję menu
            ...
}

Zadanie: uzupełnij kod metody [doMajLabelSupprimer].


10.3.9.5. Usunięcie listy kandydatów

Link [Supprimer] [9] umożliwia usunięcie pary (nazwa, głos) wybranej w punkcie (8). Po usunięciu link [Supprimer] zostanie wyłączony. Zostanie ponownie włączony dopiero po wybraniu nowej listy w punkcie (8).

W klasie [AbstractElectionsSwing] menedżerem reagującym na kliknięcie linku [Supprimer] jest następujący:


  private void jLabelSupprimerMouseClicked(java.awt.event.MouseEvent evt) {
    if(jLabelSupprimer.isEnabled()){
      doSupprimer();
    }
}

Wiersz 3 wywołuje metodę [doSupprimer] klasy [ElectionsSwing].


@Override
    protected void doSupprimer() {
        // usuwanie zaznaczonej listy, szablonu modèleNomsVoix oraz wprowadzonych list
        ...
        // aktualizacja stanu etykiet i opcji menu formularza
        Utilitaires.setEnabled(...);
    ...
}

Zadanie: uzupełnij kod metody [doSupprimer].


10.3.9.6. Zarządzanie stanem łącza [Calculer]

Powiązanie [Calculer] [10] jest aktywne tylko wtedy, gdy w [8] znajduje się co najmniej jeden element.


Zadanie: dodaj kod niezbędny do obsługi tego powiązania w metodach [doAjouter] i [doSupprimer] napisanych wcześniej. Odpowiednia opcja menu również zostanie obsłużona.


Warto wiedzieć: liczbę elementów w elemencie typu DefaultListModel uzyskuje się za pomocą metody size().

10.3.9.7. Obliczanie liczby mandatów

Łącze [Calculer] [10] umożliwia uruchomienie obliczeń mandatów i wyświetlenie wyników w (14). W przypadku niepowodzenia (wszystkie listy zostały wyeliminowane) w [15] wyświetlany jest komunikat o błędzie. W każdym przypadku po zakończeniu obliczeń link [Calculer] [10] zostaje wyłączony.

W klasie [AbstractElectionsSwing] procedurą reagującą na kliknięcie linku [Calculer] jest następująca:


  private void jLabelCalculerMouseClicked(java.awt.event.MouseEvent evt) {
    if(jLabelCalculer.isEnabled()){
      doCalculer();
    }
}

Wiersz 3 wywołuje metodę [doCalculer] klasy [ElectionsSwing].


  // listy wprowadzone przez użytkownika
  private final List<ListeElectorale> listesSaisies = new ArrayList<>();
  private ListeElectorale[] tListesSaisies;

...
  @Override
  protected void doCalculer() {
    tListesSaisies = listesSaisies.toArray(new ListeElectorale[0]);
    // obliczenie liczby miejsc
    try {
      ...
    } catch (ElectionsException ex) {
      // wyświetlanie wyjątku
      ...
      return;
    }
    // wyświetlanie wyników
    ...
    // aktualizacja stanu formularza
    Utilitaires.setEnabled(...);
}

Zadanie: uzupełnij kod metody [doCalculer].


10.3.9.8. Zapisywanie wyników w źródle danych

Powiązanie [Enregistrer] (12) umożliwia zapisanie wyników obliczeń miejsc w źródle danych. Po pomyślnym zapisaniu link [Enregistrer] zostaje wyłączony. W przypadku niepowodzenia w [15] wyświetlany jest komunikat o błędzie. W każdym przypadku link [Enregistrer] zostaje następnie wyłączony.

W klasie [AbstractElectionsSwing] menedżerem obsługującym kliknięcie na etykiecie [Enregistrer] jest następujący:


  private void jLabelEnregistrerMouseClicked(java.awt.event.MouseEvent evt) {
    if(jLabelEnregistrer.isEnabled()){
      doEnregistrer();
    }
}

Wiersz 3 wywołuje metodę [doEnregistrer] klasy [ElectionsSwing]:


@Override
    protected void doEnregistrer() {
        // wysyłanie żądania zapisu do warstwy biznesowej
        try {
            ...
        } catch (ElectionsException ex) {
            // wyświetlanie wyjątku
            ...
            // powrót do interfejsu graficznego
            return;
        }
        // aktualizacja formularza
        Utilitaires.setEnabled(...);
...
}

Zadanie: uzupełnij kod metody [doEnregistrer].


10.3.9.9. Wyczyść wyniki

Link [Effacer] (11) pozwala na wyczyszczenie wyników wyświetlanych w (14).

W klasie [AbstractElectionsSwing] procedurą obsługującą kliknięcie na etykiecie [Effacer] jest następująca:


  private void jLabelEffacerMouseClicked(java.awt.event.MouseEvent evt) {
    if(jLabelEffacer.isEnabled()){
      doEffacer();
    }
}

Wiersz 3 wywołuje metodę [doEffacer] klasy [ElectionsSwing]:


    @Override
    protected void doEffacer() {
        // czyści się listę wyników
        ....
        // aktualizacja formularza
        Utilitaires.setEnabled(...);
}

Zadanie: uzupełnij kod metody [doEffacer].


Warto wiedzieć: klasa DefaultListModel posiada metodę clear(), która usuwa wszystkie jej elementy.

10.3.10. Ulepszenia

Poprzedni interfejs graficzny można ulepszyć na różne sposoby: użytkownik może przez zapomnienie nie wprowadzić głosów ze wszystkich list obecnych w polu kombi, a ponadto może przez pomyłkę wprowadzić głosy z tej samej listy wielokrotnie.


Zadanie: ulepsz algorytm tak, aby te dwa przypadki nie mogły wystąpić. Prostym rozwiązaniem jest prowadzenie słownika wprowadzonych list, którego klucze stanowiłyby elementy listy rozwijanej. Należy również zadbać o to, aby przycisk [Calculer] był aktywny dopiero po wprowadzeniu wszystkich list.


W kursie [ref1] należy zapoznać się z klasą HashTable w punkcie 3.8.