Skip to content

10. [TD]: Реалізація шару [ui] з використанням інтерфейсу Swing

Ключові слова: багатошарова архітектура, Spring, ін’єкція залежностей, бібліотека компонентів Swing.

10.1. Support

У шарі [UI] ми хочемо створити графічний інтерфейс Swing. NetBeans має інструмент [Matisse] для створення таких інтерфейсів Swing, який перевершує те, що може запропонувати Eclipse. Інтерфейси Swing, як правило, замінюються інтерфейсами JavaFx. NetBeans та Eclipse використовують один і той самий інструмент для їх створення. Отже, якщо створювати інтерфейси JavaFx, можна залишити Eclipse у всіх рівнях багатошарової архітектури.

NetBeans може відкривати будь-який проект Maven. Тому ми використаємо попередній проект Maven і додамо до нього інтерфейс Swing. У [2] завантажуємо (File / Open project) проекти Maven трьох рівнів, які ми створили за допомогою Eclipse. Потім компілюємо їхні бінарні файли [3]. Параметри [Build] та [Clean and Build] створюють бінарний файл того проекту, до якого вони застосовуються. Ці бінарні файли розміщуються у папці [target] [4-5] проекту:

Опція [Clean] видаляє цю папку [target]. Опція [Build] відновлює її. Досвід показує, що при виникненні несподіваних проблем перше, що слід зробити, — це виконати команду [Clean and Build] для проекту, щоб переконатися, що ви працюєте саме з його останньою версією. Це особливо необхідно, коли є файли конфігурації, зміна яких не спричиняє автоматичної перекомпіляції під час виконання проекту. Тоді потрібно примусово запустити перекомпіляцію за допомогою команди [Clean and Build], щоб їхні нові версії були встановлені в папці [target].

10.2. Функціонування додатка

Повернемося до загальної архітектури додатка [Elections]:

Зараз ми розглядаємо нову реалізацію шару [ui]. Єдина реалізація, створена на даний момент, — це консольний інтерфейс. Тепер ми створюємо графічний інтерфейс.

Користувач матиме у своєму розпорядженні такий інтерфейс для взаємодії з додатком [Elections]:

Графічний інтерфейс знаходиться в шарі [ui]. Саме він взаємодіє з користувачем.

  • Під час запуску консольний додаток [main] створює екземпляри трьох шарів додатка за допомогою Spring. Це відбувається ще до того, як графічний інтерфейс стає видимим. Також на цьому етапі ініціалізації з шару [dao] запитується інформація, що характеризує вибори (кількість місць, що підлягають заповненню, виборчий поріг, списки, що беруть участь у виборах). Якщо цей етап ініціалізації завершується невдало (наприклад, неможливо отримати доступ до даних), на консолі відображається повідомлення про помилку, а графічний інтерфейс не відображається.
  • Якщо зчитування даних пройшло успішно, відображається графічний інтерфейс із такою інформацією (див. знімок екрана вище):
    • кількість вакантних місць у (2)
    • виборчий поріг у (3)
    • ідентифікатори та назви списків кандидатів у (4)
  • потім користувач присвоює кожному списку кандидатів відповідну кількість голосів за допомогою полів 4 (ідентифікатор — назва), 5 (голоси), 6 (додати).

Image

  • після цього можна скористатися посиланням (10) для підрахунку місць:

Image

  • посилання [Enregistrer] (12) дозволяє зберегти результати у джерелі даних.

10.3. Клас [ElectionsSwing], що реалізує шар [ui]

10.3.1. Проєкт NetBeans

Примітка: У розділі 22.4 описано, як отримати NetBeans.

Остаточний проект NetBeans для цього додатка матиме такий вигляд: [1]. Створіть його, дотримуючись кроків, описаних у [2-5]:

Переконайтеся, що проект налаштований для компіляції за допомогою JDK 1.8 [1-6]:

10.3.2. Налаштування Maven

Новий проєкт [elections-swing-metier-dao-jdbc] базуватиметься на попередньому проєкті [elections-console-metier-dao-jdbc]. Для цього додаємо залежність Maven наступним чином [1-3]:

10.3.3. Створення графічного інтерфейсу

Щоб створити графічний інтерфейс, можна вчинити так:

  • [1]: додавання об’єкта до пакета [elections.ui.service]
  • [2]: вибір опції [JFrame Form] у категорії [Swing GUI Forms]
  • [4]: присвоєння імені класу
  • [5]: пакет класу.
  • Завершіть роботу майстра.
  • [6]: згенерований клас
  • [7]: клас [AbstractElectionsSwing] у режимі [Design]
  • [8]: вкладка [Navigator], яка відображає дерево [9] компонентів вікна
  • [10]: вкладка [Properties], на якій відображаються властивості компонента [jFrame], вибраного на вкладці [9]
  • [11]: [JFrame] є контейнером для компонентів. Ці компоненти можна розміщувати в контейнері за різними правилами позиціонування, які називаються layouts. Тут ми обираємо layout [Free Design] [14], що дозволяє вільно розміщувати компоненти в контейнері.

Компоненти можна знайти на панелі інструментів під назвою Palette:

  • [1]: палітра
  • [2]: компонент JLabel розміщений у контейнері компонентів
  • ; клацнувши на ньому правою кнопкою миші, можна отримати доступ до різних властивостей: його імені [4], тексту [3] або ж його обробників подій [5]. Ми використовуємо [3] для встановлення тексту [6].
  • [1]: вкладка [Properties] компонента [JLabel] надає доступ до його властивостей: горизонтального позиціонування [2], вертикальне [3], шрифт тексту [4], текст [5].

Коли компонент розміщується та налаштовується на графічному інтерфейсі, а виконану роботу зберігають (Ctrl-S), у класі [AbstractElectionsSwing] генерується код:

 
 

Не слід змінювати цей код, позначений сірим кольором, оскільки він видаляється та генерується заново під час наступного збереження. У такому разі зроблені зміни будуть втрачені.

Посібник зі створення графічного інтерфейсу за допомогою NetBeans можна знайти в URL [https://netbeans.org/kb/docs/java/quickstart-gui.html?print=yes#design] (листопад 2015 р.).

Тепер ми створюємо такий інтерфейс:

Компоненти інтерфейсу такі:

тип
назва
роль
1
JMenuBar
jMenuBar1
меню
2
JLabel
jLabelSAP
кількість вакантних місць
3
JLabel
jLabelSE
виборчий поріг
4
JComboBox
jComboBoxNomsListes
перелік назв списків, що беруть участь у виборах
5
JTextField
jTextFieldVoixListe
кількість голосів за списком
6
JLabel
jLabelAjouter
щоб додати список до (8)
7,8
(JScrollPane, JList)
jListNomsVoix
назви та голоси списків
9
JLabel
jLabelSupprimer
щоб видалити з (8) список, вибраний у (8)
10
JLabel
jLabelCalculer
для підрахунку результатів виборів
11
JLabel
jLabelEffacer
для видалення результатів виборів
12
JLabel
jLabelEnregistrer
для реєстрації результатів виборів
13,14
(JScrollPane, JList)
jListResultats
для відображення результатів виборів
15,16
(JScrollPane,
JTextPane)
jTextPaneMessages
для перегляду повідомлень про відстеження

Анотація (JScrollPane, JList) [13-14] вказує, що при перетягуванні компонента [JList] у вікно він автоматично вставляється в компонент [JScrollPane], який забезпечує прокрутку списку. Саме компонент [JScrollPane] дозволяє переглядати всі елементи списку, хоча в будь-який момент часу видно лише обмежену їх кількість. Те саме стосується компонентів [JTextPane] та [15-16].

Меню можна створити таким чином:

  • [1, 2]: компонент [Menu Bar] розміщується у вікні
  • [3]: меню, згенероване за замовчуванням, як показано на вкладці [Navigator]
  • [4,5,6]: клацнувши правою кнопкою миші на пункті меню, можна:
    • змінити його текст [4], його назву [5]
    • керувати її подіями [6]
  • [7]: потрібне меню

Потрібне меню виглядає так:

рівень 1
рівень 2
Вибори
 
 
Quitter
Списки
 
 
Ajouter
 
Supprimer
Результати
 
 
Calculer
 
Effacer
 
Enregistrer
Про
 

Графічний інтерфейс можна протестувати в будь-який час:

 

Під час створення інтерфейсу до деяких міток та меню потрібно прив’язати обробник подій [Ajouter, Effacer, ...]. Ось як це зробити:

  • [1]: клацнути правою кнопкою миші на компоненті, для якого потрібно налаштувати обробку події
  • [2]: вибрати опцію [Events]
  • [3]: вибрати категорію подій
  • [4]: вибрати подію, яку потрібно налаштувати

Код Java, згенерований цією операцією, має такий вигляд:


    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 додайте тут свій код обробки:
}
  • рядки 1–5: до компонента jLabelCalculer додано обробник події. Метод addMouseListener очікує як параметр клас, що реалізує наступний інтерфейс MouseListener:
 

Інтерфейс MouseListener реалізується різними класами, серед яких клас MouseAdapter. Цей клас реалізує п’ять методів інтерфейсу MouseListener, але ці методи нічого не роблять. Тому необхідно створити похідний клас від цього класу, щоб реалізувати потрібні методи. Це зроблено у наведеному вище коді, який повторюється нижче:


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

У наведеному вище коді використовується техніка анонімного класу, описана в розділі 2.5 курсу [ref1].

У рядку 1 параметром методу addMouseListener є анонімний клас, який визначається на льоту. Це екземпляр класу, похідного від класу MouseAdapter (рядок 1), у якому перевизначається метод mouseClicked (рядки 2–4) для виконання певної дії.

Метод jLabelCalculerMouseClicked, який називається «рядок 3», визначено таким чином:


  private void jLabelCalculerMouseClicked(java.awt.event.MouseEvent evt) {
    // TODO додайте тут свій код обробки:
}

Розробник обробляє подію «MouseClicked», розміщуючи код у цьому методі.

Усі обробники подій генеруються NetBeans саме таким чином. Розробник може проігнорувати рядки коду, згенеровані NetBeans для прив’язки методу до події компонента. Він може просто розмістити свій код у рядку 2 вище. Ось приклад:


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

Якщо запустити графічний інтерфейс і натиснути на посилання [Calculer], у консолі з’явиться таке повідомлення:

  • [1]: двічі клацнути на мітці [Calculer]
  • [2]: обробник цієї події був виконаний і згенерував повідомлення mouseClicked у консолі NetBeans.

Компоненти [jComboBoxNomsListes, jListNomsVoix, jListResultats] оголошуються наступним чином:


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

Ці компоненти є списками, які зазвичай налаштовуються за допомогою типу T: типу елементів моделі, що відображаються цими компонентами. Цей тип T може бути будь-яким. Значення, що відображається у компоненті-списку, має тип [String]. За замовчуванням для відображення використовується метод [T.toString()]. Щоб краще контролювати те, що буде відображатися, тип T тут буде типом String. Отже, правильна декларація наших списків виглядає так:


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

Цей результат отримується шляхом зміни одного з властивостей компонента:

10.3.4. Розділення коду

Повернемося до структури нашого додатка:

Клас [AbstractElectionsSwing] повинен реалізовувати вищезазначений рівень [ui]. Його код, згенерований NetBeans, наразі містить лише код управління вікном та обробники подій, які поки що нічого не роблять. Вище ми бачимо, що клас [AbstractElectionsSwing] повинен буде керувати взаємодією з шаром [métier]. Це управління відбуватиметься в обробниках подій. Щоб зробити структуру коду зрозумілішою, ми вирішили розмістити його у двох класах:

  • [AbstractElectionsSwing], який залишиться таким, яким його згенерував NetBeans, за винятком деяких дрібних деталей. Цей клас сам по собі не оброблятиме жодних подій. Обробники подій будуть порожніми та оголошені абстрактними. Вони будуть реалізовані класом, похідним від [AbstractElectionsSwing].
  • [ElectionsSwing] — клас, похідний від [AbstractElectionsSwing], який реалізує всі обробники подій.

Такий тип розділення не є чимось незвичайним. Його можна, наприклад, побачити на веб-сторінках ASP.NET (версія, відмінна від MVC). Проєкт NetBeans розвивається наступним чином:

 

Код класу [AbstractElectionsSwing] розвивається наступним чином:


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

....
    // обробники подій
    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();
...
}
  • рядок 1: клас оголошено абстрактним
  • рядки 3–5: обробка кліка на пункті меню [jMenuItemCalculer]. Бачимо, що обробка події переноситься на метод doCalculer у рядку 19. Цей метод не реалізовано, і він оголошено абстрактним. Його реалізує похідний клас [ElectionsSwing];
  • рядки 9–13: обробник події clic для мітки [jLabelCalculer]. Клік завжди викликає подію, незалежно від того, чи є компонент [jLabel] активним (enabled=true) чи неактивним (enabled=false). Тут ми перевіряємо, чи він дійсно активний, щоб обробити подію;
  • рядки 15 і далі: ця техніка делегування обробки подій абстрактному методу застосовується до всіх обробників подій.

Клас [ElectionsSwing], похідний від [AbstractElectionsSwing], реалізує всі обробники подій, не реалізовані класом [AbstractElectionsSwing]:


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

    // обробники подій

    @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() {
...
    }

}
  • рядок 3: [ElectionsSwing] походить від [AbstractElectionsSwing]
  • рядки 7–50: обробники подій графічного вікна

Методи похідного класу [ElectionsSwing] будуть оперувати компонентами батьківського класу [AbstractElectionsSwing]. Наразі ці компоненти мають область дії private, що забороняє дочірньому класу [ElectionsSwing] отримати до них доступ:


private JMenuItem jMenuItemAPropos = null;

private JLabel jLabelAjouter = null;

Щоб вирішити цю проблему, слід забезпечити, щоб область дії компонентів графічного інтерфейсу була [protected]:

  • встановити для атрибута [protected] значення [3];

10.3.5. Реалізація інтерфейсу [IElectionsUI]

Повернемося до структури нашого додатка:

Вищезазначений рівень [ui] повинен надавати інтерфейс [IElectionsUI] об’єкту [main]:


package elections.ui.service;

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

Цей інтерфейс було визначено в проєкті [elections-console-metier-dao-jdbc] та описано в розділі 9.4. Оскільки цей проєкт є залежністю проєкту [swing], цей інтерфейс є відомим.

Оскільки клас [AbstractElectionsSwing] став абстрактним, Spring більше не може створювати його екземпляри. Відтепер екземпляри повинен створювати клас [ElectionsSwing]. Клас [ElectionsSwing] повинен реалізовувати інтерфейс [IElectionsUI]. Отже, його код змінюється наступним чином:


public class ElectionsSwing extends AbstractElectionsSwing implements IElectionsUI {

    // метод run інтерфейсу [ElectionsUI]
    public void run() {
...
    }

  • рядок 1: клас [ElectionsSwing] реалізує інтерфейс [IElectionsUI]
  • рядки 4–6: метод [run] цього інтерфейсу

Що повинен робити метод run? Відобразити графічне вікно. Як це зробити? Можна орієнтуватися на метод [main], який NetBeans згенерував у класі [AbstractElectionsSwing] і який виконує потрібну дію:


public static void main(String args[]) {
    /* Налаштування зовнішнього вигляду Nimbus */
    //<editor-fold defaultstate="collapsed" desc="Код налаштування зовнішнього вигляду (опціонально)">
    /* Якщо Nimbus (введений у Java SE 6) недоступний, залиште зовнішній вигляд за замовчуванням.
         * 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>

    /* Створення та відображення форми */
    java.awt.EventQueue.invokeLater(new Runnable() {
      public void run() {
        new NewJFrame().setVisible(true);
      }
    });
  }

Конструктор [AbstractElectionsSwing], який використовується у рядку 28, має такий вигляд:


  public AbstractElectionsSwing() {
    initComponents();
}
  • рядок 2: метод [initComponents] є приватним методом, згенерованим генератором графічного інтерфейсу. Його код не можна змінювати.

Метод [run] класу [ElectionsSwing] може мати такий вигляд:


  @Override
  public void run() {
    // відображення графічного інтерфейсу
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        init();
        setVisible(true);
      }
    });
}
  • рядок 6: графічний інтерфейс ініціалізується за допомогою методу [init]. Тут ми хотіли б викликати метод [initComponents] батьківського класу, але він є приватним. Тому до батьківського класу [AbstractElectionsSwing] додаємо такий метод [init]:

  protected void init(){
    initComponents();
}
  • (продовження)
    • оскільки метод [init] знаходиться у класі [AbstractElectionsSwing], він має доступ до приватного методу [initComponents] того самого класу;
    • оскільки вона має атрибут [protected], вона видима у дочірньому класі [ElectionsSwing];
  • рядок 7: графічний інтерфейс стає видимим;

Примітка: після того, як метод [run] було написано в класі [ElectionsSwing], метод [main] абстрактного класу [AbstractElectionsSwing] можна видалити.

10.3.6. Виконуваний клас

Повернемося до структури нашого додатка:

Ми хотіли б, щоб Spring створював екземпляр шару [ui] так само, як це робилося, коли цей шар реалізовувався додатком console. Для цього клас реалізації [ElectionsSwing] повинен мати посилання на шар [métier]:


@Component
public class ElectionsSwing extends AbstractElectionsSwing implements IElectionsUI{

  // довідка щодо шару [métier]
  @Autowired
  private IElectionsMetier metier;
...
  • рядок 1: клас [ElectionsSwing] є компонентом Spring;
  • рядки 5–6: ін’єкція Spring-ом посилання на рівень [métier];

Графічний інтерфейс запускається під час виконання наступного класу [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);
    }
}

Ми вже розглядали подібний код у розділі 9.5, де пояснювали код класів [AbstractBootElections] та [BootElectionsConsole]. У рядку 12 ми отримуємо бін із назвою [electionsSwing], що відповідає стандартній назві Spring для класу [ElectionsSwing].

10.3.7. Ініціалізація графічного інтерфейсу

Під час відображення графічного інтерфейсу деякі його компоненти були ініціалізовані:

Image

Вище ми бачимо:

  • що комбінований список заповнено назвами списків;
  • що вказано кількість місць, які підлягають заповненню, та виборчий поріг;
  • що деякі посилання було вимкнено;
  • що внизу вікна відображається повідомлення про успішне виконання операції;

Коли відбуватимуться ці ініціалізації? Вони можуть відбутися лише після ініціалізації поля [electionsMetier] класу [ElectionsSwing]. Адже назви списків будуть запитуватися у шарі [metier]. Ініціалізація цього поля буде виконана Spring у такому порядку:

  • використання конструктора без параметрів класу [ElectionsSwing];
  • ін'єкція залежностей, у даному випадку посилання на шар [métier];
  • виконання методу [run] класу [ElectionsSwing]:

    @Override
    public void run() {
        // відображення графічного інтерфейсу
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                init();
                setVisible(true);
            }
        });
}
  • у рядку 12 ми зазначили, що викликаємо метод [init] батьківського класу, який буде малювати компоненти графічного інтерфейсу. Ми переопределимо цей метод локально, у класі [ElectionsSwing]. Саме в цьому методі ми ініціалізуємо, цього разу з даними, компоненти вікна (комбіновані поля, мітки):

Локальний метод [init] може мати такий скелет:


public class ElectionsSwing extends AbstractElectionsSwing implements IElectionsUI {

    ...
  // довідка щодо шару [métier]
  @Autowired
  private IElectionsMetier metier;

    // ініціалізації
    public void init() {
      // генерація компонентів батьківським класом
    super.init();
    
    // запит списків до шару [metier]
        ...
        // імена списків пов’язуються з комбінованим списком jComboBoxNomsListes
        ...
        // а також параметри виборів
        ...
        // ініціалізуються мітки, пов’язані з цими двома видами інформації
        ...
        // повідомлення про успішне виконання
        ...
        // ініціалізація стану деяких компонентів форми
...
}

Зверніть увагу на 11-й рядок, де викликається метод [init] з батьківського класу.

10.3.8. Клас [Utilitaires]

У класі [Utilitaires] зібрано низку статичних допоміжних методів:

 

Клас [Utilitaires] має такий вигляд:


package istia.st.elections.ui;

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

// допоміжний клас
class Utilitaires {
    // управління станом масиву міток
    public static void setEnabled(JLabel[] labels, boolean value) {
        for (int i = 0; i < labels.length; i++) {
            labels[i].setEnabled(value);
        }
    }

    // управління станом масиву опцій меню
    public static void setEnabled(JMenuItem[] menuItems, boolean value) {
        ...
    }

}
  • рядок 9: метод setEnabled встановлює стан компонентів JLabel, визначених у масиві. Метод setEnabled компонента JLabel дозволяє активувати або деактивувати JLabel.

Завдання: на прикладі методу setEnabled у рядку 9 (,) напишіть метод setEnabled у рядку 16, який виконує те саме з компонентами JMenuItem.


10.3.9. Код класу [ElectionsSwing]

Нагадаємо загальну структуру класу [ElectionsSwing]:


package istia.st.elections.ui;

...
public class ElectionsSwing extends AbstractElectionsSwing implements IElectionsUI {

  // довідка щодо шару [métier]
  @Autowired
  private IElectionsMetier metier;


    // ініціалізації
    public void init() {
    ...
    }

    // обробники подій

    @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() {
...
    }

}

Ми розглянемо методи класу по черзі.

10.3.9.1. Метод [init]

Повернемося до графічного інтерфейсу:

Метод [init] має на меті:

  • заповнити комбінацію [4] ідентифікаторами та назвами списків у форматі [id - nom]
  • відобразити повідомлення про успішне виконання в [15]
  • ініціалізувати мітки [2] та [3]
  • відключити певні посилання

Скелет методу [init] може виглядати так:


@Override
    protected void init() {
        // генерація компонентів батьківським класом
        super.init();
        // локальні ініціалізації
        modèleNomsVoix = new DefaultListModel<>();
        jListNomsVoix.setModel(modèleNomsVoix);
        modèleRésultats = new DefaultListModel<>();
        jListResultats.setModel(modèleRésultats);
        String info;
        try {
            // запитуються списки у шару [métier]
            listes = ...
            // імена списків пов'язуються з комбінованим списком jComboBoxNomsListes
            ...
            // а також параметри виборів
            int nbSiegesAPourvoir = ...
            double seuilElectoral = ...
            // ініціалізуються мітки, пов’язані з цими двома видами інформації
            ...
            // повідомлення про успішне виконання
            info = "Source de données lue avec succès";
        } catch (ElectionsException ex1) {
            // фіксується помилка
            info = getInfoForException("Les erreurs suivantes se sont produites :", ex1);
        } catch (RuntimeException ex2) {
            // фіксується помилка
            info = getInfoForException("Les erreurs suivantes se sont produites :", ex2);
        }
        // відображається інформація
        jTextPaneMessages.setText(info);
        jTextPaneMessages.setCaretPosition(0);
        // стан форми
        Utilitaires.setEnabled(new JLabel[] { jLabelAjouter, jLabelCalculer, jLabelEnregistrer, jLabelSupprimer }, false);
        Utilitaires.setEnabled(
                new JMenuItem[] { jMenuItemAjouter, jMenuItemCalculer, jMenuItemEnregistrer, jMenuItemSupprimer }, false);
        // вирівняти вікно по центру
        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) {
        // відобразити повідомлення
        StringBuffer info = new StringBuffer(String.format("%s -------------\n", message));
        info.append(String.format("Code erreur : %d\n", ex.getCode()));
        // відображаються помилки
        for (String erreur : ex.getErreurs()) {
            info.append(String.format("-- %s\n", erreur));
        }
        return info.toString();
    }

    private String getInfoForException(String message, RuntimeException ex) {
        // відобразити повідомлення
        StringBuffer info = new StringBuffer(String.format("%s -------------\n", message));
        // вивести стек винятків
        Throwable cause = ex;
        while (cause != null) {
            info.append(String.format("-- %s\n", cause.getMessage()));
            cause = cause.getCause();
        }
        return info.toString();
    }

Завдання: доповніть код методу [init].


Що слід прочитати в курсі: компоненти JTextField, JLabel

Варто знати:

Компонент JList відображає дані, що містяться в шаблоні. За замовчуванням цей шаблон має тип DefaultListModel (рядки 2 та 3). Об’єкт типу DefaultListModel поводиться приблизно так само, як об’єкт типу ArrayList:

  • щоб додати об’єкт типу o до шаблону:

[DefaultListModel].addElement(Object o);

У цьому додатку об’єкт o завжди буде типу String.

  • щоб видалити елемент № i з шаблону:

[DefaultListModel].remove(int i);
  • Щоб отримати елемент № i з шаблону:

[DefaultListModel].elementAt(int i);

Щоб додати елемент до комбінації [jComboBoxNomsListes], слід використати метод [addItem]:


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

Компонент JTextPane має методи getText() та setText() для читання/запису тексту, що відображається.

10.3.9.2. Управління станом посилання [Ajouter]

Посилання [Ajouter] [6] є активним лише тоді, коли поле [5] голосів не є порожнім. У класі [AbstractElectionsSwing] обробник, який відстежує переміщення курсора в полі [5], виглядає наступним чином:


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

У рядку 2 викликається метод [doMajLabelAjouter] класу [ElectionsSwing].


    protected void doMajLabelAjouter() {
        // встановлює стан мітки [jLabelAjouter]
        ...
        // встановлюється стан меню [jMenuItemAjouter]
        ...
}

Завдання: доповніть код методу [doMajLabelAjouter].


10.3.9.3. Присвоїти голоси кожному списку

Для кожного списку-кандидата з (4) виконуємо такі дії:

  • вибір списку з (4)
  • введення кількості голосів у (5)
  • підтвердження шляхом натискання на посилання [Ajouter]

Помилки при введенні даних позначаються, як показано в наступному прикладі:

Image

Якщо кількість голосів правильна, список додається до компонента (8), кількість голосів стирається, а посилання [Ajouter] зникає:

У класі [AbstractElectionsSwing] обробник, який обробляє клік на посилання [Ajouter], виглядає так:


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

У рядку 3 викликається метод [doAjouter] класу [ElectionsSwing]:


  // шаблони списків JList
  private DefaultListModel<String> modèleNomsVoix = null;
  private DefaultListModel<String> modèleRésultats = null;

  // конкурсні списки
  private ListeElectorale[] listes;

  // списки, введені користувачем
  private final List<ListeElectorale> listesSaisies = new ArrayList<>();
  private ListeElectorale[] tListesSaisies;
...
  @Override
  protected void doAjouter() {
    // чи правильна кількість голосів?
    ...
    // якщо є помилка, то її повідомляють
    if (erreur) {
      JOptionPane.showMessageDialog(null, "Nombre de voix incorrect", "Elections : erreur",
              JOptionPane.INFORMATION_MESSAGE);
      jTextFieldVoixListe.requestFocus();
      // повернення до графічного інтерфейсу
      return;
    }
    // помилки немає — список зберігається
    listesSaisies.add(...);
    modèleNomsVoix.addElement(...);
    // очищаємо кількість голосів
    jTextFieldVoixListe.setText("");
    // стан форми (меню, мітки)
...
}
  • рядок 25: щоразу, коли користувач додає голоси до списку та підтверджує свій вибір, цей список поміщається у поле [listesSaisies] у рядку 9. У ньому буде збережено список з інформацією [id, version, nom, voix]. Перші три елементи інформації беруться зі списків, спочатку записаних у масиві рядка 6. Метод [getSelectedIndex] комбінованого списку дозволяє дізнатися індекс вибраного списку;

Завдання: доповніть код методу [doAjouter].


10.3.9.4. Управління станом посилання [Supprimer]

Посилання [Supprimer] [9] активне лише тоді, коли у [8] вибрано елемент.

У класі [AbstractElectionsSwing] обробник, який реагує на клік по елементу списку [8], виглядає наступним чином:


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

У рядку 2 викликається метод [doMajLabelSupprimer] класу [ElectionsSwing].


    @Override
    protected void doMajLabelSupprimer() {
            // вмикається мітка [jLabelSupprimer] та відповідна опція меню
            ...
}

Завдання: доповніть код методу [doMajLabelSupprimer].


10.3.9.5. Видалити список кандидатів

Посилання [Supprimer] [9] дозволяє видалити пару (ім’я, голос), обрану в (8). Після видалення посилання [Supprimer] вимикається. Воно знову увімкнеться лише після вибору нового списку в пункті (8).

У класі [AbstractElectionsSwing] обробник, який реагує на клік по посиланню [Supprimer], є таким:


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

Рядок 3 викликає метод [doSupprimer] класу [ElectionsSwing].


@Override
    protected void doSupprimer() {
        // видалення вибраного списку, шаблону modèleNomsVoix та введених списків
        ...
        // оновлення стану міток та опцій меню форми
        Utilitaires.setEnabled(...);
    ...
}

Завдання: доповніть код методу [doSupprimer].


10.3.9.6. Управління станом посилання [Calculer]

Посилання [Calculer] [10] є активним лише тоді, коли в [8] існує принаймні один елемент.


Завдання: додайте код, необхідний для управління цим посиланням, у методи [doAjouter] та [doSupprimer], написані раніше. Відповідний пункт меню також буде оброблено.


Важливо: кількість елементів у елементі типу DefaultListModel визначається за допомогою методу size().

10.3.9.7. Розрахунок місць

Посилання [Calculer] [10] дозволяє запустити підрахунок місць та відобразити результати в (14). У разі невдачі (усі списки були виключені) у [15] відображається повідомлення про помилку. У будь-якому разі після розрахунку посилання [Calculer] [10] деактивується.

У класі [AbstractElectionsSwing] обробник, який реагує на клік по посиланню [Calculer], є таким:


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

У рядку 3 викликається метод [doCalculer] класу [ElectionsSwing].


  // списки, введені користувачем
  private final List<ListeElectorale> listesSaisies = new ArrayList<>();
  private ListeElectorale[] tListesSaisies;

...
  @Override
  protected void doCalculer() {
    tListesSaisies = listesSaisies.toArray(new ListeElectorale[0]);
    // розрахунок місць
    try {
      ...
    } catch (ElectionsException ex) {
      // виводиться повідомлення про виняток
      ...
      return;
    }
    // відображення результатів
    ...
    // оновлення стану форми
    Utilitaires.setEnabled(...);
}

Завдання: доповніть код методу [doCalculer].


10.3.9.8. Збереження результатів у джерелі даних

Посилання [Enregistrer] (12) дозволяє зберегти результати розрахунку місць у джерелі даних. Після успішного збереження посилання [Enregistrer] деактивується. У разі невдачі в [15] відображається повідомлення про помилку. У будь-якому випадку посилання [Enregistrer] потім деактивується.

У класі [AbstractElectionsSwing] обробник, який обробляє клік на мітці [Enregistrer], є таким:


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

У рядку 3 викликається метод [doEnregistrer] класу [ElectionsSwing]:


@Override
    protected void doEnregistrer() {
        // запит на запис у бізнес-шарі
        try {
            ...
        } catch (ElectionsException ex) {
            // відображення винятку
            ...
            // повернення до графічного інтерфейсу
            return;
        }
        // оновлення форми
        Utilitaires.setEnabled(...);
...
}

Завдання: доповніть код методу [doEnregistrer].


10.3.9.9. Очистити результати

Посилання [Effacer] (11) дозволяє очистити результати, що відображаються в (14).

У класі [AbstractElectionsSwing] обробник, який обробляє клік на мітці [Effacer], виглядає так:


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

У рядку 3 викликається метод [doEffacer] класу [ElectionsSwing]:


    @Override
    protected void doEffacer() {
        // очищення списку результатів
        ....
        // оновлення форми
        Utilitaires.setEnabled(...);
}

Завдання: доповніть код методу [doEffacer].


До відома: клас DefaultListModel має метод clear(), який видаляє всі його елементи.

10.3.10. Удосконалення

Попередній графічний інтерфейс можна вдосконалити різними способами: користувач може, забувши, не ввести голоси з усіх списків, присутніх у комбінованому списку, а також може, помилково, ввести голоси з одного й того самого списку кілька разів.


Завдання: вдосконалити алгоритм так, щоб ці два випадки не могли трапитися. Простим рішенням є ведення словника введених списків, ключами якого будуть елементи комбінованого списку. Також слід забезпечити, щоб посилання [Calculer] було активним лише тоді, коли введено всі списки.


Див. у курсі [ref1]: клас HashTable у розділі 3.8.